Safe Haskell | None |
---|---|
Language | GHC2021 |
SAML2.WebSSO.API
Description
This is a partial implementation of Web SSO using the HTTP Post Binding [2/3.5].
The default API offers 3 end-points: one for retrieving the AuthnRequest
in a redirect to the
IdP; one for delivering the AuthnResponse
that will re-direct to some fixed landing page; and
one for retrieving the SP's metadata.
There are other scenarios, e.g. all resources on the page could be guarded with an authentication check and redirect the client to the IdP, and make sure that the client lands on the initally requested resource after successful authentication. With the building blocks provided by this module, it should be straight-forward to implemented all of these scenarios.
This module works best if imported qualified.
FUTUREWORK: servant-server is quite heavy. we should have a cabal flag to exclude it.
Synopsis
- meta :: (SPStore m, MonadError err m, HasConfig m) => ST -> m Issuer -> m URI -> m [ContactPerson] -> m SPMetadata
- type API = APIMeta' :<|> (APIAuthReq' :<|> APIAuthResp')
- type Cky = SimpleSetCookie CookieName
- type APIMeta = Get '[XML] SPMetadata
- type APIAuthReq = Capture "idp" IdPId :> Get '[HTML] (FormRedirect AuthnRequest)
- data FormRedirect xml = FormRedirect URI xml
- type APIAuthResp = MultipartForm Mem AuthnResponseBody :> PostRedir '[HTML] (WithCookieAndLocation ST)
- data AuthnResponseBody = AuthnResponseBody {
- authnResponseBodyAction :: forall (m :: Type -> Type) err spid extra. (SPStoreIdP (Error err) m, spid ~ IdPConfigSPId m, extra ~ IdPConfigExtra m) => Maybe spid -> m (NonEmpty Assertion, IdPConfig extra, UnvalidatedSAMLStatus)
- authnResponseBodyRaw :: MultipartData Mem
- type WithCookieAndLocation = Headers '[Header "Set-Cookie" Cky, Header "Location" URI]
- type APIMeta' = "meta" :> APIMeta
- type APIAuthReq' = "authreq" :> APIAuthReq
- type APIAuthResp' = "authresp" :> APIAuthResp
- defSPIssuer :: (Functor m, HasConfig m) => m Issuer
- defResponseURI :: (Functor m, HasConfig m) => m URI
- defContactPersons :: (Functor m, HasConfig m) => m [ContactPerson]
- parseAuthnResponseBody :: forall m err spid extra. (SPStoreIdP (Error err) m, spid ~ IdPConfigSPId m, extra ~ IdPConfigExtra m) => Maybe spid -> ST -> m (NonEmpty Assertion, IdPConfig extra, UnvalidatedSAMLStatus)
- idpToCreds :: forall m err extra. (SPStoreIdP (Error err) m, extra ~ IdPConfigExtra m) => Issuer -> IdPConfig extra -> m (NonEmpty SignCreds)
- simpleVerifyAuthnResponse :: forall m err. MonadError (Error err) m => NonEmpty SignCreds -> LBS -> m (NonEmpty Assertion)
- issuerToCreds :: forall m err. SPStoreIdP (Error err) m => Maybe Issuer -> Maybe (IdPConfigSPId m) -> m (NonEmpty SignCreds)
- allVerifies :: forall m err. MonadError (Error err) m => NonEmpty SignCreds -> LBS -> NonEmpty String -> m (NonEmpty Assertion)
- verifyADFS :: MonadError (Error err) m => NonEmpty SignCreds -> LBS -> NonEmpty String -> m (NonEmpty XmlTree)
- class HasXML xml => HasFormRedirect xml where
- formRedirectFieldName :: xml -> ST
- enterH :: SP m => String -> m ()
- authreq :: (SPStore m, SPStoreIdP err m, MonadError err m) => NominalDiffTime -> m Issuer -> IdPId -> m (FormRedirect AuthnRequest)
- leaveH :: (Monad m, Show a, SP m) => a -> m a
- authreq' :: (SPStore m, SPStoreIdP err m, MonadError err m) => m Issuer -> IdPId -> m (FormRedirect AuthnRequest)
- defReqTTL :: NominalDiffTime
- authresp :: (SPStoreIdP (Error err) m, SPStoreRequest AuthnRequest m, extra ~ IdPConfigExtra m, SPStore m) => Maybe (IdPConfigSPId m) -> m Issuer -> m URI -> (NonEmpty Assertion -> IdPConfig extra -> AccessVerdict -> m resp) -> AuthnResponseBody -> m resp
- authresp' :: forall m err extra. (SPStoreIdP (Error err) m, SP m, MonadError (Error err) m, SPStore m, extra ~ IdPConfigExtra m) => Maybe (IdPConfigSPId m) -> m Issuer -> m URI -> HandleVerdict m -> AuthnResponseBody -> m (WithCookieAndLocation ST)
- data HandleVerdict (m :: Type -> Type)
- simpleHandleVerdict :: (SP m, MonadError (Error err) m) => OnSuccessRedirect m -> AccessVerdict -> m (WithCookieAndLocation ST)
- newtype ResponseVerdict = ResponseVerdict {}
- type OnSuccessRedirect (m :: Type -> Type) = UserRef -> m (Cky, URI)
- type CookieName = "saml2-web-sso"
- data SubjectFoldCase
- simpleOnSuccess :: (Monad m, SP m) => SubjectFoldCase -> OnSuccessRedirect m
- module SAML2.WebSSO.Servant
Documentation
meta :: (SPStore m, MonadError err m, HasConfig m) => ST -> m Issuer -> m URI -> m [ContactPerson] -> m SPMetadata Source #
type API = APIMeta' :<|> (APIAuthReq' :<|> APIAuthResp') Source #
Consider rate-limiting these end-points to mitigate DOS attacks. APIAuthReq
uses database
space, and APIAuthResp
uses both database space and CPU.
type Cky = SimpleSetCookie CookieName Source #
type APIAuthReq = Capture "idp" IdPId :> Get '[HTML] (FormRedirect AuthnRequest) Source #
data FormRedirect xml Source #
- 2/3.5.4
Constructors
FormRedirect URI xml |
Instances
type APIAuthResp = MultipartForm Mem AuthnResponseBody :> PostRedir '[HTML] (WithCookieAndLocation ST) Source #
data AuthnResponseBody Source #
An AuthnResponseBody
contains a AuthnResponse
, but you need to give it an SPId for
IdP lookup and trust base for signature verification first. As a consequence, you will get
the signature validation error when looking at it, but the type still guarantees that the
signature verification cannot be circumvented.
Constructors
AuthnResponseBody | |
Fields
|
Instances
FromMultipart Mem AuthnResponseBody Source # | |
Defined in SAML2.WebSSO.API Methods fromMultipart :: MultipartData Mem -> Either String AuthnResponseBody Source # |
type APIAuthReq' = "authreq" :> APIAuthReq Source #
type APIAuthResp' = "authresp" :> APIAuthResp Source #
defSPIssuer :: (Functor m, HasConfig m) => m Issuer Source #
The Issuer
is an identifier of a SAML participant. In this case, it's the SP, ie.,
ourselves. For simplicity, we re-use the response URI here.
defResponseURI :: (Functor m, HasConfig m) => m URI Source #
The URI that AuthnResponse
values are delivered to (APIAuthResp
).
defContactPersons :: (Functor m, HasConfig m) => m [ContactPerson] Source #
parseAuthnResponseBody :: forall m err spid extra. (SPStoreIdP (Error err) m, spid ~ IdPConfigSPId m, extra ~ IdPConfigExtra m) => Maybe spid -> ST -> m (NonEmpty Assertion, IdPConfig extra, UnvalidatedSAMLStatus) Source #
Implies verification, hence the constraints and the optional service provider ID (needed for IdP lookup).
idpToCreds :: forall m err extra. (SPStoreIdP (Error err) m, extra ~ IdPConfigExtra m) => Issuer -> IdPConfig extra -> m (NonEmpty SignCreds) Source #
simpleVerifyAuthnResponse :: forall m err. MonadError (Error err) m => NonEmpty SignCreds -> LBS -> m (NonEmpty Assertion) Source #
Pull assertions sub-forest and pass unparsed xml input to verify
with a reference to
each assertion individually. The input must be a valid AuthnResponse
. All assertions
need to be signed by the issuer given in the arguments using the same key.
The assertions are returned.
NEVER PROCESS AN ASSERTION NOT RETURNED BY A SIGNATURE VERifICATION FUNCTION.
simpleVerifyAuthnResponse
ensures that the assertion list is non-empty, and (more
importantly) that the IDs are unique accross the entire document we pass to the signature
validation function. REASON: since we use xml-conduit as a parser here, and signature
validation uses a different one, we can't guarantee that the assertions we return here are
the ones of which we validated the signatures. this problem can get very real very
quickly:
https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials
issuerToCreds :: forall m err. SPStoreIdP (Error err) m => Maybe Issuer -> Maybe (IdPConfigSPId m) -> m (NonEmpty SignCreds) Source #
allVerifies :: forall m err. MonadError (Error err) m => NonEmpty SignCreds -> LBS -> NonEmpty String -> m (NonEmpty Assertion) Source #
Call verify and, if that fails, any work-arounds we have. Discard all errors from work-arounds, and throw the error from the regular verification.
verifyADFS :: MonadError (Error err) m => NonEmpty SignCreds -> LBS -> NonEmpty String -> m (NonEmpty XmlTree) Source #
ADFS illegally breaks whitespace after signing documents; here we try to fix that. https://github.com/wireapp/wire-server/issues/656 (This may also have been a copy&paste issue in customer support, but let's just leave it in just in case.)
class HasXML xml => HasFormRedirect xml where Source #
Methods
formRedirectFieldName :: xml -> ST Source #
Instances
HasFormRedirect SomeSAMLRequest Source # | |
Defined in SAML2.WebSSO.Test.Util.Misc Methods | |
HasFormRedirect AuthnRequest Source # | |
Defined in SAML2.WebSSO.API Methods |
authreq :: (SPStore m, SPStoreIdP err m, MonadError err m) => NominalDiffTime -> m Issuer -> IdPId -> m (FormRedirect AuthnRequest) Source #
Create authnreq, store it for comparison against assertions later, and return it in an HTTP redirect together with the IdP's URI.
authreq' :: (SPStore m, SPStoreIdP err m, MonadError err m) => m Issuer -> IdPId -> m (FormRedirect AuthnRequest) Source #
authreq
with request life expectancy defaulting to 8 hours.
authresp :: (SPStoreIdP (Error err) m, SPStoreRequest AuthnRequest m, extra ~ IdPConfigExtra m, SPStore m) => Maybe (IdPConfigSPId m) -> m Issuer -> m URI -> (NonEmpty Assertion -> IdPConfig extra -> AccessVerdict -> m resp) -> AuthnResponseBody -> m resp Source #
parse and validate response, and pass the verdict to a user-provided verdict handler. the
handler takes a response and a verdict (provided by this package), and can cause any effects in
m
and return anything it likes.
authresp' :: forall m err extra. (SPStoreIdP (Error err) m, SP m, MonadError (Error err) m, SPStore m, extra ~ IdPConfigExtra m) => Maybe (IdPConfigSPId m) -> m Issuer -> m URI -> HandleVerdict m -> AuthnResponseBody -> m (WithCookieAndLocation ST) Source #
a variant of authresp
with a less general verdict handler.
data HandleVerdict (m :: Type -> Type) Source #
We support two cases: redirect with a cookie, and a generic response with arbitrary status,
headers, and body. The latter case fits the ServerError
type well, but we give it a more
suitable name here.
Constructors
HandleVerdictRedirect (OnSuccessRedirect m) | |
HandleVerdictRaw (NonEmpty Assertion -> AccessVerdict -> m ResponseVerdict) |
simpleHandleVerdict :: (SP m, MonadError (Error err) m) => OnSuccessRedirect m -> AccessVerdict -> m (WithCookieAndLocation ST) Source #
newtype ResponseVerdict Source #
Constructors
ResponseVerdict | |
Fields |
Instances
Show ResponseVerdict Source # | |
Defined in SAML2.WebSSO.API Methods showsPrec :: Int -> ResponseVerdict -> ShowS # show :: ResponseVerdict -> String # showList :: [ResponseVerdict] -> ShowS # | |
Eq ResponseVerdict Source # | |
Defined in SAML2.WebSSO.API Methods (==) :: ResponseVerdict -> ResponseVerdict -> Bool # (/=) :: ResponseVerdict -> ResponseVerdict -> Bool # |
type CookieName = "saml2-web-sso" Source #
data SubjectFoldCase Source #
Constructors
SubjectFoldCase | |
SubjectDontFoldCase |
simpleOnSuccess :: (Monad m, SP m) => SubjectFoldCase -> OnSuccessRedirect m Source #
module SAML2.WebSSO.Servant