|
2 | 2 | "OAuth 2.1 authorization and token exchange" |
3 | 3 | (:require |
4 | 4 | [clojure.string :as str] |
| 5 | + [co.gaiwan.oak.domain.identity :as identity] |
5 | 6 | [co.gaiwan.oak.domain.jwk :as jwk] |
6 | 7 | [co.gaiwan.oak.domain.jwt :as jwt] |
7 | 8 | [co.gaiwan.oak.domain.oauth-authorization :as oauth-authorization] |
8 | 9 | [co.gaiwan.oak.domain.oauth-client :as oauth-client] |
9 | 10 | [co.gaiwan.oak.domain.oauth-code :as oauth-code] |
10 | 11 | [co.gaiwan.oak.domain.refresh-token :as refresh-token] |
11 | 12 | [co.gaiwan.oak.domain.scope :as scope] |
| 13 | + [co.gaiwan.oak.html.layout :as layout] |
| 14 | + [co.gaiwan.oak.html.oauth :as views] |
12 | 15 | [co.gaiwan.oak.lib.auth-middleware :as auth-mw] |
13 | | - [co.gaiwan.oak.lib.debug-middleware :as debug] |
14 | | - [co.gaiwan.oak.html.forms :as f] |
15 | 16 | [co.gaiwan.oak.util.hash :as hash] |
16 | 17 | [co.gaiwan.oak.util.jose :as jose] |
17 | 18 | [co.gaiwan.oak.util.log :as log] |
18 | 19 | [co.gaiwan.oak.util.routing :as routing] |
19 | 20 | [lambdaisland.hiccup.middleware :as hiccup-mw] |
20 | 21 | [lambdaisland.uri :as uri])) |
21 | 22 |
|
22 | | -(defn auth-error-html [extra-info] |
23 | | - [:<> |
24 | | - [:h1 "Oh no! Something went wrong."] |
25 | | - [:hr] |
26 | | - [:p "It looks like the link you clicked to sign in isn't quite right. Don't worry, this is usually a simple fix."] |
27 | | - [:p "The application you're trying to use did not pass along all information needed to let you log in. This could be because of a broken link or a typo."] |
28 | | - [:p "What you can do:" |
29 | | - [:ul |
30 | | - [:li "Head back to the application and try clicking the sign-in button again."] |
31 | | - [:li "If the issue continues, please reach out to the application's support team. They'll know exactly what to do!"]]] |
32 | | - [:p "We apologize for the inconvenience!"] |
33 | | - [:hr] |
34 | | - [:details |
35 | | - [:summary "Technical information"] |
36 | | - extra-info]]) |
37 | | - |
38 | | -(defn permission-dialog-html [oauth-client requested-scopes params] |
39 | | - (let [{:keys [client_id redirect_uri response_type scope state code_challenge code_challenge_method]} params] |
40 | | - [:<> |
41 | | - [:p (:oauth-client/client-name oauth-client) " wants to access your account."] |
42 | | - [:p "This will allow " (:oauth-client/client-name oauth-client) " to:"] |
43 | | - [:ul |
44 | | - (for [s requested-scopes] |
45 | | - [:li (scope/desc s)])] |
46 | | - [f/form {:method "post"} |
47 | | - [:input {:type "hidden", :name "client_id", :value client_id}] |
48 | | - [:input {:type "hidden", :name "redirect_uri", :value redirect_uri}] |
49 | | - [:input {:type "hidden", :name "response_type", :value response_type}] |
50 | | - [:input {:type "hidden", :name "scope", :value scope}] |
51 | | - [:input {:type "hidden", :name "code_challenge", :value code_challenge}] |
52 | | - [:input {:type "hidden", :name "code_challenge_method", :value code_challenge_method}] |
53 | | - (when state [:input {:type "hidden", :name "state", :value state}]) |
54 | | - [:button {:type "submit", :name "allow", :value "true"} "Allow"] |
55 | | - [:button {:type "submit", :name "allow", :value "false"} "Cancel"]]])) |
56 | | - |
57 | 23 | (defn error-html-response [message] |
58 | 24 | {:status 400 |
59 | 25 | :html/head [:title "Something went wrong"] |
60 | | - :html/body [auth-error-html message]}) |
| 26 | + :html/body [views/auth-error-html message]}) |
61 | 27 |
|
62 | 28 | (defn error-redirect-response [redirect-uri kvs] |
63 | 29 | {:status 302 |
|
124 | 90 | "Ask the user permission for the requested scopes" |
125 | 91 | [oauth-client scope params] |
126 | 92 | {:status 200 |
127 | | - :html/body [permission-dialog-html oauth-client (str/split scope #"\s+") params]}) |
| 93 | + :html/body [views/permission-dialog-html oauth-client (str/split scope #"\s+") params]}) |
128 | 94 |
|
129 | 95 | (defn authorize-response |
130 | 96 | "Happy path, store a code for later token exchange, and send the user back to |
|
243 | 209 | ;; Look up the authorization code |
244 | 210 | (if-let [code-entity (oauth-code/find-one db {:code code :client-uuid client-uuid})] |
245 | 211 | (let [{:oauth-code/keys [identity-id scope code-challenge code-challenge-method]} code-entity |
| 212 | + identity (identity/find-one db {:id identity-id}) |
246 | 213 | code-verifier-valid? (when (= code-challenge-method "S256") |
247 | 214 | (and code_verifier |
248 | 215 | (= code-challenge (hash/sha256-base64url code_verifier))))] |
|
260 | 227 | at-claims (jwt/access-token-claims claim-opts) |
261 | 228 | access-token (jose/build-jwt jwk at-claims) |
262 | 229 | openid? (scope/subset? #{"openid"} scope) |
263 | | - id-claims (when openid? (jwt/id-token-claims db (assoc claim-opts :access-token access-token))) |
| 230 | + offline-access? (scope/subset? #{"offline_access"} scope) |
| 231 | + id-claims (when openid? (merge (jwt/id-token-claims db (assoc claim-opts :access-token access-token)) |
| 232 | + (:identity/claims identity) |
| 233 | + {"email" "foo@bar.com"})) |
264 | 234 | id-token (when openid? (jose/build-jwt jwk id-claims)) |
265 | | - refresh-token (refresh-token/create! db {:client-id client-uuid |
266 | | - :access-token-claims at-claims |
267 | | - :id-token-claims id-claims})] |
| 235 | + refresh-token (when offline-access? |
| 236 | + (refresh-token/create! db {:client-id client-uuid |
| 237 | + :access-token-claims at-claims |
| 238 | + :id-token-claims id-claims}))] |
268 | 239 | (log/info :token/exchange {:scope scope :id-claims id-claims :at-claims at-claims}) |
269 | 240 | ;; Delete the used code |
270 | 241 | (oauth-code/delete! db {:code code :client-uuid client-uuid}) |
|
275 | 246 | (cond-> {:access_token access-token |
276 | 247 | :token_type "Bearer" |
277 | 248 | :expires_in (- (get at-claims "exp") (get at-claims "iat")) |
278 | | - :refresh_token refresh-token |
279 | 249 | :scope scope} |
| 250 | + offline-access? |
| 251 | + (assoc :refresh_token refresh-token) |
280 | 252 | id-token |
281 | 253 | (assoc :id_token id-token))}) |
282 | 254 | (error-response "server_error" "No default JWK configured"))) |
|
476 | 448 | :get #'GET-authorization-server-metadata}] |
477 | 449 | ["/oauth" {:openapi {:tags ["oauth"]}} |
478 | 450 | ["/authorize" {:name :oauth/authorize |
| 451 | + :html/layout layout/layout |
479 | 452 | :middleware [hiccup-mw/wrap-render] |
480 | 453 | :get #'GET-authorize |
481 | 454 | :post #'POST-authorize}] |
|
0 commit comments