/auth/sso Routes.

Implements the SSO routes needed for SAML and JWT. This namespace primarily provides hooks for those two backends so we can have a uniform interface both via the API and code

(ns metabase-enterprise.sso.api.sso
  (:require
   [metabase-enterprise.sso.api.interface :as sso.i]
   [metabase-enterprise.sso.integrations.jwt]
   [metabase-enterprise.sso.integrations.saml]
   [metabase-enterprise.sso.integrations.sso-settings :as sso-settings]
   [metabase.api.common :as api]
   [metabase.api.macros :as api.macros]
   [metabase.request.core :as request]
   [metabase.util :as u]
   [metabase.util.log :as log]
   [metabase.util.malli :as mu]
   [metabase.util.urls :as urls]
   [saml20-clj.core :as saml]
   [saml20-clj.encode-decode :as encode-decode]
   [stencil.core :as stencil]
   [toucan2.core :as t2]))
(set! *warn-on-reflection* true)

load the SSO integrations so their implementations for the multimethods below are available.

(comment metabase-enterprise.sso.integrations.jwt/keep-me
         metabase-enterprise.sso.integrations.saml/keep-me)

GET /auth/sso

(api.macros/defendpoint :get "/"
  "SSO entry-point for an SSO user that has not logged in yet"
  [_route-params _query-params _body request]
  (try
    (sso.i/sso-get request)
    (catch Throwable e
      (log/error #_e "Error returning SSO entry point")
      (throw e))))
(mu/defn- sso-error-page
  [^Throwable e log-direction :- [:enum :in :out]]
  {:status  (get (ex-data e) :status-code 500)
   :headers {"Content-Type" "text/html"}
   :body    (stencil/render-file "metabase_enterprise/sandbox/api/error_page"
                                 (let [message    (.getMessage e)
                                       data       (u/pprint-to-str (ex-data e))]
                                   {:logDirection   (name log-direction)
                                    :errorMessage   message
                                    :exceptionClass (.getName Exception)
                                    :additionalData data}))})

POST /auth/sso

(api.macros/defendpoint :post "/"
  "Route the SSO backends call with successful login details"
  [_route-params _query-params _body request]
  (try
    (sso.i/sso-post request)
    (catch Throwable e
      (log/error e "Error logging in")
      (sso-error-page e :in))))

------------------------------ Single Logout aka SLO ------------------------------

The url that the IdP should respond to. Not all IdPs support this, but it's a good idea to send it just in case.

(def metabase-slo-redirect-url
  "/auth/sso/handle_slo")

POST /auth/sso/logout

(api.macros/defendpoint :post "/logout"
  "Logout."
  [_route-params _query-params _body {cookies :cookies, :as _request}]
  (let [metabase-session-id (get-in cookies [request/metabase-session-cookie :value])]
    (api/check-exists? :model/Session metabase-session-id)
    (let [{:keys [email sso_source]}
          (t2/query-one {:select [:u.email :u.sso_source]
                         :from   [[:core_user :u]]
                         :join   [[:core_session :session] [:= :u.id :session.user_id]]
                         :where  [:= :session.id metabase-session-id]})]
      ;; If a user doesn't have SLO setup on their IdP,
      ;; they will never hit "/handle_slo" so we must delete the session here:
      (t2/delete! :model/Session :id metabase-session-id)
      {:saml-logout-url
       (when (and (sso-settings/saml-slo-enabled)
                  (= sso_source "saml"))
         (saml/logout-redirect-location
          :idp-url (sso-settings/saml-identity-provider-uri)
          :issuer (sso-settings/saml-application-name)
          :user-email email
          :relay-state (encode-decode/str->base64
                        (str (urls/site-url) metabase-slo-redirect-url))))})))

POST /auth/sso/handle_slo

(api.macros/defendpoint :post "/handle_slo"
  "Handles client confirmation of saml logout via slo"
  [_route-params _query-params _body request]
  (try
    (if (sso-settings/saml-slo-enabled)
      (sso.i/sso-handle-slo request)
      (throw (ex-info "SAML Single Logout is not enabled, request forbidden."
                      {:status-code 403})))
    (catch Throwable e
      (log/error e "Error handling SLO")
      (sso-error-page e :out))))