packages feed

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 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