packages feed

wai-saml2-0.7.0: src/Network/Wai/SAML2/EntityDescriptor.hs

--------------------------------------------------------------------------------
-- 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
        -- | @since 0.7
        -- The X.509 certificates for signed assertions
    ,   x509Certificates :: [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")
        let rawCertificates = descriptor
                $/ element (mdName "KeyDescriptor")
                &/ element (dsName "KeyInfo")
                &/ element (dsName "X509Data")
                &/ element (dsName "X509Certificate")
                &/ content
        x509Certificates <- traverse
            ( either fail pure
            . X509.decodeSignedObject
            . Base64.decodeLenient
            . T.encodeUtf8
            ) rawCertificates
        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

--------------------------------------------------------------------------------