Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SQL-264] JWT and Routing Enhancement Suite #456

Open
wants to merge 74 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
670b8a2
Add new /admin/verify endpoint
kelvinqian00 Oct 29, 2024
39d6c15
Add tests for new endpoint (+ JWT corruption)
kelvinqian00 Oct 29, 2024
57bfee9
Add endpoint to documentation
kelvinqian00 Oct 29, 2024
e16d6e2
Add /admin/openapi to endpoint docs
kelvinqian00 Oct 29, 2024
d645cfc
Add /admin/status to endpoint docs
kelvinqian00 Oct 29, 2024
35cfbda
Bodyless -> No Content
kelvinqian00 Oct 29, 2024
55855cd
Add JWT blocklist table to SQLite
kelvinqian00 Oct 31, 2024
a682c5f
Add JWT blocklist table to Postgres
kelvinqian00 Oct 31, 2024
ce7ee63
Add and implement backend protocol
kelvinqian00 Oct 31, 2024
877ca9d
Fix typo in HugSQL code
kelvinqian00 Oct 31, 2024
01674d0
JWT op specs
kelvinqian00 Oct 31, 2024
02f3155
JWT op inputs
kelvinqian00 Oct 31, 2024
95e82d2
JWT ops
kelvinqian00 Oct 31, 2024
a1ce210
Implement AdminJWTManager protocol
kelvinqian00 Oct 31, 2024
0c37dc1
Add tests for JWT protocol functions
kelvinqian00 Oct 31, 2024
8cda065
Fix protocol tests
kelvinqian00 Nov 1, 2024
2e29d19
TEXT -> UUID in Postgres
kelvinqian00 Nov 1, 2024
d01f6fd
Surround all admin route tests with try-finally blocks
kelvinqian00 Nov 1, 2024
d58999d
Refactor JWT validation + add blocklist check
kelvinqian00 Nov 1, 2024
970732c
Implement loguout route + interceptors
kelvinqian00 Nov 1, 2024
5f04099
Add logout tests
kelvinqian00 Nov 1, 2024
db96757
Undo anomalous fn renaming
kelvinqian00 Nov 1, 2024
1655391
Add expiration to ctx jwt data
kelvinqian00 Nov 1, 2024
3d28d00
Add logout endpoint to docs
kelvinqian00 Nov 1, 2024
170680f
Add comment
kelvinqian00 Nov 1, 2024
f78b912
Add second account to test blocking expired JWTs
kelvinqian00 Nov 1, 2024
b05e1ab
Add new renew endpoint
kelvinqian00 Nov 1, 2024
912797d
Add ult property to JWTs
kelvinqian00 Nov 1, 2024
c4f5582
Add LRSQL_JWT_ULTIMATE_EXP_TIME var and pass it to admin routes
kelvinqian00 Nov 1, 2024
2625a6a
Add separate jwt creation fn that reuses ult exp time
kelvinqian00 Nov 1, 2024
2af38c8
Add renew-admin-jwt interceptor
kelvinqian00 Nov 1, 2024
b72e933
Add jwt-expiry test block
kelvinqian00 Nov 1, 2024
10fbe36
Add to docs
kelvinqian00 Nov 1, 2024
ab7894e
Apply leeway to JWT blocklist calculations
kelvinqian00 Nov 4, 2024
81c52d1
Merge branch 'admin-logout-endpoint' into admin-jwt-renew-endpoint
kelvinqian00 Nov 4, 2024
aa8d44e
Add jwt-ultimate-exp-time to config spec
kelvinqian00 Nov 4, 2024
40d3911
Change "ultimate exp" to "refresh exp"
kelvinqian00 Nov 4, 2024
71163c3
Redo schema by directly storing JWTs
kelvinqian00 Nov 4, 2024
d53744d
Fix admin JWT protocol test
kelvinqian00 Nov 4, 2024
68d2c50
Make blocklist purging a separate protocol fn
kelvinqian00 Nov 4, 2024
7bb60a4
Handle JWT conflict errors
kelvinqian00 Nov 4, 2024
c37066d
Re-apply leeway to JWT eviction calcs
kelvinqian00 Nov 4, 2024
00b7510
Add jwt-exp-time to admin UI route response
kelvinqian00 Nov 5, 2024
d08242a
Merge branch 'main' into admin-jwt-renew-endpoint
kelvinqian00 Nov 11, 2024
7a2c874
Add jwtRefreshInterval and jwtInteractionWindow config vars
kelvinqian00 Nov 11, 2024
a5ee968
Remove jwt-exp-time from get-env return
kelvinqian00 Nov 11, 2024
62d832b
Merge branch 'main' into admin-jwt-renew-endpoint
kelvinqian00 Nov 11, 2024
b8247b4
Pass new JWT config to interceptor
kelvinqian00 Nov 11, 2024
4e40b17
Change config vars to less than or equal to
kelvinqian00 Nov 11, 2024
06f8b07
Merge branch 'main' into admin-logged-in-endpoint
kelvinqian00 Nov 11, 2024
12e18f8
Merge branch 'main' into admin-logout-endpoint
kelvinqian00 Nov 11, 2024
c8486cd
Merge branch 'admin-logout-endpoint' into admin-jwt-renew-endpoint
kelvinqian00 Nov 11, 2024
ab80835
Make route tests pass with new config numbers
kelvinqian00 Nov 11, 2024
b206ae5
Merge branch 'admin-logged-in-endpoint' into admin-logout-endpoint
kelvinqian00 Nov 11, 2024
8d5080a
Merge branch 'admin-logout-endpoint' into admin-jwt-renew-endpoint
kelvinqian00 Nov 11, 2024
07b912d
Use TIMESTAMP WITH TIME ZONE for evict_time
kelvinqian00 Nov 13, 2024
1b8a96f
Only purge blocklist on logout
kelvinqian00 Nov 13, 2024
b115a41
Fail silently upon double logout
kelvinqian00 Nov 13, 2024
ec85d87
Implement get-spa and use that for /admin
kelvinqian00 Nov 15, 2024
29c3922
Add /ui for UI routes
kelvinqian00 Nov 15, 2024
c4959bd
Remove proxy-path from get-spa
kelvinqian00 Nov 21, 2024
740f803
Use Selmer templates to inject proxy path prefix
kelvinqian00 Nov 21, 2024
7046115
Add a testing checklist in the dev docs
kelvinqian00 Nov 22, 2024
839aee7
Shorten list
kelvinqian00 Nov 27, 2024
1dbe44a
Grammar properly in the first sentence
kelvinqian00 Nov 27, 2024
52a0c7e
Add url-prefix restrictions to config spec
kelvinqian00 Dec 17, 2024
99d02f1
Mention changes in docs
kelvinqian00 Dec 17, 2024
008120c
Merge pull request #433 from yetanalytics/admin-logged-in-endpoint
kelvinqian00 Jan 14, 2025
779fff8
Merge pull request #434 from yetanalytics/admin-logout-endpoint
kelvinqian00 Jan 14, 2025
feebfb4
Merge branch 'jwt-enhancement-epic' into admin-jwt-renew-endpoint
kelvinqian00 Jan 14, 2025
74a4dd4
Merge pull request #435 from yetanalytics/admin-jwt-renew-endpoint
kelvinqian00 Jan 14, 2025
a20e679
Merge pull request #439 from yetanalytics/admin-re-route
kelvinqian00 Jan 14, 2025
5dde8e7
Merge pull request #441 from yetanalytics/dev-checklist
kelvinqian00 Jan 14, 2025
27e4ac4
Merge pull request #449 from yetanalytics/ban-admin-prefix
kelvinqian00 Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

The SQL LRS is a Clojure Web Application built on the Pedestal Framework.

### Testing

Development is primarily test-driven, which uses an exhaustive suite of unit tests. To run them locally, run a `make test-[database]` command. In addition, all tests are run for all versions in GitHub Actions CI.

However, in some situations, such as UI development, relying on the unit tests may be inadequate. In these cases, in addition to performing visual tests on the UI, one may need to test these specific scenarios:
- Login with OIDC ([demo](oidc.md#keycloak-demo))
- Proxy paths ([demo](other_demos.md#proxied-lrs-demo))
- JWT override ([JWT config vars](env_vars.md#jwt-config))

### Build

The SQL LRS can be built or run with the following Makefile targets. They can be executed with `make [target]`.
Expand Down
5 changes: 5 additions & 0 deletions doc/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ The following examples use `http://example.org` as the URL body. All methods ret
* `password` must contain at least one of the following special characters: `!@#$%^&*_-+=?`.

The response body contains a newly generated JSON Web Token (JWT) on success. A `401 UNAUTHORIZED` status code is returned if the credentials are incorrect.
- `POST http://example.org/admin/account/logout`: Log out of the current account. This will revoke any unexpired JWTs associated with the user. (NOTE: This endpoint will return a `400 BAD REQUEST` error if `LRSQL_JWT_NO_VAL` is set to `true`.)
- `GET http://example.org/admin/account/renew`: Renew the current account's login session by issuing a new JWT. For a given JWT, the renewal is only granted if the current time is less than the `ref` timestamp (which is determined by `LRSQL_JWT_REFRESH_EXP_TIME`).
- `POST http://example.org/admin/account/create`: Create a new admin account. The request body must be a JSON object that contains `username` and `password` strings. The endpoint returns a JSON object with the ID (UUID) of the newly created user on success, and returns a `409 CONFLICT` if the account already exists.
- `DELETE http://example.org/admin/account`: Delete an existing account. The JSON request body must contain a UUID `account-id` value. The endpoint returns a JSON object with the ID of the deleted account on success and returns a `404 NOT FOUND` error if the account does not exist.
- `GET http://example.org/admin/account`: Return an array of all admin accounts in the system on success.
- `GET http://example.org/admin/me`: Returns the currently authenticated admin accounts on success.
- `GET http://example.org/admin/verify`: Returns a `204 No Content` response, without a body, on success (the success conditions are the same as the `/admin/me` endpoint).

#### Admin Credential Routes

Expand All @@ -42,6 +45,8 @@ The response body contains a newly generated JSON Web Token (JWT) on success. A
#### Misc Admin Routes

- `GET http://example.org/admin/env`: Get select environment variables about the configuration which may aid in client-side operations.
- `GET http://example.org/admin/openapi`: Get an OpenAPI JSON spec of the endpoint API, which can then be visualized using an OpenAPI viewer like Swagger.
- `GET http://example.org/admin/status`: Get LRS status information, such as the number of statements in the LRS.
- `DELETE http://example.org/admin/agents`: Runs a *hard delete* of all records of an actor, and associated records (statements, attachments, etc). Intended for privacy purposes like GDPR. Body should be a JSON object of form `{"actor-ifi":<actor-ifi>}`. Disabled unless the configuration variable enableAdminDeleteActor to be set to `true`.

### Reaction Management Routes
Expand Down
7 changes: 5 additions & 2 deletions doc/env_vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ _NOTE:_ `LRSQL_STMT_RETRY_LIMIT` and `LRSQL_STMT_RETRY_BUDGET` are used to mitig
| `LRSQL_HTTP_HOST` | `httpHost` | The host that the webserver will run on. | `0.0.0.0` |
| `LRSQL_HTTP_PORT` | `httpPort` | The HTTP port that the webserver will be open on. | `8080` |
| `LRSQL_SSL_PORT` | `sslPort` | The HTTPS port that the webserver will be open on. | `8443` |
| `LRSQL_URL_PREFIX` | `urlPrefix` | The prefix of the webserver URL path, e.g. the prefix in `http://0.0.0.0:8080/xapi` is `/xapi`. Used when constructing the `more` value for multi-statement queries. *(Note: Only applies to LRS xapi endpoints, not admin/ui endpoints)* | `/xapi` |
| `LRSQL_URL_PREFIX` | `urlPrefix` | The prefix of the webserver URL path, e.g. the prefix in `http://0.0.0.0:8080/xapi` is `/xapi`. Used when constructing the `more` value for multi-statement queries. Cannot start with `/admin`. *(Note: Only applies to LRS xapi endpoints, not admin/ui endpoints)* | `/xapi` |
| `LRSQL_PROXY_PATH` | `proxyPath` | This path modification is exclusively for use with a proxy, such as apache or nginx or a load balancer, where a path is added to prefix the entire application (such as `https://www.mysystem.com/mylrs/xapi/statements`). This does not actually change the routes of the application, it informs the admin frontend where to look for the server endpoints based on the proxied setup, and thus must be used in conjunction with a third party proxy. If used, the value must start with a leading `/` but not end with one (e.g. `/mylrs` is valid, as is `/mylrs/b` but `/mylrs/` is not). Use with caution. | Not Set |

#### TLS/SSL Certificate
Expand All @@ -141,8 +141,11 @@ _NOTE:_ `LRSQL_STMT_RETRY_LIMIT` and `LRSQL_STMT_RETRY_BUDGET` are used to mitig

| Env Var | Config | Description | Default |
| --------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `LRSQL_JWT_EXP_TIME` | `jwtExpTime` | The amount of time, in seconds, after a JWT is created when it expires. Since JWTs are not revocable, **this this time should be short** (i.e. one hour or less). | `3600` (one hour) |
| `LRSQL_JWT_EXP_TIME` | `jwtExpTime` | The amount of time, in seconds, after a JWT is created when it expires. **This this time should be short** (i.e. one hour or less). | `3600` (one hour) |
| `LRSQL_JWT_EXP_LEEWAY` | `jwtExpLeeway` | The amount of time, in seconds, before or after the expiration instant when a JWT should still count as un-expired. Used to compensate for clock desync. Applied to both LRS and OIDC tokens. | `1` (one second) |
| `LRSQL_JWT_REFRESH_EXP_TIME` | `jwtRefreshExpTime` | The amount of time, in seconds, after a JWT is issued upon an initial login (_not_ a login renewal), after which login renewal can no longer be performed. Note: this is unaffected by expiration leeway. | `86400` (one day) |
| `LRSQL_JWT_REFRESH_INTERVAL` | `jwtRefreshInterval` | The amount of time that the client should poll the server in order to refresh the JWT. This **must** be less than `LRSQL_JWT_EXP_TIME`. | `3540` (59 minutes) |
| `LRSQL_JWT_INTERACTION_WINDOW` | `jwtInteractionWindow` | The amount of time before a potential JWT refresh that the client checks for interaction. This **must** be less than or equal to `LRSQL_JWT_REFRESH_INTERVAL`. | `600` (10 minutes) |
| `LRSQL_JWT_NO_VAL` | `jwtNoVal` | (**DANGEROUS!**) This flag removes JWT Token Validation and simply accepts token claims as configured by the associated variables below. It is extremely unlikely that you need this as it is for very specific proxy-overwrite authentication scenarios, and it poses a serious threat to system security if enabled. | `false` |
| `LRSQL_JWT_NO_VAL_UNAME` | `jwtNoValUname` | (**DANGEROUS!** See `LRSQL_JWT_NO_VAL`) This variable configures which claim key to use for the username when token validation is turned off. | Not Set |
| `LRSQL_JWT_NO_VAL_ISSUER` | `jwtNoValIssuer` | (**DANGEROUS!** See `LRSQL_JWT_NO_VAL`) This variable configures which claim key to use for the issuer when token validation is turned off. | Not Set |
Expand Down
3 changes: 3 additions & 0 deletions resources/lrsql/config/prod/default/webserver.edn
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
:key-enable-selfie #boolean #or [#env LRSQL_KEY_ENABLE_SELFIE true]
:jwt-exp-time #long #or [#env LRSQL_JWT_EXP_TIME 3600]
:jwt-exp-leeway #long #or [#env LRSQL_JWT_EXP_LEEWAY 1]
:jwt-refresh-exp-time #long #or [#env LRSQL_JWT_REFRESH_EXP_TIME 86400]
:jwt-refresh-interval #long #or [#env LRSQL_JWT_REFRESH_INTERVAL 3540]
:jwt-interaction-window #long #or [#env LRSQL_JWT_INTERACTION_WINDOW 600]
:jwt-no-val #boolean #or [#env LRSQL_JWT_NO_VAL false]
:jwt-no-val-uname #or [#env LRSQL_JWT_NO_VAL_UNAME nil]
:jwt-no-val-issuer #or [#env LRSQL_JWT_NO_VAL_ISSUER nil]
Expand Down
3 changes: 3 additions & 0 deletions resources/lrsql/config/test/default/webserver.edn
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
:key-enable-selfie true
:jwt-exp-time 3600
:jwt-exp-leeway 1
:jwt-refresh-exp-time 86400
:jwt-refresh-interval 3540
:jwt-interaction-window 600
:jwt-no-val false
:jwt-no-val-uname nil
:jwt-no-val-issuer nil
Expand Down
11 changes: 10 additions & 1 deletion src/db/postgres/lrsql/postgres/record.clj
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
(when (nil? (check-statement-to-actor-cascading-delete tx))
(add-statement-to-actor-cascading-delete! tx))
(when (some? (query-varchar-exists tx))
(convert-varchars-to-text! tx)))
(convert-varchars-to-text! tx))
(create-blocked-jwt-table! tx))

bp/BackendUtil
(-txn-retry? [_ ex]
Expand Down Expand Up @@ -197,6 +198,14 @@
(-query-account-count-local [_ tx]
(query-account-count-local tx))

bp/JWTBlocklistBackend
(-insert-blocked-jwt! [_ tx input]
(insert-blocked-jwt! tx input))
(-delete-blocked-jwt-by-time! [_ tx input]
(delete-blocked-jwt-by-time! tx input))
(-query-blocked-jwt [_ tx input]
(query-blocked-jwt-exists tx input))

bp/CredentialBackend
(-insert-credential! [_ tx input]
(insert-credential! tx input))
Expand Down
11 changes: 11 additions & 0 deletions src/db/postgres/lrsql/postgres/sql/ddl.sql
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,14 @@ ALTER TABLE lrs_credential ALTER COLUMN secret_key TYPE TEXT;
ALTER TABLE credential_to_scope ALTER COLUMN api_key TYPE TEXT;
ALTER TABLE credential_to_scope ALTER COLUMN secret_key TYPE TEXT;
ALTER TABLE reaction ALTER COLUMN title TYPE TEXT;

/* Migration 2024-10-31 - Add JWT Blocklist Table */

-- :name create-blocked-jwt-table!
-- :command :execute
-- :doc Create the `blocked_jwt` table and associated indexes if they do not exist yet.
CREATE TABLE IF NOT EXISTS blocked_jwt (
jwt TEXT PRIMARY KEY,
evict_time TIMESTAMP WITH TIME ZONE
);
CREATE INDEX IF NOT EXISTS blocked_jwt_evict_time_idx ON blocked_jwt(evict_time);
9 changes: 9 additions & 0 deletions src/db/postgres/lrsql/postgres/sql/delete.sql
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,12 @@ DELETE FROM state_document WHERE agent_ifi = :actor-ifi;

DELETE FROM actor WHERE actor_ifi = :actor-ifi;
------------------end delete-actor--------------------

/* JWT Blocklist */

-- :name delete-blocked-jwt-by-time!
-- :command :execute
-- :result :affected
-- :doc Delete all blocked JWTs where `:current-time` is past the eviction time.
DELETE FROM blocked_jwt
WHERE evict_time <= :current-time;
12 changes: 12 additions & 0 deletions src/db/postgres/lrsql/postgres/sql/insert.sql
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,15 @@ INSERT INTO reaction (
) VALUES (
:primary-key, :title, :ruleset, :active, :created, :modified
);

/* JWT Blocklist */

-- :name insert-blocked-jwt!
-- :command :insert
-- :result :affected
-- :doc Insert a `:jwt` and a `:eviction-time` into the blocklist.
INSERT INTO blocked_jwt (
jwt, evict_time
) VALUES (
:jwt, :eviction-time
);
9 changes: 9 additions & 0 deletions src/db/postgres/lrsql/postgres/sql/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,12 @@ WITH RECURSIVE trigger_history (statement_id, reaction_id, trigger_id) AS (
SELECT reaction_id
FROM trigger_history
WHERE reaction_id IS NOT NULL;

/* JWT Blocklist */

-- :name query-blocked-jwt-exists
-- :command :query
-- :result :one
-- :doc Query that `:jwt` is in the blocklist.
SELECT 1 FROM blocked_jwt
WHERE jwt = :jwt;
10 changes: 10 additions & 0 deletions src/db/sqlite/lrsql/sqlite/record.clj
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@
(xapi-statement-add-trigger-id! tx))
(when-not (some? (query-statement-to-actor-has-cascade-delete tx))
(update-schema-simple! tx alter-statement-to-actor-add-cascade-delete!))
(create-blocked-jwt-table! tx)
(create-blocked-jwt-evict-time-idx! tx)
(log/infof "sqlite schema_version: %d"
(:schema_version (query-schema-version tx))))

Expand Down Expand Up @@ -237,6 +239,14 @@
(-query-account-count-local [_ tx]
(query-account-count-local tx))

bp/JWTBlocklistBackend
(-insert-blocked-jwt! [_ tx input]
(insert-blocked-jwt! tx input))
(-delete-blocked-jwt-by-time! [_ tx input]
(delete-blocked-jwt-by-time! tx input))
(-query-blocked-jwt [_ tx input]
(query-blocked-jwt-exists tx input))

bp/CredentialBackend
(-insert-credential! [_ tx input]
(insert-credential! tx input))
Expand Down
18 changes: 16 additions & 2 deletions src/db/sqlite/lrsql/sqlite/sql/ddl.sql
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,6 @@ CREATE INDEX IF NOT EXISTS sts_ancestor_id_idx ON statement_to_statement(ancesto
-- :doc Create an index on the `statement_to_statement.descendant_id` column.
CREATE INDEX IF NOT EXISTS sts_descendant_id_idx ON statement_to_statement(descendant_id)



/* Document Tables */

-- :name create-state-document-table!
Expand Down Expand Up @@ -524,3 +522,19 @@ SET sql = 'CREATE TABLE statement_to_actor (
FOREIGN KEY (actor_ifi, actor_type) REFERENCES actor(actor_ifi, actor_type)
)'
WHERE type = 'table' AND name = 'statement_to_actor'
;

/* Migration 2024-10-31 - Add JWT Blocklist Table */

-- :name create-blocked-jwt-table!
-- :command :execute
-- :doc Create the `blocked_jwt` table if it does not exist yet.
CREATE TABLE IF NOT EXISTS blocked_jwt (
jwt TEXT PRIMARY KEY,
evict_time TIMESTAMP
);

-- :name create-blocked-jwt-evict-time-idx!
-- :command :execute
-- :doc Create the `blocked_jwt_evict_time_idx` table if it does not exist yet.
CREATE INDEX IF NOT EXISTS blocked_jwt_evict_time_idx ON blocked_jwt(evict_time);
9 changes: 9 additions & 0 deletions src/db/sqlite/lrsql/sqlite/sql/delete.sql
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,12 @@ DELETE FROM state_document WHERE agent_ifi = :actor-ifi
-- :command :execute
-- :result :affected
DELETE FROM actor WHERE actor_ifi = :actor-ifi

/* JWT Blocklist */

-- :name delete-blocked-jwt-by-time!
-- :command :execute
-- :result :affected
-- :doc Delete all blocked JWTs where `:current-time` is past the eviction time.
DELETE FROM blocked_jwt
WHERE evict_time <= :current-time;
12 changes: 12 additions & 0 deletions src/db/sqlite/lrsql/sqlite/sql/insert.sql
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,15 @@ INSERT INTO reaction (
) VALUES (
:primary-key, :title, :ruleset, :active, :created, :modified
);

/* JWT Blocklist */

-- :name insert-blocked-jwt!
-- :command :insert
-- :result :affected
-- :doc Insert a `:jwt` and a `:eviction-time` into the blocklist.
INSERT INTO blocked_jwt (
jwt, evict_time
) VALUES (
:jwt, :eviction-time
);
9 changes: 9 additions & 0 deletions src/db/sqlite/lrsql/sqlite/sql/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,12 @@ WITH RECURSIVE trigger_history (statement_id, reaction_id, trigger_id) AS (
SELECT reaction_id
FROM trigger_history
WHERE reaction_id IS NOT NULL;

/* JWT Blocklist */

-- :name query-blocked-jwt-exists
-- :command :query
-- :result :one
-- :doc Query that `:jwt` is in the blocklist.
SELECT 1 FROM blocked_jwt
WHERE jwt = :jwt
68 changes: 65 additions & 3 deletions src/main/lrsql/admin/interceptors/account.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns lrsql.admin.interceptors.account
(:require [clojure.spec.alpha :as s]
[java-time.api :as jt]
[io.pedestal.interceptor :refer [interceptor]]
[io.pedestal.interceptor.chain :as chain]
[lrsql.admin.protocol :as adp]
Expand Down Expand Up @@ -250,19 +251,80 @@
:response
{:status 200 :body result})))}))

(def no-content
"Return a 204 No Content response, without a body."
(interceptor
{:name ::get-no-content
:enter
(fn get-account [ctx]
(assoc ctx :response {:status 204}))}))

;; JWT interceptors for admin

(defn generate-jwt
"Upon account login, generate a new JSON web token."
[secret exp]
[secret exp ref leeway]
(interceptor
{:name ::generate-jwt
:enter
(fn generate-jwt [ctx]
(let [{{:keys [account-id]} ::data}
(let [{lrs :com.yetanalytics/lrs
{:keys [account-id]} ::data}
ctx
json-web-token
(admin-u/account-id->jwt account-id secret exp)]
(admin-u/account-id->jwt account-id secret exp ref)]
(adp/-purge-blocklist lrs leeway) ; Update blocklist upon login
(assoc ctx
:response
{:status 200
:body {:account-id account-id
:json-web-token json-web-token}})))}))

(defn renew-admin-jwt
[secret exp]
(interceptor
{:name ::renew-jwt
:enter
(fn renew-jwt [ctx]
(let [{{:keys [account-id refresh-exp]} ::jwt/data} ctx
curr-time (u/current-time)]
(if (jt/before? curr-time refresh-exp)
(let [json-web-token (admin-u/account-id->jwt* account-id
secret
exp
refresh-exp)]
(assoc ctx
:response
{:status 200
:body {:account-id account-id
:json-web-token json-web-token}}))
(assoc (chain/terminate ctx)
:response
{:status 401
:body {:error "Attempting JWT login after refresh expiry."}}))))}))

(def ^:private block-admin-jwt-error-msg
"This operation is unsupported when `LRSQL_JWT_NO_VAL` is set to `true`.")

(defn block-admin-jwt
"Add the current JWT to the blocklist. Return an error if we are in
no-val mode."
[exp leeway no-val?]
(interceptor
{:name ::add-jwt-to-blocklist
:enter
(fn add-jwt-to-blocklist [ctx]
(if-not no-val?
(let [{lrs :com.yetanalytics/lrs
{:keys [jwt account-id]} :lrsql.admin.interceptors.jwt/data}
ctx]
(adp/-purge-blocklist lrs leeway) ; Update blocklist upon logout
(adp/-block-jwt lrs jwt exp)
(assoc (chain/terminate ctx)
:response
{:status 200
:body {:account-id account-id}}))
(assoc (chain/terminate ctx)
:response
{:status 400
:body {:error block-admin-jwt-error-msg}})))}))
Loading
Loading