summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormbg <>2020-06-29 13:59:00 (GMT)
committerhdiff <hdiff@hdiff.luite.com>2020-06-29 13:59:00 (GMT)
commite353e4f02381f7be73c2ad62888df077da51a8dc (patch)
tree4f665a24c0f8b64611b9521ffcf757a95b575f82
parentff9fa227823e71b9aefeec17888ba39c12874219 (diff)
version 0.2.0.00.2.0.0
-rw-r--r--CHANGELOG.md6
-rw-r--r--README.md91
-rw-r--r--src/Network/Wai/SAML2.hs43
-rw-r--r--src/Network/Wai/SAML2/Assertion.hs2
-rw-r--r--wai-saml2.cabal5
5 files changed, 129 insertions, 18 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eedf049..252fc84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 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
-* Initial release \ No newline at end of file
+* Initial release
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ae0c24b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,91 @@
+# wai-saml2
+
+![GitHub](https://img.shields.io/github/license/mbg/wai-saml2)
+![Haskell CI](https://github.com/mbg/wai-saml2/workflows/Haskell/badge.svg?branch=master)
+
+A Haskell library which implements SAML2 assertion validation as WAI middleware. This can be used by a Haskell web application (the service provider, SP) to perform identity provider (IdP) initiated authentication, i.e. SAML2-based authentication where the authentication begins at the IdP-end, the IdP authenticates the user, and then gets the user to submit a SAML2 assertion back to the SP (known as "unsolicited SSO" within e.g. [the Shibboleth project](https://wiki.shibboleth.net/confluence/display/IDP30/UnsolicitedSSOConfiguration#UnsolicitedSSOConfiguration-SAML2.0)).
+
+## Completeness
+
+There are currently a number of limitations to this library:
+
+* As mentioned above, while the library implements IdP-initiated authentication, it does not yet implement SP-initiated authentication (where the SP submits a login request to the IdP).
+
+* The library does not currently support the full SAML2 specification and makes certain assumptions about what the IdP's responses contain. It will most likely fail with any IdPs which do not send responses in the same format. If you wish to use this library and encounter problems with your IdP, please open an issue or a pull request which implements support accordingly.
+ * The library expects that SAML _assertions_ will be encrypted and will decrypt these. There is currently no support for plain-text assertions.
+
+## Security
+
+The library is estimated to be sufficiently robust for use in a production environment. If you wish to implement this middleware, please note the following:
+
+* You __must__ store IDs of assertions you see. If an assertion is successfully validated by this library, you __must__ check that you have not previous seen the assertion ID in order to prevent replay attacks.
+
+* You __must not__ expose any errors to a client as that could severely compromise the security of the system as attackers may be able to use the errors to narrow down valid SAML responses. You __should__ log and monitor errors though as they may indicate attacks on your system. Ensure that log files containing errors from the SAML2 middleware are stored securely.
+
+## Usage
+
+### Preliminaries
+
+You need to have registered your service provider with the identity provider. You need to have access to the IdP's metadata, which will contain the public key used for signature validation.
+
+### Configuration
+
+The `saml2Config` function may be used to construct `SAML2Config` values. It expects at least the SP's private key and the IdP's public key as arguments, but you should almost certainly customise the configuration further. The private and public keys can be loaded with functions from the `Data.X509` and `Data.X509.File` modules (from the `x509` and `x509-store` packages, respectively):
+
+```haskell
+(saml2Config spPrivateKey idpPublicKey){
+ saml2AssertionPath = "/sso/assert",
+ saml2ExpectedIssuer = Just "https://idp.sp.com/saml2",
+ saml2ExpectedDestination = Just "https://example.com/sso/assert",
+}
+```
+
+The configuration options are documented in the Haddock documentation for the `Network.Wai.SAML2.Config` module.
+
+### Implementation
+
+Two interfaces to the middleware are provided. See the Haddock documentation for the `Network.Wai.SAML2` module for full usage examples. An example using the `saml2Callback` variant is shown below, where `cfg` is a `SAML2Config` value and `app` is your existing WAI application:
+
+```haskell
+saml2Callback cfg callback mainApp
+ where callback (Left err) app req sendResponse = do
+ -- 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
+ -- 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
+ -- 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
+ -- 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
+ 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
+```
+
+## Contributions
+
+Please see [CONTRIBUTING.md](CONTRIBUTING.md)
+
+## References
+
+* [SAML2 specification](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html)
+* [Exclusive XML Canonicalisation](https://www.w3.org/TR/xml-exc-c14n/)
+* [XML Signature Syntax and Processing](https://www.w3.org/TR/xmldsig-core1/)
+* [XML Encryption Syntax and Processing](https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/Overview.html)
diff --git a/src/Network/Wai/SAML2.hs b/src/Network/Wai/SAML2.hs
index d377437..4d76843 100644
--- a/src/Network/Wai/SAML2.hs
+++ b/src/Network/Wai/SAML2.hs
@@ -30,6 +30,7 @@ module Network.Wai.SAML2 (
--------------------------------------------------------------------------------
+import qualified Data.ByteString as BS
import qualified Data.Vault.Lazy as V
import Network.Wai
@@ -70,20 +71,20 @@ isPOST = (=="POST") . requestMethod
-- > --
-- > -- you may also want to return e.g. a HTTP 400 or 401 status
-- >
--- > callback (Right assertion) 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
-- > -- 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 <- tryRetrieveAssertion (assertionId (assertion result))
-- >
-- > case result of
-- > Just something -> -- a replay attack has occurred
-- > Nothing -> do
-- > -- store the assertion id somewhere
--- > storeAssertion (assertionId assertion)
+-- > storeAssertion (assertionId (assertion result))
-- >
-- > -- the assertion is valid and you can now e.g.
-- > -- retrieve user data from your database
@@ -94,7 +95,7 @@ isPOST = (=="POST") . requestMethod
-- the given @config@. If the middleware intercepts a request to the
-- endpoint given by @config@, the result will be passed to @callback@.
saml2Callback :: SAML2Config
- -> (Either SAML2Error Assertion -> Middleware)
+ -> (Either SAML2Error Result -> Middleware)
-> Middleware
saml2Callback cfg callback app req sendResponse = do
let path = rawPathInfo req
@@ -110,16 +111,15 @@ saml2Callback cfg callback app req sendResponse = do
-- parse the request
(body, _) <- parseRequestBodyEx bodyOpts lbsBackEnd req
- case body of
- -- extract the SAML response from the request body
- [("SAMLResponse",val)] -> do
+ case lookup "SAMLResponse" body of
+ Just val -> do
result <- validateResponse cfg val
-
+ let rs = lookup "RelayState" body
-- call the callback
- callback result app req sendResponse
-
+ callback (Result rs <$> result) app req sendResponse
-- the request does not contain the expected payload
- _ -> callback (Left InvalidRequest) app req sendResponse
+ Nothing -> callback (Left InvalidRequest) app req sendResponse
+
-- not one of the paths we need to handle, pass the request on to the
-- inner application
| otherwise -> app req sendResponse
@@ -166,6 +166,12 @@ saml2Callback cfg callback app req sendResponse = do
assertionKey :: V.Key Assertion
assertionKey = unsafePerformIO V.newKey
+-- | 'relayStateKey' is a vault key for retrieving the relay state
+-- from request vaults if the 'saml2Vault' 'Middleware' is used
+-- and the assertion is valid.
+relayStateKey :: V.Key (Maybe BS.ByteString)
+relayStateKey = unsafePerformIO V.newKey
+
-- | 'errorKey' is a vault key for retrieving SAML2 errors from request vaults
-- if the 'saml2Vault' 'Middleware' is used.
errorKey :: V.Key SAML2Error
@@ -182,9 +188,18 @@ saml2Vault cfg = saml2Callback cfg callback
app req{
vault = V.insert errorKey err (vault req)
} sendResponse
- callback (Right assertion) app req sendResponse = do
- app req{
- vault = V.insert assertionKey assertion (vault req)
+ callback (Right result) app req sendResponse = do
+ app req{
+ vault = V.insert assertionKey (assertion result)
+ $ V.insert relayStateKey (relayState result) (vault req)
} sendResponse
--------------------------------------------------------------------------------
+
+-- | Represents the result of validating a SAML2 response.
+data Result = Result {
+ -- | An optional relay state, as provided in the POST request.
+ relayState :: !(Maybe BS.ByteString),
+ -- | The assertion obtained from the response that has been validated.
+ assertion :: !Assertion
+} deriving (Eq, Show)
diff --git a/src/Network/Wai/SAML2/Assertion.hs b/src/Network/Wai/SAML2/Assertion.hs
index 534a304..1acbff3 100644
--- a/src/Network/Wai/SAML2/Assertion.hs
+++ b/src/Network/Wai/SAML2/Assertion.hs
@@ -16,7 +16,7 @@ module Network.Wai.SAML2.Assertion (
AttributeStatement,
parseAttributeStatement,
Assertion(..)
-) where
+) where
--------------------------------------------------------------------------------
diff --git a/wai-saml2.cabal b/wai-saml2.cabal
index 22e24d1..b73d3b6 100644
--- a/wai-saml2.cabal
+++ b/wai-saml2.cabal
@@ -4,10 +4,10 @@ cabal-version: 1.12
--
-- see: https://github.com/sol/hpack
--
--- hash: c0664fef77f28dfe3a12d2dfaff84d77d236c7b0b2641d4da57a4889dce066f1
+-- hash: ddb0bf4bfb873f58ec1a0de52ad6c91db0957cdde75805acb20f431229689da2
name: wai-saml2
-version: 0.1.0.0
+version: 0.2.0.0
synopsis: SAML2 assertion validation as WAI middleware
description: A Haskell library which implements SAML2 assertion validation as WAI middleware
category: Security
@@ -20,6 +20,7 @@ license: MIT
license-file: LICENSE
build-type: Simple
extra-source-files:
+ README.md
CHANGELOG.md
source-repository head