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