-
adam-james authored
* Adjust JWT and SAML fetch-and-update user to save new attributes Before this change, JWT/SAML logins would attempt to update attributes, but never considered the first-name or last-name attributes. * Attempts to fix tests to prevent pulluting test users with "Unknown" * No deleting users. * Unit tests checking that first/last names are updated for SSO users When an SSO user is first logged in, they might not have first_name and/or last_name keys. This is allowed, but the names will be "Unknown" in the app-db. Subsequently, a User may log in again with SSO but have fisrt/last name attributes, which should update the Metabase user data in the app-db. These unit tests set up such a scenario to check that the :first_name and :last_name keys are indeed updated. * Adjust Enterprise LDAP to also use SSO-UTILS Trying to unify the LDAP implementation with JWT/SAML a bit here. * Lint error * Reverting LDAP ns changes to get the PR unstuck This is to keep the ball rolling on SSO fixes. I'll add LDAP as an item in the Epic to address this separately.
adam-james authored* Adjust JWT and SAML fetch-and-update user to save new attributes Before this change, JWT/SAML logins would attempt to update attributes, but never considered the first-name or last-name attributes. * Attempts to fix tests to prevent pulluting test users with "Unknown" * No deleting users. * Unit tests checking that first/last names are updated for SSO users When an SSO user is first logged in, they might not have first_name and/or last_name keys. This is allowed, but the names will be "Unknown" in the app-db. Subsequently, a User may log in again with SSO but have fisrt/last name attributes, which should update the Metabase user data in the app-db. These unit tests set up such a scenario to check that the :first_name and :last_name keys are indeed updated. * Adjust Enterprise LDAP to also use SSO-UTILS Trying to unify the LDAP implementation with JWT/SAML a bit here. * Lint error * Reverting LDAP ns changes to get the PR unstuck This is to keep the ball rolling on SSO fixes. I'll add LDAP as an item in the Epic to address this separately.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
jwt.clj 5.10 KiB
(ns metabase-enterprise.sso.integrations.jwt
"Implementation of the JWT backend for sso"
(:require [buddy.sign.jwt :as jwt]
[clojure.string :as str]
[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.api.common :as api]
[metabase.api.session :as api.session]
[metabase.integrations.common :as integrations.common]
[metabase.server.middleware.session :as mw.session]
[metabase.server.request.util :as request.u]
[metabase.util.i18n :refer [trs tru]]
[ring.util.response :as response])
(:import java.net.URLEncoder))
(defn fetch-or-create-user!
"Returns a session map for the given `email`. Will create the user if needed."
[first-name last-name email user-attributes]
(when-not (sso-settings/jwt-configured?)
(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/create-new-sso-user! (merge user
(when-not first-name {:first_name (trs "Unknown")})
(when-not last-name {:last_name (trs "Unknown")}))))))
(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))
(defn- jwt-data->login-attributes [jwt-data]
(dissoc jwt-data
(jwt-attribute-email)
(jwt-attribute-firstname)
(jwt-attribute-lastname)
:iat
:max_age))
;; JWTs use seconds since Epoch, not milliseconds since Epoch for the `iat` and `max_age` time. 3 minutes is the time
;; used by Zendesk for their JWT SSO, so it seemed like a good place for us to start
(def ^:private ^:const three-minutes-in-seconds 180)
(defn- group-names->ids
"Translate a user's group names to a set of MB group IDs using the configured mappings"
[group-names]
(set (mapcat (sso-settings/jwt-group-mappings)
(map keyword group-names))))
(defn- all-mapped-group-ids
"Returns the set of all MB group IDs that have configured mappings"
[]
(-> (sso-settings/jwt-group-mappings)
vals
flatten
set))
(defn- sync-groups!
"Sync a user's groups based on mappings configured in the JWT settings"
[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)]
(integrations.common/sync-group-memberships! user
(group-names->ids group-names)
(all-mapped-group-ids))))))
(defn- login-jwt-user
[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)
(assoc (ex-data e) :status-code 401)
e))))
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 (api.session/create-session! :sso user (request.u/device-info request))]
(sync-groups! user jwt-data)
(mw.session/set-session-cookie request (response/redirect redirect-url) session))))
(defn- check-jwt-enabled []
(api/check (sso-settings/jwt-configured?)
[400 (tru "JWT SSO has not been enabled and/or configured")]))
(defmethod sso.i/sso-get :jwt
[{{:keys [jwt redirect]} :params, :as request}]
(check-jwt-enabled)
(if jwt
(login-jwt-user jwt request)
(let [idp (sso-settings/jwt-identity-provider-uri)
return-to-param (if (str/includes? idp "?") "&return_to=" "?return_to=")]
(response/redirect (str idp (when redirect
(str return-to-param redirect)))))))
(defmethod sso.i/sso-post :jwt
[_]
(throw (ex-info "POST not valid for JWT SSO requests" {:status-code 400})))