Skip to content

Enable use of the request to build user's identity #33

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

Merged
merged 2 commits into from
May 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 18 additions & 7 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ You can inject your own authorization rules, via:
Notice you could add the following keys in the configuration passed to ~mk-wrap-authentication~, ~mk-wrap-authorization~ and ~wrap-jwt-auth-fn:~

#+begin_src clojure
(s/defschema Config
"Initialized internal Configuration"
(s/defschema Config*
(st/merge
{:allow-unauthenticated-access?
(describe s/Bool
Expand All @@ -118,9 +117,6 @@ Notice you could add the following keys in the configuration passed to ~mk-wrap-
:jwt-max-lifetime-in-sec
(describe s/Num
"Maximal number of second a JWT does not expires")
:post-jwt-format-fn
(describe (s/=> s/Any JWTClaims)
"A function taking the JWT claims and building an Identity object suitable for your needs")
:error-handler
(describe (s/=> s/Any)
"A function that given a JWTError returns a ring response.")
Expand All @@ -129,18 +125,33 @@ Notice you could add the following keys in the configuration passed to ~mk-wrap-
(describe s/Num
"When the JWT does not contain any nbf claim, the number of seconds to remove from iat claim. Default 60.")}
(st/optional-keys
{:pubkey-fn (describe (s/=> s/Any s/Str)
{:post-jwt-format-fn
(describe (s/=> s/Any JWTClaims)
"A function taking the JWT claims and building an Identity object suitable for your needs")
:post-jwt-format-with-request-fn
(describe (s/=> s/Any JWTClaims)
"A function taking the JWT claims and the request, and building an Identity object suitable for your needs")
:pubkey-fn (describe (s/=> s/Any s/Str)
"A function returning a public key (takes precedence over pubkey-path)")
:pubkey-fn-arg-fn (describe (s/=> s/Any s/Any)
"A function that will be applied to the argument (the raw JWT) of `pubkey-fn`")
:post-jwt-format-fn-arg-fn (describe (s/=> s/Any s/Any)
"A function that will be applied to the argument (the raw JWT) of `post-jwt-format-fn`")
"A function that will be applied to the argument (the raw JWT) of `post-jwt-format-fn` or `post-jwt-format-with-request-fn`")
:pubkey-path (describe s/Str
"The path to find the public key that will be used to check the JWT signature")
:jwt-check-fn
(describe (s/=> s/Bool JWT JWTClaims)
(str "A function that take a JWT, claims and return a sequence of string containing errors."
"The check is considered successful if this function returns nil, or a sequence containing only nil values."))})))

(s/defschema Config
"Initialized internal Configuration"
(s/constrained
Config*
(fn [{:keys [post-jwt-format-fn post-jwt-format-with-request-fn]}]
(or post-jwt-format-fn
post-jwt-format-with-request-fn))
"One of `post-jwt-format-fn` or `post-jwt-format-with-request-fn` is required. `post-jwt-format-with-request-fn` has precedence."))
#+end_src

By default if no JWT authorization header is found the request is terminated with
Expand Down
5 changes: 4 additions & 1 deletion src/ring_jwt_middleware/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
pubkey-fn
is-revoked-fn
post-jwt-format-fn
post-jwt-format-with-request-fn
post-jwt-format-fn-arg-fn
pubkey-fn-arg-fn]
:as config} (->config user-config)
Expand All @@ -188,7 +189,9 @@
{:level :error
:exception e
:jwt jwt})))]
(->pure {:identity (post-jwt-format-fn (post-jwt-format-fn-arg-fn jwt))
(->pure {:identity (if post-jwt-format-with-request-fn
(post-jwt-format-with-request-fn (post-jwt-format-fn-arg-fn jwt) request)
(post-jwt-format-fn (post-jwt-format-fn-arg-fn jwt)))
:jwt (:claims jwt)}))]
(handler (into request (<-result authentication-result))))))))

Expand Down
27 changes: 18 additions & 9 deletions src/ring_jwt_middleware/schemas.clj
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@
(with-meta s {:description description})
s))

(s/defschema Config
"Initialized internal Configuration"
(s/defschema Config*
(st/merge
{:allow-unauthenticated-access?
(describe s/Bool
Expand All @@ -57,31 +56,41 @@
:jwt-max-lifetime-in-sec
(describe s/Num
"Maximal number of second a JWT does not expires")
:post-jwt-format-fn
(describe (s/=> s/Any JWTClaims)
"A function taking the JWT claims and building an Identity object suitable for your needs")
:error-handler
(describe (s/=> s/Any)
"A function that given a JWTError returns a ring response.")

:default-allowed-clock-skew-in-seconds
(describe s/Num
"When the JWT does not contain any nbf claim, the number of seconds to remove from iat claim. Default 60.")}
(st/optional-keys
{:pubkey-fn (describe (s/=> s/Any s/Str)
{:post-jwt-format-fn
(describe (s/=> s/Any JWTClaims)
"A function taking the JWT claims and building an Identity object suitable for your needs")
:post-jwt-format-with-request-fn
(describe (s/=> s/Any JWTClaims)
"A function taking the JWT claims and the request, and building an Identity object suitable for your needs")
:pubkey-fn (describe (s/=> s/Any s/Str)
"A function returning a public key (takes precedence over pubkey-path)")
:pubkey-fn-arg-fn (describe (s/=> s/Any s/Any)
"A function that will be applied to the argument (the raw JWT) of `pubkey-fn`")
:post-jwt-format-fn-arg-fn (describe (s/=> s/Any s/Any)
"A function that will be applied to the argument (the raw JWT) of `post-jwt-format-fn`")
"A function that will be applied to the argument (the raw JWT) of `post-jwt-format-fn` or `post-jwt-format-with-request-fn`")
:pubkey-path (describe s/Str
"The path to find the public key that will be used to check the JWT signature")
:jwt-check-fn
(describe (s/=> s/Bool JWT JWTClaims)
(str "A function that take a JWT, claims and return a sequence of string containing errors."
"The check is considered successful if this function returns nil, or a sequence containing only nil values."))})))

(s/defschema Config
"Initialized internal Configuration"
(s/constrained
Config*
(fn [{:keys [post-jwt-format-fn post-jwt-format-with-request-fn]}]
(or post-jwt-format-fn
post-jwt-format-with-request-fn))
"One of `post-jwt-format-fn` or `post-jwt-format-with-request-fn` is required. `post-jwt-format-with-request-fn` has precedence."))

(s/defschema UserConfig
"Middleware Configuration"
(st/optional-keys Config))
(st/optional-keys Config*))
24 changes: 17 additions & 7 deletions test/ring_jwt_middleware/core_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
[& args]
(constantly (to-epoch (apply jt/local-date-time args))))


(jt/available-zone-ids)

(def jwt-token-1
Expand Down Expand Up @@ -150,7 +149,7 @@

(deftest validate-errors-test
(let [cfg (config/->config {:current-epoch fixed-current-epoch
:pubkey-path "resources/cert/jwt-key-1.pub"})]
:pubkey-path "resources/cert/jwt-key-1.pub"})]

(is (result/success? (sut/validate-jwt cfg "jwt" decoded-jwt-1-claims)))
(is (= {:jwt-error {:jwt {}
Expand Down Expand Up @@ -389,7 +388,6 @@
(is (= 200
(:status (ring-fn req))))))


(testing "multiple keys support"
(let [pubkey-fn (fn [claims]
(case (:iss claims)
Expand Down Expand Up @@ -461,7 +459,6 @@
{:error :no_jwt, :error_description "No JWT found in HTTP headers"}}}
(ring-fn req-auth-header-not-jwt)))))


(testing "revocation test"
(let [revoke-handler (handler-with-mid-cfg {:is-revoked-fn (constantly true)})
no-revoke-handler (handler-with-mid-cfg {:is-revoked-fn (constantly false)})]
Expand All @@ -475,12 +472,11 @@
(is (= {:error :internal-error
:error_description "Internal Error"}
(select-keys (:body (revoke-handler req))
[:error :error_description]))
[:error :error_description]))
"is-revoked-fn can provide specific errors")
(is (= 200 (:status (no-revoke-handler req))))
(is (= "[email protected]"
(get-in (no-revoke-handler req) [:body :identity]))))
)
(get-in (no-revoke-handler req) [:body :identity])))))

(testing "post jwt transformation test"
(let [post-transform (fn [m] {:user {:id (:sub m)}
Expand All @@ -491,6 +487,20 @@
:org {:id "bar"}}
(get-in (ring-fn req) [:body :identity])))))

(testing "post-jwt-format-with-request-fn takes precedence over post-jwt-format-fn"
(let [post-transform (fn [m] {:user {:id (:sub m)}
:org {:id (:foo m)}})
post-transform-with-request (fn [m _req] {:user {:id (:sub m)}
:org {:id (:foo m)}
:headers-available? true})
ring-fn (handler-with-mid-cfg {:post-jwt-format-fn post-transform
:post-jwt-format-with-request-fn post-transform-with-request})]
(is (= 200 (:status (ring-fn req))))
(is (= {:user {:id "[email protected]"}
:org {:id "bar"}
:headers-available? true}
(get-in (ring-fn req) [:body :identity])))))

(testing "post jwt transformation test using `post-jwt-format-fn-arg-fn`"
(let [post-transform (fn [m] {:user {:id (-> m :claims :sub)}
:org {:id (-> m :claims :foo)}
Expand Down