Skip to content

Commit

Permalink
Merge branch 'admin-logout-endpoint' into admin-jwt-renew-endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
kelvinqian00 committed Nov 11, 2024
2 parents 4e40b17 + 12e18f8 commit c8486cd
Show file tree
Hide file tree
Showing 21 changed files with 139 additions and 195 deletions.
2 changes: 0 additions & 2 deletions src/db/postgres/lrsql/postgres/record.clj
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,6 @@
bp/JWTBlocklistBackend
(-insert-blocked-jwt! [_ tx input]
(insert-blocked-jwt! tx input))
(-delete-blocked-jwt-by-account! [_ tx input]
(delete-blocked-jwt-by-account! tx input))
(-delete-blocked-jwt-by-time! [_ tx input]
(delete-blocked-jwt-by-time! tx input))
(-query-blocked-jwt [_ tx input]
Expand Down
8 changes: 3 additions & 5 deletions src/db/postgres/lrsql/postgres/sql/ddl.sql
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,7 @@ ALTER TABLE reaction ALTER COLUMN title TYPE TEXT;
-- :command :execute
-- :doc Create the `blocked_jwt` table and associated indexes if they do not exist yet.
CREATE TABLE IF NOT EXISTS blocked_jwt (
account_id UUID NOT NULL REFERENCES admin_account(id) ON DELETE CASCADE,
expiration TIMESTAMP,
PRIMARY KEY (account_id, expiration)
jwt TEXT PRIMARY KEY,
evict_time TIMESTAMP
);
CREATE INDEX IF NOT EXISTS blocked_jwt_account_id_idx ON blocked_jwt(account_id);
CREATE INDEX IF NOT EXISTS blocked_jwt_expiration_idx ON blocked_jwt(expiration);
CREATE INDEX IF NOT EXISTS blocked_jwt_evict_time_idx ON blocked_jwt(evict_time);
11 changes: 2 additions & 9 deletions src/db/postgres/lrsql/postgres/sql/delete.sql
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,9 @@ DELETE FROM actor WHERE actor_ifi = :actor-ifi;

/* JWT Blocklist */

-- :name delete-blocked-jwt-by-account!
-- :command :execute
-- :result :affected
-- :doc Delete all blocked JWTs associated with a given `:account-id`.
DELETE FROM blocked_jwt
WHERE account_id = :account-id;

-- :name delete-blocked-jwt-by-time!
-- :command :execute
-- :result :affected
-- :doc Delete all blocked JWTs where `:current-time` is past the expiration time.
-- :doc Delete all blocked JWTs where `:current-time` is past the eviction time.
DELETE FROM blocked_jwt
WHERE expiration <= :current-time;
WHERE evict_time <= :current-time;
6 changes: 3 additions & 3 deletions src/db/postgres/lrsql/postgres/sql/insert.sql
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ INSERT INTO reaction (
-- :name insert-blocked-jwt!
-- :command :insert
-- :result :affected
-- :doc Given a JWT-extracted `:account-id` and `:expiration`, insert a new blocked JWT.
-- :doc Insert a `:jwt` and a `:eviction-time` into the blocklist.
INSERT INTO blocked_jwt (
account_id, expiration
jwt, evict_time
) VALUES (
:account-id, :expiration
:jwt, :eviction-time
);
5 changes: 2 additions & 3 deletions src/db/postgres/lrsql/postgres/sql/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,6 @@ WHERE reaction_id IS NOT NULL;
-- :name query-blocked-jwt-exists
-- :command :query
-- :result :one
-- :doc Query that at least one blocked JWT with `:username` and whose expiration is after `:current-time` exists.
-- :doc Query that `:jwt` is in the blocklist.
SELECT 1 FROM blocked_jwt
WHERE account_id = :account-id
AND :current-time < expiration;
WHERE jwt = :jwt;
7 changes: 2 additions & 5 deletions src/db/sqlite/lrsql/sqlite/record.clj
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@
(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-account-id-idx! tx)
(create-blocked-jwt-expiration-idx! tx)
(create-blocked-jwt-evict-time-idx! tx)
(log/infof "sqlite schema_version: %d"
(:schema_version (query-schema-version tx))))

Expand Down Expand Up @@ -243,13 +242,11 @@
bp/JWTBlocklistBackend
(-insert-blocked-jwt! [_ tx input]
(insert-blocked-jwt! tx input))
(-delete-blocked-jwt-by-account! [_ tx input]
(delete-blocked-jwt-by-account! 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
16 changes: 5 additions & 11 deletions src/db/sqlite/lrsql/sqlite/sql/ddl.sql
Original file line number Diff line number Diff line change
Expand Up @@ -530,17 +530,11 @@ WHERE type = 'table' AND name = 'statement_to_actor'
-- :command :execute
-- :doc Create the `blocked_jwt` table if it does not exist yet.
CREATE TABLE IF NOT EXISTS blocked_jwt (
account_id TEXT NOT NULL REFERENCES admin_account(id) ON DELETE CASCADE,
expiration TIMESTAMP,
PRIMARY KEY (account_id, expiration)
jwt TEXT PRIMARY KEY,
evict_time TIMESTAMP
);

-- :name create-blocked-jwt-account-id-idx!
-- :name create-blocked-jwt-evict-time-idx!
-- :command :execute
-- :doc Create the `blocked_jwt_account_id_idx` table if it does not exist yet.
CREATE INDEX IF NOT EXISTS blocked_jwt_account_id_idx ON blocked_jwt(account_id);

-- :name create-blocked-jwt-expiration-idx!
-- :command :execute
-- :doc Create the `blocked_jwt_expiration_idx` table if it does not exist yet.
CREATE INDEX IF NOT EXISTS blocked_jwt_expiration_idx ON blocked_jwt(expiration);
-- :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);
11 changes: 2 additions & 9 deletions src/db/sqlite/lrsql/sqlite/sql/delete.sql
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,9 @@ DELETE FROM actor WHERE actor_ifi = :actor-ifi

/* JWT Blocklist */

-- :name delete-blocked-jwt-by-account!
-- :command :execute
-- :result :affected
-- :doc Delete all blocked JWTs associated with a given `:account-id`.
DELETE FROM blocked_jwt
WHERE account_id = :account-id;

-- :name delete-blocked-jwt-by-time!
-- :command :execute
-- :result :affected
-- :doc Delete all blocked JWTs where `:current-time` is past the expiration time.
-- :doc Delete all blocked JWTs where `:current-time` is past the eviction time.
DELETE FROM blocked_jwt
WHERE expiration <= :current-time;
WHERE evict_time <= :current-time;
6 changes: 3 additions & 3 deletions src/db/sqlite/lrsql/sqlite/sql/insert.sql
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ INSERT INTO reaction (
-- :name insert-blocked-jwt!
-- :command :insert
-- :result :affected
-- :doc Given a JWT-extracted `:account-id` and `:expiration`, insert a new blocked JWT.
-- :doc Insert a `:jwt` and a `:eviction-time` into the blocklist.
INSERT INTO blocked_jwt (
account_id, expiration
jwt, evict_time
) VALUES (
:account-id, :expiration
:jwt, :eviction-time
);
5 changes: 2 additions & 3 deletions src/db/sqlite/lrsql/sqlite/sql/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,6 @@ WHERE reaction_id IS NOT NULL;
-- :name query-blocked-jwt-exists
-- :command :query
-- :result :one
-- :doc Query that at least one blocked JWT with `:account_id` and whose expiration is after `:current-time` exists.
-- :doc Query that `:jwt` is in the blocklist.
SELECT 1 FROM blocked_jwt
WHERE account_id = :account-id
AND :current-time < expiration;
WHERE jwt = :jwt
39 changes: 17 additions & 22 deletions src/main/lrsql/admin/interceptors/account.clj
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,6 @@
{:status 401
:body {:error "Invalid Account Credentials"}}))))}))

(defn unblock-admin-jwts
"Remove all JWTs associated with the user account from the blocklist."
[leeway]
(interceptor
{:name ::remove-jwt-from-blocklist
:enter
(fn remove-jwt-from-blocklist [ctx]
(let [{lrs :com.yetanalytics/lrs
{:keys [account-id]} ::data} ctx]
(adp/-unblock-jwts lrs account-id leeway)
ctx))}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Terminal Interceptors
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down Expand Up @@ -267,15 +255,17 @@

(defn generate-jwt
"Upon account login, generate a new JSON web token."
[secret exp ref]
[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 ref)]
(adp/-purge-blocklist lrs leeway) ; Update blocklist upon login
(assoc ctx
:response
{:status 200
Expand Down Expand Up @@ -311,21 +301,26 @@
(defn block-admin-jwt
"Add the current JWT to the blocklist. Return an error if we are in
no-val mode."
[leeway no-val?]
[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 [account-id expiration]}
:lrsql.admin.interceptors.jwt/data}
{:keys [jwt account-id]} :lrsql.admin.interceptors.jwt/data}
ctx]
(adp/-block-jwt lrs account-id expiration leeway)
(assoc (chain/terminate ctx)
:response
{:status 200
:body {:account-id account-id}}))
(adp/-purge-blocklist lrs leeway) ; Update blocklist upon logout
(let [result (adp/-block-jwt lrs jwt exp)]
(if-not (contains? result :error)
(assoc (chain/terminate ctx)
:response
{:status 200
:body {:account-id account-id}})
(assoc (chain/terminate ctx)
:response
{:status 409
:body {:error "JWT has already been revoked."}}))))
(assoc (chain/terminate ctx)
:response
{:status 400
Expand Down
9 changes: 4 additions & 5 deletions src/main/lrsql/admin/interceptors/jwt.clj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
(let [{result* :result} (adp/-ensure-account-oidc lrs username issuer)]
(if (keyword? result*)
result*
(assoc result :account-id result*)))))
(assoc result :account-id result* :jwt token)))))
(catch Exception ex
;; We want any error here to return a 401, but we log
(log/warnf ex "No-val JWT Error: %s" (ex-message ex))
Expand All @@ -34,12 +34,11 @@
"Normal JWT, normal signature verification and blocklist check."
[lrs token secret leeway]
(try
(let [{:keys [account-id] :as result}
(admin-u/jwt->payload token secret leeway)]
(let [result (admin-u/jwt->payload token secret leeway)]
(if (keyword? result)
result
(if-not (adp/-jwt-blocked? lrs account-id leeway)
result
(if-not (adp/-jwt-blocked? lrs token)
(assoc result :jwt token)
:lrsql.admin/unauthorized-token-error)))
(catch Exception ex
;; We want any error here to return a 401, but we log
Expand Down
12 changes: 6 additions & 6 deletions src/main/lrsql/admin/protocol.clj
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
"Update the password for an admin account given old and new passwords."))

(defprotocol AdminJWTManager
(-block-jwt [this account-id expiration leeway]
"Block the JWT with this account, expiration time, and leeway. Purge expired JWTs from the blocklist if necessary.")
(-unblock-jwts [this account-id leeway]
"Unblock the JWTs associated with this account. Purge expired JWTs from the blocklist if necessary.")
(-jwt-blocked? [this account-id leeway]
"Is an unexpired JWT with this account on the blocklist?"))
(-purge-blocklist [this leeway]
"Purge the blocklist of any JWTs that have expired since they were added.")
(-block-jwt [this jwt expiration]
"Block `jwt` and apply an associated `expiration` number of seconds. Returns an error if `jwt` is already in the blocklist.")
(-jwt-blocked? [this jwt]
"Is `jwt` on the blocklist?"))

(defprotocol APIKeyManager
(-create-api-keys [this account-id scopes]
Expand Down
7 changes: 4 additions & 3 deletions src/main/lrsql/admin/routes.clj
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
(ai/validate-params
:strict? false)
ai/authenticate-admin
(ai/unblock-admin-jwts jwt-leeway)
(ai/generate-jwt jwt-secret jwt-exp jwt-ref))
(ai/generate-jwt
jwt-secret jwt-exp jwt-ref jwt-leeway))
:route-name :lrsql.admin.account/login]
{:description "Log into an existing account"
:requestBody (g/request (gs/o {:username :t#string
Expand All @@ -58,7 +58,8 @@
(ji/validate-jwt
jwt-secret jwt-leeway no-val-opts)
ji/validate-jwt-account
(ai/block-admin-jwt jwt-leeway no-val?))
(ai/block-admin-jwt
jwt-exp jwt-leeway no-val?))
:route-name :lrsql.admin.account/logout]
{:description "Log out of this account"
:operationId :logout
Expand Down
1 change: 0 additions & 1 deletion src/main/lrsql/backend/protocol.clj
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@
(defprotocol JWTBlocklistBackend
;; Commands
(-insert-blocked-jwt! [this tx input])
(-delete-blocked-jwt-by-account! [this tx input])
(-delete-blocked-jwt-by-time! [this tx input])
;; Queries
(-query-blocked-jwt [this tx input]))
Expand Down
48 changes: 23 additions & 25 deletions src/main/lrsql/input/admin/jwt.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
[lrsql.spec.admin.jwt :as jwts]
[lrsql.util :as u]))

(defn- eviction-time
"Generate the current time, offset by `exp` number of seconds later."
[exp]
(-> (u/current-time)
(u/offset-time exp :seconds)))

(defn- current-time
"Generate the current time, offset by `leeway` number of seconds earlier.
Expand All @@ -12,35 +18,27 @@
(u/offset-time (* -1 leeway) :seconds)))

(s/fdef query-blocked-jwt-input
:args (s/cat :account-id ::jwts/account-id
:leeway ::jwts/leeway)
:args (s/cat :jwt ::jwts/jwt)
:ret jwts/query-blocked-jwt-input-spec)

(defn query-blocked-jwt-input
[account-id leeway]
{:account-id account-id
:current-time (current-time leeway)})
[jwt]
{:jwt jwt})

(s/fdef insert-blocked-jwt-input
:args (s/cat :account-id ::jwts/account-id
:expiration ::jwts/expiration
:leeway ::jwts/leeway)
:ret (s/and jwts/insert-blocked-jwt-input-spec
jwts/delete-blocked-jwt-time-input-spec))
:args (s/cat :jwt ::jwts/jwt
:exp ::jwts/exp)
:ret jwts/insert-blocked-jwt-input-spec)

(defn insert-blocked-jwt-input
[account-id expiration leeway]
{:account-id account-id
:expiration expiration
:current-time (current-time leeway)})

(s/fdef delete-blocked-jwts-input
:args (s/cat :account-id ::jwts/account-id
:leeway ::jwts/leeway)
:ret (s/and jwts/delete-blocked-jwt-account-input-spec
jwts/delete-blocked-jwt-time-input-spec))

(defn delete-blocked-jwts-input
[account-id leeway]
{:account-id account-id
:current-time (current-time leeway)})
[jwt exp]
{:jwt jwt
:eviction-time (eviction-time exp)})

(s/fdef purge-blocklist-input
:args (s/cat :leeway ::jwts/leeway)
:ret jwts/purge-blocklist-input-spec)

(defn purge-blocklist-input
[leeway]
{:current-time (current-time leeway)})
Loading

0 comments on commit c8486cd

Please sign in to comment.