mirror of
https://github.com/penpot/penpot.git
synced 2026-09-14 07:59:36 +00:00
✨ Enforce absolute maximum session duration (#11447)
Session tokens now carry an :exp claim anchored to created-at (not modified-at), so activity cannot extend the session beyond the absolute maximum (default 30 days, configurable via PENPOT_AUTH_TOKEN_COOKIE_MAX_AGE_ABSOLUTE). The existing token verification already rejects expired tokens, so enforcement is automatic. Also extends the GC task to purge expired http_session_v2 rows, which were previously never cleaned up. Closes #11444 AI-assisted-by: longcat-2.0
This commit is contained in:
parent
e67ebacef2
commit
d6e82f44a6
@ -58,6 +58,7 @@
|
|||||||
:objects-storage-fs-directory "assets"
|
:objects-storage-fs-directory "assets"
|
||||||
|
|
||||||
:auth-token-cookie-name "auth-token"
|
:auth-token-cookie-name "auth-token"
|
||||||
|
:auth-token-cookie-max-age-absolute (ct/duration {:days 30})
|
||||||
|
|
||||||
:assets-path "/internal/assets/"
|
:assets-path "/internal/assets/"
|
||||||
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
||||||
@ -206,6 +207,7 @@
|
|||||||
|
|
||||||
[:auth-token-cookie-name {:optional true} :string]
|
[:auth-token-cookie-name {:optional true} :string]
|
||||||
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
|
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
|
||||||
|
[:auth-token-cookie-max-age-absolute {:optional true} ::ct/duration]
|
||||||
|
|
||||||
[:registration-domain-whitelist {:optional true} [::sm/set :string]]
|
[:registration-domain-whitelist {:optional true} [::sm/set :string]]
|
||||||
[:email-verify-threshold {:optional true} ::ct/duration]
|
[:email-verify-threshold {:optional true} ::ct/duration]
|
||||||
|
|||||||
@ -36,6 +36,9 @@
|
|||||||
;; Default age for automatic session renewal
|
;; Default age for automatic session renewal
|
||||||
(def default-renewal-max-age (ct/duration {:hours 6}))
|
(def default-renewal-max-age (ct/duration {:hours 6}))
|
||||||
|
|
||||||
|
;; Default absolute maximum session duration
|
||||||
|
(def default-cookie-max-age-absolute (ct/duration {:days 30}))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; PROTOCOLS
|
;; PROTOCOLS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -169,15 +172,19 @@
|
|||||||
|
|
||||||
(defn- assign-token
|
(defn- assign-token
|
||||||
[cfg session]
|
[cfg session]
|
||||||
(let [claims {:iss "authentication"
|
(let [absolute-max-age (cf/get :auth-token-cookie-max-age-absolute default-cookie-max-age-absolute)
|
||||||
:aud "penpot"
|
claims {:iss "authentication"
|
||||||
:sid (:id session)
|
:aud "penpot"
|
||||||
:iat (:modified-at session)
|
:sid (:id session)
|
||||||
:uid (:profile-id session)
|
:iat (:modified-at session)
|
||||||
:sso-provider-id (:sso-provider-id session)
|
:uid (:profile-id session)
|
||||||
:sso-session-id (:sso-session-id session)}
|
:sso-provider-id (:sso-provider-id session)
|
||||||
header {:kid 1 :ver 1}
|
:sso-session-id (:sso-session-id session)}
|
||||||
token (tokens/generate cfg claims header)]
|
claims (if (:created-at session)
|
||||||
|
(assoc claims :exp (ct/plus (:created-at session) absolute-max-age))
|
||||||
|
claims)
|
||||||
|
header {:kid 1 :ver 1}
|
||||||
|
token (tokens/generate cfg claims header)]
|
||||||
(assoc session :token token)))
|
(assoc session :token token)))
|
||||||
|
|
||||||
(defn create-fn
|
(defn create-fn
|
||||||
@ -353,15 +360,23 @@
|
|||||||
or (updated_at is null and
|
or (updated_at is null and
|
||||||
created_at < ?::timestamptz)")
|
created_at < ?::timestamptz)")
|
||||||
|
|
||||||
|
(def ^:private
|
||||||
|
sql:delete-expired-v2
|
||||||
|
"DELETE FROM http_session_v2
|
||||||
|
WHERE created_at < ?::timestamptz")
|
||||||
|
|
||||||
(defn- collect-expired-tasks
|
(defn- collect-expired-tasks
|
||||||
[{:keys [::db/conn ::tasks/max-age]}]
|
[{:keys [::db/conn ::tasks/max-age]}]
|
||||||
(let [threshold (ct/minus (ct/now) max-age)
|
(let [threshold (ct/minus (ct/now) max-age)
|
||||||
result (-> (db/exec-one! conn [sql:delete-expired threshold threshold])
|
result-legacy (-> (db/exec-one! conn [sql:delete-expired threshold threshold])
|
||||||
(db/get-update-count))]
|
(db/get-update-count))
|
||||||
|
result-v2 (-> (db/exec-one! conn [sql:delete-expired-v2 threshold])
|
||||||
|
(db/get-update-count))]
|
||||||
(l/dbg :task "gc"
|
(l/dbg :task "gc"
|
||||||
:hint "clean http sessions"
|
:hint "clean http sessions"
|
||||||
:deleted result)
|
:deleted-legacy result-legacy
|
||||||
result))
|
:deleted-v2 result-v2)
|
||||||
|
(+ result-legacy result-v2)))
|
||||||
|
|
||||||
(defmethod ig/init-key ::tasks/gc
|
(defmethod ig/init-key ::tasks/gc
|
||||||
[_ {:keys [::tasks/max-age] :as cfg}]
|
[_ {:keys [::tasks/max-age] :as cfg}]
|
||||||
|
|||||||
@ -277,6 +277,70 @@
|
|||||||
(t/is (= (:id session) (:sid claims)))
|
(t/is (= (:id session) (:sid claims)))
|
||||||
(t/is (= (:id profile) (:uid claims)))))
|
(t/is (= (:id profile) (:uid claims)))))
|
||||||
|
|
||||||
|
(t/deftest session-token-contains-exp-claim
|
||||||
|
(let [cfg th/*system*
|
||||||
|
manager (session/inmemory-manager)
|
||||||
|
profile (th/create-profile* 1)
|
||||||
|
session (->> (session/create-session manager {:profile-id (:id profile)
|
||||||
|
:user-agent "user agent"})
|
||||||
|
(#'session/assign-token cfg))
|
||||||
|
claims (tokens/decode cfg (:token session))
|
||||||
|
exp (:exp claims)]
|
||||||
|
(t/is (some? exp) "session token should contain :exp claim")
|
||||||
|
(t/is (ct/inst? exp) "exp should be an instant")))
|
||||||
|
|
||||||
|
(t/deftest session-token-exp-based-on-created-at
|
||||||
|
(let [cfg th/*system*
|
||||||
|
manager (session/inmemory-manager)
|
||||||
|
profile (th/create-profile* 1)
|
||||||
|
session (->> (session/create-session manager {:profile-id (:id profile)
|
||||||
|
:user-agent "user agent"})
|
||||||
|
(#'session/assign-token cfg))
|
||||||
|
claims (tokens/decode cfg (:token session))
|
||||||
|
expected-exp (ct/plus (:created-at session) (ct/duration {:days 30}))]
|
||||||
|
(t/is (some? (:exp claims)) "session token should contain :exp claim")
|
||||||
|
(t/is (= (inst-ms (:exp claims))
|
||||||
|
(inst-ms expected-exp))
|
||||||
|
"exp should equal created-at + 30 days")))
|
||||||
|
|
||||||
|
(t/deftest session-token-past-exp-is-rejected
|
||||||
|
(let [cfg th/*system*
|
||||||
|
manager (session/inmemory-manager)
|
||||||
|
profile (th/create-profile* 1)
|
||||||
|
session (->> (session/create-session manager {:profile-id (:id profile)
|
||||||
|
:user-agent "user agent"})
|
||||||
|
(#'session/assign-token cfg))
|
||||||
|
claims (tokens/decode cfg (:token session))
|
||||||
|
;; Manually create a token with exp in the past
|
||||||
|
past-claims (assoc claims :exp (ct/minus (ct/now) (ct/duration {:days 1})))
|
||||||
|
header {:kid 1 :ver 1}
|
||||||
|
past-token (tokens/generate cfg past-claims header)]
|
||||||
|
(t/is (nil? (session/decode-token cfg past-token))
|
||||||
|
"token with exp in the past should be rejected")))
|
||||||
|
|
||||||
|
(t/deftest session-renewal-preserves-original-exp
|
||||||
|
(let [cfg th/*system*
|
||||||
|
manager (session/inmemory-manager)
|
||||||
|
profile (th/create-profile* 1)
|
||||||
|
handler (-> (fn [req] req)
|
||||||
|
(#'session/wrap-authz {::session/manager manager})
|
||||||
|
(#'mw/wrap-auth {:bearer (partial session/decode-token cfg)
|
||||||
|
:cookie (partial session/decode-token cfg)}))
|
||||||
|
session (->> (session/create-session manager {:profile-id (:id profile)
|
||||||
|
:user-agent "user agent"})
|
||||||
|
(#'session/assign-token cfg))
|
||||||
|
original-exp (:exp (tokens/decode cfg (:token session)))
|
||||||
|
;; Force renewal by setting modified-at to 7 hours ago
|
||||||
|
old-session (assoc session :modified-at (ct/minus (ct/now) (ct/duration {:hours 7})))
|
||||||
|
response (handler (make-dummy-request {:cookies {"auth-token" (:token old-session)}}))
|
||||||
|
{:keys [token claims]} (get response ::http/auth-data)
|
||||||
|
new-exp (:exp claims)]
|
||||||
|
(t/is (some? original-exp) "original token should have :exp")
|
||||||
|
(t/is (some? new-exp) "renewed token should have :exp")
|
||||||
|
(t/is (= (inst-ms original-exp)
|
||||||
|
(inst-ms new-exp))
|
||||||
|
"renewed token should preserve original :exp, not extend it")))
|
||||||
|
|
||||||
(t/deftest parse-request-illegal-argument-exception
|
(t/deftest parse-request-illegal-argument-exception
|
||||||
;; clojure.data.json raises IllegalArgumentException (case
|
;; clojure.data.json raises IllegalArgumentException (case
|
||||||
;; fall-through) on several kinds of malformed input. The
|
;; fall-through) on several kinds of malformed input. The
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user