Skip to content

Commit ebd8652

Browse files
authored
Add full support for status codes (#42)
1 parent f9d6467 commit ebd8652

9 files changed

+197
-17
lines changed

.editorconfig

+3
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ insert_final_newline = true
1111

1212
[*.{yml,yaml}]
1313
indent_size = 2
14+
15+
[*.expected]
16+
insert_final_newline = false

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* Added a new module `Network.Wai.SAML2.NameIDFormat`
1212
* 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))
1313
* Validate audience restrictions ([#35](https://github.com/mbg/wai-saml2/pull/35) by [@Philonous](https://github.com/Philonous))
14+
* Handle status codes according to the SAML2 specification ([#42](https://github.com/mbg/wai-saml2/pull/42)) by [@mbg](https://github.com/mbg)
1415

1516
## 0.3
1617

package.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies:
4040
- mtl >= 2.2.1 && < 3
4141
- c14n >= 0.1.0.1 && < 1
4242
- zlib >= 0.6.0.0 && < 0.7
43+
- network-uri >= 2.0 && < 3
4344

4445
library:
4546
source-dirs: src

src/Network/Wai/SAML2/StatusCode.hs

+179-12
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,206 @@
55
-- file in the root directory of this source tree. --
66
--------------------------------------------------------------------------------
77

8-
-- | SAML2 status codes.
8+
-- | The SAML2 specification distinguishes between the topmost status code,
9+
-- which is required and must contain a status value from a specific list of
10+
-- status codes, and subordinate status codes, which are optional and may
11+
-- contain arbitrary URIs.
912
module Network.Wai.SAML2.StatusCode (
10-
StatusCode(..)
13+
StatusCode(..),
14+
StatusCodeValue(..)
1115
) where
1216

1317
--------------------------------------------------------------------------------
1418

1519
import Control.Monad
1620

21+
import Data.Maybe
1722
import qualified Data.Text as T
1823

1924
import Text.XML.Cursor
2025

26+
import Network.URI (URI, parseURI)
2127
import Network.Wai.SAML2.XML
2228

2329
--------------------------------------------------------------------------------
2430

25-
-- | Enumerates SAML2 status codes.
31+
-- | Represents SAML2 status codes, which are comprised of a status value
32+
-- and an optional, subordinate status.
2633
data StatusCode
34+
= MkStatusCode {
35+
-- | The status code value.
36+
statusCodeValue :: !StatusCodeValue,
37+
-- | An optional, subordinate status code.
38+
statusCodeSubordinate :: !(Maybe StatusCode)
39+
}
40+
deriving (Eq, Show)
41+
42+
-- | Enumerates SAML2 status code values.
43+
--
44+
-- @since 0.4
45+
data StatusCodeValue
2746
-- | The response indicates success!
2847
= Success
48+
-- | The request could not be performed due to an error on the part of the
49+
-- requester.
50+
| Requester
51+
-- | The request could not be performed due to an error on the part of the
52+
-- SAML responder or SAML authority.
53+
| Responder
54+
-- | The SAML responder could not process the request because the version
55+
-- of the request message was incorrect.
56+
| VersionMismatch
57+
-- | The responding provider was unable to successfully authenticate the
58+
-- principal.
59+
| AuthnFailed
60+
-- | Unexpected or invalid content was encountered within a
61+
-- @\<saml:Attribute\>@ or @\<saml:AttributeValue\>@ element.
62+
| InvalidAttrNameOrValue
63+
-- | The responding provider cannot or will not support the requested name
64+
-- identifier policy.
65+
| InvalidNameIDPolicy
66+
-- | The specified authentication context requirements cannot be met by the
67+
-- responder.
68+
| NoAuthnContext
69+
-- | Used by an intermediary to indicate that none of the supported
70+
-- identity provider @\<Loc\>@ elements in an @\<IDPList\>@ can be resolved
71+
-- or that none of the supported identity providers are available.
72+
| NoAvailableIDP
73+
-- | Indicates the responding provider cannot authenticate the principal
74+
-- passively, as has been requested.
75+
| NoPassive
76+
-- | Used by an intermediary to indicate that none of the identity
77+
-- providers in an @\<IDPList\>@ are supported by the intermediary.
78+
| NoSupportedIDP
79+
-- | Used by a session authority to indicate to a session participant that
80+
-- it was not able to propagate logout to all other session participants.
81+
| PartialLogout
82+
-- | Indicates that a responding provider cannot authenticate the principal
83+
-- directly and is not permitted to proxy the request further.
84+
| ProxyCountExceeded
85+
-- | The SAML responder or SAML authority is able to process the request
86+
-- but has chosen not to respond. This status code MAY be used when there
87+
-- is concern about the security context of the request message or the
88+
-- sequence of request messages received from a particular requester.
89+
| RequestDenied
90+
-- | The SAML responder or SAML authority does not support the request.
91+
| RequestUnsupported
92+
-- | The SAML responder cannot process any requests with the protocol
93+
-- version specified in the request.
94+
| RequestVersionDeprecated
95+
-- | The SAML responder cannot process the request because the protocol
96+
-- version specified in the request message is a major upgrade from the
97+
-- highest protocol version supported by the responder.
98+
| RequestVersionTooHigh
99+
-- | The SAML responder cannot process the request because the protocol
100+
-- version specified in the request message is too low.
101+
| RequestVersionTooLow
102+
-- | The resource value provided in the request message is invalid or
103+
-- unrecognized.
104+
| ResourceNotRecognized
105+
-- | The response message would contain more elements than the SAML
106+
-- responder is able to return.
107+
| TooManyResponses
108+
-- | An entity that has no knowledge of a particular attribute profile
109+
-- has been presented with an attribute drawn from that profile.
110+
| UnknownAttrProfile
111+
-- | The responding provider does not recognize the principal specified
112+
-- or implied by the request.
113+
| UnknownPrincipal
114+
-- | The SAML responder cannot properly fulfil the request using the
115+
-- protocol binding specified in the request.
116+
| UnsupportedBinding
117+
-- | The SAML2 specification notes that a status code value can be any
118+
-- valid URI and that additional subordinate status codes may be
119+
-- introduced in the future.
120+
| OtherStatus URI
29121
deriving (Eq, Show)
30122

31123
instance FromXML StatusCode where
32-
parseXML cursor =
33-
let value = T.concat
34-
$ cursor
35-
$/ element (saml2pName "Status")
36-
&/ element (saml2pName "StatusCode")
37-
>=> attribute "Value"
38-
in case value of
39-
"urn:oasis:names:tc:SAML:2.0:status:Success" -> pure Success
40-
_ -> fail "Not a valid status code."
124+
parseXML = parseStatusCode True
125+
126+
-- | `parseStatusCode` @isTopLevel cursor@ attempts to parse a @<StatusCode>@
127+
-- element from the XML @cursor@. The SAML2 specification distinguishes
128+
-- between the topmost status code, which is required and must contain a
129+
-- status value from a specific list of status codes, and subordinate status
130+
-- codes. The @isTopLevel@ value indicates whether we are parsing a top-level
131+
-- @<StatusCode>@ element or not and therefore controls which status codes
132+
-- values we accept as valid.
133+
--
134+
-- @since 0.4
135+
parseStatusCode :: MonadFail m => Bool -> Cursor -> m StatusCode
136+
parseStatusCode isTopLevel cursor = do
137+
statusCodeValue <- oneOrFail "Value is a required attribute" $
138+
cursor $/
139+
element (saml2pName "Status") &/
140+
element (saml2pName "StatusCode") >=>
141+
parseStatusCodeValue isTopLevel
142+
let statusCodeSubordinate = listToMaybe (
143+
cursor $/
144+
element (saml2pName "Status") &/
145+
element (saml2pName "StatusCode")) >>=
146+
parseStatusCode False
147+
148+
pure MkStatusCode{..}
41149

150+
-- | `parseStatusCodeValue` @isTopLevel cursor@ attempts to parse a status code
151+
-- value from the XML @cursor@. The @isTopLevel@ value determines which values
152+
-- we permit as valid status code values. See the note for `parseStatusCode`.
153+
--
154+
-- @since 0.4
155+
parseStatusCodeValue :: MonadFail m => Bool -> Cursor -> m StatusCodeValue
156+
parseStatusCodeValue isTopLevel cursor =
157+
case T.concat $ attribute "Value" cursor of
158+
-- the following status codes are always permitted
159+
"urn:oasis:names:tc:SAML:2.0:status:Success" -> pure Success
160+
"urn:oasis:names:tc:SAML:2.0:status:Requester" -> pure Requester
161+
"urn:oasis:names:tc:SAML:2.0:status:Responder" -> pure Responder
162+
"urn:oasis:names:tc:SAML:2.0:status:VersionMismatch" ->
163+
pure VersionMismatch
164+
-- the following are only permitted for subordinate elements
165+
"urn:oasis:names:tc:SAML:2.0:status:AuthnFailed" | not isTopLevel ->
166+
pure AuthnFailed
167+
"urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue" | not isTopLevel ->
168+
pure InvalidAttrNameOrValue
169+
"urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy" | not isTopLevel ->
170+
pure InvalidNameIDPolicy
171+
"urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext" | not isTopLevel ->
172+
pure NoAuthnContext
173+
"urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP" | not isTopLevel ->
174+
pure NoAvailableIDP
175+
"urn:oasis:names:tc:SAML:2.0:status:NoPassive" | not isTopLevel ->
176+
pure NoPassive
177+
"urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP" | not isTopLevel ->
178+
pure NoSupportedIDP
179+
"urn:oasis:names:tc:SAML:2.0:status:PartialLogout" | not isTopLevel ->
180+
pure PartialLogout
181+
"urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded" | not isTopLevel ->
182+
pure ProxyCountExceeded
183+
"urn:oasis:names:tc:SAML:2.0:status:RequestDenied" | not isTopLevel ->
184+
pure RequestDenied
185+
"urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported" | not isTopLevel ->
186+
pure RequestUnsupported
187+
"urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated" | not isTopLevel ->
188+
pure RequestVersionDeprecated
189+
"urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh" | not isTopLevel ->
190+
pure RequestVersionTooHigh
191+
"urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow" | not isTopLevel ->
192+
pure RequestVersionTooLow
193+
"urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized" | not isTopLevel ->
194+
pure ResourceNotRecognized
195+
"urn:oasis:names:tc:SAML:2.0:status:TooManyResponses" | not isTopLevel ->
196+
pure TooManyResponses
197+
"urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile" | not isTopLevel ->
198+
pure UnknownAttrProfile
199+
"urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal" | not isTopLevel ->
200+
pure UnknownPrincipal
201+
"urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding" | not isTopLevel ->
202+
pure UnsupportedBinding
203+
uriString | not isTopLevel -> case parseURI $ T.unpack uriString of
204+
Nothing -> fail $ "Not a valid status code: " <> T.unpack uriString
205+
Just uri -> pure $ OtherStatus uri
206+
-- not a valid URI or a status code that's not supported at the
207+
-- top-level
208+
xs -> fail $ "Not a valid status code: " <> T.unpack xs
42209

43210
--------------------------------------------------------------------------------

src/Network/Wai/SAML2/Validation.hs

+2-2
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ validateSAMLResponse :: SAML2Config
8989
validateSAMLResponse cfg responseXmlDoc samlResponse now = do
9090

9191
-- check that the response indicates success
92-
case responseStatusCode samlResponse of
92+
case statusCodeValue $ responseStatusCode samlResponse of
9393
Success -> pure ()
94-
status -> throwError $ Unsuccessful status
94+
_status -> throwError $ Unsuccessful $ responseStatusCode samlResponse
9595

9696
-- check that the destination is as expected, if the configuration
9797
-- expects us to validate this

tests/data/google.xml.expected

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ Response
99
, responseVersion = "2.0"
1010
, responseIssuer =
1111
"https://accounts.google.com/o/saml2?idpid=C01aa60hc"
12-
, responseStatusCode = Success
12+
, responseStatusCode =
13+
MkStatusCode
14+
{ statusCodeValue = Success , statusCodeSubordinate = Nothing }
1315
, responseSignature =
1416
Signature
1517
{ signatureInfo =

tests/data/keycloak.xml.expected

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ Response
66
, responseIssueInstant = 2022-08-01 00:32:49.365 UTC
77
, responseVersion = "2.0"
88
, responseIssuer = "http://localhost:8080/realms/HERP"
9-
, responseStatusCode = Success
9+
, responseStatusCode =
10+
MkStatusCode
11+
{ statusCodeValue = Success , statusCodeSubordinate = Nothing }
1012
, responseSignature =
1113
Signature
1214
{ signatureInfo =

tests/data/okta.xml.expected

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ Response
66
, responseIssueInstant = 2022-07-28 13:53:54.059 UTC
77
, responseVersion = "2.0"
88
, responseIssuer = "http://www.okta.com/exk1s7jqodj4muVTl697"
9-
, responseStatusCode = Success
9+
, responseStatusCode =
10+
MkStatusCode
11+
{ statusCodeValue = Success , statusCodeSubordinate = Nothing }
1012
, responseSignature =
1113
Signature
1214
{ signatureInfo =

wai-saml2.cabal

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ library
6868
, data-default-class <1
6969
, http-types <1
7070
, mtl >=2.2.1 && <3
71+
, network-uri >=2.0 && <3
7172
, text <2.1
7273
, time >=1.9 && <2
7374
, vault >=0.3 && <1
@@ -103,6 +104,7 @@ test-suite parser
103104
, filepath
104105
, http-types <1
105106
, mtl >=2.2.1 && <3
107+
, network-uri >=2.0 && <3
106108
, pretty-show
107109
, tasty
108110
, tasty-golden

0 commit comments

Comments
 (0)