Skip to content

Commit c9f1428

Browse files
committed
Implement the full user authentication flow in the example
1 parent 91aebb2 commit c9f1428

14 files changed

+105
-15
lines changed
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# User authentication demo
2+
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.
5+
6+
This demonstrates how to implement:
7+
- [a signup form](./sign%20up.sql)
8+
- [a login form](./sign%20in.sql)
9+
- [a logout button](./logout.sql)
10+
- [secured pages](./protected_page.sql) that can only be accessed by logged-in users
11+
12+
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).
13+
14+
## Caveats
15+
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+
- your connection to the database is encrypted [(use SSL)](https://www.postgresql.org/docs/current/ssl-tcp.html). It should be the case by default if you use a recent version of PostgreSQL and a popular distribution.
21+
22+
## Screenshots
23+
24+
| Signup form | Login form | Protected page |
25+
| --- | --- | --- |
26+
| ![signup form](./screenshots/signup.png) | ![login form](./screenshots/signin.png) | ![protected page](./screenshots/secret.png) |
27+
| ![home](./screenshots/homepage.png) | ![duplicate username](./screenshots/duplicate-user.png) | ![signup success](./screenshots/signup-success.png) |
28+
29+
## How it works
30+
31+
### User creation
32+
33+
The [a signup form](./sign%20up.sql) is a simple form that is handled by [`create_user.sql`](./create_user.sql).
34+
You could restrict user creation to existing administrators and create an initial administrator in a database migration.
35+
36+
### User login
37+
38+
The [a 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
40+
41+
```sql
42+
SELECT * FROM users WHERE username = :username AND password = crypt(:password, password);
43+
```
44+
45+
If the login is successful, an entry is added to the [`login_session`](./sqlpage/migrations/0000_init.sql) table with a random session id.
46+
The session id is then stored in a cookie on the user's browser.
47+
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.
49+
50+
### Protected pages
51+
52+
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.
55+
56+
### User logout
57+
58+
The cookie can be deleted in the browser by navigating to [`./logout.sql`](./logout.sql).

examples/user-authentication/create_user.sql

+15-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,18 @@ WITH inserted_user AS (
44
ON CONFLICT (username) DO NOTHING
55
RETURNING username
66
)
7-
SELECT 'text' AS component,
8-
COALESCE(
9-
'Welcome, ' || (SELECT username FROM inserted_user) || '! Your user account was successfully created. You can now [log in](sign%20in.sql).',
10-
'Sorry, this user name is already taken.'
11-
) AS contents_md;
7+
SELECT 'hero' AS component,
8+
'Welcome' AS title,
9+
'Welcome, ' || username || '! Your user account was successfully created. You can now log in.' AS description,
10+
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Community_wp20.png/974px-Community_wp20.png' AS image,
11+
'sign in.sql' AS link,
12+
'Log in' AS link_text
13+
FROM inserted_user
14+
UNION ALL
15+
SELECT 'hero' AS component,
16+
'Sorry' AS title,
17+
'Sorry, this user name is already taken.' AS description_md,
18+
'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,
20+
'Try again' AS link_text
21+
WHERE NOT EXISTS (SELECT 1 FROM inserted_user);

examples/user-authentication/index.sql

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ 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-
'protected_page.sql' AS link,
12+
'login_check.sql' AS link,
1313
'Access protected page' AS link_text;

examples/user-authentication/login.sql

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,5 @@ WHERE username = :username
66
RETURNING 'cookie' AS component,
77
'session' AS name,
88
id AS value;
9-
109
SELECT 'http_header' AS component,
11-
'protected_page.sql' AS location;
10+
'login_check.sql' AS location;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Checks if the login was successful, and redirects to the right page.
2+
SELECT 'http_header' AS component,
3+
CASE WHEN is_valid_session(sqlpage.cookie('session'))
4+
THEN 'protected_page.sql'
5+
ELSE 'sign in.sql'
6+
END AS location;
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DELETE FROM login_session WHERE id = sqlpage.cookie('session');
2+
SELECT 'cookie' AS component, 'session' AS name, TRUE AS remove;
3+
4+
SELECT 'http_header' AS component, 'login.sql' AS location;
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1+
SELECT raise_error('Invalid credentials, please log in') WHERE NOT is_valid_session(sqlpage.cookie('session'));
12

2-
SELECT 'text' AS component,
3-
'This content is [top secret](https://youtu.be/dQw4w9WgXcQ). You cannot view it if you are not connected.' AS contents_md;
3+
SELECT 'shell' AS component, 'Protected page' AS title, 'lock' AS icon, '/' AS link, 'logout' AS menu_item;
44

5-
SELECT EXISTS(SELECT 1 FROM login_session WHERE id=sqlpage.cookie('session')) AS contents;
6-
SELECT 'debug' AS component;
7-
SELECT * FROM login_session;
8-
SELECT sqlpage.cookie('session');
5+
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;
Loading
Loading
Loading
Loading
Loading
Loading

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,19 @@ CREATE TABLE login_session (
1010
id TEXT PRIMARY KEY DEFAULT encode(gen_random_bytes(128), 'hex'),
1111
username TEXT NOT NULL REFERENCES user_info(username),
1212
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
13-
);
13+
);
14+
15+
16+
-- Returns true if the session is valid, false otherwise
17+
CREATE FUNCTION is_valid_session(user_session text) RETURNS boolean AS $$
18+
BEGIN
19+
RETURN EXISTS(SELECT 1 FROM login_session WHERE id=user_session);
20+
END;
21+
$$ LANGUAGE plpgsql;
22+
23+
-- Takes a session id, does nothing if it is valid, throws an error otherwise.
24+
CREATE FUNCTION raise_error(error_message_text text) RETURNS void AS $$
25+
BEGIN
26+
RAISE EXCEPTION '%', error_message_text;
27+
END;
28+
$$ LANGUAGE plpgsql;

0 commit comments

Comments
 (0)