packages feed

wai-saml2-0.4: src/Network/Wai/SAML2/Assertion.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.                            --
--------------------------------------------------------------------------------

-- | Types to represent SAML2 assertions and functions to parse them from XML.
module Network.Wai.SAML2.Assertion (
    SubjectConfirmationMethod(..),
    SubjectConfirmation(..),
    Subject(..),
    NameID(..),
    Conditions(..),
    AudienceRestriction(..),
    AuthnStatement(..),
    AssertionAttribute(..),
    AttributeStatement,
    parseAttributeStatement,
    Assertion(..)
) where

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

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

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

-- | Enumerates different subject confirmation methods.
-- See http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0-cd-02.html#4.2.1.Subject%20Confirmation%20|outline
data SubjectConfirmationMethod
    = HolderOfKey -- ^ urn:oasis:names:tc:SAML:2.0:cm:holder-of-key
    | SenderVouches -- ^ urn:oasis:names:tc:SAML:2.0:cm:sender-vouches
    | 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
        "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
        _ -> fail "Not a valid SubjectConfirmationMethod."

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

-- | Represents a subject confirmation record.
data SubjectConfirmation = SubjectConfirmation {
    -- | The subject confirmation method used.
    subjectConfirmationMethod :: !SubjectConfirmationMethod,
    -- | The address of the subject.
    subjectConfirmationAddress :: !T.Text,
    -- | A timestamp.
    subjectConfirmationNotOnOrAfter :: !UTCTime,
    -- | The recipient.
    subjectConfirmationRecipient :: !T.Text
} deriving (Eq, Show)

instance FromXML SubjectConfirmation where
    parseXML cursor = do
        method <- parseXML cursor

        notOnOrAfter <- parseUTCTime $ T.concat $
            cursor $/ element (saml2Name "SubjectConfirmationData")
                  >=> attribute "NotOnOrAfter"

        pure SubjectConfirmation{
            subjectConfirmationMethod = method,
            subjectConfirmationAddress = T.concat $
                cursor $/ element (saml2Name "SubjectConfirmationData")
                      >=> attribute "Address",
            subjectConfirmationNotOnOrAfter = notOnOrAfter,
            subjectConfirmationRecipient = T.concat $
                cursor $/ element (saml2Name "SubjectConfirmationData")
                      >=> 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
-- 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
} deriving (Eq, Show)

instance FromXML NameID where
    parseXML cursor = do
        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.
data Subject = Subject {
    -- | The list of subject confirmation elements, if any.
    subjectConfirmations :: ![SubjectConfirmation],
    -- | An identifier for the subject of the assertion.
    subjectNameID :: !NameID
} deriving (Eq, Show)

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,
            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.
    --
    -- @since 0.4
    conditionsAudienceRestrictions :: ![AudienceRestriction]
} deriving (Eq, Show)

-- Reference [Conditions]
instance FromXML Conditions where
    parseXML cursor = do
        notBefore <- parseUTCTime $
            T.concat $ attribute "NotBefore" cursor
        notOnOrAfter <- parseUTCTime $
            T.concat $ attribute "NotOnOrAfter" cursor

        pure Conditions{
            conditionsNotBefore = notBefore,
            conditionsNotOnOrAfter = notOnOrAfter,
            conditionsAudienceRestrictions =
                cursor $/ element (saml2Name "AudienceRestriction")
                    >=> parseXML
        }

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

-- | SAML2 authentication statements.

-- Reference [AuthnStatement]
data AuthnStatement = AuthnStatement {
    -- | The timestamp when the assertion was issued.
    authnStatementInstant :: !UTCTime,
    -- | The session index.
    authnStatementSessionIndex :: !T.Text,
    -- | The statement locality.
    authnStatementLocality :: !T.Text
} deriving (Eq, Show)

instance FromXML AuthnStatement where
    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")
                    >=> attribute "Address"
        }

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

-- | SAML2 assertion attributes.
data AssertionAttribute = AssertionAttribute {
    -- | The name of the attribute.
    attributeName :: !T.Text,
    -- | A friendly attribute name, if it exists.
    attributeFriendlyName :: !(Maybe T.Text),
    -- | The name format.
    attributeNameFormat :: !T.Text,
    -- | The value of the attribute.
    attributeValue :: !T.Text
} deriving (Eq, Show)

instance FromXML AssertionAttribute where
    parseXML cursor = do
        pure AssertionAttribute{
            attributeName = T.concat $ attribute "Name" cursor,
            attributeFriendlyName =
                toMaybeText $ attribute "FriendlyName" cursor,
            attributeNameFormat = T.concat $ attribute "NameFormat" cursor,
            attributeValue = T.concat $
                cursor $/ element (saml2Name "AttributeValue") &/ content
        }

-- | SAML2 assertion statements (collections of assertion attributes).
type AttributeStatement = [AssertionAttribute]

-- | 'parseAttributeStatement' @cursor@ parses an 'AttributeStatement'.
parseAttributeStatement :: Cursor -> AttributeStatement
parseAttributeStatement cursor =
    cursor $/ element (saml2Name "Attribute") >=> parseXML

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

-- | 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.
    assertionId :: !T.Text,
    -- | The date and time when the assertion was issued.
    assertionIssued :: !UTCTime,
    -- | The name of the entity that issued this assertion.
    assertionIssuer :: !T.Text,
    -- | The subject of the assertion.
    assertionSubject :: !Subject,
    -- | The conditions under which the assertion is issued.
    assertionConditions :: !Conditions,
    -- | The authentication statement included in the assertion.
    assertionAuthnStatement :: !AuthnStatement,
    -- | The assertion's attribute statement.
    assertionAttributeStatement :: !AttributeStatement
} deriving (Eq, Show)

-- Reference [Assertion]
instance FromXML Assertion where
    parseXML cursor = do
        issueInstant <- parseUTCTime $
            T.concat $ attribute "IssueInstant" cursor

        subject <- oneOrFail "Subject is required" $
            cursor $/ element (saml2Name "Subject") >=> parseXML

        conditions <- oneOrFail "Conditions are required" $
            cursor $/ element (saml2Name "Conditions") >=> parseXML

        authnStatement <- oneOrFail "AuthnStatement is required" $
            cursor $/ element (saml2Name "AuthnStatement") >=> parseXML

        pure Assertion{
            assertionId = T.concat $ attribute "ID" cursor,
            assertionIssued = issueInstant,
            assertionIssuer = T.concat $
                cursor $/ element (saml2Name "Issuer") &/ content,
            assertionSubject = subject,
            assertionConditions = conditions,
            assertionAuthnStatement = authnStatement,
            assertionAttributeStatement =
                cursor $/ element (saml2Name "AttributeStatement")
                    >=> parseAttributeStatement
        }

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

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