mirror of
https://github.com/penpot/penpot.git
synced 2025-02-02 12:28:54 -05:00
✨ Add the ability to parse OIDC JWT token
If jwks-uri is provided or properly discovered, they will be used for unsign JWT token and get use info data from that token instead of making an additional call to the userinfo endpoint
This commit is contained in:
parent
e61aaaecf3
commit
6339b07fba
2 changed files with 122 additions and 72 deletions
|
@ -25,6 +25,9 @@
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.json :as json]
|
[app.util.json :as json]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[buddy.core.keys :as keys]
|
||||||
|
[buddy.sign.jws :as jws]
|
||||||
|
[buddy.sign.jwt :as jwt]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
@ -48,36 +51,29 @@
|
||||||
|
|
||||||
(defn- discover-oidc-config
|
(defn- discover-oidc-config
|
||||||
[cfg {:keys [base-uri] :as opts}]
|
[cfg {:keys [base-uri] :as opts}]
|
||||||
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
|
(let [uri (dm/str (u/join base-uri ".well-known/openid-configuration"))
|
||||||
response (ex/try! (http/req! cfg
|
rsp (http/req! cfg {:method :get :uri uri} {:sync? true})]
|
||||||
{:method :get :uri (str discovery-uri)}
|
(if (= 200 (:status rsp))
|
||||||
{:sync? true}))]
|
(let [data (-> rsp :body json/decode)
|
||||||
(cond
|
|
||||||
(ex/exception? response)
|
|
||||||
(do
|
|
||||||
(l/warn :hint "unable to discover oidc configuration"
|
|
||||||
:discover-uri (str discovery-uri)
|
|
||||||
:cause response)
|
|
||||||
nil)
|
|
||||||
|
|
||||||
(= 200 (:status response))
|
|
||||||
(let [data (json/decode (:body response))
|
|
||||||
token-uri (get data :token_endpoint)
|
token-uri (get data :token_endpoint)
|
||||||
auth-uri (get data :authorization_endpoint)
|
auth-uri (get data :authorization_endpoint)
|
||||||
user-uri (get data :userinfo_endpoint)]
|
user-uri (get data :userinfo_endpoint)
|
||||||
|
jwks-uri (get data :jwks_uri)]
|
||||||
|
|
||||||
(l/debug :hint "oidc uris discovered"
|
(l/debug :hint "oidc uris discovered"
|
||||||
:token-uri token-uri
|
:token-uri token-uri
|
||||||
:auth-uri auth-uri
|
:auth-uri auth-uri
|
||||||
:user-uri user-uri)
|
:user-uri user-uri
|
||||||
|
:jwks-uri jwks-uri)
|
||||||
|
|
||||||
{:token-uri token-uri
|
{:token-uri token-uri
|
||||||
:auth-uri auth-uri
|
:auth-uri auth-uri
|
||||||
:user-uri user-uri})
|
:user-uri user-uri
|
||||||
|
:jwks-uri jwks-uri})
|
||||||
:else
|
|
||||||
(do
|
(do
|
||||||
(l/warn :hint "unable to discover OIDC configuration"
|
(l/warn :hint "unable to discover OIDC configuration"
|
||||||
:uri (str discovery-uri)
|
:discover-uri uri
|
||||||
:response-status-code (:status response))
|
:http-status (:status rsp))
|
||||||
nil))))
|
nil))))
|
||||||
|
|
||||||
(defn- prepare-oidc-opts
|
(defn- prepare-oidc-opts
|
||||||
|
@ -88,6 +84,7 @@
|
||||||
:token-uri (cf/get :oidc-token-uri)
|
:token-uri (cf/get :oidc-token-uri)
|
||||||
:auth-uri (cf/get :oidc-auth-uri)
|
:auth-uri (cf/get :oidc-auth-uri)
|
||||||
:user-uri (cf/get :oidc-user-uri)
|
:user-uri (cf/get :oidc-user-uri)
|
||||||
|
:jwks-uri (cf/get :oidc-jwks-uri)
|
||||||
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
|
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
|
||||||
:roles-attr (cf/get :oidc-roles-attr)
|
:roles-attr (cf/get :oidc-roles-attr)
|
||||||
:roles (cf/get :oidc-roles)
|
:roles (cf/get :oidc-roles)
|
||||||
|
@ -102,8 +99,42 @@
|
||||||
(string? (:user-uri opts))
|
(string? (:user-uri opts))
|
||||||
(string? (:auth-uri opts)))
|
(string? (:auth-uri opts)))
|
||||||
opts
|
opts
|
||||||
(some-> (discover-oidc-config cfg opts)
|
(try
|
||||||
(merge opts {:discover? true}))))))
|
(-> (discover-oidc-config cfg opts)
|
||||||
|
(merge opts {:discover? true}))
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/warn :hint "unable to discover OIDC configuration"
|
||||||
|
:cause cause)))))))
|
||||||
|
|
||||||
|
(defn- process-oidc-jwks
|
||||||
|
[keys]
|
||||||
|
(reduce (fn [result {:keys [kid] :as kdata}]
|
||||||
|
(let [pkey (ex/try! (keys/jwk->public-key kdata))]
|
||||||
|
(if (ex/exception? pkey)
|
||||||
|
(do
|
||||||
|
(l/warn :hint "unable to create public key"
|
||||||
|
:kid (:kid kdata)
|
||||||
|
:cause pkey)
|
||||||
|
result)
|
||||||
|
(assoc result kid pkey))))
|
||||||
|
{}
|
||||||
|
keys))
|
||||||
|
|
||||||
|
(defn- fetch-oidc-jwks
|
||||||
|
[cfg {:keys [jwks-uri]}]
|
||||||
|
(when jwks-uri
|
||||||
|
(try
|
||||||
|
(let [{:keys [status body]} (http/req! cfg {:method :get :uri jwks-uri} {:sync? true})]
|
||||||
|
(if (= 200 status)
|
||||||
|
(-> body json/decode :keys process-oidc-jwks)
|
||||||
|
(do
|
||||||
|
(l/warn :hint "unable to retrieve JWKs (unexpected response status code)"
|
||||||
|
:http-status status
|
||||||
|
:http-body body)
|
||||||
|
nil)))
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/warn :hint "unable to retrieve JWKs (unexpected exception)"
|
||||||
|
:cause cause)))))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::providers/generic [_]
|
(defmethod ig/pre-init-spec ::providers/generic [_]
|
||||||
(s/keys :req [::http/client]))
|
(s/keys :req [::http/client]))
|
||||||
|
@ -112,7 +143,7 @@
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(when (contains? cf/flags :login-with-oidc)
|
(when (contains? cf/flags :login-with-oidc)
|
||||||
(if-let [opts (prepare-oidc-opts cfg)]
|
(if-let [opts (prepare-oidc-opts cfg)]
|
||||||
(do
|
(let [jwks (fetch-oidc-jwks cfg opts)]
|
||||||
(l/info :hint "provider initialized"
|
(l/info :hint "provider initialized"
|
||||||
:provider "oidc"
|
:provider "oidc"
|
||||||
:method (if (:discover? opts) "discover" "manual")
|
:method (if (:discover? opts) "discover" "manual")
|
||||||
|
@ -123,8 +154,9 @@
|
||||||
:user-uri (:user-uri opts)
|
:user-uri (:user-uri opts)
|
||||||
:token-uri (:token-uri opts)
|
:token-uri (:token-uri opts)
|
||||||
:roles-attr (:roles-attr opts)
|
:roles-attr (:roles-attr opts)
|
||||||
:roles (:roles opts))
|
:roles (:roles opts)
|
||||||
opts)
|
:keys (str/join "," (map str (keys jwks))))
|
||||||
|
(assoc opts :jwks jwks))
|
||||||
(do
|
(do
|
||||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "oidc")
|
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "oidc")
|
||||||
nil))))
|
nil))))
|
||||||
|
@ -165,7 +197,7 @@
|
||||||
[cfg tdata props]
|
[cfg tdata props]
|
||||||
(or (some-> props :github/email)
|
(or (some-> props :github/email)
|
||||||
(let [params {:uri "https://api.github.com/user/emails"
|
(let [params {:uri "https://api.github.com/user/emails"
|
||||||
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
|
:headers {"Authorization" (dm/str (:token/type tdata) " " (:token/access tdata))}
|
||||||
:timeout 6000
|
:timeout 6000
|
||||||
:method :get}
|
:method :get}
|
||||||
|
|
||||||
|
@ -274,7 +306,7 @@
|
||||||
{}
|
{}
|
||||||
props))
|
props))
|
||||||
|
|
||||||
(defn retrieve-access-token
|
(defn fetch-access-token
|
||||||
[{:keys [provider] :as cfg} code]
|
[{:keys [provider] :as cfg} code]
|
||||||
(let [params {:client_id (:client-id provider)
|
(let [params {:client_id (:client-id provider)
|
||||||
:client_secret (:client-secret provider)
|
:client_secret (:client-secret provider)
|
||||||
|
@ -298,8 +330,9 @@
|
||||||
(l/trace :hint "access token response" :status status :body body)
|
(l/trace :hint "access token response" :status status :body body)
|
||||||
(if (= status 200)
|
(if (= status 200)
|
||||||
(let [data (json/decode body)]
|
(let [data (json/decode body)]
|
||||||
{:token (get data :access_token)
|
{:token/access (get data :access_token)
|
||||||
:type (get data :token_type)})
|
:token/id (get data :id_token)
|
||||||
|
:token/type (get data :token_type)})
|
||||||
|
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
:code :unable-to-retrieve-token
|
:code :unable-to-retrieve-token
|
||||||
|
@ -307,12 +340,11 @@
|
||||||
:http-status status
|
:http-status status
|
||||||
:http-body body)))))
|
:http-body body)))))
|
||||||
|
|
||||||
(defn- retrieve-user-info
|
(defn- process-user-info
|
||||||
[{:keys [provider] :as cfg} tdata]
|
[provider tdata info]
|
||||||
(letfn [(get-email [props]
|
(letfn [(get-email [props]
|
||||||
;; Allow providers hook into this for custom email
|
;; Allow providers hook into this for custom email
|
||||||
;; retrieval method.
|
;; retrieval method.
|
||||||
|
|
||||||
(if-let [get-email-fn (:get-email-fn provider)]
|
(if-let [get-email-fn (:get-email-fn provider)]
|
||||||
(get-email-fn tdata props)
|
(get-email-fn tdata props)
|
||||||
(let [attr-kw (cf/get :oidc-email-attr "email")
|
(let [attr-kw (cf/get :oidc-email-attr "email")
|
||||||
|
@ -323,48 +355,53 @@
|
||||||
(let [attr-kw (cf/get :oidc-name-attr "name")
|
(let [attr-kw (cf/get :oidc-name-attr "name")
|
||||||
attr-ph (parse-attr-path provider attr-kw)]
|
attr-ph (parse-attr-path provider attr-kw)]
|
||||||
(get-in props attr-ph)))
|
(get-in props attr-ph)))
|
||||||
|
]
|
||||||
|
|
||||||
(process-response [response]
|
(let [props (qualify-props provider info)
|
||||||
(let [info (-> response :body json/decode)
|
email (get-email props)]
|
||||||
props (qualify-props provider info)
|
{:backend (:name provider)
|
||||||
email (get-email props)]
|
:fullname (or (get-name props) email)
|
||||||
{:backend (:name provider)
|
:email email
|
||||||
:fullname (or (get-name props) email)
|
:props props})))
|
||||||
:email email
|
|
||||||
:props props}))]
|
|
||||||
|
|
||||||
(l/trace :hint "request user info"
|
(defn- fetch-user-info
|
||||||
:uri (:user-uri provider)
|
[{:keys [provider] :as cfg} tdata]
|
||||||
:token (obfuscate-string (:token tdata))
|
(l/trace :hint "fetch user info"
|
||||||
:token-type (:type tdata))
|
:uri (:user-uri provider)
|
||||||
|
:token (obfuscate-string (:token/access tdata)))
|
||||||
|
|
||||||
(let [request {:uri (:user-uri provider)
|
(let [params {:uri (:user-uri provider)
|
||||||
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
|
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
|
||||||
:timeout 6000
|
:timeout 6000
|
||||||
:method :get}
|
:method :get}
|
||||||
response (http/req! cfg request {:sync? true})]
|
response (http/req! cfg params {:sync? true})]
|
||||||
|
|
||||||
(l/trace :hint "user info response"
|
(l/trace :hint "user info response"
|
||||||
:status (:status response)
|
:status (:status response)
|
||||||
:body (:body response))
|
:body (:body response))
|
||||||
|
|
||||||
(when-not (s/int-in-range? 200 300 (:status response))
|
(when-not (s/int-in-range? 200 300 (:status response))
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
:code :unable-to-retrieve-user-info
|
:code :unable-to-retrieve-user-info
|
||||||
:hint "unable to retrieve user info"
|
:hint "unable to retrieve user info"
|
||||||
:http-status (:status response)
|
:http-status (:status response)
|
||||||
:http-body (:body response)))
|
:http-body (:body response)))
|
||||||
|
|
||||||
(let [info (process-response response)]
|
(-> response :body json/decode)))
|
||||||
(l/trace :hint "authentication info" :info info)
|
|
||||||
|
|
||||||
(when-not (s/valid? ::info info)
|
(defn- get-user-info
|
||||||
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
|
[{:keys [provider]} tdata]
|
||||||
(ex/raise :type :internal
|
(try
|
||||||
:code :incomplete-user-info
|
(let [{:keys [kid alg] :as theader} (jws/decode-header (:token/id tdata))]
|
||||||
:hint "inconmplete user info"
|
(when-let [key (if (str/starts-with? (name alg) "hs")
|
||||||
:info info))
|
(:client-secret provider)
|
||||||
info))))
|
(get-in provider [:jwks kid]))]
|
||||||
|
|
||||||
|
(let [claims (jwt/unsign (:token/id tdata) key {:alg alg})]
|
||||||
|
(dissoc claims :exp :iss :iat :sid :aud :sub))))
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/warn :hint "unable to get user info from JWT token (unexpected exception)"
|
||||||
|
:cause cause))))
|
||||||
|
|
||||||
(s/def ::backend ::us/not-empty-string)
|
(s/def ::backend ::us/not-empty-string)
|
||||||
(s/def ::email ::us/not-empty-string)
|
(s/def ::email ::us/not-empty-string)
|
||||||
|
@ -377,7 +414,7 @@
|
||||||
::props]))
|
::props]))
|
||||||
|
|
||||||
(defn get-info
|
(defn get-info
|
||||||
[{:keys [provider] :as cfg} {:keys [params] :as request}]
|
[{:keys [provider ::main/props] :as cfg} {:keys [params] :as request}]
|
||||||
(when-let [error (get params :error)]
|
(when-let [error (get params :error)]
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
:code :error-on-retrieving-code
|
:code :error-on-retrieving-code
|
||||||
|
@ -386,9 +423,20 @@
|
||||||
|
|
||||||
(let [state (get params :state)
|
(let [state (get params :state)
|
||||||
code (get params :code)
|
code (get params :code)
|
||||||
state (tokens/verify (::main/props cfg) {:token state :iss :oauth})
|
state (tokens/verify props {:token state :iss :oauth})
|
||||||
token (retrieve-access-token cfg code)
|
tdata (fetch-access-token cfg code)
|
||||||
info (retrieve-user-info cfg token)]
|
info (or (get-user-info cfg tdata)
|
||||||
|
(fetch-user-info cfg tdata))
|
||||||
|
info (process-user-info provider tdata info)]
|
||||||
|
|
||||||
|
(l/trace :hint "user info" :info info)
|
||||||
|
|
||||||
|
(when-not (s/valid? ::info info)
|
||||||
|
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :incomplete-user-info
|
||||||
|
:hint "inconmplete user info"
|
||||||
|
:info info))
|
||||||
|
|
||||||
;; If the provider is OIDC, we can proceed to check
|
;; If the provider is OIDC, we can proceed to check
|
||||||
;; roles if they are defined.
|
;; roles if they are defined.
|
||||||
|
|
|
@ -151,6 +151,7 @@
|
||||||
(s/def ::oidc-token-uri ::us/string)
|
(s/def ::oidc-token-uri ::us/string)
|
||||||
(s/def ::oidc-auth-uri ::us/string)
|
(s/def ::oidc-auth-uri ::us/string)
|
||||||
(s/def ::oidc-user-uri ::us/string)
|
(s/def ::oidc-user-uri ::us/string)
|
||||||
|
(s/def ::oidc-jwks-uri ::us/string)
|
||||||
(s/def ::oidc-scopes ::us/set-of-strings)
|
(s/def ::oidc-scopes ::us/set-of-strings)
|
||||||
(s/def ::oidc-roles ::us/set-of-strings)
|
(s/def ::oidc-roles ::us/set-of-strings)
|
||||||
(s/def ::oidc-roles-attr ::us/string)
|
(s/def ::oidc-roles-attr ::us/string)
|
||||||
|
@ -245,6 +246,7 @@
|
||||||
::oidc-token-uri
|
::oidc-token-uri
|
||||||
::oidc-auth-uri
|
::oidc-auth-uri
|
||||||
::oidc-user-uri
|
::oidc-user-uri
|
||||||
|
::oidc-jwks-uri
|
||||||
::oidc-scopes
|
::oidc-scopes
|
||||||
::oidc-roles-attr
|
::oidc-roles-attr
|
||||||
::oidc-email-attr
|
::oidc-email-attr
|
||||||
|
|
Loading…
Add table
Reference in a new issue