Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Alejandro Alonso 2023-06-28 12:49:26 +02:00
commit 1afdbcfbaa
2 changed files with 122 additions and 72 deletions

View file

@ -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,26 +355,26 @@
(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)
props (qualify-props provider info)
email (get-email props)] email (get-email props)]
{:backend (:name provider) {:backend (:name provider)
:fullname (or (get-name props) email) :fullname (or (get-name props) email)
:email email :email email
:props props}))] :props props})))
(l/trace :hint "request user info" (defn- fetch-user-info
[{:keys [provider] :as cfg} tdata]
(l/trace :hint "fetch user info"
:uri (:user-uri provider) :uri (:user-uri provider)
:token (obfuscate-string (:token tdata)) :token (obfuscate-string (:token/access tdata)))
:token-type (:type 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)
@ -355,16 +387,21 @@
: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.

View file

@ -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