wai-saml2 0.2.1.3 → 0.3.0.0
raw patch · 15 files changed
+391/−280 lines, 15 filesdep +filepathdep +pretty-showdep +tastydep ~basedep ~bytestringdep ~xml-conduitPVP ok
version bump matches the API change (PVP)
Dependencies added: filepath, pretty-show, tasty, tasty-golden, wai-saml2
Dependency ranges changed: base, bytestring, xml-conduit
API changes (from Hackage documentation)
+ Network.Wai.SAML2.Assertion: [subjectNameId] :: Subject -> !SubjectNameID
+ Network.Wai.SAML2.Assertion: instance GHC.Classes.Eq Network.Wai.SAML2.Assertion.SubjectNameID
+ Network.Wai.SAML2.Assertion: instance GHC.Show.Show Network.Wai.SAML2.Assertion.SubjectNameID
+ Network.Wai.SAML2.Assertion: instance Network.Wai.SAML2.XML.FromXML Network.Wai.SAML2.Assertion.SubjectNameID
- Network.Wai.SAML2.Assertion: Subject :: ![SubjectConfirmation] -> Subject
+ Network.Wai.SAML2.Assertion: Subject :: ![SubjectConfirmation] -> !SubjectNameID -> Subject
- Network.Wai.SAML2.XML.Encrypted: EncryptedKey :: !Text -> !Text -> !EncryptionMethod -> !KeyInfo -> !CipherData -> EncryptedKey
+ Network.Wai.SAML2.XML.Encrypted: EncryptedKey :: !Text -> !Text -> !EncryptionMethod -> !Maybe KeyInfo -> !CipherData -> EncryptedKey
- Network.Wai.SAML2.XML.Encrypted: [encryptedKeyData] :: EncryptedKey -> !KeyInfo
+ Network.Wai.SAML2.XML.Encrypted: [encryptedKeyData] :: EncryptedKey -> !Maybe KeyInfo
Files
- CHANGELOG.md +23/−3
- src/Network/Wai/SAML2.hs +34/−34
- src/Network/Wai/SAML2/Assertion.hs +52/−33
- src/Network/Wai/SAML2/C14N.hs +4/−4
- src/Network/Wai/SAML2/Config.hs +4/−4
- src/Network/Wai/SAML2/Error.hs +12/−12
- src/Network/Wai/SAML2/KeyInfo.hs +4/−4
- src/Network/Wai/SAML2/Response.hs +24/−25
- src/Network/Wai/SAML2/Signature.hs +32/−32
- src/Network/Wai/SAML2/StatusCode.hs +8/−8
- src/Network/Wai/SAML2/Validation.hs +67/−67
- src/Network/Wai/SAML2/XML.hs +18/−18
- src/Network/Wai/SAML2/XML/Encrypted.hs +42/−34
- tests/Parser.hs +28/−0
- wai-saml2.cabal +39/−2
CHANGELOG.md view
@@ -1,11 +1,31 @@-# 0.2.1+# Changelog for `wai-saml2` +## Unreleased++* Improve parse error handling and make `encryptedKeyData` optional ([#11](https://github.com/mbg/wai-saml2/pull/11) by [@Philonous](https://github.com/Philonous))+* Add `subjectNameId` to `Subject` type ([#13](https://github.com/mbg/wai-saml2/pull/13) by [@kdxu](https://github.com/kdxu))+* Support the response format used by Okta, in which the `EncryptedAssertion` element is structured differently ([#12](https://github.com/mbg/wai-saml2/pull/12) by [@fumieval](https://github.com/fumieval))++## 0.2.1.3++* Metadata updates.++## 0.2.1.2++No changes.++## 0.2.1.1++* Export `Result` type from `Network.Wai.SAML2` module.++## 0.2.1+ * Fix missing export of `relayStateKey` and change its type. -# 0.2.0+## 0.2.0 * Added parsing for RelayState from form data, as sent by e.g. Shibboleth when a `target` query string parameter is passed to the unsolicited SSO endpoint. -# 0.1.0+## 0.1.0 * Initial release
src/Network/Wai/SAML2.hs view
@@ -9,7 +9,7 @@ -- interfaces are supported (with equivalent functionality): one which simply -- stores the outcome of the validation process in the request vault and one -- which passes the outcome to a callback.-module Network.Wai.SAML2 ( +module Network.Wai.SAML2 ( -- * Callback-based middleware -- -- $callbackBasedMiddleware@@ -17,7 +17,7 @@ saml2Callback, -- * Vault-based middleware- -- + -- -- $vaultBasedMiddleware assertionKey, errorKey,@@ -28,7 +28,7 @@ module Network.Wai.SAML2.Config, module Network.Wai.SAML2.Error, module Network.Wai.SAML2.Assertion-) where +) where -------------------------------------------------------------------------------- @@ -36,7 +36,7 @@ import Data.Maybe (fromMaybe) import qualified Data.Vault.Lazy as V -import Network.Wai +import Network.Wai import Network.Wai.Parse import Network.Wai.SAML2.Config import Network.Wai.SAML2.Validation@@ -48,7 +48,7 @@ -------------------------------------------------------------------------------- -- | Checks whether the request method of @request@ is @"POST"@.-isPOST :: Request -> Bool +isPOST :: Request -> Bool isPOST = (=="POST") . requestMethod --------------------------------------------------------------------------------@@ -67,44 +67,44 @@ -- > -- a POST request was made to the assertion endpoint, but -- > -- something went wrong, details of which are provided by -- > -- the error: this should probably be logged as it may--- > -- indicate that an attack was attempted against the +-- > -- indicate that an attack was attempted against the -- > -- endpoint, but you *must* not show the error -- > -- to the client as it would severely compromise -- > -- system security--- > -- +-- > -- -- > -- you may also want to return e.g. a HTTP 400 or 401 status -- >--- > callback (Right result) app req sendResponse = do +-- > callback (Right result) app req sendResponse = do -- > -- a POST request was made to the assertion endpoint and the--- > -- SAML2 response was successfully validated: --- > -- you *must* check that you have not encountered the +-- > -- SAML2 response was successfully validated:+-- > -- you *must* check that you have not encountered the -- > -- assertion ID before; we assume that there is a -- > -- computation tryRetrieveAssertion which looks up -- > -- assertions by ID in e.g. a database -- > result <- tryRetrieveAssertion (assertionId (assertion result))--- > --- > case result of +-- >+-- > case result of -- > Just something -> -- a replay attack has occurred -- > Nothing -> do -- > -- store the assertion id somewhere -- > storeAssertion (assertionId (assertion result))--- > +-- > -- > -- the assertion is valid and you can now e.g. -- > -- retrieve user data from your database -- > -- before proceeding with the request by e.g. -- > -- redirecting them to the main view -- | 'saml2Callback' @config callback@ produces SAML2 'Middleware' for--- the given @config@. If the middleware intercepts a request to the +-- the given @config@. If the middleware intercepts a request to the -- endpoint given by @config@, the result will be passed to @callback@.-saml2Callback :: SAML2Config +saml2Callback :: SAML2Config -> (Either SAML2Error Result -> Middleware) -> Middleware-saml2Callback cfg callback app req sendResponse = do - let path = rawPathInfo req +saml2Callback cfg callback app req sendResponse = do+ let path = rawPathInfo req -- check if we need to handle this request- if | path == saml2AssertionPath cfg && isPOST req -> do + if | path == saml2AssertionPath cfg && isPOST req -> do -- default request parse options, but do not allow files; -- we are not expecting any let bodyOpts = setMaxRequestNumFiles 0@@ -112,7 +112,7 @@ $ defaultParseRequestBodyOptions -- parse the request- (body, _) <- parseRequestBodyEx bodyOpts lbsBackEnd req + (body, _) <- parseRequestBodyEx bodyOpts lbsBackEnd req case lookup "SAMLResponse" body of Just val -> do@@ -133,14 +133,14 @@ -- -- This is a simpler-to-use 'Middleware' which stores the outcome of a request -- made to the assertation endpoint in the request vault. The inner WAI--- application can then check of the presence of an assertion or an error with --- 'V.lookup' and 'assertionKey' or 'errorKey' respectively. At most one of +-- application can then check of the presence of an assertion or an error with+-- 'V.lookup' and 'assertionKey' or 'errorKey' respectively. At most one of -- the two locations will be populated for a given request, i.e. it is not -- possible for an assertion to be validated and an error to occur. ----- > saml2Vault cfg $ \app req sendResponse -> do --- > case V.lookup errorKey (vault req) of --- > Just err -> +-- > saml2Vault cfg $ \app req sendResponse -> do+-- > case V.lookup errorKey (vault req) of+-- > Just err -> -- > -- log the error, but you *must* not show the error -- > -- to the client as it would severely compromise -- > -- system security@@ -148,20 +148,20 @@ -- > -- > case V.lookup assertionKey (vault req) of -- > Nothing -> pure () -- carry on--- > Just assertion -> do +-- > Just assertion -> do -- > -- a valid assertion was processed by the middleware,--- > -- you *must* check that you have not encountered the +-- > -- you *must* check that you have not encountered the -- > -- assertion ID before; we assume that there is a -- > -- computation tryRetrieveAssertion which looks up -- > -- assertions by ID in e.g. a database -- > result <- tryRetrieveAssertion (assertionId assertion)--- > --- > case result of +-- >+-- > case result of -- > Just something -> -- a replay attack has occurred -- > Nothing -> do -- > -- store the assertion id somewhere -- > storeAssertion (assertionId assertion)--- > +-- > -- > -- the assertion is valid -- | 'assertionKey' is a vault key for retrieving assertions from@@ -182,22 +182,22 @@ -- | 'saml2Vault' @config@ produces SAML2 'Middleware' for the given @config@. saml2Vault :: SAML2Config -> Middleware-saml2Vault cfg = saml2Callback cfg callback - -- if the middleware intercepts a request containing a SAML2 response at +saml2Vault cfg = saml2Callback cfg callback+ -- if the middleware intercepts a request containing a SAML2 response at -- the configured endpoint, the outcome of processing response will be -- passed to this callback: we store the result in the corresponding -- entry in the request vault where callback (Left err) app req sendResponse = do app req{ vault = V.insert errorKey err (vault req)- } sendResponse + } sendResponse callback (Right result) app req sendResponse = do- let mRelayState = relayState result + let mRelayState = relayState result let vlt = vault req app req{ vault = V.insert assertionKey (assertion result)- $ fromMaybe vlt $ mRelayState >>= \rs -> + $ fromMaybe vlt $ mRelayState >>= \rs -> pure $ V.insert relayStateKey rs vlt } sendResponse
src/Network/Wai/SAML2/Assertion.hs view
@@ -39,8 +39,8 @@ | Bearer -- ^ urn:oasis:names:tc:SAML:2.0:cm:bearer deriving (Eq, Show) -instance FromXML SubjectConfirmationMethod where - parseXML cursor = case T.concat $ attribute "Method" cursor of +instance FromXML SubjectConfirmationMethod where+ parseXML cursor = case T.concat $ attribute "Method" cursor of "urn:oasis:names:tc:SAML:2.0:cm:holder-of-key" -> pure HolderOfKey "urn:oasis:names:tc:SAML:2.0:cm:sender-vouches" -> pure SenderVouches "urn:oasis:names:tc:SAML:2.0:cm:bearer" -> pure Bearer@@ -60,12 +60,12 @@ subjectConfirmationRecipient :: !T.Text } deriving (Eq, Show) -instance FromXML SubjectConfirmation where - parseXML cursor = do +instance FromXML SubjectConfirmation where+ parseXML cursor = do method <- parseXML cursor notOnOrAfter <- parseUTCTime $ T.concat $- cursor $/ element (saml2Name "SubjectConfirmationData") + cursor $/ element (saml2Name "SubjectConfirmationData") >=> attribute "NotOnOrAfter" pure SubjectConfirmation{@@ -79,19 +79,38 @@ >=> attribute "Recipient" } ++-- | The @<NameID>@ of a subject.+-- See http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0-cd-02.html#4.4.2.Assertion,%20Subject,%20and%20Statement%20Structure|outline+data SubjectNameID = SubjectNameID {+ -- | Some textual identifier for the subject, such as an email address.+ nameIdValue :: !T.Text+} deriving (Eq, Show)++instance FromXML SubjectNameID where+ parseXML cursor = do+ pure SubjectNameID {+ nameIdValue = T.concat $ cursor $/ content+ }+ -- | The subject of the assertion. data Subject = Subject { -- | The list of subject confirmation elements, if any.- subjectConfirmations :: ![SubjectConfirmation]+ subjectConfirmations :: ![SubjectConfirmation],+ -- | An identifier for the subject of the assertion.+ subjectNameId :: !SubjectNameID } deriving (Eq, Show) -instance FromXML Subject where - parseXML cursor = do - confirmations <- sequence $ +instance FromXML Subject where+ parseXML cursor = do+ confirmations <- sequence $ cursor $/ element (saml2Name "SubjectConfirmation") &| parseXML+ nameId <- oneOrFail "SubjectNameID is required" $+ cursor $/ element (saml2Name "NameID") >=> parseXML pure Subject{- subjectConfirmations = confirmations+ subjectConfirmations = confirmations,+ subjectNameId = nameId } --------------------------------------------------------------------------------@@ -107,16 +126,16 @@ } deriving (Eq, Show) instance FromXML Conditions where- parseXML cursor = do - notBefore <- parseUTCTime $ + parseXML cursor = do+ notBefore <- parseUTCTime $ T.concat $ attribute "NotBefore" cursor- notOnOrAfter <- parseUTCTime $ + notOnOrAfter <- parseUTCTime $ T.concat $ attribute "NotOnOrAfter" cursor pure Conditions{ conditionsNotBefore = notBefore, conditionsNotOnOrAfter = notOnOrAfter,- conditionsAudience = T.concat $ + conditionsAudience = T.concat $ cursor $/ element (saml2Name "AudienceRestriction") &/ element (saml2Name "Audience") &/ content@@ -135,16 +154,16 @@ } deriving (Eq, Show) instance FromXML AuthnStatement where- parseXML cursor = do - issueInstant <- parseUTCTime $ + parseXML cursor = do+ issueInstant <- parseUTCTime $ T.concat $ attribute "AuthnInstant" cursor pure AuthnStatement{ authnStatementInstant = issueInstant,- authnStatementSessionIndex = T.concat $ - attribute "SessionIndex" cursor, - authnStatementLocality = T.concat $ - cursor $/ element (saml2Name "SubjectLocality") + authnStatementSessionIndex = T.concat $+ attribute "SessionIndex" cursor,+ authnStatementLocality = T.concat $+ cursor $/ element (saml2Name "SubjectLocality") >=> attribute "Address" } @@ -163,13 +182,13 @@ } deriving (Eq, Show) instance FromXML AssertionAttribute where- parseXML cursor = do + parseXML cursor = do pure AssertionAttribute{ attributeName = T.concat $ attribute "Name" cursor,- attributeFriendlyName = + attributeFriendlyName = toMaybeText $ attribute "FriendlyName" cursor, attributeNameFormat = T.concat $ attribute "NameFormat" cursor,- attributeValue = T.concat $ + attributeValue = T.concat $ cursor $/ element (saml2Name "AttributeValue") &/ content } @@ -178,7 +197,7 @@ -- | 'parseAttributeStatement' @cursor@ parses an 'AttributeStatement'. parseAttributeStatement :: Cursor -> AttributeStatement-parseAttributeStatement cursor = +parseAttributeStatement cursor = cursor $/ element (saml2Name "Attribute") >=> parseXML --------------------------------------------------------------------------------@@ -186,7 +205,7 @@ -- | Represents a SAML2 assertion. data Assertion = Assertion { -- | The unique ID of this assertion. It is important to keep track of- -- these in order to avoid replay attacks. + -- these in order to avoid replay attacks. assertionId :: !T.Text, -- | The date and time when the assertion was issued. assertionIssued :: !UTCTime,@@ -202,9 +221,9 @@ assertionAttributeStatement :: !AttributeStatement } deriving (Eq, Show) -instance FromXML Assertion where - parseXML cursor = do - issueInstant <- parseUTCTime $ +instance FromXML Assertion where+ parseXML cursor = do+ issueInstant <- parseUTCTime $ T.concat $ attribute "IssueInstant" cursor subject <- oneOrFail "Subject is required" $@@ -213,20 +232,20 @@ conditions <- oneOrFail "Conditions are required" $ cursor $/ element (saml2Name "Conditions") >=> parseXML - authnStatement <- oneOrFail "AuthnStatement is required" $ + authnStatement <- oneOrFail "AuthnStatement is required" $ cursor $/ element (saml2Name "AuthnStatement") >=> parseXML pure Assertion{ assertionId = T.concat $ attribute "ID" cursor, assertionIssued = issueInstant,- assertionIssuer = T.concat $ + assertionIssuer = T.concat $ cursor $/ element (saml2Name "Issuer") &/ content, assertionSubject = subject, assertionConditions = conditions, assertionAuthnStatement = authnStatement,- assertionAttributeStatement = - cursor $/ element (saml2Name "AttributeStatement") - >=> parseAttributeStatement + assertionAttributeStatement =+ cursor $/ element (saml2Name "AttributeStatement")+ >=> parseAttributeStatement } --------------------------------------------------------------------------------
src/Network/Wai/SAML2/C14N.hs view
@@ -5,11 +5,11 @@ -- file in the root directory of this source tree. -- -------------------------------------------------------------------------------- --- | A high-level interface to XML canonicalisation for the purpose of +-- | A high-level interface to XML canonicalisation for the purpose of -- SAML2 signature validation. module Network.Wai.SAML2.C14N ( canonicalise-) where +) where -------------------------------------------------------------------------------- @@ -27,8 +27,8 @@ -- | The options we want to use for canonicalisation of XML documents. c14nOpts :: [CInt]-c14nOpts = - [ xml_opt_noent +c14nOpts =+ [ xml_opt_noent , xml_opt_dtdload , xml_opt_dtdattr -- disable network access
src/Network/Wai/SAML2/Config.hs view
@@ -9,7 +9,7 @@ module Network.Wai.SAML2.Config ( SAML2Config(..), saml2Config-) where +) where -------------------------------------------------------------------------------- @@ -24,7 +24,7 @@ -- | The path relative to the root of the web application at which the -- middleware should listen for SAML2 assertions (e.g. /sso/assert). saml2AssertionPath :: !BS.ByteString,- -- | The service provider's private key, used to decrypt data from + -- | The service provider's private key, used to decrypt data from -- the identity provider. saml2PrivateKey :: !PrivateKey, -- | The identity provider's public key, used to validate@@ -42,8 +42,8 @@ } -- | 'saml2Config' @privateKey publicKey@ constructs a 'SAML2Config' value--- with the most basic set of options possible using @privateKey@ as the --- SP's private key and @publicKey@ as the IdP's public key. You should +-- with the most basic set of options possible using @privateKey@ as the+-- SP's private key and @publicKey@ as the IdP's public key. You should -- almost certainly change the resulting settings. saml2Config :: PrivateKey -> PublicKey -> SAML2Config saml2Config privKey pubKey = SAML2Config{
src/Network/Wai/SAML2/Error.hs view
@@ -8,7 +8,7 @@ -- | SAML2-related errors. module Network.Wai.SAML2.Error ( SAML2Error(..)-) where +) where -------------------------------------------------------------------------------- @@ -24,13 +24,13 @@ -------------------------------------------------------------------------------- -- | Enumerates errors that may arise in the SAML2 middleware.-data SAML2Error +data SAML2Error -- | The response received from the client is not valid XML.- = InvalidResponseXml SomeException + = InvalidResponseXml SomeException -- | The assertion is not valid XML.- | InvalidAssertionXml SomeException + | InvalidAssertionXml SomeException -- | The response is not a valid SAML2 response.- | InvalidResponse IOException + | InvalidResponse IOException -- | The assertion is not a valid SAML2 assertion. | InvalidAssertion IOException -- | The issuer is not who we expected.@@ -44,19 +44,19 @@ -- | Failed to canonicalise some XML. | CanonicalisationFailure IOException -- | Unable to decrypt the AES key.- | DecryptionFailure RSA.Error + | DecryptionFailure RSA.Error -- | The initialisation vector for a symmetric cipher is invalid.- | InvalidIV + | InvalidIV -- | The padding for a blockcipher is invalid.- | InvalidPadding + | InvalidPadding -- | The signature is incorrect.- | InvalidSignature + | InvalidSignature -- | The digest is incorrect.- | InvalidDigest + | InvalidDigest -- | The assertion is not valid.- | NotValid + | NotValid -- | A general crypto error occurred.- | CryptoError CryptoError + | CryptoError CryptoError -- | The request made to the configured endpoint is not valid. | InvalidRequest deriving Show
src/Network/Wai/SAML2/KeyInfo.hs view
@@ -8,7 +8,7 @@ -- | Types to represent keys that are contained in SAML2 responses. module Network.Wai.SAML2.KeyInfo ( KeyInfo(..)-) where +) where -------------------------------------------------------------------------------- @@ -28,13 +28,13 @@ keyInfoCertificate :: BS.ByteString } deriving (Eq, Show) -instance FromXML KeyInfo where +instance FromXML KeyInfo where parseXML cursor = pure KeyInfo{- keyInfoCertificate = + keyInfoCertificate = encodeUtf8 $ T.concat $ cursor $/ element (dsName "X509Data") &/ element (dsName "X509Certificate") &/ content }- + --------------------------------------------------------------------------------
src/Network/Wai/SAML2/Response.hs view
@@ -15,7 +15,7 @@ -- * Re-exports module Network.Wai.SAML2.StatusCode, module Network.Wai.SAML2.Signature-) where +) where -------------------------------------------------------------------------------- @@ -52,64 +52,63 @@ responseEncryptedAssertion :: !EncryptedAssertion } deriving (Eq, Show) -instance FromXML Response where +instance FromXML Response where parseXML cursor = do- issueInstant <- parseUTCTime - $ T.concat + issueInstant <- parseUTCTime+ $ T.concat $ attribute "IssueInstant" cursor statusCode <- case parseXML cursor of Nothing -> fail "Invalid status code" Just sc -> pure sc - encAssertion <- oneOrFail "EncryptedAssertion is required" - $ cursor+ encAssertion <- oneOrFail "EncryptedAssertion is required"+ ( cursor $/ element (saml2Name "EncryptedAssertion")- &/ element (xencName "EncryptedData")- >=> parseXML+ ) >>= parseXML - signature <- oneOrFail "Signature is required" $- cursor $/ element (dsName "Signature") >=> parseXML+ signature <- oneOrFail "Signature is required" (+ cursor $/ element (dsName "Signature") ) >>= parseXML pure Response{ responseDestination = T.concat $ attribute "Destination" cursor, responseId = T.concat $ attribute "ID" cursor, responseIssueInstant = issueInstant, responseVersion = T.concat $ attribute "Version" cursor,- responseIssuer = T.concat $ + responseIssuer = T.concat $ cursor $/ element (saml2Name "Issuer") &/ content, responseStatusCode = statusCode, responseSignature = signature, responseEncryptedAssertion = encAssertion }- + -------------------------------------------------------------------------------- -- | Returns 'True' if the argument is not a @<Signature>@ element.-isNotSignature :: Node -> Bool -isNotSignature (NodeElement e) = elementName e /= dsName "Signature" +isNotSignature :: Node -> Bool+isNotSignature (NodeElement e) = elementName e /= dsName "Signature" isNotSignature _ = True -- | 'removeSignature' @document@ removes all @<Signature>@ elements from -- @document@ and returns the resulting document. removeSignature :: Document -> Document-removeSignature (Document prologue root misc) = +removeSignature (Document prologue root misc) = let Element n attr ns = root in Document prologue (Element n attr (filter isNotSignature ns)) misc- + -- | Returns all nodes at @cursor@.-nodes :: Cursor -> [Node]-nodes = pure . node +nodes :: MonadFail m => Cursor -> m Node+nodes = pure . node --- | 'extractSignedInfo' @cursor@ extracts the SignedInfo element from the +-- | 'extractSignedInfo' @cursor@ extracts the SignedInfo element from the -- document reprsented by @cursor@. extractSignedInfo :: MonadFail m => Cursor -> m Element-extractSignedInfo cursor = do - NodeElement signedInfo <- oneOrFail "SignedInfo is required" - $ cursor - $/ element (dsName "Signature") - &/ element (dsName "SignedInfo") - >=> nodes+extractSignedInfo cursor = do+ NodeElement signedInfo <- oneOrFail "SignedInfo is required"+ ( cursor+ $/ element (dsName "Signature")+ &/ element (dsName "SignedInfo")+ ) >>= nodes pure signedInfo --------------------------------------------------------------------------------
src/Network/Wai/SAML2/Signature.hs view
@@ -13,12 +13,12 @@ SignedInfo(..), Reference(..), Signature(..)-) where +) where -------------------------------------------------------------------------------- import qualified Data.ByteString as BS-import qualified Data.Text as T +import qualified Data.Text as T import Data.Text.Encoding import Text.XML.Cursor@@ -28,28 +28,28 @@ -------------------------------------------------------------------------------- -- | Enumerates XML canonicalisation methods.-data CanonicalisationMethod +data CanonicalisationMethod -- | Original C14N 1.0 specification.- = C14N_1_0 + = C14N_1_0 -- | Exclusive C14N 1.0 specification. | C14N_EXC_1_0 -- | C14N 1.1 specification. | C14N_1_1 deriving (Eq, Show) -instance FromXML CanonicalisationMethod where - parseXML cursor = +instance FromXML CanonicalisationMethod where+ parseXML cursor = case T.concat $ attribute "Algorithm" cursor of "http://www.w3.org/2001/10/xml-exc-c14n#" -> pure C14N_EXC_1_0 _ -> fail "Not a valid CanonicalisationMethod" -- | Enumerates signature methods.-data SignatureMethod +data SignatureMethod -- | RSA with SHA256 digest = RSA_SHA256 deriving (Eq, Show) -instance FromXML SignatureMethod where +instance FromXML SignatureMethod where parseXML cursor = case T.concat $ attribute "Algorithm" cursor of "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" -> pure RSA_SHA256 _ -> fail "Not a valid SignatureMethod"@@ -62,7 +62,7 @@ = DigestSHA256 deriving (Eq, Show) -instance FromXML DigestMethod where +instance FromXML DigestMethod where parseXML cursor = case T.concat $ attribute "Algorithm" cursor of "http://www.w3.org/2001/04/xmlenc#sha256" -> pure DigestSHA256 _ -> fail "Not a valid DigestMethod"@@ -71,7 +71,7 @@ data Reference = Reference { -- | The URI of the entity that is referenced. referenceURI :: !T.Text,- -- | The method that was used to calculate the digest for the + -- | The method that was used to calculate the digest for the -- entity that is referenced. referenceDigestMethod :: !DigestMethod, -- | The digest of the entity that was calculated by the IdP.@@ -79,13 +79,13 @@ } deriving (Eq, Show) instance FromXML Reference where- parseXML cursor = do + parseXML cursor = do -- the reference starts with a #, drop it let uri = T.drop 1 $ T.concat $ attribute "URI" cursor - digestMethod <- oneOrFail "DigestMethod is required" $- cursor $/ element (dsName "DigestMethod") - >=> parseXML+ digestMethod <- oneOrFail "DigestMethod is required" (+ cursor $/ element (dsName "DigestMethod")+ ) >>= parseXML let digestValue = encodeUtf8 $ T.concat $ cursor $/ element (dsName "DigestValue") &/ content@@ -109,25 +109,25 @@ signedInfoReference :: !Reference } deriving (Eq, Show) -instance FromXML SignedInfo where - parseXML cursor = do - canonicalisationMethod <- +instance FromXML SignedInfo where+ parseXML cursor = do+ canonicalisationMethod <- oneOrFail "CanonicalizationMethod is required"- $ cursor - $/ element (dsName "CanonicalizationMethod") - >=> parseXML+ ( cursor+ $/ element (dsName "CanonicalizationMethod")+ ) >>= parseXML - signatureMethod <- - oneOrFail "SignatureMethod is required" - $ cursor + signatureMethod <-+ oneOrFail "SignatureMethod is required"+ ( cursor $/ element (dsName "SignatureMethod")- >=> parseXML+ ) >>= parseXML - reference <- - oneOrFail "Reference is required" - $ cursor + reference <-+ oneOrFail "Reference is required"+ ( cursor $/ element (dsName "Reference")- >=> parseXML+ ) >>= parseXML pure SignedInfo{ signedInfoCanonicalisationMethod = canonicalisationMethod,@@ -143,10 +143,10 @@ signatureValue :: !BS.ByteString } deriving (Eq, Show) -instance FromXML Signature where - parseXML cursor = do - info <- oneOrFail "SignedInfo is required" $ - cursor $/ element (dsName "SignedInfo") >=> parseXML+instance FromXML Signature where+ parseXML cursor = do+ info <- oneOrFail "SignedInfo is required" (+ cursor $/ element (dsName "SignedInfo") ) >>= parseXML let value = encodeUtf8 $ T.concat $ cursor $/ element (dsName "SignatureValue") &/ content
src/Network/Wai/SAML2/StatusCode.hs view
@@ -8,7 +8,7 @@ -- | SAML2 status codes. module Network.Wai.SAML2.StatusCode ( StatusCode(..)-) where +) where -------------------------------------------------------------------------------- @@ -24,20 +24,20 @@ -- | Enumerates SAML2 status codes. data StatusCode- -- | The response indicates success! + -- | The response indicates success! = Success deriving (Eq, Show) -instance FromXML StatusCode where - parseXML cursor = - let value = T.concat - $ cursor +instance FromXML StatusCode where+ parseXML cursor =+ let value = T.concat+ $ cursor $/ element (saml2pName "Status")- &/ element (saml2pName "StatusCode") + &/ element (saml2pName "StatusCode") >=> attribute "Value" in case value of "urn:oasis:names:tc:SAML:2.0:status:Success" -> pure Success _ -> fail "Not a valid status code."- + --------------------------------------------------------------------------------
src/Network/Wai/SAML2/Validation.hs view
@@ -9,7 +9,7 @@ module Network.Wai.SAML2.Validation ( validateResponse, ansiX923-) where +) where -------------------------------------------------------------------------------- @@ -23,9 +23,9 @@ import Crypto.Cipher.AES import Crypto.Cipher.Types -import qualified Data.ByteString as BS +import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as BS-import qualified Data.ByteString.Lazy as LBS +import qualified Data.ByteString.Lazy as LBS import Data.Default.Class import Data.Time @@ -43,48 +43,48 @@ -------------------------------------------------------------------------------- -- | 'validateResponse' @cfg responseData@ validates a SAML2 response contained--- in Base64-encoded @responseData@. -validateResponse :: SAML2Config - -> BS.ByteString +-- in Base64-encoded @responseData@.+validateResponse :: SAML2Config+ -> BS.ByteString -> IO (Either SAML2Error Assertion)-validateResponse cfg responseData = runExceptT $ do +validateResponse cfg responseData = runExceptT $ do -- get the current time- now <- liftIO $ getCurrentTime + now <- liftIO $ getCurrentTime -- the response data is Base64-encoded; decode it let resXmlDocData = BS.decodeLenient responseData - -- try to parse the XML document; throw an exception if it is not + -- try to parse the XML document; throw an exception if it is not -- a valid XML document- responseXmlDoc <- case XML.parseLBS def (LBS.fromStrict resXmlDocData) of - Left err -> throwError $ InvalidResponseXml err + responseXmlDoc <- case XML.parseLBS def (LBS.fromStrict resXmlDocData) of+ Left err -> throwError $ InvalidResponseXml err Right responseXmlDoc -> pure responseXmlDoc -- try to parse the XML document into a structured SAML2 response- resParseResult <- liftIO $ try $ - parseXML (XML.fromDocument responseXmlDoc) - - samlResponse <- case resParseResult of + resParseResult <- liftIO $ try $+ parseXML (XML.fromDocument responseXmlDoc)++ samlResponse <- case resParseResult of Left err -> throwError $ InvalidResponse err- Right samlResponse -> pure samlResponse + Right samlResponse -> pure samlResponse -- check that the response indicates success- case responseStatusCode samlResponse of + case responseStatusCode samlResponse of Success -> pure () status -> throwError $ Unsuccessful status -- check that the destination is as expected, if the configuration- -- expects us to validate this - let destination = responseDestination samlResponse + -- expects us to validate this+ let destination = responseDestination samlResponse - case saml2ExpectedDestination cfg of - Just expectedDestination - | destination /= expectedDestination -> + case saml2ExpectedDestination cfg of+ Just expectedDestination+ | destination /= expectedDestination -> throwError $ UnexpectedDestination destination _ -> pure () -- check that the issuer is as expected, if the configuration- -- expects us to validate this + -- expects us to validate this let issuer = responseIssuer samlResponse case saml2ExpectedIssuer cfg of@@ -94,10 +94,10 @@ -- ***CORE VALIDATION*** -- See https://www.w3.org/TR/xmldsig-core1/#sec-CoreValidation- -- + -- -- *REFERENCE VALIDATION*- -- 1. We extract the SignedInfo element from the SAML2 response's - -- Signature element. This element contains + -- 1. We extract the SignedInfo element from the SAML2 response's+ -- Signature element. This element contains signedInfo <- extractSignedInfo (XML.fromDocument responseXmlDoc) -- construct a new XML document from the SignedInfo element and render@@ -106,17 +106,17 @@ let signedInfoXml = XML.renderLBS def doc -- canonicalise the textual representation of the SignedInfo element- signedInfoCanonResult <- liftIO $ try $ + signedInfoCanonResult <- liftIO $ try $ canonicalise (LBS.toStrict signedInfoXml) - normalisedSignedInfo <- case signedInfoCanonResult of + normalisedSignedInfo <- case signedInfoCanonResult of Left err -> throwError $ CanonicalisationFailure err- Right result -> pure result + Right result -> pure result -- 2. At this point we should dereference all elements identified by -- Reference elements inside the SignedInfo element. However, we do -- not currently do that and instead just assume that there is only- -- one Reference element which targets the overall Response. + -- one Reference element which targets the overall Response. -- We sanity check this, just in case we are wrong since we do not -- want an attacker to be able to exploit this. let documentId = responseId samlResponse@@ -125,13 +125,13 @@ $ signatureInfo $ responseSignature samlResponse - if documentId /= referenceId + if documentId /= referenceId then throwError $ UnexpectedReference referenceId else pure () -- Now that we have sanity checked that we should indeed validate -- the entire Response, we need to remove the Signature element- -- from it (since the Response cannot possibly have been hashed with + -- from it (since the Response cannot possibly have been hashed with -- the Signature element present). First remove the Signature element: let docMinusSignature = removeSignature responseXmlDoc @@ -139,9 +139,9 @@ let renderedXml = XML.renderLBS def docMinusSignature refCanonResult <- liftIO $ try $ canonicalise (LBS.toStrict renderedXml) - normalised <- case refCanonResult of + normalised <- case refCanonResult of Left err -> throwError $ CanonicalisationFailure err- Right result -> pure result + Right result -> pure result -- next, compute the hash for the normalised document and extract the -- existing hash from the response; both hash values must be the same@@ -149,19 +149,19 @@ -- then the response has not been tampered with, assuming that the -- Signature has not been tampered with, which we validate next let documentHash = hashWith SHA256 normalised- let referenceHash = digestFromByteString - $ BS.decodeLenient - $ referenceDigestValue - $ signedInfoReference - $ signatureInfo + let referenceHash = digestFromByteString+ $ BS.decodeLenient+ $ referenceDigestValue+ $ signedInfoReference+ $ signatureInfo $ responseSignature samlResponse - if Just documentHash /= referenceHash + if Just documentHash /= referenceHash then throwError InvalidDigest else pure () -- *SIGNATURE VALIDATION*- -- We need to check that the SignedInfo element has not been tampered + -- We need to check that the SignedInfo element has not been tampered -- with, which we do by checking the signature contained in the response; -- first: extract the signature data from the response let sig = BS.decodeLenient $ signatureValue $ responseSignature samlResponse@@ -170,9 +170,9 @@ -- check that the signature is correct let pubKey = saml2PublicKey cfg - if PKCS15.verify (Just SHA256) pubKey normalisedSignedInfo sig + if PKCS15.verify (Just SHA256) pubKey normalisedSignedInfo sig then pure ()- else throwError InvalidSignature + else throwError InvalidSignature -- ***ASSERTION DECRYPTION*** -- the SAML assertion is AES-encrypted and we need to acquire the key@@ -181,27 +181,27 @@ -- the key used to decrypt the assertion let pk = saml2PrivateKey cfg let encryptedAssertion = responseEncryptedAssertion samlResponse- - oaepResult <- liftIO $ OAEP.decryptSafer (OAEP.defaultOAEPParams SHA1) pk - $ BS.decodeLenient - $ cipherValue - $ encryptedKeyCipher - $ encryptedAssertionKey ++ oaepResult <- liftIO $ OAEP.decryptSafer (OAEP.defaultOAEPParams SHA1) pk+ $ BS.decodeLenient+ $ cipherValue+ $ encryptedKeyCipher+ $ encryptedAssertionKey $ encryptedAssertion - aesKey <- case oaepResult of + aesKey <- case oaepResult of Left err -> throwError $ DecryptionFailure err Right cipherData -> pure cipherData - -- next we can decrypt the assertion; initialise AES128 with + -- next we can decrypt the assertion; initialise AES128 with -- the key we have just decrypted- xmlData <- case cipherInit aesKey of - CryptoFailed err -> throwError $ CryptoError err + xmlData <- case cipherInit aesKey of+ CryptoFailed err -> throwError $ CryptoError err CryptoPassed aes128 -> do -- get the AES ciphertext- let cipherText = BS.decodeLenient - $ cipherValue - $ encryptedAssertionCipher + let cipherText = BS.decodeLenient+ $ cipherValue+ $ encryptedAssertionCipher $ encryptedAssertion -- the IV used for AES is 128bits (16 bytes) prepended@@ -209,29 +209,29 @@ let (ivBytes, xmlBytes) = BS.splitAt 16 cipherText -- convert the bytes into the IV- case makeIV ivBytes of + case makeIV ivBytes of Nothing -> throwError InvalidIV Just iv -> do -- run AES to decrypt the assertion let plaintext = cbcDecrypt (aes128 :: AES128) iv xmlBytes -- remove padding from the plaintext- case ansiX923 plaintext of + case ansiX923 plaintext of Nothing -> throwError InvalidPadding Just xmlData -> pure xmlData -- try to parse the assertion that we decrypted earlier- assertion <- case XML.parseLBS def (LBS.fromStrict xmlData) of - Left err -> throwError $ InvalidAssertionXml err + assertion <- case XML.parseLBS def (LBS.fromStrict xmlData) of+ Left err -> throwError $ InvalidAssertionXml err Right assertDoc -> do -- try to convert the assertion document into a more -- structured representation- assertParseResult <- liftIO $ try $ + assertParseResult <- liftIO $ try $ parseXML (XML.fromDocument assertDoc) - case assertParseResult of + case assertParseResult of Left err -> throwError $ InvalidAssertion err- Right assertion -> pure assertion + Right assertion -> pure assertion -- validate that the assertion is valid at this point in time let Conditions{..} = assertionConditions assertion@@ -244,14 +244,14 @@ -- all checks out, return the assertion pure assertion --- | 'ansiX923' @plaintext@ removes ANSI X9.23 padding from @plaintext@. +-- | 'ansiX923' @plaintext@ removes ANSI X9.23 padding from @plaintext@. -- See https://en.wikipedia.org/wiki/Padding_(cryptography)#ANSI_X9.23-ansiX923 :: BS.ByteString -> Maybe BS.ByteString +ansiX923 :: BS.ByteString -> Maybe BS.ByteString ansiX923 d- | len == 0 = Nothing + | len == 0 = Nothing | padLen < 1 || padLen > len = Nothing | otherwise = Just content- where len = BS.length d + where len = BS.length d padBytes = BS.index d (len-1) padLen = fromIntegral padBytes (content,_) = BS.splitAt (len - padLen) d
src/Network/Wai/SAML2/XML.hs view
@@ -20,7 +20,7 @@ -- * XML parsing FromXML(..), oneOrFail-) where +) where -------------------------------------------------------------------------------- @@ -30,52 +30,52 @@ import Text.XML import Text.XML.Cursor --------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- --- | 'saml2Name' @name@ constructs a 'Name' for @name@ in the +-- | 'saml2Name' @name@ constructs a 'Name' for @name@ in the -- urn:oasis:names:tc:SAML:2.0:assertion namespace.-saml2Name :: T.Text -> Name -saml2Name name = +saml2Name :: T.Text -> Name+saml2Name name = Name name (Just "urn:oasis:names:tc:SAML:2.0:assertion") (Just "saml2") --- | 'saml2pName' @name@ constructs a 'Name' for @name@ in the +-- | 'saml2pName' @name@ constructs a 'Name' for @name@ in the -- urn:oasis:names:tc:SAML:2.0:protocol namespace.-saml2pName :: T.Text -> Name +saml2pName :: T.Text -> Name saml2pName name = Name name (Just "urn:oasis:names:tc:SAML:2.0:protocol") (Just "saml2p") --- | 'xencName' @name@ constructs a 'Name' for @name@ in the +-- | 'xencName' @name@ constructs a 'Name' for @name@ in the -- http://www.w3.org/2001/04/xmlenc# namespace.-xencName :: T.Text -> Name +xencName :: T.Text -> Name xencName name = Name name (Just "http://www.w3.org/2001/04/xmlenc#") (Just "xenc") --- | 'dsName' @name@ constructs a 'Name' for @name@ in the +-- | 'dsName' @name@ constructs a 'Name' for @name@ in the -- http://www.w3.org/2000/09/xmldsig# namespace.-dsName :: T.Text -> Name +dsName :: T.Text -> Name dsName name = Name name (Just "http://www.w3.org/2000/09/xmldsig#") (Just "ds") --- | 'toMaybeText' @xs@ returns 'Nothing' if @xs@ is the empty list, or +-- | 'toMaybeText' @xs@ returns 'Nothing' if @xs@ is the empty list, or -- the result of concatenating @xs@ wrapped in 'Just' otherwise. toMaybeText :: [T.Text] -> Maybe T.Text-toMaybeText [] = Nothing +toMaybeText [] = Nothing toMaybeText xs = Just $ T.concat xs -- | The time format used by SAML2.-timeFormat :: String +timeFormat :: String timeFormat = "%Y-%m-%dT%H:%M:%S%QZ" -- | 'parseUTCTime' @text@ parses @text@ into a 'UTCTime' value. parseUTCTime :: MonadFail m => T.Text -> m UTCTime-parseUTCTime value = - parseTimeM False defaultTimeLocale timeFormat (T.unpack value) +parseUTCTime value =+ parseTimeM False defaultTimeLocale timeFormat (T.unpack value) -- | A class of types which can be parsed from XML.-class FromXML a where +class FromXML a where parseXML :: MonadFail m => Cursor -> m a --- | 'oneOrFail' @message xs@ throws an 'XMLException' with @message@ if +-- | 'oneOrFail' @message xs@ throws an 'XMLException' with @message@ if -- @xs@ is the empty list. If @xs@ has at least one element, the first is -- returned and all others are discarded. oneOrFail :: MonadFail m => String -> [a] -> m a
src/Network/Wai/SAML2/XML/Encrypted.hs view
@@ -12,7 +12,7 @@ EncryptionMethod(..), EncryptedKey(..), EncryptedAssertion(..)-) where +) where -------------------------------------------------------------------------------- @@ -32,11 +32,11 @@ cipherValue :: !BS.ByteString } deriving (Eq, Show) -instance FromXML CipherData where +instance FromXML CipherData where parseXML cursor = pure CipherData{ cipherValue = encodeUtf8- $ T.concat - $ cursor + $ T.concat+ $ cursor $/ element (xencName "CipherValue") &/ content }@@ -51,15 +51,15 @@ encryptionMethodDigestAlgorithm :: !(Maybe T.Text) } deriving (Eq, Show) -instance FromXML EncryptionMethod where +instance FromXML EncryptionMethod where parseXML cursor = pure EncryptionMethod{- encryptionMethodAlgorithm = + encryptionMethodAlgorithm = T.concat $ attribute "Algorithm" cursor,- encryptionMethodDigestAlgorithm = - toMaybeText $ cursor - $/ element (dsName "DigestMethod") + encryptionMethodDigestAlgorithm =+ toMaybeText $ cursor+ $/ element (dsName "DigestMethod") >=> attribute "Algorithm"- } + } -------------------------------------------------------------------------------- @@ -72,25 +72,25 @@ -- | The method used to encrypt the key. encryptedKeyMethod :: !EncryptionMethod, -- | The key data.- encryptedKeyData :: !KeyInfo,+ encryptedKeyData :: !(Maybe KeyInfo), -- | The ciphertext. encryptedKeyCipher :: !CipherData } deriving (Eq, Show) -instance FromXML EncryptedKey where +instance FromXML EncryptedKey where parseXML cursor = do- method <- oneOrFail "EncryptionMethod is required" $- cursor $/ element (xencName "EncryptionMethod") - >=> parseXML+ method <- oneOrFail "EncryptionMethod is required" (+ cursor $/ element (xencName "EncryptionMethod")+ ) >>= parseXML - keyData <- oneOrFail "KeyInfo is required" $- cursor $/ element (dsName "KeyInfo") - >=> parseXML+ keyData <- case cursor $/ element (dsName "KeyInfo") of+ [] -> return Nothing+ (keyInfo :_) -> Just <$> parseXML keyInfo - cipher <- oneOrFail "CipherData is required" $+ cipher <- oneOrFail "CipherData is required" ( cursor $/ element (xencName "CipherData")- >=> parseXML- + ) >>= parseXML+ pure EncryptedKey{ encryptedKeyId = T.concat $ attribute "Id" cursor, encryptedKeyRecipient = T.concat $ attribute "Recipient" cursor,@@ -111,23 +111,31 @@ encryptedAssertionCipher :: !CipherData } deriving (Eq, Show) -instance FromXML EncryptedAssertion where +instance FromXML EncryptedAssertion where parseXML cursor = do- algorithm <- oneOrFail "Algorithm is required" - $ cursor - $/ element (xencName "EncryptionMethod")- >=> parseXML + encryptedData <- oneOrFail "EncryptedData is required"+ $ cursor+ $/ element (xencName "EncryptedData") - keyInfo <- oneOrFail "KeyInfo is required" - $ cursor - $/ element (dsName "KeyInfo") - &/ element (xencName "EncryptedKey") - >=> parseXML+ algorithm <- oneOrFail "Algorithm is required"+ $ encryptedData+ $/ element (xencName "EncryptionMethod")+ >=> parseXML - cipher <- oneOrFail "CipherData is required" - $ cursor + keyInfo <- oneOrFail "EncryptedKey is required" $ mconcat+ [ cursor $/ element (xencName "EncryptedKey")+ >=> parseXML+ , cursor+ $/ element (xencName "EncryptedData")+ &/ element (dsName "KeyInfo")+ &/ element (xencName "EncryptedKey")+ >=> parseXML+ ]++ cipher <- oneOrFail "CipherData is required"+ ( encryptedData $/ element (xencName "CipherData")- >=> parseXML + ) >>= parseXML pure EncryptedAssertion{ encryptedAssertionAlgorithm = algorithm,
+ tests/Parser.hs view
@@ -0,0 +1,28 @@+import Network.Wai.SAML2.Response+import Network.Wai.SAML2.XML+import System.FilePath+import Test.Tasty+import Test.Tasty.Golden+import Text.Show.Pretty+import Text.XML.Cursor+import qualified Data.ByteString.Lazy.Char8 as BC+import qualified Text.XML as XML++run :: FilePath -> IO BC.ByteString+run src = do+ doc <- XML.readFile XML.def src+ resp <- parseXML (fromDocument doc)+ pure $ BC.pack $ ppShow (resp :: Response)++main :: IO ()+main = defaultMain $ testGroup "Parse SAML2 response"+ [ mkGolden $ prefix </> "keycloak.xml"+ , mkGolden $ prefix </> "okta.xml"+ ]+ where+ prefix = "tests/data"+ mkGolden path = goldenVsStringDiff+ (takeBaseName path)+ (\ref new -> ["diff", "-u", ref, new])+ (path <.> "expected")+ (run path)
wai-saml2.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.34.4.+-- This file has been generated from package.yaml by hpack version 0.35.0. -- -- see: https://github.com/sol/hpack name: wai-saml2-version: 0.2.1.3+version: 0.3.0.0 synopsis: SAML2 assertion validation as WAI middleware description: A Haskell library which implements SAML2 assertion validation as WAI middleware category: Security@@ -66,4 +66,41 @@ , x509 <2 , x509-store <2 , xml-conduit <2+ default-language: Haskell2010++test-suite parser+ type: exitcode-stdio-1.0+ main-is: Parser.hs+ other-modules:+ Paths_wai_saml2+ hs-source-dirs:+ tests+ default-extensions:+ OverloadedStrings+ MultiWayIf+ RecordWildCards+ FlexibleInstances+ ghc-options: -Wall -Wcompat+ build-depends:+ base+ , base64-bytestring >=0.1 && <2+ , bytestring+ , c14n >=0.1.0.1 && <1+ , cryptonite <1+ , data-default-class <1+ , filepath+ , http-types <1+ , mtl >=2.2.1 && <3+ , pretty-show+ , tasty+ , tasty-golden+ , text <2+ , time >=1.9 && <2+ , vault >=0.3 && <1+ , wai >=3.0 && <4+ , wai-extra >=3.0 && <4+ , wai-saml2+ , x509 <2+ , x509-store <2+ , xml-conduit default-language: Haskell2010