packages feed

wai-saml2 0.3.0.1 → 0.4

raw patch · 19 files changed

+1122/−172 lines, 19 filesdep +base16-bytestringdep +containersdep +network-uridep ~textPVP ok

version bump matches the API change (PVP)

Dependencies added: base16-bytestring, containers, network-uri, zlib

Dependency ranges changed: text

API changes (from Hackage documentation)

- Network.Wai.SAML2.Assertion: [conditionsAudience] :: Conditions -> !Text
- 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: [response] :: Result -> !Response
+ Network.Wai.SAML2.Assertion: AudienceRestriction :: ![Text] -> AudienceRestriction
+ Network.Wai.SAML2.Assertion: NameID :: !Maybe Text -> !Maybe Text -> !Maybe Text -> !Maybe NameIDFormat -> !Text -> NameID
+ Network.Wai.SAML2.Assertion: [audienceRestrictionAudience] :: AudienceRestriction -> ![Text]
+ Network.Wai.SAML2.Assertion: [conditionsAudienceRestrictions] :: Conditions -> ![AudienceRestriction]
+ Network.Wai.SAML2.Assertion: [nameIDFormat] :: NameID -> !Maybe NameIDFormat
+ Network.Wai.SAML2.Assertion: [nameIDQualifier] :: NameID -> !Maybe Text
+ Network.Wai.SAML2.Assertion: [nameIDSPNameQualifier] :: NameID -> !Maybe Text
+ Network.Wai.SAML2.Assertion: [nameIDSPProvidedID] :: NameID -> !Maybe Text
+ Network.Wai.SAML2.Assertion: [nameIDValue] :: NameID -> !Text
+ Network.Wai.SAML2.Assertion: [subjectNameID] :: Subject -> !NameID
+ Network.Wai.SAML2.Assertion: data AudienceRestriction
+ Network.Wai.SAML2.Assertion: data NameID
+ Network.Wai.SAML2.Assertion: instance GHC.Classes.Eq Network.Wai.SAML2.Assertion.AudienceRestriction
+ Network.Wai.SAML2.Assertion: instance GHC.Classes.Eq Network.Wai.SAML2.Assertion.NameID
+ Network.Wai.SAML2.Assertion: instance GHC.Show.Show Network.Wai.SAML2.Assertion.AudienceRestriction
+ Network.Wai.SAML2.Assertion: instance GHC.Show.Show Network.Wai.SAML2.Assertion.NameID
+ Network.Wai.SAML2.Assertion: instance Network.Wai.SAML2.XML.FromXML Network.Wai.SAML2.Assertion.AudienceRestriction
+ Network.Wai.SAML2.Assertion: instance Network.Wai.SAML2.XML.FromXML Network.Wai.SAML2.Assertion.NameID
+ Network.Wai.SAML2.Config: [saml2Audiences] :: SAML2Config -> ![Text]
+ Network.Wai.SAML2.Config: [saml2RequireEncryptedAssertion] :: SAML2Config -> !Bool
+ Network.Wai.SAML2.Config: saml2ConfigNoEncryption :: PublicKey -> SAML2Config
+ Network.Wai.SAML2.EntityDescriptor: HTTPArtifact :: Binding
+ Network.Wai.SAML2.EntityDescriptor: HTTPPost :: Binding
+ Network.Wai.SAML2.EntityDescriptor: HTTPRedirect :: Binding
+ Network.Wai.SAML2.EntityDescriptor: IDPSSODescriptor :: Text -> SignedExact Certificate -> [Text] -> [(Binding, Text)] -> IDPSSODescriptor
+ Network.Wai.SAML2.EntityDescriptor: PAOS :: Binding
+ Network.Wai.SAML2.EntityDescriptor: SOAP :: Binding
+ Network.Wai.SAML2.EntityDescriptor: URLEncodingDEFLATE :: Binding
+ Network.Wai.SAML2.EntityDescriptor: [entityID] :: IDPSSODescriptor -> Text
+ Network.Wai.SAML2.EntityDescriptor: [nameIDFormats] :: IDPSSODescriptor -> [Text]
+ Network.Wai.SAML2.EntityDescriptor: [singleSignOnServices] :: IDPSSODescriptor -> [(Binding, Text)]
+ Network.Wai.SAML2.EntityDescriptor: [x509Certificate] :: IDPSSODescriptor -> SignedExact Certificate
+ Network.Wai.SAML2.EntityDescriptor: data Binding
+ Network.Wai.SAML2.EntityDescriptor: data IDPSSODescriptor
+ Network.Wai.SAML2.EntityDescriptor: instance GHC.Classes.Eq Network.Wai.SAML2.EntityDescriptor.Binding
+ Network.Wai.SAML2.EntityDescriptor: instance GHC.Show.Show Network.Wai.SAML2.EntityDescriptor.Binding
+ Network.Wai.SAML2.EntityDescriptor: instance GHC.Show.Show Network.Wai.SAML2.EntityDescriptor.IDPSSODescriptor
+ Network.Wai.SAML2.EntityDescriptor: instance Network.Wai.SAML2.XML.FromXML Network.Wai.SAML2.EntityDescriptor.IDPSSODescriptor
+ Network.Wai.SAML2.Error: AudienceMismatch :: [Text] -> SAML2Error
+ Network.Wai.SAML2.Error: EncryptedAssertionNotSupported :: SAML2Error
+ Network.Wai.SAML2.Error: EncryptedAssertionRequired :: SAML2Error
+ Network.Wai.SAML2.NameIDFormat: EmailAddress :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: Entity :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: Federated :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: KerberosPrincipalName :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: Persistent :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: Provider :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: Transient :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: Unspecified :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: WindowsDomainQualifiedName :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: X509SubjectName :: NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: data NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: instance GHC.Classes.Eq Network.Wai.SAML2.NameIDFormat.NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: instance GHC.Classes.Ord Network.Wai.SAML2.NameIDFormat.NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: instance GHC.Generics.Generic Network.Wai.SAML2.NameIDFormat.NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: instance GHC.Show.Show Network.Wai.SAML2.NameIDFormat.NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: parseNameIDFormat :: MonadFail m => Text -> m NameIDFormat
+ Network.Wai.SAML2.NameIDFormat: showNameIDFormat :: NameIDFormat -> Text
+ Network.Wai.SAML2.Request: AuthnRequest :: !UTCTime -> !Text -> !Text -> !Bool -> !NameIDFormat -> AuthnRequest
+ Network.Wai.SAML2.Request: [authnRequestAllowCreate] :: AuthnRequest -> !Bool
+ Network.Wai.SAML2.Request: [authnRequestID] :: AuthnRequest -> !Text
+ Network.Wai.SAML2.Request: [authnRequestIssuer] :: AuthnRequest -> !Text
+ Network.Wai.SAML2.Request: [authnRequestNameIDFormat] :: AuthnRequest -> !NameIDFormat
+ Network.Wai.SAML2.Request: [authnRequestTimestamp] :: AuthnRequest -> !UTCTime
+ Network.Wai.SAML2.Request: data AuthnRequest
+ Network.Wai.SAML2.Request: instance GHC.Classes.Eq Network.Wai.SAML2.Request.AuthnRequest
+ Network.Wai.SAML2.Request: instance GHC.Show.Show Network.Wai.SAML2.Request.AuthnRequest
+ Network.Wai.SAML2.Request: issueAuthnRequest :: Text -> IO AuthnRequest
+ Network.Wai.SAML2.Request: renderBase64 :: AuthnRequest -> ByteString
+ Network.Wai.SAML2.Request: renderUrlEncodingDeflate :: AuthnRequest -> ByteString
+ Network.Wai.SAML2.Request: renderXML :: AuthnRequest -> ByteString
+ Network.Wai.SAML2.Response: [responseAssertion] :: Response -> !Maybe Assertion
+ Network.Wai.SAML2.Response: [responseInResponseTo] :: Response -> !Maybe Text
+ Network.Wai.SAML2.StatusCode: AuthnFailed :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: InvalidAttrNameOrValue :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: InvalidNameIDPolicy :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: MkStatusCode :: !StatusCodeValue -> !Maybe StatusCode -> StatusCode
+ Network.Wai.SAML2.StatusCode: NoAuthnContext :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: NoAvailableIDP :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: NoPassive :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: NoSupportedIDP :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: OtherStatus :: URI -> StatusCodeValue
+ Network.Wai.SAML2.StatusCode: PartialLogout :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: ProxyCountExceeded :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: RequestDenied :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: RequestUnsupported :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: RequestVersionDeprecated :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: RequestVersionTooHigh :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: RequestVersionTooLow :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: Requester :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: ResourceNotRecognized :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: Responder :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: TooManyResponses :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: UnknownAttrProfile :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: UnknownPrincipal :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: UnsupportedBinding :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: VersionMismatch :: StatusCodeValue
+ Network.Wai.SAML2.StatusCode: [statusCodeSubordinate] :: StatusCode -> !Maybe StatusCode
+ Network.Wai.SAML2.StatusCode: [statusCodeValue] :: StatusCode -> !StatusCodeValue
+ Network.Wai.SAML2.StatusCode: data StatusCodeValue
+ Network.Wai.SAML2.StatusCode: instance GHC.Classes.Eq Network.Wai.SAML2.StatusCode.StatusCodeValue
+ Network.Wai.SAML2.StatusCode: instance GHC.Show.Show Network.Wai.SAML2.StatusCode.StatusCodeValue
+ Network.Wai.SAML2.Validation: decodeResponse :: ByteString -> ExceptT SAML2Error IO (Document, Response)
+ Network.Wai.SAML2.Validation: validateSAMLResponse :: SAML2Config -> Document -> Response -> UTCTime -> ExceptT SAML2Error IO Assertion
+ Network.Wai.SAML2.XML: mdName :: Text -> Name
+ Network.Wai.SAML2.XML: showUTCTime :: UTCTime -> Text
- Network.Wai.SAML2: Result :: !Maybe ByteString -> !Assertion -> Result
+ Network.Wai.SAML2: Result :: !Maybe ByteString -> !Assertion -> !Response -> Result
- Network.Wai.SAML2.Assertion: Conditions :: !UTCTime -> !UTCTime -> !Text -> Conditions
+ Network.Wai.SAML2.Assertion: Conditions :: !UTCTime -> !UTCTime -> ![AudienceRestriction] -> Conditions
- Network.Wai.SAML2.Assertion: Subject :: ![SubjectConfirmation] -> !SubjectNameID -> Subject
+ Network.Wai.SAML2.Assertion: Subject :: ![SubjectConfirmation] -> !NameID -> Subject
- Network.Wai.SAML2.Config: SAML2Config :: !ByteString -> !PrivateKey -> !PublicKey -> !Maybe Text -> !Maybe Text -> !Bool -> SAML2Config
+ Network.Wai.SAML2.Config: SAML2Config :: !ByteString -> !Maybe PrivateKey -> !PublicKey -> !Maybe Text -> !Maybe Text -> ![Text] -> !Bool -> !Bool -> SAML2Config
- Network.Wai.SAML2.Config: [saml2PrivateKey] :: SAML2Config -> !PrivateKey
+ Network.Wai.SAML2.Config: [saml2PrivateKey] :: SAML2Config -> !Maybe PrivateKey
- Network.Wai.SAML2.Response: Response :: !Text -> !Text -> !UTCTime -> !Text -> !Text -> !StatusCode -> !Signature -> !EncryptedAssertion -> Response
+ Network.Wai.SAML2.Response: Response :: !Text -> !Maybe Text -> !Text -> !UTCTime -> !Text -> !Text -> !StatusCode -> !Signature -> !Maybe Assertion -> !Maybe EncryptedAssertion -> Response
- Network.Wai.SAML2.Response: [responseEncryptedAssertion] :: Response -> !EncryptedAssertion
+ Network.Wai.SAML2.Response: [responseEncryptedAssertion] :: Response -> !Maybe EncryptedAssertion
- Network.Wai.SAML2.StatusCode: Success :: StatusCode
+ Network.Wai.SAML2.StatusCode: Success :: StatusCodeValue
- Network.Wai.SAML2.Validation: validateResponse :: SAML2Config -> ByteString -> IO (Either SAML2Error Assertion)
+ Network.Wai.SAML2.Validation: validateResponse :: SAML2Config -> ByteString -> IO (Either SAML2Error (Assertion, Response))

Files

CHANGELOG.md view
@@ -1,14 +1,27 @@ # Changelog for `wai-saml2` +## 0.4++-   Split `validateResponse` into `decodeResponse` and `validateSAMLResponse` ([#31](https://github.com/mbg/wai-saml2/pull/31) by [@fumieval](https://github.com/fumieval))+-   Exported `NameID` (formerly `NameId`), and renamed `subjectNameId` to `subjectNameID`+-   Support GHC 9.4 ([#36](https://github.com/mbg/wai-saml2/pull/36) by [@mbg](https://github.com/mbg))+-   Add new module `Network.Wai.SAML2.Request` with `AuthnRequest` generation for SP-initiated login flow ([#19](https://github.com/mbg/wai-saml2/pull/19) by [@fumieval](https://github.com/fumieval))+-   Changed the `saml2PrivateKey` field to be optional and added `saml2ConfigNoEncryption` which takes a `PublicKey` only ([#37](https://github.com/mbg/wai-saml2/pull/37) by [@fumieval](https://github.com/fumieval))+-   Added `showUTCTime` to `Network.Wai.SAML2.XML`+-   Added a new module `Network.Wai.SAML2.NameIDFormat` ([#21](https://github.com/mbg/wai-saml2/pull/21) by [@fumieval](https://github.com/fumieval))+-   Added new field `response` to `Result` which contains the full, decoded SAML response ([#33](https://github.com/mbg/wai-saml2/pull/33) by [@Philonous](https://github.com/Philonous))+-   Validate audience restrictions ([#35](https://github.com/mbg/wai-saml2/pull/35) by [@Philonous](https://github.com/Philonous))+-   Handle status codes according to the SAML2 specification ([#42](https://github.com/mbg/wai-saml2/pull/42)) by [@mbg](https://github.com/mbg)+ ## 0.3 -* 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))+-   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.+-   Metadata updates.  ## 0.2.1.2 @@ -16,16 +29,16 @@  ## 0.2.1.1 -* Export `Result` type from `Network.Wai.SAML2` module.+-   Export `Result` type from `Network.Wai.SAML2` module.  ## 0.2.1 -* Fix missing export of `relayStateKey` and change its type.+-   Fix missing export of `relayStateKey` and change its type.  ## 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.+-   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 -* Initial release+-   Initial release
README.md view
@@ -5,17 +5,16 @@ ![stackage-nightly](https://github.com/mbg/wai-saml2/workflows/stackage-nightly/badge.svg) [![Hackage](https://img.shields.io/hackage/v/wai-saml2)](https://hackage.haskell.org/package/wai-saml2) -A Haskell library which implements SAML2 assertion validation as WAI middleware. This can be used by a Haskell web application (the service provider, SP) to perform identity provider (IdP) initiated authentication, i.e. SAML2-based authentication where the authentication begins at the IdP-end, the IdP authenticates the user, and then gets the user to submit a SAML2 assertion back to the SP (known as "unsolicited SSO" within e.g. [the Shibboleth project](https://wiki.shibboleth.net/confluence/display/IDP30/UnsolicitedSSOConfiguration#UnsolicitedSSOConfiguration-SAML2.0)). +A Haskell library which implements SAML2 assertion validation as WAI middleware. This can be used by a Haskell web application (the service provider, SP) to perform identity provider (IdP) initiated authentication, i.e. SAML2-based authentication where the authentication begins at the IdP-end, the IdP authenticates the user, and then gets the user to submit a SAML2 assertion back to the SP (known as "unsolicited SSO" within e.g. [the Shibboleth project](https://wiki.shibboleth.net/confluence/display/IDP30/UnsolicitedSSOConfiguration#UnsolicitedSSOConfiguration-SAML2.0)).  ## Completeness  There are currently a number of limitations to this library: -* As mentioned above, while the library implements IdP-initiated authentication, it does not yet implement SP-initiated authentication (where the SP submits a login request to the IdP).+* The library implements IdP-initiated authentication and has some support for SP-initiated authentication (See the documentation for the `Network.Wai.SAML2.Request` module)  * The library does not currently support the full SAML2 specification and makes certain assumptions about what the IdP's responses contain. It will most likely fail with any IdPs which do not send responses in the same format. If you wish to use this library and encounter problems with your IdP, please open an issue or a pull request which implements support accordingly.-  * The library expects that SAML _assertions_ will be encrypted and will decrypt these. There is currently no support for plain-text assertions.-  + ## Security  The library is estimated to be sufficiently robust for use in a production environment. If you wish to implement this middleware, please note the following:@@ -26,20 +25,20 @@  ## Usage -### Preliminaries +### Preliminaries  You need to have registered your service provider with the identity provider. You need to have access to the IdP's metadata, which will contain the public key used for signature validation.  ### Configuration -The `saml2Config` function may be used to construct `SAML2Config` values. It expects at least the SP's private key and the IdP's public key as arguments, but you should almost certainly customise the configuration further. The private and public keys can be loaded with functions from the `Data.X509` and `Data.X509.File` modules (from the `x509` and `x509-store` packages, respectively):+The `saml2Config` function may be used to construct `SAML2Config` values. It expects at least the SP's private key and the IdP's public key as arguments (even when mandatory encryption is disabled) but you should almost certainly customise the configuration further. The private and public keys can be loaded with functions from the `Data.X509` and `Data.X509.File` modules (from the `x509` and `x509-store` packages, respectively):  ```haskell (saml2Config spPrivateKey idpPublicKey){     saml2AssertionPath = "/sso/assert",     saml2ExpectedIssuer = Just "https://idp.sp.com/saml2",     saml2ExpectedDestination = Just "https://example.com/sso/assert",-} +} ```  The configuration options are documented in the Haddock documentation for the `Network.Wai.SAML2.Config` module.@@ -54,27 +53,27 @@            -- 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.
src/Network/Wai/SAML2.hs view
@@ -33,6 +33,7 @@ --------------------------------------------------------------------------------  import qualified Data.ByteString as BS+import Data.Functor ((<&>)) import Data.Maybe (fromMaybe) import qualified Data.Vault.Lazy as V @@ -42,6 +43,7 @@ import Network.Wai.SAML2.Validation import Network.Wai.SAML2.Assertion import Network.Wai.SAML2.Error+import qualified Network.Wai.SAML2.Response as SAML2  import System.IO.Unsafe (unsafePerformIO) @@ -104,7 +106,7 @@     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 then do             -- default request parse options, but do not allow files;             -- we are not expecting any             let bodyOpts = setMaxRequestNumFiles 0@@ -116,16 +118,23 @@              case lookup "SAMLResponse" body of                 Just val -> do-                    result <- validateResponse cfg val                     let rs = lookup "RelayState" body+                    result <- validateResponse cfg val <&>+                                  fmap (\(assertion, response) ->+                                            Result{+                                                assertion = assertion,+                                                relayState = rs,+                                                response = response+                                            })+                     -- call the callback-                    callback  (Result rs <$> result) app req sendResponse+                    callback result app req sendResponse                 -- the request does not contain the expected payload                 Nothing -> callback (Left InvalidRequest) app req sendResponse -       -- not one of the paths we need to handle, pass the request on to the-       -- inner application-       | otherwise -> app req sendResponse+    -- not one of the paths we need to handle, pass the request on to the+    -- inner application+    else app req sendResponse  -------------------------------------------------------------------------------- @@ -208,7 +217,11 @@     -- | An optional relay state, as provided in the POST request.     relayState :: !(Maybe BS.ByteString),     -- | The assertion obtained from the response that has been validated.-    assertion :: !Assertion+    assertion :: !Assertion,+    -- | The full response obtained from the IdP.+    --+    -- @since 0.4+    response :: !SAML2.Response } deriving (Eq, Show)  --------------------------------------------------------------------------------
src/Network/Wai/SAML2/Assertion.hs view
@@ -10,7 +10,9 @@     SubjectConfirmationMethod(..),     SubjectConfirmation(..),     Subject(..),+    NameID(..),     Conditions(..),+    AudienceRestriction(..),     AuthnStatement(..),     AssertionAttribute(..),     AttributeStatement,@@ -22,11 +24,13 @@  import Control.Monad +import Data.Maybe (listToMaybe) import qualified Data.Text as T import Data.Time  import Text.XML.Cursor +import Network.Wai.SAML2.NameIDFormat import Network.Wai.SAML2.XML  --------------------------------------------------------------------------------@@ -82,15 +86,35 @@  -- | 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 {+-- and https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=13+--+-- @since 0.4+data NameID = NameID {+    -- | The domain that qualifies the name. Allows names from different sources+    -- to used together without colliding+    nameIDQualifier :: !(Maybe T.Text),+    -- | Additionally qualifies the name with the name of the service provider+    nameIDSPNameQualifier :: !(Maybe T.Text),+    -- | Name provided by a service provider+    nameIDSPProvidedID :: !(Maybe T.Text),+    -- | A URI reference describing the format of the value. If not specified it+    -- defaults to @urn:oasis:names:tc:SAML:1.0:nameid-format:unspecified@+    nameIDFormat :: !(Maybe NameIDFormat),     -- | Some textual identifier for the subject, such as an email address.-    nameIdValue :: !T.Text+    nameIDValue :: !T.Text } deriving (Eq, Show) -instance FromXML SubjectNameID where+instance FromXML NameID where     parseXML cursor = do-        pure SubjectNameID {-            nameIdValue = T.concat $ cursor $/ content+        nameIDFormat <- traverse parseNameIDFormat+            $ listToMaybe (attribute "Format" cursor)+        pure NameID {+            nameIDQualifier = listToMaybe $ attribute "NameQualifier" cursor,+            nameIDSPNameQualifier =+                listToMaybe $ attribute "SPNameQualifier" cursor,+            nameIDSPProvidedID = listToMaybe $ attribute "SPProvidedID" cursor,+            nameIDFormat = nameIDFormat,+            nameIDValue = T.concat $ cursor $/ content         }  -- | The subject of the assertion.@@ -98,33 +122,61 @@     -- | The list of subject confirmation elements, if any.     subjectConfirmations :: ![SubjectConfirmation],     -- | An identifier for the subject of the assertion.-    subjectNameId :: !SubjectNameID+    subjectNameID :: !NameID } deriving (Eq, Show)  instance FromXML Subject where     parseXML cursor = do         confirmations <- sequence $             cursor $/ element (saml2Name "SubjectConfirmation") &| parseXML-        nameId <- oneOrFail "SubjectNameID is required" $+        nameID <- oneOrFail "SubjectNameID is required" $             cursor $/ element (saml2Name "NameID") >=> parseXML          pure Subject{             subjectConfirmations = confirmations,-            subjectNameId        = nameId+            subjectNameID        = nameID         }  -------------------------------------------------------------------------------- +-- | An audience restriction.+--+-- @since 0.4++-- Reference [AudienceRestriction]+data AudienceRestriction = AudienceRestriction {+    -- | A URI reference that identifies an intended audience. For the+    -- corresponding assertion to be valid, the client has to be a member of one+    -- or more of these audiences+    audienceRestrictionAudience :: ![T.Text]+} deriving (Eq, Show)++-- Reference [AudienceRestriction]+instance FromXML AudienceRestriction where+    parseXML cursor =+        pure AudienceRestriction{+            audienceRestrictionAudience =+                let elements = cursor $/ element (saml2Name "Audience")+                in [ T.concat $ element $/ content+                   | element <- elements+                   ]+        }+ -- | Conditions under which a SAML assertion is issued.++-- Reference [Conditions] data Conditions = Conditions {     -- | The time when the assertion is valid from (inclusive).     conditionsNotBefore :: !UTCTime,     -- | The time the assertion is valid to (not inclusive).     conditionsNotOnOrAfter :: !UTCTime,     -- | The intended audience of the assertion.-    conditionsAudience :: !T.Text+    --+    -- @since 0.4+    conditionsAudienceRestrictions :: ![AudienceRestriction] } deriving (Eq, Show) +-- Reference [Conditions] instance FromXML Conditions where     parseXML cursor = do         notBefore <- parseUTCTime $@@ -135,15 +187,16 @@         pure Conditions{             conditionsNotBefore = notBefore,             conditionsNotOnOrAfter = notOnOrAfter,-            conditionsAudience = T.concat $+            conditionsAudienceRestrictions =                 cursor $/ element (saml2Name "AudienceRestriction")-                    &/ element (saml2Name "Audience")-                    &/ content+                    >=> parseXML         }  --------------------------------------------------------------------------------  -- | SAML2 authentication statements.++-- Reference [AuthnStatement] data AuthnStatement = AuthnStatement {     -- | The timestamp when the assertion was issued.     authnStatementInstant :: !UTCTime,@@ -203,6 +256,8 @@ --------------------------------------------------------------------------------  -- | Represents a SAML2 assertion.++-- Reference [Assertion] data Assertion = Assertion {     -- | The unique ID of this assertion. It is important to keep track of     -- these in order to avoid replay attacks.@@ -221,6 +276,7 @@     assertionAttributeStatement :: !AttributeStatement } deriving (Eq, Show) +-- Reference [Assertion] instance FromXML Assertion where     parseXML cursor = do         issueInstant <- parseUTCTime $@@ -249,3 +305,19 @@         }  --------------------------------------------------------------------------------++-- Reference [AuthnStatement]+--   Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=26+--   Section: 2.7.2 Element <AuthnStatement>++-- Reference [Assertion]+-- Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=15+-- Section 2.3.3 Element <Assertion>++-- Reference [Conditions]+-- Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=21+-- 2.5.1 Element <Conditions>++-- Reference [AudienceRestriction]+-- Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=23+-- Section: 2.5.1.4 Elements <AudienceRestriction> and <Audience>
src/Network/Wai/SAML2/Config.hs view
@@ -8,7 +8,8 @@ -- | Configuration types and smart constructors for the SAML2 middleware. module Network.Wai.SAML2.Config (     SAML2Config(..),-    saml2Config+    saml2Config,+    saml2ConfigNoEncryption ) where  --------------------------------------------------------------------------------@@ -26,7 +27,8 @@     saml2AssertionPath :: !BS.ByteString,     -- | The service provider's private key, used to decrypt data from     -- the identity provider.-    saml2PrivateKey :: !PrivateKey,+    -- when set to Nothing, it rejects encrypted assertions.+    saml2PrivateKey :: !(Maybe PrivateKey),     -- | The identity provider's public key, used to validate     -- signatures.     saml2PublicKey :: !PublicKey,@@ -35,24 +37,49 @@     saml2ExpectedIssuer :: !(Maybe T.Text),     -- | The URL we expect the SAML2 response to contain as destination.     saml2ExpectedDestination :: !(Maybe T.Text),+    -- | The audiences we are a member of. An assertions is only valid if we are+    -- a member of at least one of its audiences. Leaving this empty disables+    -- the check+    --+    -- @since 0.4+    saml2Audiences :: ![T.Text],     -- | A value indicating whether to disable time validity checks. This     -- should not be set to 'True' in a production environment, but may     -- be useful for testing purposes.-    saml2DisableTimeValidation :: !Bool+    saml2DisableTimeValidation :: !Bool,+    -- | Always decrypt assertions using 'saml2PrivateKey' and reject plaintext assertions.+    --+    -- @since 0.4+    saml2RequireEncryptedAssertion :: !Bool }  -- | '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 -- almost certainly change the resulting settings.+-- This requires encrypted assertions by default. saml2Config :: PrivateKey -> PublicKey -> SAML2Config-saml2Config privKey pubKey = SAML2Config{+saml2Config privKey pubKey = (saml2ConfigNoEncryption pubKey){+    saml2PrivateKey = Just privKey,+    saml2RequireEncryptedAssertion = True+}++-- | 'saml2ConfigNoEncryption' @publicKey@ constructs a 'SAML2Config' value+-- with the most basic set of options possible using @publicKey@ as the+-- IdP's public key. You should almost certainly change the resulting settings.+--+-- @since 0.4.0.0+--+saml2ConfigNoEncryption :: PublicKey -> SAML2Config+saml2ConfigNoEncryption pubKey = SAML2Config{     saml2AssertionPath = "/sso/assert",-    saml2PrivateKey = privKey,+    saml2PrivateKey = Nothing,     saml2PublicKey = pubKey,     saml2ExpectedIssuer = Nothing,     saml2ExpectedDestination = Nothing,-    saml2DisableTimeValidation = False+    saml2Audiences = [],+    saml2DisableTimeValidation = False,+    saml2RequireEncryptedAssertion = False }  --------------------------------------------------------------------------------
+ src/Network/Wai/SAML2/EntityDescriptor.hs view
@@ -0,0 +1,107 @@+--------------------------------------------------------------------------------+-- SAML2 Middleware for WAI                                                   --+--------------------------------------------------------------------------------+-- This source code is licensed under the MIT license found in the LICENSE    --+-- file in the root directory of this source tree.                            --+--------------------------------------------------------------------------------++{-# LANGUAGE LambdaCase #-}++-- | This module provides a data type for IDP metadata containing certificate,+-- SSO URLs etc.+--+-- @since 0.4+module Network.Wai.SAML2.EntityDescriptor (+    IDPSSODescriptor(..),+    Binding(..)+) where++--------------------------------------------------------------------------------++import qualified Data.ByteString.Base64 as Base64+import qualified Data.X509 as X509+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T++import Network.Wai.SAML2.XML++import Text.XML.Cursor++--------------------------------------------------------------------------------++-- | Describes metadata of an identity provider.+-- See also section 2.4.3 of [Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0](https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf).+data IDPSSODescriptor+    = IDPSSODescriptor {+        -- | IdP Entity ID. 'Network.Wai.SAML2.Config.saml2ExpectedIssuer' should be compared against this identifier+        entityID :: Text+        -- | The X.509 certificate for signed assertions+    ,   x509Certificate :: X509.SignedExact X509.Certificate+        -- | Supported NameID formats+    ,   nameIDFormats :: [Text]+        -- | List of SSO urls corresponding to 'Binding's+    ,   singleSignOnServices :: [(Binding, Text)]+    } deriving Show++-- | urn:oasis:names:tc:SAML:2.0:bindings+-- https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf+data Binding+    -- | SAML protocol messages are transmitted within the base64-encoded content of an HTML form control+    = HTTPPost+    -- | SAML protocol messages are transmitted within URL parameters+    | HTTPRedirect+    -- | The request and/or response are transmitted by reference using a small stand-in called an artifact+    | HTTPArtifact+    -- | Reverse HTTP Binding for SOAP specification+    | PAOS+    -- | SOAP is a lightweight protocol intended for exchanging structured information in a decentralized, distributed environment+    | SOAP+    -- | SAML protocol messages are encoded into a URL via the DEFLATE compression method+    | URLEncodingDEFLATE+    deriving (Show, Eq)++instance FromXML IDPSSODescriptor where+    parseXML cursor = do+        let entityID = T.concat $ attribute "entityID" cursor+        descriptor <- oneOrFail "IDPSSODescriptor is required"+            $ cursor $/ element (mdName "IDPSSODescriptor")+        rawCertificate <- oneOrFail "X509Certificate is required" $ descriptor+            $/ element (mdName "KeyDescriptor")+            &/ element (dsName "KeyInfo")+            &/ element (dsName "X509Data")+            &/ element (dsName "X509Certificate")+            &/ content+        x509Certificate <- either fail pure+            $ X509.decodeSignedObject+            $ Base64.decodeLenient+            $ T.encodeUtf8 rawCertificate+        let nameIDFormats = descriptor+                $/ element (mdName "NameIDFormat")+                &/ content+        singleSignOnServices <- traverse parseService+            $ descriptor $/ element (mdName "SingleSignOnService")+        pure IDPSSODescriptor{..}++-- | `parseService` @cursor@ attempts to parse a pair of a `Binding` value+-- and a location given as a `Text` value from the XML @cursor@.+parseService :: MonadFail m => Cursor -> m (Binding, Text)+parseService cursor = do+    binding <- oneOrFail "Binding is required" (attribute "Binding" cursor)+        >>= parseBinding+    location <- oneOrFail "Location is required" $ attribute "Location" cursor+    pure (binding, location)++-- | `parseBinding` @uri@ attempts to parse a `Binding` value from @uri@.+parseBinding :: MonadFail m => Text -> m Binding+parseBinding = \case+    "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" -> pure HTTPArtifact+    "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" -> pure HTTPPost+    "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" -> pure HTTPRedirect+    "urn:oasis:names:tc:SAML:2.0:bindings:PAOS" -> pure PAOS+    "urn:oasis:names:tc:SAML:2.0:bindings:SOAP" -> pure SOAP+    "urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE"+        -> pure URLEncodingDEFLATE+    other -> fail $ "Unknown Binding: " <> T.unpack other++--------------------------------------------------------------------------------
src/Network/Wai/SAML2/Error.hs view
@@ -55,10 +55,24 @@     | InvalidDigest     -- | The assertion is not valid.     | NotValid+    -- | Audience restrictions don't match our audiences.+    --+    -- @since 0.4+    | AudienceMismatch [T.Text]     -- | A general crypto error occurred.     | CryptoError CryptoError     -- | The request made to the configured endpoint is not valid.     | InvalidRequest+    -- | The configuration requires an encrypted assertion, but got a+    -- plaintext assertion.+    --+    -- @since 0.4+    | EncryptedAssertionRequired+    -- | The configuration does not support decryption, but got an encrypted+    -- assertion.+    --+    -- @since 0.4+    | EncryptedAssertionNotSupported     deriving Show  --------------------------------------------------------------------------------
+ src/Network/Wai/SAML2/NameIDFormat.hs view
@@ -0,0 +1,90 @@+--------------------------------------------------------------------------------+-- SAML2 Middleware for WAI                                                   --+--------------------------------------------------------------------------------+-- This source code is licensed under the MIT license found in the LICENSE    --+-- file in the root directory of this source tree.                            --+--------------------------------------------------------------------------------++{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}++-- | This modules defines the `NameIDFormat` type, which specifies the format+-- of name identifiers in an assertion.+--+-- @since 0.4+module Network.Wai.SAML2.NameIDFormat (+    NameIDFormat(..),+    parseNameIDFormat,+    showNameIDFormat+) where++--------------------------------------------------------------------------------++import GHC.Generics (Generic)++import Data.Text (Text, unpack)++--------------------------------------------------------------------------------++-- | Format of the subject identifier.+-- See 8.3 Name Identifier Format Identifiers in https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf+data NameIDFormat+    -- | The interpretation is left to individual implementations+    = Unspecified+    -- | @addr-spec@ as defined in IETF RFC 2822+    | EmailAddress+    -- | contents of the @<ds:X509SubjectName>@ element in the XML Signature Recommendation+    | X509SubjectName+    -- | String of the form @DomainName\UserName@+    | WindowsDomainQualifiedName+    -- | Kerberos principal name using the format @name[/instance]@REALM@+    | KerberosPrincipalName+    -- | identifier of an entity that provides SAML-based services+    -- (such as a SAML authority, requester, or responder) or is a participant in SAML profiles (such as a service+    -- provider supporting the browser SSO profile)+    | Entity+    -- | identifier of a provider of SAML-based services+    -- (such as a SAML authority) or a participant in SAML+    -- profiles (such as a service provider supporting the browser profiles)+    | Provider+    -- | persistent opaque identifier that corresponds to an identity+    -- federation between an identity provider and a service provider+    | Federated+    -- | an identifier with transient semantics and SHOULD be treated+    -- as an opaque and temporary value by the relying party+    | Transient+    -- | persistent opaque identifier for a principal that is specific to+    -- an identity provider and a service provider or affiliation of service providers+    | Persistent+    deriving (Eq, Ord, Show, Generic)++-- | Parse a 'NameIDFormat' (prefixed by @urn:oasis:names:tc:SAML:*:nameid-format@).+parseNameIDFormat :: MonadFail m => Text -> m NameIDFormat+parseNameIDFormat = \case+    "urn:oasis:names:tc:SAML:1.1:nameid-format:Kerberos" -> pure KerberosPrincipalName+    "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName" -> pure WindowsDomainQualifiedName+    "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName" -> pure X509SubjectName+    "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" -> pure EmailAddress+    "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" -> pure Unspecified+    "urn:oasis:names:tc:SAML:2.0:nameid-format:entity" -> pure Entity+    "urn:oasis:names:tc:SAML:2.0:nameid-format:federated" -> pure Federated+    "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" -> pure Persistent+    "urn:oasis:names:tc:SAML:2.0:nameid-format:provider" -> pure Provider+    "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" -> pure Transient+    unknown -> fail $ "parseNameIDFormat: unknown format " <> unpack unknown++-- | Displays a `NameIDFormat` value as a `Text` value.+showNameIDFormat :: NameIDFormat -> Text+showNameIDFormat = \case+    KerberosPrincipalName -> "urn:oasis:names:tc:SAML:1.1:nameid-format:Kerberos"+    WindowsDomainQualifiedName -> "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName"+    X509SubjectName -> "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"+    EmailAddress -> "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"+    Unspecified -> "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"+    Entity -> "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"+    Federated -> "urn:oasis:names:tc:SAML:2.0:nameid-format:federated"+    Persistent -> "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"+    Provider -> "urn:oasis:names:tc:SAML:2.0:nameid-format:provider"+    Transient -> "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"++--------------------------------------------------------------------------------
+ src/Network/Wai/SAML2/Request.hs view
@@ -0,0 +1,169 @@+-------------------------------------------------------------------------------+-- SAML2 Middleware for WAI                                                  --+-------------------------------------------------------------------------------+-- This source code is licensed under the MIT license found in the LICENSE   --+-- file in the root directory of this source tree.                           --+-------------------------------------------------------------------------------++-- | Defines types and functions for SP-initiated SSO. Use `issueAuthnRequest`+-- to initialise an `AuthnRequest` value which stores the parameters for the+-- authentication request you wish to issue to the IdP. You can update this+-- value as required.+--+-- Use `renderBase64` to render the request for use with a HTTP POST binding [1], or+-- `renderUrlEncodingDeflate` for HTTP redirect binding[2] respectively.+-- You may wish to read+-- the [SAML2 overview for this process](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0-cd-02.html#5.1.2.SP-Initiated%20SSO:%20%20Redirect/POST%20Bindings|outline).+--+-- * [1] https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf#page=21+--   Section 3.5 HTTP POST Binding+-- * [2] https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf#page=15+--   Section 3.4 HTTP Redirect Binding+--+-- @since 0.4+module Network.Wai.SAML2.Request (+    AuthnRequest(..),+    issueAuthnRequest,+    renderBase64,+    renderUrlEncodingDeflate,+    renderXML,+) where++-------------------------------------------------------------------------------++import Crypto.Random++import Data.Time.Clock++import Network.Wai.SAML2.NameIDFormat+import Network.Wai.SAML2.XML++import Text.XML++import qualified Codec.Compression.Zlib.Raw as Deflate+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as Base16+import qualified Data.ByteString.Base64 as Base64+import qualified Data.ByteString.Lazy as BL+import qualified Data.Map.Strict as Map+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.HTTP.Types (urlEncode)++-------------------------------------------------------------------------------++-- | Parameters for SP-initiated SSO+data AuthnRequest+    -- Reference [AuthnRequest]+    = AuthnRequest {+        -- | The time at which 'AuthnRequest' was created.+        authnRequestTimestamp :: !UTCTime+        -- | Unique identifier for 'AuthnRequest' which should be preserved+        -- by the IdP in its response.+    ,   authnRequestID :: !T.Text+        -- | SP Entity ID+    ,   authnRequestIssuer :: !T.Text+        -- | Allow IdP to generate a new identifier+    ,   authnRequestAllowCreate :: !Bool+        -- | The URI reference corresponding to a name identifier format+    ,   authnRequestNameIDFormat :: !NameIDFormat+    }+    deriving (Eq, Show)++-- | Creates a default 'AuthnRequest' with the current timestamp and a+-- randomly-generated ID.+issueAuthnRequest+    :: T.Text -- ^ SP Entity ID+    -> IO AuthnRequest+issueAuthnRequest authnRequestIssuer = do+    authnRequestTimestamp <- getCurrentTime+    -- Digits are not allowed in initial position+    -- Reference [ID Values]+    authnRequestID <- ("id" <>) . T.decodeUtf8 . Base16.encode <$> getRandomBytes 16+    pure AuthnRequest{+        authnRequestAllowCreate = True+    ,   authnRequestNameIDFormat = Transient+    ,   ..+    }++-- | Renders an `AuthnRequest` for SP initiated SSO according to+-- @urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE@ and suitable for+-- use with HTTP Redirect binding+--+-- The value should be sent as a query parameter named @SAMLRequest@+renderUrlEncodingDeflate :: AuthnRequest -> B.ByteString+renderUrlEncodingDeflate request =+    urlEncode True $ Base64.encode $ BL.toStrict $ Deflate.compress $ renderXML request++-- | Renders and base64-encodes an `AuthnRequest` for SP initiated SSO suitable+-- for use with HTTP POST binding+--+-- If used in an HTTP POST binding, the value should be sent as an invisible+-- form control named @SAMLRequest@+renderBase64 :: AuthnRequest -> B.ByteString+renderBase64 request = Base64.encode $ BL.toStrict $ renderXML request++-- | Render an `AuthnRequest` as XML+renderXML :: AuthnRequest -> BL.ByteString+renderXML AuthnRequest{..} =+    renderLBS def $+    -- Reference [HTTP redirect binding]+    Document{+        documentPrologue = Prologue [] Nothing []+    ,   documentRoot = root+    ,   documentEpilogue = []+    }+    where+        timestamp = showUTCTime authnRequestTimestamp+        root = Element+            (saml2pName "AuthnRequest")+            (Map.fromList+                [ ("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol")+                , ("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")+                , ("ID", authnRequestID) -- Reference [RequestAbstractType] and see [ID Values]+                , ("Version", "2.0")  -- [RequestAbstractType]+                , ("IssueInstant", timestamp) -- [RequestAbstractType]+                , ("AssertionConsumerServiceIndex", "1") -- [AuthnRequest]+                ])+            [NodeElement issuer, NodeElement nameIdPolicy]+        -- Reference [RequestAbstractType]+        issuer = Element+            (saml2Name "Issuer")+            mempty+            [NodeContent authnRequestIssuer]+        -- Reference [AuthnRequest]+        nameIdPolicy = Element+            (saml2pName "NameIDPolicy")+            (Map.fromList+                [ ("allowCreate"+                    , if authnRequestAllowCreate then "true" else "false")+                , ("Format", showNameIDFormat authnRequestNameIDFormat)+                ])+            []++-------------------------------------------------------------------------------++-- Reference [RequestAbstractType]+-- Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=36+-- Section: 3.2.1 Complex Type RequestAbstractType++-- Reference [AuthnRequest]+-- Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=48+-- Section: 3.4.1 Element <AuthnRequest>++-- Reference [HTTP redirect binding]+-- Source:+-- https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf#page=15+-- Section: 3.4 HTTP Redirect Binding++-- Reference [ID Values]+-- Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=9+-- Section: 1.3.4 ID and ID Reference Values+-- Note: ID Values must conform to "xs:ID", which in turn has a restriction of "xs:NCName" (non-colonized name).+-- In practice that means they are a string consisting of+-- first 1 of: Letter or '_'+-- then 0 or more of: Letter, Digit, '.', '-', '_',  CombiningChar, Extender+--+-- Definitions of character classes: https://www.w3.org/TR/2000/WD-xml-2e-20000814#CharClasses+-- Compare e.g. https://stackoverflow.com/questions/1631396/what-is-an-xsncname-type-and-when-should-it-be-used+--and https://www.w3.org/TR/xmlschema-2/#dt-ccesN (see \i and \c, bute not that colons are excluded)
src/Network/Wai/SAML2/Response.hs view
@@ -19,12 +19,14 @@  -------------------------------------------------------------------------------- +import Data.Maybe (listToMaybe) import qualified Data.Text as T import Data.Time  import Text.XML import Text.XML.Cursor +import Network.Wai.SAML2.Assertion import Network.Wai.SAML2.XML import Network.Wai.SAML2.XML.Encrypted import Network.Wai.SAML2.StatusCode@@ -33,9 +35,15 @@ --------------------------------------------------------------------------------  -- | Represents SAML2 responses.++-- Reference [StatusResponseType] data Response = Response {     -- | The intended destination of this response.     responseDestination :: !T.Text,+    -- | The ID of the request this responds corresponds to, if any.+    --+    -- @since 0.4+    responseInResponseTo :: !(Maybe T.Text),     -- | The unique ID of the response.     responseId :: !T.Text,     -- | The timestamp when the response was issued.@@ -48,11 +56,18 @@     responseStatusCode :: !StatusCode,     -- | The response signature.     responseSignature :: !Signature,-    -- | The (encrypted) assertion.-    responseEncryptedAssertion :: !EncryptedAssertion+    -- | The unencrypted assertion.+    --+    -- @since 0.4+    responseAssertion :: !(Maybe Assertion),+    -- | The encrypted assertion.+    --+    -- @since 0.4+    responseEncryptedAssertion :: !(Maybe EncryptedAssertion) } deriving (Eq, Show)  instance FromXML Response where+    -- Reference [StatusResponseType]     parseXML cursor = do         issueInstant <- parseUTCTime                       $ T.concat@@ -62,8 +77,13 @@             Nothing -> fail "Invalid status code"             Just sc -> pure sc -        encAssertion <- oneOrFail "EncryptedAssertion is required"-                    (   cursor+        let assertion = listToMaybe+                    $ ( cursor+                    $/  element (saml2Name "Assertion")+                    ) >>= parseXML++        let encAssertion = listToMaybe+                    $ ( cursor                     $/  element (saml2Name "EncryptedAssertion")                     ) >>= parseXML @@ -73,12 +93,14 @@         pure Response{             responseDestination = T.concat $ attribute "Destination" cursor,             responseId = T.concat $ attribute "ID" cursor,+            responseInResponseTo = listToMaybe $ attribute "InResponseTo" cursor,             responseIssueInstant = issueInstant,             responseVersion = T.concat $ attribute "Version" cursor,             responseIssuer = T.concat $                 cursor $/ element (saml2Name "Issuer") &/ content,             responseStatusCode = statusCode,             responseSignature = signature,+            responseAssertion = assertion,             responseEncryptedAssertion = encAssertion         } @@ -112,3 +134,7 @@     pure signedInfo  --------------------------------------------------------------------------------++-- Reference [StatusResponseType]+--   Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=38+--   Section: 3.2.2 Complex Type StatusResponseType
src/Network/Wai/SAML2/StatusCode.hs view
@@ -5,39 +5,206 @@ -- file in the root directory of this source tree.                            -- -------------------------------------------------------------------------------- --- | SAML2 status codes.+-- | The SAML2 specification distinguishes between the topmost status code,+-- which is required and must contain a status value from a specific list of+-- status codes, and subordinate status codes, which are optional and may+-- contain arbitrary URIs. module Network.Wai.SAML2.StatusCode (-    StatusCode(..)+    StatusCode(..),+    StatusCodeValue(..) ) where  --------------------------------------------------------------------------------  import Control.Monad +import Data.Maybe import qualified Data.Text as T  import Text.XML.Cursor +import Network.URI (URI, parseURI) import Network.Wai.SAML2.XML  -------------------------------------------------------------------------------- --- | Enumerates SAML2 status codes.+-- | Represents SAML2 status codes, which are comprised of a status value+-- and an optional, subordinate status. data StatusCode+    = MkStatusCode {+        -- | The status code value.+        statusCodeValue :: !StatusCodeValue,+        -- | An optional, subordinate status code.+        statusCodeSubordinate :: !(Maybe StatusCode)+    }+    deriving (Eq, Show)++-- | Enumerates SAML2 status code values.+--+-- @since 0.4+data StatusCodeValue     -- | The response indicates success!     = Success+    -- | The request could not be performed due to an error on the part of the+    -- requester.+    | Requester+    -- | The request could not be performed due to an error on the part of the+    -- SAML responder or SAML authority.+    | Responder+    -- | The SAML responder could not process the request because the version+    -- of the request message was incorrect.+    | VersionMismatch+    -- | The responding provider was unable to successfully authenticate the+    -- principal.+    | AuthnFailed+    -- | Unexpected or invalid content was encountered within a+    -- @\<saml:Attribute\>@ or @\<saml:AttributeValue\>@ element.+    | InvalidAttrNameOrValue+    -- | The responding provider cannot or will not support the requested name+    -- identifier policy.+    | InvalidNameIDPolicy+    -- | The specified authentication context requirements cannot be met by the+    -- responder.+    | NoAuthnContext+    -- | Used by an intermediary to indicate that none of the supported+    -- identity provider @\<Loc\>@ elements in an @\<IDPList\>@ can be resolved+    -- or that none of the supported identity providers are available.+    | NoAvailableIDP+    -- | Indicates the responding provider cannot authenticate the principal+    -- passively, as has been requested.+    | NoPassive+    -- | Used by an intermediary to indicate that none of the identity+    -- providers in an @\<IDPList\>@ are supported by the intermediary.+    | NoSupportedIDP+    -- | Used by a session authority to indicate to a session participant that+    -- it was not able to propagate logout to all other session participants.+    | PartialLogout+    -- | Indicates that a responding provider cannot authenticate the principal+    -- directly and is not permitted to proxy the request further.+    | ProxyCountExceeded+    -- | The SAML responder or SAML authority is able to process the request+    -- but has chosen not to respond. This status code MAY be used when there+    -- is concern about the security context of the request message or the+    -- sequence of request messages received from a particular requester.+    | RequestDenied+    -- | The SAML responder or SAML authority does not support the request.+    | RequestUnsupported+    -- | The SAML responder cannot process any requests with the protocol+    --  version specified in the request.+    | RequestVersionDeprecated+    -- | The SAML responder cannot process the request because the protocol+    -- version specified in the request message is a major upgrade from the+    -- highest protocol version supported by the responder.+    | RequestVersionTooHigh+    -- | The SAML responder cannot process the request because the protocol+    -- version specified in the request message is too low.+    | RequestVersionTooLow+    -- | The resource value provided in the request message is invalid or+    -- unrecognized.+    | ResourceNotRecognized+    -- | The response message would contain more elements than the SAML+    -- responder is able to return.+    | TooManyResponses+    -- | An entity that has no knowledge of a particular attribute profile+    -- has been presented with an attribute drawn from that profile.+    | UnknownAttrProfile+    -- | The responding provider does not recognize the principal specified+    -- or implied by the request.+    | UnknownPrincipal+    -- | The SAML responder cannot properly fulfil the request using the+    -- protocol binding specified in the request.+    | UnsupportedBinding+    -- | The SAML2 specification notes that a status code value can be any+    -- valid URI and that additional subordinate status codes may be+    -- introduced in the future.+    | OtherStatus URI     deriving (Eq, Show)  instance FromXML StatusCode where-    parseXML cursor =-        let value = T.concat-                $   cursor-                $/  element (saml2pName "Status")-                &/  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."+    parseXML = parseStatusCode True +-- | `parseStatusCode` @isTopLevel cursor@ attempts to parse a @<StatusCode>@+-- element from the XML @cursor@. The SAML2 specification distinguishes+-- between the topmost status code, which is required and must contain a+-- status value from a specific list of status codes, and subordinate status+-- codes. The @isTopLevel@ value indicates whether we are parsing a top-level+-- @<StatusCode>@ element or not and therefore controls which status codes+-- values we accept as valid.+--+-- @since 0.4+parseStatusCode :: MonadFail m => Bool -> Cursor -> m StatusCode+parseStatusCode isTopLevel cursor = do+    statusCodeValue <- oneOrFail "Value is a required attribute" $+        cursor $/+            element (saml2pName "Status") &/+            element (saml2pName "StatusCode") >=>+            parseStatusCodeValue isTopLevel+    let statusCodeSubordinate = listToMaybe (+            cursor $/+                element (saml2pName "Status") &/+                element (saml2pName "StatusCode")) >>=+                parseStatusCode False++    pure MkStatusCode{..}++-- | `parseStatusCodeValue` @isTopLevel cursor@ attempts to parse a status code+-- value from the XML @cursor@. The @isTopLevel@ value determines which values+-- we permit as valid status code values. See the note for `parseStatusCode`.+--+-- @since 0.4+parseStatusCodeValue :: MonadFail m => Bool -> Cursor -> m StatusCodeValue+parseStatusCodeValue isTopLevel cursor =+    case T.concat $ attribute "Value" cursor of+        -- the following status codes are always permitted+        "urn:oasis:names:tc:SAML:2.0:status:Success" -> pure Success+        "urn:oasis:names:tc:SAML:2.0:status:Requester" -> pure Requester+        "urn:oasis:names:tc:SAML:2.0:status:Responder" -> pure Responder+        "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch" ->+            pure VersionMismatch+        -- the following are only permitted for subordinate elements+        "urn:oasis:names:tc:SAML:2.0:status:AuthnFailed" | not isTopLevel ->+            pure AuthnFailed+        "urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue" | not isTopLevel ->+            pure InvalidAttrNameOrValue+        "urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy" | not isTopLevel ->+            pure InvalidNameIDPolicy+        "urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext" | not isTopLevel ->+            pure NoAuthnContext+        "urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP" | not isTopLevel ->+            pure NoAvailableIDP+        "urn:oasis:names:tc:SAML:2.0:status:NoPassive" | not isTopLevel ->+            pure NoPassive+        "urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP" | not isTopLevel ->+            pure NoSupportedIDP+        "urn:oasis:names:tc:SAML:2.0:status:PartialLogout" | not isTopLevel ->+            pure PartialLogout+        "urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded" | not isTopLevel ->+            pure ProxyCountExceeded+        "urn:oasis:names:tc:SAML:2.0:status:RequestDenied" | not isTopLevel ->+            pure RequestDenied+        "urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported" | not isTopLevel ->+            pure RequestUnsupported+        "urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated" | not isTopLevel ->+            pure RequestVersionDeprecated+        "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh" | not isTopLevel ->+            pure RequestVersionTooHigh+        "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow" | not isTopLevel ->+            pure RequestVersionTooLow+        "urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized" | not isTopLevel ->+            pure ResourceNotRecognized+        "urn:oasis:names:tc:SAML:2.0:status:TooManyResponses" | not isTopLevel ->+            pure TooManyResponses+        "urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile" | not isTopLevel ->+            pure UnknownAttrProfile+        "urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal" | not isTopLevel ->+            pure UnknownPrincipal+        "urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding" | not isTopLevel ->+            pure UnsupportedBinding+        uriString | not isTopLevel -> case parseURI $ T.unpack uriString of+            Nothing -> fail $ "Not a valid status code: " <> T.unpack uriString+            Just uri -> pure $ OtherStatus uri+        -- not a valid URI or a status code that's not supported at the+        -- top-level+        xs -> fail $ "Not a valid status code: " <> T.unpack xs  --------------------------------------------------------------------------------
src/Network/Wai/SAML2/Validation.hs view
@@ -8,6 +8,8 @@ -- | Functions to process and validate SAML2 respones. module Network.Wai.SAML2.Validation (     validateResponse,+    decodeResponse,+    validateSAMLResponse,     ansiX923 ) where @@ -20,6 +22,7 @@ import Crypto.Hash import qualified Crypto.PubKey.RSA.OAEP as OAEP import Crypto.PubKey.RSA.PKCS15 as PKCS15+import Crypto.PubKey.RSA.Types (PrivateKey) import Crypto.Cipher.AES import Crypto.Cipher.Types @@ -46,11 +49,21 @@ -- in Base64-encoded @responseData@. validateResponse :: SAML2Config                  -> BS.ByteString-                 -> IO (Either SAML2Error Assertion)+                 -> IO (Either SAML2Error (Assertion, Response)) validateResponse cfg responseData = runExceptT $ do     -- get the current time-    now <- liftIO $ getCurrentTime+    now <- liftIO getCurrentTime +    (responseXmlDoc, samlResponse) <- decodeResponse responseData+    assertion <- validateSAMLResponse cfg responseXmlDoc samlResponse now+    pure (assertion, samlResponse)++-- | 'decodeResponse' @responseData@ decodes a SAML2 response contained+-- in Base64-encoded @responseData@.+--+-- @since 0.4+decodeResponse :: BS.ByteString -> ExceptT SAML2Error IO (XML.Document, Response)+decodeResponse responseData = do     -- the response data is Base64-encoded; decode it     let resXmlDocData = BS.decodeLenient responseData @@ -64,14 +77,25 @@     resParseResult <- liftIO $ try $         parseXML (XML.fromDocument responseXmlDoc) -    samlResponse <- case resParseResult of+    case resParseResult of         Left err -> throwError $ InvalidResponse err-        Right samlResponse -> pure samlResponse+        Right samlResponse -> pure (responseXmlDoc, samlResponse) +-- | 'validateSAMLResponse' @cfg doc response timestamp@ validates a decoded SAML2+-- response using the given @timestamp@.+--+-- @since 0.4+validateSAMLResponse :: SAML2Config+                     -> XML.Document+                     -> Response+                     -> UTCTime+                     -> ExceptT SAML2Error IO Assertion+validateSAMLResponse cfg responseXmlDoc samlResponse now = do+     -- check that the response indicates success-    case responseStatusCode samlResponse of+    case statusCodeValue $ responseStatusCode samlResponse of         Success -> pure ()-        status -> throwError $ Unsuccessful status+        _status -> throwError $ Unsuccessful $ responseStatusCode samlResponse      -- check that the destination is as expected, if the configuration     -- expects us to validate this@@ -174,14 +198,46 @@     then pure ()     else throwError InvalidSignature -    --  ***ASSERTION DECRYPTION***-    -- the SAML assertion is AES-encrypted and we need to acquire the key-    -- to decrypt it; the key itself is RSA-encrypted:-    -- get the private key from the configuration and use it to decrypt-    -- the key used to decrypt the assertion-    let pk = saml2PrivateKey cfg-    let encryptedAssertion = responseEncryptedAssertion samlResponse+    assertion <- case responseEncryptedAssertion samlResponse of+        Just encrypted -> case saml2PrivateKey cfg of+            Just pk -> decryptAssertion pk encrypted+            Nothing -> throwError EncryptedAssertionNotSupported+        Nothing+            | saml2RequireEncryptedAssertion cfg -> throwError EncryptedAssertionRequired+            | otherwise -> case responseAssertion samlResponse of+                Just plain -> pure plain+                Nothing -> throwError $ InvalidResponse $ userError "Assertion or EncryptedAssertion is required" +    -- validate that the assertion is valid at this point in time+    let Conditions{..} = assertionConditions assertion++    -- Reference [NotBefore and NotOnOrAfter]+    when ((now < conditionsNotBefore || now >= conditionsNotOnOrAfter) &&+           not (saml2DisableTimeValidation cfg))+          $ throwError NotValid++    -- Reference [AudienceRestriction]+    -- Note [Validating AudienceRestrictions]+    case saml2Audiences cfg of+        -- Check disabled+        [] -> pure ()+        ourAudiences ->+           forM_ conditionsAudienceRestrictions $+              \(AudienceRestriction audiences) ->+                 unless (any (`elem` ourAudiences) audiences)+                   $ throwError (AudienceMismatch audiences)++    -- all checks out, return the assertion+    pure assertion++-- | `decryptAssertion` @key encryptedAssertion@ decrypts the AES key in+-- @encryptedAssertion@ using `key`, then decrypts the contents using+-- the AES key.+--+-- @since 0.4+decryptAssertion :: PrivateKey -> EncryptedAssertion -> ExceptT SAML2Error IO Assertion+decryptAssertion pk encryptedAssertion = do+     oaepResult <- liftIO $ OAEP.decryptSafer (OAEP.defaultOAEPParams SHA1) pk         $ BS.decodeLenient         $ cipherValue@@ -221,7 +277,7 @@                         Just xmlData -> pure xmlData      -- try to parse the assertion that we decrypted earlier-    assertion <- case XML.parseLBS def (LBS.fromStrict xmlData) of+    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@@ -233,17 +289,6 @@                 Left err -> throwError $ InvalidAssertion err                 Right assertion -> pure assertion -    -- validate that the assertion is valid at this point in time-    let Conditions{..} = assertionConditions assertion--    if (now < conditionsNotBefore || now >= conditionsNotOnOrAfter) &&-        not (saml2DisableTimeValidation cfg)-    then throwError NotValid-    else pure ()--    -- all checks out, return the assertion-    pure assertion- -- | '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@@ -257,3 +302,22 @@           (content,_) = BS.splitAt (len - padLen) d  --------------------------------------------------------------------------------++-- Reference [NotBefore and NotOnOrAfter]+-- Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=23+-- Section: 2.5.1.2 Attributes NotBefore and NotOnOrAfter++-- Reference [AudienceRestriction]+-- Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=23+-- Section: 2.5.1.4 Elements <AudienceRestriction> and <Audience>++-- Note [Validating AudienceRestrictions]+--+-- > Note that multiple <AudienceRestriction> elements MAY be included in a single+-- > assertion, and each MUST be evaluated independently. The effect of this+-- > requirement and the preceding definition is that within a given condition,+-- > the audiences form a disjunction (an "OR") while multiple conditions form a+-- > conjunction (an "AND").+--+-- Source: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=24+-- Lines 922-925
src/Network/Wai/SAML2/XML.hs view
@@ -12,9 +12,11 @@     saml2pName,     xencName,     dsName,+    mdName,      -- * Utility functions     toMaybeText,+    showUTCTime,     parseUTCTime,      -- * XML parsing@@ -26,6 +28,7 @@  import qualified Data.Text as T import Data.Time+import Data.Time.Format.ISO8601 (iso8601ParseM)  import Text.XML import Text.XML.Cursor@@ -56,6 +59,13 @@ dsName name =     Name name (Just "http://www.w3.org/2000/09/xmldsig#") (Just "ds") +-- | `mdName` @name@ constructs a `Name` for @name@ in the+-- @urn:oasis:names:tc:SAML:2.0:metadata@ namespace.+mdName :: T.Text -> Name+mdName name =+    Name name (Just "urn:oasis:names:tc:SAML:2.0:metadata") (Just "md")++ -- | '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@@ -64,12 +74,19 @@  -- | The time format used by SAML2. timeFormat :: String-timeFormat = "%Y-%m-%dT%H:%M:%S%QZ"+timeFormat = "%Y-%m-%dT%H:%M:%S%6QZ" +-- | Display a 'UTCTime' as an ISO8601 timestamp including up to+-- 6 digits for the microseconds.+--+-- @since 0.4.0.0+--+showUTCTime :: UTCTime -> T.Text+showUTCTime = T.pack . formatTime defaultTimeLocale timeFormat+ -- | '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 = iso8601ParseM . T.unpack  -- | A class of types which can be parsed from XML. class FromXML a where
tests/Parser.hs view
@@ -1,3 +1,7 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE TypeApplications #-}+import Network.Wai.SAML2.EntityDescriptor import Network.Wai.SAML2.Response import Network.Wai.SAML2.XML import System.FilePath@@ -8,21 +12,25 @@ import qualified Data.ByteString.Lazy.Char8 as BC import qualified Text.XML as XML -run :: FilePath -> IO BC.ByteString+run :: forall t. (FromXML t, Show t) => FilePath -> IO BC.ByteString run src = do     doc <- XML.readFile XML.def src     resp <- parseXML (fromDocument doc)-    pure $ BC.pack $ ppShow (resp :: Response)+    pure $ BC.pack $ ppShow (resp :: t)  main :: IO () main = defaultMain $ testGroup "Parse SAML2 response"-    [ mkGolden $ prefix </> "keycloak.xml"-    , mkGolden $ prefix </> "okta.xml"+    [ mkGolden @Response $ prefix </> "keycloak.xml"+    , mkGolden @Response $ prefix </> "okta.xml"+    , mkGolden @Response $ prefix </> "google.xml"+    , mkGolden @IDPSSODescriptor $ prefix </> "metadata/keycloak.xml"+    , mkGolden @IDPSSODescriptor $ prefix </> "metadata/google.xml"     ]     where         prefix = "tests/data"+        mkGolden :: forall t. (FromXML t, Show t) => FilePath -> TestTree         mkGolden path = goldenVsStringDiff                 (takeBaseName path)                 (\ref new -> ["diff", "-u", ref, new])                 (path <.> "expected")-                (run path)+                (run @t path)
+ tests/data/google.xml view
@@ -0,0 +1,66 @@+<?xml version="1.0" encoding="UTF-8" standalone="no"?>+<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://herp.loopback.ja-sore.de/auth/page/saml2/login" ID="_b9917f3478ff3f776cb351547379d0bc" InResponseTo="1WKzv/yHPExwtkwhgObMyn/GVaTIIZAc+DqEfqe1YuYyjmw4YitoSHGhI/xufWqEHUhrSPtrzzyi5938vebpVg==" IssueInstant="2022-09-20T10:30:41.151Z" Version="2.0">+  <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=C01aa60hc</saml2:Issuer>+  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">+    <ds:SignedInfo>+      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>+      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>+      <ds:Reference URI="#_b9917f3478ff3f776cb351547379d0bc">+        <ds:Transforms>+          <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>+          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>+        </ds:Transforms>+        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>+        <ds:DigestValue>z3YXe+aIiyAYbSGqxRiYPwTO38JNZad2WBgqinjiX8g=</ds:DigestValue>+      </ds:Reference>+    </ds:SignedInfo>+    <ds:SignatureValue>S6IAb+4yYyNVIfnqM7ZIrFSKbzi0RgVqk6qNL0ehzFZhcaArqN/8S0oCDrrc+0B2ygCkpbDKt03/+vNIxq862VYa1Y39n8w0/eIKc+m3beNS6e+n316o3LCtSTPvgC2kE96Jh44MOUX9z/KIYxXiPGgdi+nnlxK6L1dZGrMKKBxEFiBJMhFI42gWADyrUz311PDImM+rVhINJT9NyD1i1G++PILLer/BlGwg5n+wNQHmjt4pV3m+8zMMIrfLjZT/YX8s7+1Xn1Y3bMzo6PnsbvS7OX1YGuN4cPn9f4MLu5Vcn9lWo/r++zuZay/pS0mEqTmeOOMGk4BOseiU/hAra5NDgQ==</ds:SignatureValue>+    <ds:KeyInfo>+      <ds:X509Data>+        <ds:X509SubjectName>ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc.</ds:X509SubjectName>+        <ds:X509Certificate>MIIDdDCCAlygAwIBAgIGAYIgDLo9MA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ+bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv+b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMjIwNzIx+MDkxODAwWhcNMjcwNzIwMDkxODAwWjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN+TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx+CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A+MIIBCgKCAQEA9KQNdYuN7oPUsJ1SK2L58egBELYIWr5cRd9PXuV1k1s2hzt2vMoPS1l788+/hVb4+CtZCeabZDFYfNMbS2olbhG06FrS9+ptm2pKucXqEp+KM9FozmK/qn7+geQl7uHdcT0xRqfVK46Wr+RB4S8VdbK9WIK2prCvHIPu1O9eH+m9g+G1Y116RIqKKGfGbsWdhyNY47+LcxXzThgdfi4BPwcB/i+K/X/i4qNje4FLHrAJQ7S6bRiTXPyv7q9Md96R1T9hXdFA9siW9rmrJCg/H022TrqxwctlIkQjPGj+yskF3vCcwuIj5M8PuP37k6AuiBZdV73gG8t8SgoMHhElWSLKowIDAQABMA0GCSqGSIb3DQEBCwUA+A4IBAQDuIthaqITon7u3aZg70NutC54EsQ6NLfWsbwgadOahb5EtGnBkKB7bairnTSAMtH4wLnNj+BrgxswnxVd5DdrmqpuoY+Pn3hAXYBCFwmAzEEYwmfkZbTVa7AcT6bw6nOqHrfnYump9LhUwxI4dV+4Yr2YkLL767CeSMLv58GBlFAqxP63HVMa1BJAhZhjamqvSYQCJZYnBG7Nuy2ek6Y5kHQM/8L0h6n+gx1Jv0nPvPI+9NlWRkUhDN/jQkzxKcYnCNlT/bi3xY+QjFWD4z/Qx2h9hrxZtLRtcrp6WqUmpXha+dq1JsmSbXwRj4Tu7IVZ4tYqeY/mOJGYf8fr5h/Q8GRCd</ds:X509Certificate>+      </ds:X509Data>+    </ds:KeyInfo>+  </ds:Signature>+  <saml2p:Status>+    <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>+  </saml2p:Status>+  <saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_e7e8860fa89585efa9c8013a0f8116a2" IssueInstant="2022-09-20T10:30:41.151Z" Version="2.0">+    <saml2:Issuer>https://accounts.google.com/o/saml2?idpid=C01aa60hc</saml2:Issuer>+    <saml2:Subject>+      <saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">sdlc-standard@herp.chat</saml2:NameID>+      <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">+        <saml2:SubjectConfirmationData InResponseTo="1WKzv/yHPExwtkwhgObMyn/GVaTIIZAc+DqEfqe1YuYyjmw4YitoSHGhI/xufWqEHUhrSPtrzzyi5938vebpVg==" NotOnOrAfter="2022-09-20T10:35:41.151Z" Recipient="https://herp.loopback.ja-sore.de/auth/page/saml2/login"/>+      </saml2:SubjectConfirmation>+    </saml2:Subject>+    <saml2:Conditions NotBefore="2022-09-20T10:25:41.151Z" NotOnOrAfter="2022-09-20T10:35:41.151Z">+      <saml2:AudienceRestriction>+        <saml2:Audience>https://v1.herp.cloud/</saml2:Audience>+      </saml2:AudienceRestriction>+    </saml2:Conditions>+    <saml2:AuthnStatement AuthnInstant="2022-09-20T09:33:25.000Z" SessionIndex="_e7e8860fa89585efa9c8013a0f8116a2">+      <saml2:AuthnContext>+        <saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>+      </saml2:AuthnContext>+    </saml2:AuthnStatement>+  </saml2:Assertion>+</saml2p:Response>
+ tests/data/google.xml.expected view
@@ -0,0 +1,77 @@+Response+  { responseDestination =+      "https://herp.loopback.ja-sore.de/auth/page/saml2/login"+  , responseInResponseTo =+      Just+        "1WKzv/yHPExwtkwhgObMyn/GVaTIIZAc+DqEfqe1YuYyjmw4YitoSHGhI/xufWqEHUhrSPtrzzyi5938vebpVg=="+  , responseId = "_b9917f3478ff3f776cb351547379d0bc"+  , responseIssueInstant = 2022-09-20 10:30:41.151 UTC+  , responseVersion = "2.0"+  , responseIssuer =+      "https://accounts.google.com/o/saml2?idpid=C01aa60hc"+  , responseStatusCode =+      MkStatusCode+        { statusCodeValue = Success , statusCodeSubordinate = Nothing }+  , responseSignature =+      Signature+        { signatureInfo =+            SignedInfo+              { signedInfoCanonicalisationMethod = C14N_EXC_1_0+              , signedInfoSignatureMethod = RSA_SHA256+              , signedInfoReference =+                  Reference+                    { referenceURI = "_b9917f3478ff3f776cb351547379d0bc"+                    , referenceDigestMethod = DigestSHA256+                    , referenceDigestValue =+                        "z3YXe+aIiyAYbSGqxRiYPwTO38JNZad2WBgqinjiX8g="+                    }+              }+        , signatureValue =+            "S6IAb+4yYyNVIfnqM7ZIrFSKbzi0RgVqk6qNL0ehzFZhcaArqN/8S0oCDrrc+0B2ygCkpbDKt03/\nvNIxq862VYa1Y39n8w0/eIKc+m3beNS6e+n316o3LCtSTPvgC2kE96Jh44MOUX9z/KIYxXiPGgdi\nnnlxK6L1dZGrMKKBxEFiBJMhFI42gWADyrUz311PDImM+rVhINJT9NyD1i1G++PILLer/BlGwg5n\nwNQHmjt4pV3m+8zMMIrfLjZT/YX8s7+1Xn1Y3bMzo6PnsbvS7OX1YGuN4cPn9f4MLu5Vcn9lWo/r\n+zuZay/pS0mEqTmeOOMGk4BOseiU/hAra5NDgQ=="+        }+  , responseAssertion =+      Just+        Assertion+          { assertionId = "_e7e8860fa89585efa9c8013a0f8116a2"+          , assertionIssued = 2022-09-20 10:30:41.151 UTC+          , assertionIssuer =+              "https://accounts.google.com/o/saml2?idpid=C01aa60hc"+          , assertionSubject =+              Subject+                { subjectConfirmations =+                    [ SubjectConfirmation+                        { subjectConfirmationMethod = Bearer+                        , subjectConfirmationAddress = ""+                        , subjectConfirmationNotOnOrAfter = 2022-09-20 10:35:41.151 UTC+                        , subjectConfirmationRecipient =+                            "https://herp.loopback.ja-sore.de/auth/page/saml2/login"+                        }+                    ]+                , subjectNameID =+                    NameID+                      { nameIDQualifier = Nothing+                      , nameIDSPNameQualifier = Nothing+                      , nameIDSPProvidedID = Nothing+                      , nameIDFormat = Just EmailAddress+                      , nameIDValue = "sdlc-standard@herp.chat"+                      }+                }+          , assertionConditions =+              Conditions+                { conditionsNotBefore = 2022-09-20 10:25:41.151 UTC+                , conditionsNotOnOrAfter = 2022-09-20 10:35:41.151 UTC+                , conditionsAudienceRestrictions =+                    [ AudienceRestriction+                        { audienceRestrictionAudience = [ "https://v1.herp.cloud/" ] }+                    ]+                }+          , assertionAuthnStatement =+              AuthnStatement+                { authnStatementInstant = 2022-09-20 09:33:25 UTC+                , authnStatementSessionIndex = "_e7e8860fa89585efa9c8013a0f8116a2"+                , authnStatementLocality = ""+                }+          , assertionAttributeStatement = []+          }+  , responseEncryptedAssertion = Nothing+  }
tests/data/keycloak.xml.expected view
@@ -1,11 +1,14 @@ Response   { responseDestination =       "http://loopback.ja-sore.de:3000/auth/page/saml2/login"+  , responseInResponseTo = Nothing   , responseId = "ID_5b1d000b-3a5e-4dfe-aa4e-b7bf1e3efbfd"   , responseIssueInstant = 2022-08-01 00:32:49.365 UTC   , responseVersion = "2.0"   , responseIssuer = "http://localhost:8080/realms/HERP"-  , responseStatusCode = Success+  , responseStatusCode =+      MkStatusCode+        { statusCodeValue = Success , statusCodeSubordinate = Nothing }   , responseSignature =       Signature         { signatureInfo =@@ -23,35 +26,37 @@         , signatureValue =             "b9vgIBQ1yNvUYgNmfAyuQJXOJ68PMfRvNAZEa93tnzZXHPEsf7/F49xI6/mlYI/T9pDxYnFcfl7kPMxgz4ssvMjwUEgAR3G3ZrNv4gPMUPmbZnXe0KG8yU9AskK90ya/T11kQfI21cSlA8FrLPTGP2X97yErR10mIDvEJ/m5dWra95cGLx/ntjaSIqNJpVgpHhRxieS4Lw+zeWe/nVuznXQnb8VRhCq18ikL/u23+YhYT3ws3iXQssJ2BosX9JJt0O+X31sIHJIWHsxbI69NLJ782bVDDkI1PNF8MKoa8gSEiLsNSmp3SyXtMPzaRIBguksl9xbnmYmsJDQg6kFVlQ=="         }+  , responseAssertion = Nothing   , responseEncryptedAssertion =-      EncryptedAssertion-        { encryptedAssertionAlgorithm =-            EncryptionMethod-              { encryptionMethodAlgorithm =-                  "http://www.w3.org/2001/04/xmlenc#aes128-cbc"-              , encryptionMethodDigestAlgorithm = Nothing-              }-        , encryptedAssertionKey =-            EncryptedKey-              { encryptedKeyId = ""-              , encryptedKeyRecipient = ""-              , encryptedKeyMethod =-                  EncryptionMethod-                    { encryptionMethodAlgorithm =-                        "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"-                    , encryptionMethodDigestAlgorithm = Nothing-                    }-              , encryptedKeyData = Nothing-              , encryptedKeyCipher =-                  CipherData-                    { cipherValue =-                        "lpbo7DUIyBBVk57bZTQNMiRclHbPFw/ETEPpVA94UdVedgKhFD0muHsdahj4EKMRVkaRqw7c6anprpmI0WLy5Jx8ndvB8cjpOutxSdCnI3MDyhyR81Rsv4QfsxHlOUnaB6360HcQqUj1kdRNyRdAOP44C/qNd5Y20gOFxrQFAdlNP0fLWRdY6Xippv0BwsC7Thwuz7acPkYCG+5Mk1Up8GNK24MzG27gpafuSmKcnrPG2aoBiOmXwOx4aBX7FtzOjAZtbtdx7lenIJtM6p7Tq8T2iD5rfv0hW8szeOhzpe0idY240vHK3IY5Zy27fkNqQC9KvZLW49m/1TszbIb21w=="-                    }-              }-        , encryptedAssertionCipher =-            CipherData-              { cipherValue =-                  "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"-              }-        }+      Just+        EncryptedAssertion+          { encryptedAssertionAlgorithm =+              EncryptionMethod+                { encryptionMethodAlgorithm =+                    "http://www.w3.org/2001/04/xmlenc#aes128-cbc"+                , encryptionMethodDigestAlgorithm = Nothing+                }+          , encryptedAssertionKey =+              EncryptedKey+                { encryptedKeyId = ""+                , encryptedKeyRecipient = ""+                , encryptedKeyMethod =+                    EncryptionMethod+                      { encryptionMethodAlgorithm =+                          "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"+                      , encryptionMethodDigestAlgorithm = Nothing+                      }+                , encryptedKeyData = Nothing+                , encryptedKeyCipher =+                    CipherData+                      { cipherValue =+                          "lpbo7DUIyBBVk57bZTQNMiRclHbPFw/ETEPpVA94UdVedgKhFD0muHsdahj4EKMRVkaRqw7c6anprpmI0WLy5Jx8ndvB8cjpOutxSdCnI3MDyhyR81Rsv4QfsxHlOUnaB6360HcQqUj1kdRNyRdAOP44C/qNd5Y20gOFxrQFAdlNP0fLWRdY6Xippv0BwsC7Thwuz7acPkYCG+5Mk1Up8GNK24MzG27gpafuSmKcnrPG2aoBiOmXwOx4aBX7FtzOjAZtbtdx7lenIJtM6p7Tq8T2iD5rfv0hW8szeOhzpe0idY240vHK3IY5Zy27fkNqQC9KvZLW49m/1TszbIb21w=="+                      }+                }+          , encryptedAssertionCipher =+              CipherData+                { cipherValue =+                    "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"+                }+          }   }
tests/data/okta.xml.expected view
@@ -1,11 +1,14 @@ Response   { responseDestination =       "http://loopback.ja-sore.de:3000/auth/page/saml2/login"+  , responseInResponseTo = Nothing   , responseId = "id36905492230634463108368061"   , responseIssueInstant = 2022-07-28 13:53:54.059 UTC   , responseVersion = "2.0"   , responseIssuer = "http://www.okta.com/exk1s7jqodj4muVTl697"-  , responseStatusCode = Success+  , responseStatusCode =+      MkStatusCode+        { statusCodeValue = Success , statusCodeSubordinate = Nothing }   , responseSignature =       Signature         { signatureInfo =@@ -23,41 +26,43 @@         , signatureValue =             "eKymDiZIxFd+oJCIhdHQpI+s2nQYFBVH7TBoZaqXizJdNnNPpNlKM5wds33zP6a72GWOL/2n8WRtW+xSnPTmw5PumEuUhWbO2EizfU/SdBL4HKxxJt0nntkDRhdIoHBi+9Gy80So4NGr82B2clnhpj74GW0Ko8A0bt2sHzdQ9NaJ5ru4Qvx0Fk/UCBAGAYobryjAc9Tb5qxxgHGMmPUHG5CprRYTINKZlGF1Jl88VSdTbOYmGjJ4b6GY8pLR2qLt4LErVPUSI4vEUuPFU2f/9/i8D39hzWtJOCzuaUZ5tdKU3Fyjsgoa7ooDfZVzb4howWhQ9zPUgwjZEd9tbW2EwA=="         }+  , responseAssertion = Nothing   , responseEncryptedAssertion =-      EncryptedAssertion-        { encryptedAssertionAlgorithm =-            EncryptionMethod-              { encryptionMethodAlgorithm =-                  "http://www.w3.org/2001/04/xmlenc#aes256-cbc"-              , encryptionMethodDigestAlgorithm = Nothing-              }-        , encryptedAssertionKey =-            EncryptedKey-              { encryptedKeyId = "_7e060a182b19459d857d25b5fc5117be"-              , encryptedKeyRecipient = ""-              , encryptedKeyMethod =-                  EncryptionMethod-                    { encryptionMethodAlgorithm =-                        "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"-                    , encryptionMethodDigestAlgorithm =-                        Just "http://www.w3.org/2000/09/xmldsig#sha1"-                    }-              , encryptedKeyData =-                  Just-                    KeyInfo-                      { keyInfoCertificate =-                          "MIIDZDCCAkwCCQCxU2RvdbbDijANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJKUDEOMAwGA1UE\nCAwFVG9reW8xEDAOBgNVBAcMB0dvdGFuZGExEzARBgNVBAoMCkhFUlAsIEluYy4xFjAUBgNVBAsM\nDUlUIERlcGFydG1lbnQxFjAUBgNVBAMMDXYxLmhlcnAuY2xvdWQwHhcNMjIwNzI4MTIwNTEwWhcN\nMzIwNjA1MTIwNTEwWjB0MQswCQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xEDAOBgNVBAcMB0dv\ndGFuZGExEzARBgNVBAoMCkhFUlAsIEluYy4xFjAUBgNVBAsMDUlUIERlcGFydG1lbnQxFjAUBgNV\nBAMMDXYxLmhlcnAuY2xvdWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiX1XfOukm\nSEe7b3r9OtbBYXfcJ4o6pg5ASSCfLJaaIFfUk3sbaR9WYyzzcYd7UtfBooFpVZu+Bqek5FdnYSXF\nxs0lyFFGizQfE1jcUQ3gAacD1pMyG/4hdCqEGSAAcjqjv4jNpScUkG+oTmdYpdfIUGHkVdcINw5W\ndPpV+timnRYa32z6paRpNNUSJ3ZYEfxvB3pdy3WRy3GNgnZ9xiWSgNUrliJjYJ2U8RfMNneKI9+g\nfmXyVZI6xAqKu7d7rq52zH8tDpUxWTbItvr2g40L8XUV/3D9D2c9wemI9IaJRHua9bJfBPSsKiA7\nPhdWCuHrglm/Sky4IH294o7WVS8rAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAM/XNOGB9PYxUA9Y\ng5iOMt/UgLSpA4ahjZFQO0TbUxM9OfwtACTQ3tyTp19IhOa8o52uqUP2O7l5+lN8Xn16AlKHSYRE\nXAe0AQJykPkSGiaZg+lEEECaV7C3BSYNzH3EHjPGG5B9e3oaNbl1/BUr1c6mSFJuy0sGYQH7cxTv\n+qCT7u+WhcVuKbR4DuIVcei30qD9dyc4QzIWMqJ77yjgB1c+4UZOZdol0BF4HyCycck4XlUd9A35\n/W4idya7HIVZjEc3ENUwvQCOF6TB8mJVAz795KIam3kvOcvHIEaKL3Y+byVbP1GOLwvuyAnDG7ly\nIDFXtXbrQVFhGxhpW+gM+5c="+      Just+        EncryptedAssertion+          { encryptedAssertionAlgorithm =+              EncryptionMethod+                { encryptionMethodAlgorithm =+                    "http://www.w3.org/2001/04/xmlenc#aes256-cbc"+                , encryptionMethodDigestAlgorithm = Nothing+                }+          , encryptedAssertionKey =+              EncryptedKey+                { encryptedKeyId = "_7e060a182b19459d857d25b5fc5117be"+                , encryptedKeyRecipient = ""+                , encryptedKeyMethod =+                    EncryptionMethod+                      { encryptionMethodAlgorithm =+                          "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"+                      , encryptionMethodDigestAlgorithm =+                          Just "http://www.w3.org/2000/09/xmldsig#sha1"                       }-              , encryptedKeyCipher =-                  CipherData-                    { cipherValue =-                        "SNRV9HQlcViYCbDPPn5NHUb6h9sel8pqHVwcKmz59JDA4PFiiFYeEEyQbksAmtgB7nuHptgOpSYodDjLnABUnVb2uqs+P2kRA+1NTkYT7TNf5g1dHpO49GFmp/YpvRaQ8Ld+X186rKrGMgvYKkFWRMpZ7Fg+f19KFgoVmngfoHe1MKma/eiESgEwj/YlrbUc2UhJcxgrPwyTXUCiB55tE8CRhOFFpUWfzrrklqf4BSXdQQKqUkDVbwlmShMXDEb3mmJkNKd8deshKejr0ASAhXQ54PSvc8+7qwFw8bser/sojaJzbS8Cc7b0EDukR4HMbXjR5iMVl3BL6QRy4FxS+g=="-                    }-              }-        , encryptedAssertionCipher =-            CipherData-              { cipherValue =-                  "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"-              }-        }+                , encryptedKeyData =+                    Just+                      KeyInfo+                        { keyInfoCertificate =+                            "MIIDZDCCAkwCCQCxU2RvdbbDijANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJKUDEOMAwGA1UE\nCAwFVG9reW8xEDAOBgNVBAcMB0dvdGFuZGExEzARBgNVBAoMCkhFUlAsIEluYy4xFjAUBgNVBAsM\nDUlUIERlcGFydG1lbnQxFjAUBgNVBAMMDXYxLmhlcnAuY2xvdWQwHhcNMjIwNzI4MTIwNTEwWhcN\nMzIwNjA1MTIwNTEwWjB0MQswCQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xEDAOBgNVBAcMB0dv\ndGFuZGExEzARBgNVBAoMCkhFUlAsIEluYy4xFjAUBgNVBAsMDUlUIERlcGFydG1lbnQxFjAUBgNV\nBAMMDXYxLmhlcnAuY2xvdWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiX1XfOukm\nSEe7b3r9OtbBYXfcJ4o6pg5ASSCfLJaaIFfUk3sbaR9WYyzzcYd7UtfBooFpVZu+Bqek5FdnYSXF\nxs0lyFFGizQfE1jcUQ3gAacD1pMyG/4hdCqEGSAAcjqjv4jNpScUkG+oTmdYpdfIUGHkVdcINw5W\ndPpV+timnRYa32z6paRpNNUSJ3ZYEfxvB3pdy3WRy3GNgnZ9xiWSgNUrliJjYJ2U8RfMNneKI9+g\nfmXyVZI6xAqKu7d7rq52zH8tDpUxWTbItvr2g40L8XUV/3D9D2c9wemI9IaJRHua9bJfBPSsKiA7\nPhdWCuHrglm/Sky4IH294o7WVS8rAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAM/XNOGB9PYxUA9Y\ng5iOMt/UgLSpA4ahjZFQO0TbUxM9OfwtACTQ3tyTp19IhOa8o52uqUP2O7l5+lN8Xn16AlKHSYRE\nXAe0AQJykPkSGiaZg+lEEECaV7C3BSYNzH3EHjPGG5B9e3oaNbl1/BUr1c6mSFJuy0sGYQH7cxTv\n+qCT7u+WhcVuKbR4DuIVcei30qD9dyc4QzIWMqJ77yjgB1c+4UZOZdol0BF4HyCycck4XlUd9A35\n/W4idya7HIVZjEc3ENUwvQCOF6TB8mJVAz795KIam3kvOcvHIEaKL3Y+byVbP1GOLwvuyAnDG7ly\nIDFXtXbrQVFhGxhpW+gM+5c="+                        }+                , encryptedKeyCipher =+                    CipherData+                      { cipherValue =+                          "SNRV9HQlcViYCbDPPn5NHUb6h9sel8pqHVwcKmz59JDA4PFiiFYeEEyQbksAmtgB7nuHptgOpSYodDjLnABUnVb2uqs+P2kRA+1NTkYT7TNf5g1dHpO49GFmp/YpvRaQ8Ld+X186rKrGMgvYKkFWRMpZ7Fg+f19KFgoVmngfoHe1MKma/eiESgEwj/YlrbUc2UhJcxgrPwyTXUCiB55tE8CRhOFFpUWfzrrklqf4BSXdQQKqUkDVbwlmShMXDEb3mmJkNKd8deshKejr0ASAhXQ54PSvc8+7qwFw8bser/sojaJzbS8Cc7b0EDukR4HMbXjR5iMVl3BL6QRy4FxS+g=="+                      }+                }+          , encryptedAssertionCipher =+              CipherData+                { cipherValue =+                    "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"+                }+          }   }
wai-saml2.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.0.+-- This file has been generated from package.yaml by hpack version 0.35.1. -- -- see: https://github.com/sol/hpack  name:           wai-saml2-version:        0.3.0.1+version:        0.4 synopsis:       SAML2 assertion validation as WAI middleware description:    A Haskell library which implements SAML2 assertion validation as WAI middleware category:       Security@@ -20,6 +20,8 @@ extra-source-files:     README.md     CHANGELOG.md+    tests/data/google.xml+    tests/data/google.xml.expected     tests/data/keycloak.xml     tests/data/keycloak.xml.expected     tests/data/okta.xml@@ -35,8 +37,11 @@       Network.Wai.SAML2.Assertion       Network.Wai.SAML2.C14N       Network.Wai.SAML2.Config+      Network.Wai.SAML2.EntityDescriptor       Network.Wai.SAML2.Error       Network.Wai.SAML2.KeyInfo+      Network.Wai.SAML2.NameIDFormat+      Network.Wai.SAML2.Request       Network.Wai.SAML2.Response       Network.Wai.SAML2.Signature       Network.Wai.SAML2.StatusCode@@ -48,21 +53,23 @@   hs-source-dirs:       src   default-extensions:+      FlexibleInstances       OverloadedStrings-      MultiWayIf       RecordWildCards-      FlexibleInstances   ghc-options: -W   build-depends:       base >=4.8 && <5+    , base16-bytestring >=0.1 && <1.1     , base64-bytestring >=0.1 && <2     , bytestring >=0.9 && <0.12     , c14n >=0.1.0.1 && <1+    , containers ==0.6.*     , cryptonite <1     , data-default-class <1     , http-types <1     , mtl >=2.2.1 && <3-    , text <2+    , network-uri >=2.0 && <3+    , text <2.1     , time >=1.9 && <2     , vault >=0.3 && <1     , wai >=3.0 && <4@@ -70,6 +77,7 @@     , x509 <2     , x509-store <2     , xml-conduit <2+    , zlib >=0.6.0.0 && <0.7   default-language: Haskell2010  test-suite parser@@ -80,25 +88,27 @@   hs-source-dirs:       tests   default-extensions:+      FlexibleInstances       OverloadedStrings-      MultiWayIf       RecordWildCards-      FlexibleInstances   ghc-options: -Wall -Wcompat   build-depends:       base+    , base16-bytestring >=0.1 && <1.1     , base64-bytestring >=0.1 && <2     , bytestring     , c14n >=0.1.0.1 && <1+    , containers ==0.6.*     , cryptonite <1     , data-default-class <1     , filepath     , http-types <1     , mtl >=2.2.1 && <3+    , network-uri >=2.0 && <3     , pretty-show     , tasty     , tasty-golden-    , text <2+    , text <2.1     , time >=1.9 && <2     , vault >=0.3 && <1     , wai >=3.0 && <4@@ -107,4 +117,5 @@     , x509 <2     , x509-store <2     , xml-conduit+    , zlib >=0.6.0.0 && <0.7   default-language: Haskell2010