Implement the full user authentication flow in the example
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# User authentication demo
|
||||
|
||||
This example demonstrates how to manually handle user authentication with SQLpage and PostgreSQL.
|
||||
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.
|
||||
|
||||
This demonstrates how to implement:
|
||||
- [a signup form](./sign%20up.sql)
|
||||
- [a login form](./sign%20in.sql)
|
||||
- [a logout button](./logout.sql)
|
||||
- [secured pages](./protected_page.sql) that can only be accessed by logged-in users
|
||||
|
||||
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).
|
||||
|
||||
## Caveats
|
||||
|
||||
In this example, we store encrypted user passwords in the database, but we let the database itself handle the encryption.
|
||||
For that to be safe, you need to make sure that:
|
||||
- the database is not accessible by untrusted users
|
||||
- the database logs and configuration files are not accessible by untrusted users
|
||||
- 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.
|
||||
|
||||
## Screenshots
|
||||
|
||||
| Signup form | Login form | Protected page |
|
||||
| --- | --- | --- |
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|
||||
## How it works
|
||||
|
||||
### User creation
|
||||
|
||||
The [a signup form](./sign%20up.sql) is a simple form that is handled by [`create_user.sql`](./create_user.sql).
|
||||
You could restrict user creation to existing administrators and create an initial administrator in a database migration.
|
||||
|
||||
### User login
|
||||
|
||||
The [a login form](./sign%20in.sql) is a simple form that is handled by [`login.sql`](./login.sql).
|
||||
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
|
||||
|
||||
```sql
|
||||
SELECT * FROM users WHERE username = :username AND password = crypt(:password, password);
|
||||
```
|
||||
|
||||
If the login is successful, an entry is added to the [`login_session`](./sqlpage/migrations/0000_init.sql) table with a random session id.
|
||||
The session id is then stored in a cookie on the user's browser.
|
||||
|
||||
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.
|
||||
|
||||
### Protected pages
|
||||
|
||||
Protected pages are pages that can only be accessed by logged-in users.
|
||||
There is an example in [`protected_page.sql`](./protected_page.sql) that uses a simple [postgresql stored procedure](./sqlpage/migrations/0000_init.sql)
|
||||
to raise an error (and thus prevent content rendering) if the user is not logged in.
|
||||
|
||||
### User logout
|
||||
|
||||
The cookie can be deleted in the browser by navigating to [`./logout.sql`](./logout.sql).
|
||||
@@ -4,8 +4,18 @@ WITH inserted_user AS (
|
||||
ON CONFLICT (username) DO NOTHING
|
||||
RETURNING username
|
||||
)
|
||||
SELECT 'text' AS component,
|
||||
COALESCE(
|
||||
'Welcome, ' || (SELECT username FROM inserted_user) || '! Your user account was successfully created. You can now [log in](sign%20in.sql).',
|
||||
'Sorry, this user name is already taken.'
|
||||
) AS contents_md;
|
||||
SELECT 'hero' AS component,
|
||||
'Welcome' AS title,
|
||||
'Welcome, ' || username || '! Your user account was successfully created. You can now log in.' AS description,
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Community_wp20.png/974px-Community_wp20.png' AS image,
|
||||
'sign in.sql' AS link,
|
||||
'Log in' AS link_text
|
||||
FROM inserted_user
|
||||
UNION ALL
|
||||
SELECT 'hero' AS component,
|
||||
'Sorry' AS title,
|
||||
'Sorry, this user name is already taken.' AS description_md,
|
||||
'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,
|
||||
'sign up.sql' AS link,
|
||||
'Try again' AS link_text
|
||||
WHERE NOT EXISTS (SELECT 1 FROM inserted_user);
|
||||
@@ -9,5 +9,5 @@ SELECT 'hero' AS component,
|
||||
'SQLPage Authentication Demo' AS title,
|
||||
'This application requires signing up to view the protected page.' AS description_md,
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Community_wp20.png/974px-Community_wp20.png' AS image,
|
||||
'protected_page.sql' AS link,
|
||||
'login_check.sql' AS link,
|
||||
'Access protected page' AS link_text;
|
||||
@@ -6,6 +6,5 @@ WHERE username = :username
|
||||
RETURNING 'cookie' AS component,
|
||||
'session' AS name,
|
||||
id AS value;
|
||||
|
||||
SELECT 'http_header' AS component,
|
||||
'protected_page.sql' AS location;
|
||||
'login_check.sql' AS location;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Checks if the login was successful, and redirects to the right page.
|
||||
SELECT 'http_header' AS component,
|
||||
CASE WHEN is_valid_session(sqlpage.cookie('session'))
|
||||
THEN 'protected_page.sql'
|
||||
ELSE 'sign in.sql'
|
||||
END AS location;
|
||||
@@ -0,0 +1,4 @@
|
||||
DELETE FROM login_session WHERE id = sqlpage.cookie('session');
|
||||
SELECT 'cookie' AS component, 'session' AS name, TRUE AS remove;
|
||||
|
||||
SELECT 'http_header' AS component, 'login.sql' AS location;
|
||||
@@ -1,8 +1,6 @@
|
||||
SELECT raise_error('Invalid credentials, please log in') WHERE NOT is_valid_session(sqlpage.cookie('session'));
|
||||
|
||||
SELECT 'shell' AS component, 'Protected page' AS title, 'lock' AS icon, '/' AS link, 'logout' AS menu_item;
|
||||
|
||||
SELECT 'text' AS component,
|
||||
'This content is [top secret](https://youtu.be/dQw4w9WgXcQ). You cannot view it if you are not connected.' AS contents_md;
|
||||
|
||||
SELECT EXISTS(SELECT 1 FROM login_session WHERE id=sqlpage.cookie('session')) AS contents;
|
||||
SELECT 'debug' AS component;
|
||||
SELECT * FROM login_session;
|
||||
SELECT sqlpage.cookie('session');
|
||||
'This content is [top secret](https://youtu.be/dQw4w9WgXcQ). You cannot view it if you are not connected.' AS contents_md;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 610 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -10,4 +10,19 @@ CREATE TABLE login_session (
|
||||
id TEXT PRIMARY KEY DEFAULT encode(gen_random_bytes(128), 'hex'),
|
||||
username TEXT NOT NULL REFERENCES user_info(username),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
);
|
||||
|
||||
|
||||
-- Returns true if the session is valid, false otherwise
|
||||
CREATE FUNCTION is_valid_session(user_session text) RETURNS boolean AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS(SELECT 1 FROM login_session WHERE id=user_session);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Takes a session id, does nothing if it is valid, throws an error otherwise.
|
||||
CREATE FUNCTION raise_error(error_message_text text) RETURNS void AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION '%', error_message_text;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
Reference in New Issue
Block a user