Implementation of the JWT backend for sso | (ns metabase-enterprise.sso.integrations.jwt (:require [buddy.sign.jwt :as jwt] [clojure.string :as str] [java-time.api :as t] [metabase-enterprise.sso.api.interface :as sso.i] [metabase-enterprise.sso.integrations.sso-settings :as sso-settings] [metabase-enterprise.sso.integrations.sso-utils :as sso-utils] [metabase.embed.settings :as embed.settings] [metabase.premium-features.core :as premium-features] [metabase.request.core :as request] [metabase.session.models.session :as session] [metabase.sso.core :as sso] [metabase.util.i18n :refer [tru]] [ring.util.response :as response]) (:import (java.net URLEncoder))) |
(set! *warn-on-reflection* true) | |
Returns a session map for the given | (defn fetch-or-create-user! [first-name last-name email user-attributes] (when-not (sso-settings/jwt-enabled) (throw (IllegalArgumentException. (str (tru "Can''t create new JWT user when JWT is not configured"))))) (let [user {:first_name first-name :last_name last-name :email email :sso_source :jwt :login_attributes user-attributes}] (or (sso-utils/fetch-and-update-login-attributes! user) (sso-utils/check-user-provisioning :jwt) (sso-utils/create-new-sso-user! user)))) |
(def ^:private ^{:arglists '([])} jwt-attribute-email (comp keyword sso-settings/jwt-attribute-email)) | |
(def ^:private ^{:arglists '([])} jwt-attribute-firstname (comp keyword sso-settings/jwt-attribute-firstname)) | |
(def ^:private ^{:arglists '([])} jwt-attribute-lastname (comp keyword sso-settings/jwt-attribute-lastname)) | |
(def ^:private ^{:arglists '([])} jwt-attribute-groups (comp keyword sso-settings/jwt-attribute-groups)) | |
Registered claims in the JWT standard which we should not interpret as login attributes | (def ^:private registered-claims [:iss :iat :sub :aud :exp :nbf :jti]) |
(defn- jwt-data->login-attributes [jwt-data] (apply dissoc jwt-data (jwt-attribute-email) (jwt-attribute-firstname) (jwt-attribute-lastname) registered-claims)) | |
JWTs use seconds since Epoch, not milliseconds since Epoch for the | (def ^:private ^:const three-minutes-in-seconds 180) |
Translate a user's group names to a set of MB group IDs using the configured mappings | (defn- group-names->ids [group-names] (set (mapcat (sso-settings/jwt-group-mappings) (map keyword group-names)))) |
Returns the set of all MB group IDs that have configured mappings | (defn- all-mapped-group-ids [] (-> (sso-settings/jwt-group-mappings) vals flatten set)) |
Sync a user's groups based on mappings configured in the JWT settings | (defn- sync-groups! [user jwt-data] (when (sso-settings/jwt-group-sync) (when-let [groups-attribute (jwt-attribute-groups)] (when-let [group-names (get jwt-data groups-attribute)] (sso/sync-group-memberships! user (group-names->ids group-names) (all-mapped-group-ids)))))) |
(defn- session-data [jwt {{redirect :return_to} :params, :as request}] (let [redirect-url (or redirect (URLEncoder/encode "/"))] (sso-utils/check-sso-redirect redirect-url) (let [jwt-data (try (jwt/unsign jwt (sso-settings/jwt-shared-secret) {:max-age three-minutes-in-seconds}) (catch Throwable e (throw (ex-info (ex-message e) {:status "error-jwt-bad-unsigning" :status-code 401})))) login-attrs (jwt-data->login-attributes jwt-data) email (get jwt-data (jwt-attribute-email)) first-name (get jwt-data (jwt-attribute-firstname)) last-name (get jwt-data (jwt-attribute-lastname)) user (fetch-or-create-user! first-name last-name email login-attrs) session (session/create-session! :sso user (request/device-info request))] (sync-groups! user jwt-data) {:session session, :redirect-url redirect-url, :jwt-data jwt-data}))) | |
(defn- check-jwt-enabled [] (when-not (sso-settings/jwt-configured) (throw (ex-info (tru "JWT SSO has not been configured") {:status "error-sso-jwt-not-configured" :status-code 402}))) (when-not (sso-settings/jwt-enabled) (throw (ex-info (tru "JWT SSO has not been enabled") {:status "error-sso-jwt-disabled" :status-code 402}))) true) | |
(defn ^:private generate-response-token [session jwt-data] (if-not (embed.settings/enable-embedding-sdk) (throw (ex-info (tru "SDK Embedding is disabled. Enable it in the Embedding settings.") {:status "error-embedding-sdk-disabled" :status-code 402})) (response/response {:status :ok :id (:id session) :exp (:exp jwt-data) :iat (:iat jwt-data)}))) | |
(defn ^:private redirect-to-idp [idp redirect] (let [return-to-param (if (str/includes? idp "?") "&return_to=" "?return_to=")] (response/redirect (str idp (when redirect (str return-to-param redirect)))))) | |
(defn ^:private handle-jwt-authentication [{:keys [session redirect-url jwt-data]} token request] (if token (generate-response-token session jwt-data) (request/set-session-cookies request (response/redirect redirect-url) session (t/zoned-date-time (t/zone-id "GMT"))))) | |
(defmethod sso.i/sso-get :jwt [{{:keys [jwt redirect token] :or {token nil}} :params, :as request}] (premium-features/assert-has-feature :sso-jwt (tru "JWT-based authentication")) (check-jwt-enabled) (if jwt (handle-jwt-authentication (session-data jwt request) token request) (redirect-to-idp (sso-settings/jwt-identity-provider-uri) redirect))) | |
(defmethod sso.i/sso-post :jwt [_] (throw (ex-info (tru "POST not valid for JWT SSO requests") {:status "error-post-jwt-not-valid" :status-code 501}))) | |