Skip to content

Commit d466981

Browse files
committed
rewrite the user authentication example to use the latest features
1 parent 5f45326 commit d466981

13 files changed

+105
-51
lines changed

examples/user-authentication/README.md

+44-16
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
11
# User authentication demo
22

3-
This example demonstrates how to manually handle user authentication with SQLpage and PostgreSQL.
4-
All the user and password management is done in the database, using the standard [pgcrypto](https://www.postgresql.org/docs/current/pgcrypto.html) postgresql extension.
3+
This example demonstrates how to handle user authentication with SQLpage.
4+
5+
It uses a PostgreSQL database to store user information and session ids,
6+
but the same principles can be applied to other databases.
7+
8+
All the user and password management is done in SQLPage,
9+
which uses [best practices](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#maximum-password-lengths) for password storage.
510

611
This demonstrates how to implement:
7-
- [a signup form](./sign%20up.sql)
8-
- [a login form](./sign%20in.sql)
12+
- [a signup form](./signup.sql)
13+
- [a login form](./signin.sql)
914
- [a logout button](./logout.sql)
1015
- [secured pages](./protected_page.sql) that can only be accessed by logged-in users
1116

1217
User authentication is a complex topic, and you can follow the work on implementing differenet authentication methods in [this issue](https://github.com/lovasoa/SQLpage/issues/12).
1318

19+
## How to run
20+
21+
Install [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
22+
23+
Then run the following command in this directory:
24+
25+
```bash
26+
docker-compose up
27+
```
28+
29+
Then open [http://localhost:8080](http://localhost:8080) in your browser.
30+
1431
## Caveats
1532

16-
In this example, we store encrypted user passwords in the database, but we let the database itself handle the encryption.
17-
For that to be safe, you need to make sure that:
18-
- the database is not accessible by untrusted users
19-
- the database logs and configuration files are not accessible by untrusted users
20-
- either your connection to the database is encrypted [(use SSL)](https://www.postgresql.org/docs/current/ssl-tcp.html) or you can trust all the machines on the network between your application and the database. Connections should be encrypted by default if you use a recent version of PostgreSQL and a popular distribution.
33+
In this example, we handle user creation and login in SQLpage.
34+
35+
If you are implementing user authentication in an public application with potentially sensitive data,
36+
you should propably read the [Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) from OWASP.
2137

2238
## Screenshots
2339

@@ -30,28 +46,40 @@ For that to be safe, you need to make sure that:
3046

3147
### User creation
3248

33-
The [signup form](./sign%20up.sql) is a simple form that is handled by [`create_user.sql`](./create_user.sql).
49+
The [signup form](./signup.sql) is a simple form that is handled by [`create_user.sql`](./create_user.sql).
3450
You could restrict user creation to existing administrators and create an initial administrator in a database migration.
3551

3652
### User login
3753

38-
The [login form](./sign%20in.sql) is a simple form that is handled by [`login.sql`](./login.sql).
39-
It checks that the username exists and that the password is correct using the [pgcrypto](https://www.postgresql.org/docs/current/pgcrypto.html) extension with
54+
The [login form](./signin.sql) is a simple form that is handled by [`login.sql`](./login.sql).
55+
56+
`login.sql` checks that the username exists and that the password is correct using the [authentication component](https://sql.ophir.dev/documentation.sql?component=authentication#component) extension with
4057

4158
```sql
42-
SELECT * FROM users WHERE username = :username AND password = crypt(:password, password);
59+
SELECT 'authentication' AS component,
60+
'signin.sql' AS link,
61+
(SELECT password_hash FROM user_info WHERE username = :username) AS password_hash,
62+
:password AS password;
4363
```
4464

4565
If the login is successful, an entry is added to the [`login_session`](./sqlpage/migrations/0000_init.sql) table with a random session id.
66+
67+
If it is not, the authentication component will redirect the user to the login page and stop the execution of the page.
68+
4669
The session id is then stored in a cookie on the user's browser.
4770

48-
The user is then redirected to [`./check_login.sql`](./check_login.sql) that checks that the session id is valid and redirects back to the login page if it is not.
71+
The user is then redirected to [`./protected_page.sql`](./protected_page.sql) which will check that the user is logged in.
4972

5073
### Protected pages
5174

5275
Protected pages are pages that can only be accessed by logged-in users.
53-
There is an example in [`protected_page.sql`](./protected_page.sql) that uses a simple [postgresql stored procedure](./sqlpage/migrations/0000_init.sql)
54-
to raise an error (and thus prevent content rendering) if the user is not logged in.
76+
77+
There is an example in [`protected_page.sql`](./protected_page.sql) that uses
78+
the [`redirect`](https://sql.ophir.dev/documentation.sql?component=redirect#component)
79+
component to redirect the user to the login page if they are not logged in.
80+
81+
Checking whether the user is logged in is as simple as checking that session id returned by [`sqlpage.cookie('session')`](https://sql.ophir.dev/functions.sql?function=cookie#function) exists in the [`login_session`](./sqlpage/migrations/0000_init.sql) table.
82+
5583

5684
### User logout
5785

Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
WITH inserted_user AS (
22
INSERT INTO user_info (username, password_hash)
3-
VALUES (:username, crypt(:password, gen_salt('bf', 10)))
3+
VALUES (:username, sqlpage.hash_password(:password))
44
ON CONFLICT (username) DO NOTHING
55
RETURNING username
66
)
77
SELECT 'hero' AS component,
88
'Welcome' AS title,
99
'Welcome, ' || username || '! Your user account was successfully created. You can now log in.' AS description,
1010
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Community_wp20.png/974px-Community_wp20.png' AS image,
11-
'sign in.sql' AS link,
11+
'signin.sql' AS link,
1212
'Log in' AS link_text
1313
FROM inserted_user
1414
UNION ALL
1515
SELECT 'hero' AS component,
1616
'Sorry' AS title,
1717
'Sorry, this user name is already taken.' AS description_md,
1818
'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Sad_face_of_a_Wayuu_Woman.jpg/640px-Sad_face_of_a_Wayuu_Woman.jpg' AS image,
19-
'sign up.sql' AS link,
19+
'signup.sql' AS link,
2020
'Try again' AS link_text
2121
WHERE NOT EXISTS (SELECT 1 FROM inserted_user);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
SELECT 'form' AS component;
2+
SELECT 'password' AS name, 'Password to create a hash for' AS label, :password AS value;
3+
4+
SELECT 'code' AS component;
5+
SELECT sqlpage.hash_password(:password) AS contents;

examples/user-authentication/index.sql

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ SELECT 'shell' AS component,
22
'User Management App' AS title,
33
'user' AS icon,
44
'/' AS link,
5-
'sign in' AS menu_item,
6-
'sign up' AS menu_item;
5+
'signin' AS menu_item,
6+
'signup' AS menu_item;
77

88
SELECT 'hero' AS component,
99
'SQLPage Authentication Demo' AS title,
1010
'This application requires signing up to view the protected page.' AS description_md,
1111
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Community_wp20.png/974px-Community_wp20.png' AS image,
12-
'login_check.sql' AS link,
12+
'protected_page.sql' AS link,
1313
'Access protected page' AS link_text;
+15-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
INSERT INTO login_session (username)
2-
SELECT username
3-
FROM user_info
4-
WHERE username = :username
5-
AND password_hash = crypt(:password, password_hash)
6-
RETURNING 'cookie' AS component,
7-
'session' AS name,
8-
id AS value;
9-
SELECT 'http_header' AS component,
10-
'login_check.sql' AS location;
1+
-- The authentication component will stop the execution of the page and redirect the user to the login page if
2+
-- the password is incorrect or if the user does not exist.
3+
SELECT 'authentication' AS component,
4+
'signin.sql?error' AS link,
5+
(SELECT password_hash FROM user_info WHERE username = :username) AS password_hash,
6+
:password AS password;
7+
8+
-- Generate a random 32 characters session ID, insert it into the database,
9+
-- and save it in a cookie on the user's browser.
10+
INSERT INTO login_session (id, username)
11+
VALUES (sqlpage.random_string(32), :username)
12+
RETURNING 'cookie' AS component, 'session' AS name, id AS value;
13+
14+
-- Redirect the user to the protected page.
15+
SELECT 'redirect' AS component, 'protected_page.sql' AS link;

examples/user-authentication/login_check.sql

-6
This file was deleted.
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
DELETE FROM login_session WHERE id = sqlpage.cookie('session');
22
SELECT 'cookie' AS component, 'session' AS name, TRUE AS remove;
33

4-
SELECT 'http_header' AS component, 'login.sql' AS location;
4+
SELECT 'redirect' AS component, '/' AS location;
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
SELECT raise_error('Invalid credentials, please log in') WHERE NOT is_valid_session(sqlpage.cookie('session'));
1+
SELECT 'redirect' AS component,
2+
'signin.sql?error' AS link
3+
WHERE logged_in_user(sqlpage.cookie('session')) IS NULL;
24

35
SELECT 'shell' AS component, 'Protected page' AS title, 'lock' AS icon, '/' AS link, 'logout' AS menu_item;
46

57
SELECT 'text' AS component,
6-
'This content is [top secret](https://youtu.be/dQw4w9WgXcQ). You cannot view it if you are not connected.' AS contents_md;
8+
'Welcome, ' || logged_in_user(sqlpage.cookie('session')) || ' !' AS title,
9+
'This content is [top secret](https://youtu.be/dQw4w9WgXcQ).
10+
You cannot view it if you are not connected.' AS contents_md;

examples/user-authentication/sign in.sql

-7
This file was deleted.
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
SELECT 'form' AS component,
2+
'Sign in' AS title,
3+
'Sign in' AS validate,
4+
'login.sql' AS action;
5+
6+
SELECT 'username' AS name;
7+
SELECT 'password' AS name, 'password' AS type;
8+
9+
SELECT 'alert' as component,
10+
'Sorry' as title,
11+
'We could not authenticate you. Please log in or [create an account](signup.sql).' as description_md,
12+
'alert-circle' as icon,
13+
'red' as color
14+
WHERE $error IS NOT NULL;

examples/user-authentication/sign up.sql renamed to examples/user-authentication/signup.sql

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ SELECT 'form' AS component,
44
'create_user.sql' AS action;
55

66
SELECT 'username' AS name;
7-
SELECT 'password' AS name, 'password' AS type;
7+
SELECT 'password' AS name, 'password' AS type, '^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$' AS pattern, 'Password must be at least 8 characters long and contain at least one letter and one number.' AS description;
88
SELECT 'terms' AS name, 'I accept the terms and conditions' AS label, TRUE AS required, FALSE AS value, 'checkbox' AS type;

examples/user-authentication/sqlpage/migrations/0000_init.sql

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ CREATE TABLE user_info (
44
);
55

66
CREATE TABLE login_session (
7-
id TEXT PRIMARY KEY DEFAULT encode(gen_random_bytes(128), 'hex'),
7+
id TEXT PRIMARY KEY,
88
username TEXT NOT NULL REFERENCES user_info(username),
99
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
10-
);
10+
);
11+
12+
-- A small pure utility function to get the current user from a session cookie.
13+
-- In a database that does not support functions, you could inline this query
14+
-- or use a view if you need more information from the user table
15+
CREATE FUNCTION logged_in_user(session_id TEXT) RETURNS TEXT AS $$
16+
SELECT username FROM login_session WHERE id = session_id;
17+
$$ LANGUAGE SQL STABLE;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Creates an initial user called 'admin'
2+
-- with a password hash that was generated using the 'generate_password_hash.sql' page.
3+
INSERT INTO user_info (username, password_hash)
4+
VALUES ('admin', '$argon2id$v=19$m=19456,t=2,p=1$IiReWDP0ocWvia+fTdozJw$53EozOKX7HkpvOdoWHjsh9yKvRN2TmQm/PjYBeaOqqc');

0 commit comments

Comments
 (0)