-------------------------------------------------------------------------------- -- 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. -- -------------------------------------------------------------------------------- -- | 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(..), 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 -------------------------------------------------------------------------------- -- | 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 -- @\@ or @\@ 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 @\@ elements in an @\@ 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 @\@ 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 = parseStatusCode True -- | `parseStatusCode` @isTopLevel cursor@ attempts to parse a @@ -- 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 -- @@ 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 --------------------------------------------------------------------------------