Skip to content

metabase/saml20-clj

 
 

Repository files navigation

saml20-clj

Downloads Linters Tests Release codecov License -cljdoc badge

Clojars Project

This is a SAML 2.0 Clojure library for SSO acting as a thin wrapper around the Java libraries OpenSAML v4 and some utility functions from OneLogin's SAML library This library allows a Clojure application to act as a Service Provider (SP).

Important note!

The underlying Java library, OpenSAML, is no longer available in Maven Central, so you'll have to add the repository yourself. deps.edn example:

{:mvn/repos
 {"opensaml"{:url "https://build.shibboleth.net/nexus/content/repositories/releases/"}}}

2.0.0 Usage

Creating metadata

In order for an identityprovider to understand you as a service-provider, you need to provide metadata about your service. This is done in the following manner:

(in-ns my-saml.core
  (:require [saml20-clj.core :as saml-core]
            [saml20-clj.coerce :as saml-coerce]))

(def config {:app-name "My Fancy App"
             :acs-url "https://my-app.com/saml/login"
             :slo-url "https://my-app.com/saml/logout"})

(def credentials {:alias "my-saml-secrets"
                  :filename "path/to/keystorefile.jks"
                  :password "s1krit"})

(def metadata (-> {:sp-cert (saml-coerce/->X509Certificate credentials)}
                  (merge config)
                  saml-core/metadata))

Recording Requests

You can keep track of which requests are in flight to determine whether responses correspond to valid requests we've issued and whether we've already got a response for a request (e.g. to replay attacks) by using a StateManager. This library ships with a simple in-memory state manager suitable for a single instance, but you can create your own implementation if you need something more sophisticated.

(require '[saml20-clj.core :as saml])

(def state-manager (saml/in-memory-state-manager))

Logging In (SSO)

Requests

Basic usage for requests to the IdP looks like:

(require '[saml20-clj.core :as saml])

;; create a new request
(-> (saml/request
     {:sp-name          "My SP Name"
      :acs-url          "http://sp.example.com/demo1/index.php?acs"
      :idp-url          "http://idp.example.com/SSOService.php"
      :issuer           "http://sp.example.com/demo1/metadata.php"
      ;; state manager (discussed above) is optional, but if passed `request` will record the newly created request.
      :state-manager    state-manager
      ;; :credential is optional. If passed, sign the request with this key and attach public key data, if present
      :credential       sp-private-key})
    ;; create a Ring redirect response to the IDP URL; pass the request as base-64 encoded `SAMLRequest` query parameter
    (saml/idp-redirect-response "http://idp.example.com/SSOService.php"
                                ;; This is RelayState. In the old version of the lib it was encrypted. In some cases,
                                ;; like this it's not really sensitive so it doesn't need to be encrypted. Adding
                                ;; automatic encryption support back is on the TODO list
                                "http://sp.example.com/please/redirect/me/to/here"))

The :credential can be used to sign the request to the IdP, and attach any public key information (if present). It will happily accept several formats, depending on the use-case:

  • private-key: A PEM formatted string
  • [public-cert private-key]: A tuple containing an X509 Certificate and a private key, both in PEM format
  • {:filepath "/path/to/keystore" :password "keystore-password" :alias "key-alias"}: A map describing a keystore and alias used.

Responses

Basic usage for responses from the IdP looks like this (assuming a Ring request):

(require '[saml20-clj.core :as saml])
(require '[saml20-clj.encode-decode :as saml-decode])

(-> request
    :params
    :SAML-response
    saml-decode/base64->str
    ;; Coerce the response to an OpenSAML `Response`. This can be anything from a raw XML string to a parsed
    ;; `org.w3c.dom.Document`
    saml/->Response
    ;; decrypt and validate the response. Returns decrypted response
    (saml/validate idp-cert sp-private-key options)
    ;; convert the Assertions to a convenient Clojure map so you can do something with them
    saml/assertions)

validate options

validate accepts an options map that allows you to configure what validations are done, as well as the stateful parameters (if relevent) those validations are verified against. The list of options and their defaults are shown below:

{ ;; e.g. "http://sp.example.com/demo1/index.php?acs" The assertion consumer service URL. It is *required*
 ;; always pass this value, as the SAML20 spec dictates that any Recipient field within a <SubjectConfirmationData>
 ;; must be checked against the :acs-url.
 :acs-url                      nil

 ;; The ID of the request we (the SP) sent to the IdP. ID is generated on our end, and should be something like a UUID
 ;; rather than a sequential number. If non-nil, the :in-response-to validator checks that <SubjectConfirmationData>
 ;; nodes have a value of InResponseTo that matches an ID.
 ;;
 ;; The state manager implementation that ships with this library does not keep request state; InResponseTo validation
 ;; is provided as an option in case you write your own more sophisticated implementation.
 :request-id                   (str (java.util.UUID/randomUUID))

 ;; If passed, must refer to an implementation of the StateManager protocol (either the built-in `in-memory-state-manager`
 ;; suitable for a single instance or a custom implementation suitable for your deployment). The StateManager, if enabled,
 ;; should record the `:request-id` and verify it against any returning response. Please refer to `state.clj` for implementation
 ;; details.

 ;; Note that enforcement of the `:state-manager` requires enabling the `:valid-request-id` response validator (which is
 ;; enabled by default).
 :state-manager		       nil

 ;; whether this response was solicited (i.e., in response to a request we sent to the IdP). If this is false, the
 ;; :in-response-to validator checks that the request-id is nil.
 :solicited?                   true

 ;; maximum amount of clock skew to allow for the :not-on-or-after and :not-before validators
 :allowable-clock-skew-seconds 180

 ;; address of the client. If set, the :address validator will check that <SubjectConfirmationData> nodes have an
 ;; Address matching this value *iff* Address is present. Address is optional attribute.
 :user-agent-address           nil

 ;; Unique identifier of the IdP. Also referred to as Entity ID or 'Issuer'. If passed, the `:issuer` validators will check
 ;; that the <Issuer> property for <Response> and <Assertion>s matches.
 :issuer                      nil

 ;; :response-validators and :assertion-validators are validation functions that run and check that the Response is
 ;; valid. If a check fails, these methods will throw an Exception. You can exclude some of these validators or add
 ;; your own by passing different values for these keys. Both types of validators are defined as multimethods; you can
 ;; add custom validators by adding more method implementations to their respective multimethods.

 ;; :response-validators are validation functions that run once for the entire Response. They are defined as
 ;; implementations of the saml20-clj.sp.response/validate-response multimethod.
 :response-validators (see below)


 ;; :assertion validators are validation functions that run against every Assertion in the response. They are defined
 ;; as implementations of saml20-clj.sp.response/validate-assertion.
 :assertion-validators (see below)
}

Default :response-validators

[;; If the <Response> itself is signed, verifies that this signature is matches the Response itself and matches the
 ;; IdP certificate. If Response is not signed, this validator does nothing.
 :signature

 ;; requires that either the <Response> is signed, *every* <Assertion> is signed.
 :require-signature

 ;; If the :issuer option is passed and <Response> has <Issuer> information, checks that these match.
 :issuer

 ;; validates the request ID with :state-manager if it is passed as an option. This does not validate that the value
 ;; matches InResponseTo -- that is done by :in-response-to.
 :valid-request-id]

Default :assertion-validators

[;; If <Assertion> is signed, the signature matches the Assertion and the IdP certificate. If <Assertion> is not
 ;; signed, this validator does nothing.
 :signature

 ;; If set, validation will ensure that all Assertions in the response are encrypted. If *any* unencrypted Assertions
 ;; are present, verification will fail
 :require-encryption

 ;; If :acs-url is non-nil, and <SubjectConfirmationData> is present, checks that <SubjectConfirmationData> has a
 ;; Recipient attribute matching this value.
 :recipient

 ;; If the :issuer option is passed, checks that Assertions have <Issuer> information and that it matches :issuer.
 :issuer

 ;; If <SubjectConfirmationData> is present, has a NotOnOrAfter attribute, and its value is in the future,
 ;; accounting for :allowable-clock-skew-seconds
 :not-on-or-after

 ;; If <SubjectConfirmationData> has a NotBefore attribute, checks that this value is in the past, accounting for
 ;; :allowable-clock-skew-seconds
 :not-before

 ;; If :request-id is non-nil and <SubjectConfirmationData> is present, checks that <SubjectConfirmationData> has an
 ;; InResponseTo attribute matching :request-id.
 :in-response-to

 ;; If :user-agent-address is non-nil and <SubjectConfirmationData> has an Address attribute, checks that Address
 ;; matches this value.
 :address]

Logging Out (SLO)

Requests

Basic usage for logging out is to send the client a redirect to the IdP, with a LogoutResponse SAML message. This is done in the following manner:

(request/idp-logout-redirect-response
  "Your SP Name"
  "logmeout@example.com" ;; the user's email
  "http://sp.example.com/demo1/metadata.php"
  (encode-decode/str->base64 "http://sp.example.com/demo1/metadata.php"))
  "my_random_id_42") ;; req-id is optional, and will get created for you.

Some clients will prefer that you send them the SAMLRequest as a query parameter, and they will handle the redirect, for that purpose you can use the logout-redirect-location function, which will include the RelayState and SAMLRequest as query parameters.

(request/logout-redirect-location
   {:issuer     "http://sp.example.com/demo1/metadata.php"
    :user-email "user@example.com"
    :idp-url    "http://idp.example.com/SSOService.php"
    :request-id "ONELOGIN_109707f0030a5d00620c9d9df97f627afe9dcc24"
    :relay-state (encode-decode/str->base64 "http://sp.example.com/demo1/metadata.php")})

;; =>
;; "http://idp.example.com/SSOService.php?SAMLRequest=fVLLbs<snip>&RelayState=aHR<snip>"

Responses

The IdP will redirect the client back to you, with a SAMLResponse in their query-params. You can validate this response by checking for the SAMLResponse's Status.

Differences from the original saml20-clj library

This repository is forked from vlacs/saml20-clj, and at this point is more or less a complete re-write.

  • Other improvements:
    • Uses OpenSAML v4 instead of OpenSAML v2 which was EOL'ed in 2016
    • Tons of bug fixes, such as saml20-clj.shared/base64->inflate->str not actually calling byte-inflate at all
    • Fixed millions of reflection warnings
    • Removed duplicate functions
    • Support for XML signing with SHA-256 instead of SHA-1, which is required by ADFS by default (via k2n/saml20-clj)
    • Support for Clojure 1.10+
    • Support for base-64 encodings that contain newlines
    • Removed lots of dependencies on other libraries
    • Reorganized code
    • Removed tons of duplicate/unnecessary, untested code
    • Fixed <Assertion> signatures not being validated
    • Added Single Logout (SLO)

License

Distributed under the Eclipse Public License, the same as Clojure.

About

A Clojure SAML 2.0 library for SSO

Topics

Resources

Security policy

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Clojure 99.7%
  • Other 0.3%