packages feed

ucam-webauth (empty) → 0.1.0.0

raw patch · 10 files changed

+1206/−0 lines, 10 filesdep +QuickCheckdep +aesondep +attoparsec

Dependencies added: QuickCheck, aeson, attoparsec, base, bytestring, containers, cryptonite, errors, generic-random, here, hspec, http-api-data, http-types, microlens, microlens-mtl, mtl, parser-combinators, pem, quickcheck-instances, text, time, time-qq, ucam-webauth-types, x509

Files

+ Changelog.md view
@@ -0,0 +1,14 @@+# Changelog++This changelog is shared among all the packages within the same project.++## 0.1.0.0 - 2018-12-09++Initial versions of `ucam-webauth` and `ucam-webauth-types`.++---++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/).
+ LICENSE view
@@ -0,0 +1,203 @@++                                 Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "{}"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright {yyyy} {name of copyright owner}++   Licensed under the Apache License, Version 2.0 (the "License");+   you may not use this file except in compliance with the License.+   You may obtain a copy of the License at++       http://www.apache.org/licenses/LICENSE-2.0++   Unless required by applicable law or agreed to in writing, software+   distributed under the License is distributed on an "AS IS" BASIS,+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+   See the License for the specific language governing permissions and+   limitations under the License.+   
+ README.md view
@@ -0,0 +1,56 @@+---+title:  Ucam Webauth  +author: David Baynard  +date:   09 Dec 2018  +...++[![Build Status](https://travis-ci.com/dbaynard/UcamWebauth.svg?branch=develop)](https://travis-ci.com/dbaynard/UcamWebauth)++# <https://raven.cam.ac.uk/project/>++The University of Cambridge _Raven_ service uses the _Ucam Webauth_ protocol.++This repository contains a number of Haskell libraries to interact with this system.++# ucam-webauth++[![Hackage — ucam-webauth](https://img.shields.io/hackage/v/ucam-webauth.svg?style=flat)](https://hackage.haskell.org/package/ucam-webauth)+[![ucam-webauth on Stackage LTS 13](http://stackage.org/package/ucam-webauth/badge/lts-13)](http://stackage.org/lts-13/package/ucam-webauth)+[![ucam-webauth on Stackage Nightly](http://stackage.org/package/ucam-webauth/badge/nightly)](http://stackage.org/nightly/package/ucam-webauth)++This implements the client authentication protocol; specifically, the validation.++# ucam-webauth-types++[![Hackage — ucam-webauth-types](https://img.shields.io/hackage/v/ucam-webauth-types.svg?style=flat)](https://hackage.haskell.org/package/ucam-webauth-types)+[![ucam-webauth-types on Stackage LTS 13](http://stackage.org/package/ucam-webauth-types/badge/lts-13)](http://stackage.org/lts-13/package/ucam-webauth-types)+[![ucam-webauth-types on Stackage Nightly](http://stackage.org/package/ucam-webauth-types/badge/nightly)](http://stackage.org/nightly/package/ucam-webauth-types)++This implements data types for the client authentication protocol.++There is an internal package which is *not* recommended for use.+Its only purpose is to split the core functionality among packages for minimal ghcjs dependencies.++# raven-wai++[![Hackage — raven-wai](https://img.shields.io/hackage/v/raven-wai.svg?style=flat)](https://hackage.haskell.org/package/raven-wai)+[![raven-wai on Stackage LTS 13](http://stackage.org/package/raven-wai/badge/lts-13)](http://stackage.org/lts-13/package/raven-wai)+[![raven-wai on Stackage Nightly](http://stackage.org/package/raven-wai/badge/nightly)](http://stackage.org/nightly/package/raven-wai)++This adds [wai](//hackage.haskell.org/package/wai) middleware enabling authentication using _Raven_.++# servant-raven++[![Hackage — servant-raven](https://img.shields.io/hackage/v/servant-raven.svg?style=flat)](https://hackage.haskell.org/package/servant-raven)+[![servant-raven on Stackage LTS 13](http://stackage.org/package/servant-raven/badge/lts-13)](http://stackage.org/lts-13/package/servant-raven)+[![servant-raven on Stackage Nightly](http://stackage.org/package/servant-raven/badge/nightly)](http://stackage.org/nightly/package/servant-raven)++API combinators for [servant](//hackage.haskell.org/package/servant), using [servant-auth](//hackage.haskell.org/package/servant-auth).++# servant-raven-server++[![Hackage — servant-raven-server](https://img.shields.io/hackage/v/servant-raven-server.svg?style=flat)](https://hackage.haskell.org/package/servant-raven-server)+[![servant-raven-server on Stackage LTS 13](http://stackage.org/package/servant-raven-server/badge/lts-13)](http://stackage.org/lts-13/package/servant-raven-server)+[![servant-raven-server on Stackage Nightly](http://stackage.org/package/servant-raven-server/badge/nightly)](http://stackage.org/nightly/package/servant-raven-server)++The handlers for [servant](//hackage.haskell.org/package/servant).
+ src/UcamWebauth.hs view
@@ -0,0 +1,310 @@+{-|+Module      : UcamWebauth+Description : The Ucam-Webauth protocol, from the University of Cambridge+Maintainer  : David Baynard <ucamwebauth@baynard.me>++Key parts of the implementation of the protocol itself.++<https://raven.cam.ac.uk/project/waa2wls-protocol.txt>++-}++{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE+    PackageImports+  , DataKinds+  , FlexibleContexts+  , FlexibleInstances+  , OverloadedStrings+  , RecordWildCards+  , ScopedTypeVariables+  , TupleSections+  , ViewPatterns+  #-}++module UcamWebauth+  ( module X++  -- Parser+  , ucamResponseParser++  , authInfo+  , maybeAuthInfo+  , authCode++  , validateAuthResponse+  ) where++import           "base"               Control.Applicative+import           "errors"             Control.Error hiding (catMaybes)+import           "base"               Control.Monad+import           "mtl"                Control.Monad.Except+import           "cryptonite"         Crypto.Hash.Algorithms+import           "cryptonite"         Crypto.PubKey.RSA.PKCS15+import           "cryptonite"         Crypto.PubKey.RSA.Types+import           "aeson"              Data.Aeson.Types (FromJSON, ToJSON)+import           "attoparsec"         Data.Attoparsec.ByteString.Char8 hiding (count, take)+import           "base"               Data.Bifunctor+import           "bytestring"         Data.ByteString (ByteString)+import qualified "bytestring"         Data.ByteString.Char8 as B+import           "base"               Data.Coerce+import           "base"               Data.List.NonEmpty (NonEmpty)+import           "containers"         Data.Map.Strict (Map)+import qualified "containers"         Data.Map.Strict as MapS+import           "pem"                Data.PEM+import           "text"               Data.Text (Text)+import qualified "text"               Data.Text as T+import           "text"               Data.Text.Encoding hiding (decodeASCII)+import           "time"               Data.Time (diffUTCTime, getCurrentTime)+import           "x509"               Data.X509+import           "microlens"          Lens.Micro+import           "microlens-mtl"      Lens.Micro.Mtl+import           "base"               System.IO (withFile, IOMode(..))+import           "ucam-webauth-types" UcamWebauth.Data as X+import           "ucam-webauth-types" UcamWebauth.Data.Internal+import           "this"               UcamWebauth.Internal+import           "this"               UcamWebauth.Parser+import           "this"               UcamWebauth.WLS+import           "http-api-data"      Web.HttpApiData++------------------------------------------------------------------------------+-- * Top level functions++{-|+  If the supplied response is valid and corresponds to the settigns, return a 'UcamWebauthInfo' value.++-}+authInfo+  :: (MonadIO m, MonadPlus m, MonadError Text m)+  => SetWAA a+  -> MaybeValidResponse a+  -> m (UcamWebauthInfo a)+authInfo waa = getAuthInfo <=< validateAuthResponse waa++{-|+  'maybeAuthInfo' takes the 'AuthRequest' from its environment, and a 'ByteString' containing the @WLS@+  response, and if the response is valid, returns a 'UcamWebauthInfo' value.++  TODO When the errors returned can be usefully used, ensure this correctly returns a lifted+  'Either b (UcamWebauthInfo a)' response.+-}+maybeAuthInfo+  :: (FromJSON a, MonadIO m, MonadPlus m, MonadError Text m)+  => SetWAA a+  -> ByteString+  -> m (UcamWebauthInfo a)+maybeAuthInfo waa = getAuthInfo <=< maybeAuthCode waa++{-|+  A helper function to parse and validate a response from a @WLS@.+-}+maybeAuthCode+  :: (FromJSON a, MonadIO m, MonadPlus m, MonadError Text m)+  => SetWAA a+  -> ByteString+  -> m (ValidResponse a)+maybeAuthCode waa = validateAuthResponse waa <=< authCode++{-|+  Parse the response from a @WLS@.+-}+authCode+  :: (FromJSON a, MonadPlus m)+  => ByteString+  -> m (MaybeValidResponse a)+authCode = liftMaybe . maybeResult . flip feed "" . parse ucamResponseParser++-- | Parse a not-yet-validated 'SignedAuthResponse' from a form response.+--+-- The orphan instance is necessary as this requires the parser defined in+-- this package.+instance FromJSON a => FromHttpApiData (MaybeValidResponse a) where+  parseQueryParam = first T.pack . parseOnly ucamResponseParser . encodeUtf8++instance ToJSON a => ToHttpApiData (MaybeValidResponse a) where+  toUrlPiece = wlsEncodeSign++instance FromJSON a => FromHttpApiData (AuthResponse a) where+  parseQueryParam = first T.pack . parseOnly ucamAuthResponseParser . encodeUtf8++instance ToJSON a => ToHttpApiData (AuthResponse a) where+  toUrlPiece = wlsEncode++------------------------------------------------------------------------------+-- * Validation++guardE+  :: forall e m . (MonadError e m, Alternative m)+  => e -> Bool -> m ()+guardE e boolean = guard boolean <|> throwError e++{-|+  Validate the Authentication Response++  1. Validate the key id+  2. Validate the cryptographic signature against the relevant key+  3. Validate the issue time+  4. Validate the url is the same as that transmitted+  5. Validate the auth and sso values are valid++  The protocol requires a valid input, but 'ucamResponseParser' holds all the relevant logic+  as the only way to produce a 'SignedAuthResponse' is with the parser.++  This is the only way to produce a 'Valid' 'SignedAuthResponse', and therefore an 'AuthInfo'.+-}+validateAuthResponse+  :: forall a m . (MonadIO m, MonadPlus m, MonadError Text m)+  => SetWAA a+  -> MaybeValidResponse a+  -> m (ValidResponse a)+validateAuthResponse waa sar = do+    guardE "Key invalid" .+      validateKid waa =<< liftMaybe (sar ^. ucamAKid)+    guardE "Signature invalid" <=<+      validateSig waa $ sar+    guardE "Issue time invalid" <=<+      validateIssueTime waa $ sar ^. ucamAResponse+    guardE "Url does not match transmittion" .+      validateUrl waa $ sar ^. ucamAResponse+    guardE "Authentication type invalid" <=<+      validateAuthTypes waa $ sar ^. ucamAResponse+    return . makeValid $ sar+  where+    makeValid :: MaybeValidResponse a -> ValidResponse a+    makeValid = coerce++------------------------------------------------------------------------------+-- ** Key ID++{-|+  Check the kid is valid+-}+validateKid :: SetWAA a -> KeyID -> Bool+validateKid = flip elem . view (wSet . validKids) . configWAA++------------------------------------------------------------------------------+-- ** Signature++{-|+  Validate the signature, getting the key using 'readRSAKeyFile'+-}+validateSig+  :: (MonadPlus m, MonadIO m)+  => SetWAA a+  -> MaybeValidResponse a+  -> m Bool+validateSig (configWAA -> waa) = validateSigKey (readRSAKeyFile $ waa ^. wSet . importedKeys)++decodeRSAPubKey+  :: ByteString      -- ^ The data representing a public key as PEM.+  -> Maybe PublicKey -- ^ @'Just' 'PublicKey'@ if RSA, 'Nothing' otherwise.+decodeRSAPubKey = hush . f++  where+    f :: ByteString -> Either String PublicKey+    f = getRSAKey . certPubKey . getCertificate+      <=< decodeSignedCertificate . pemContent+      <=< headErr "Empty list"+      <=< pemParseBS++    getRSAKey :: Alternative f => PubKey -> f PublicKey+    getRSAKey (PubKeyRSA x) = pure x+    getRSAKey _ = empty++{-|+  This assumes keys are PEM self-signed certificates in the ‘static’ directory, named++  @pubkey/key/.crt@++  where @/key/@ should be replaced by the 'KeyID' /e.g./ @pubkey2.crt@+-}+readRSAKeyFile+  :: (MonadIO m, Alternative m)+  => Map KeyID ByteString+  -> KeyID+  -> m PublicKey+readRSAKeyFile keymap key = case key `MapS.lookup` keymap of+  Just bs -> liftMaybe . decodeRSAPubKey $ bs+  Nothing -> let file = "static/pubkey" <> (B.unpack . unKeyID) key <> ".crt" in+    liftMaybe <=< liftIO . withFile file ReadMode+    $ pure . decodeRSAPubKey <=< B.hGetContents++validateSigKey+  :: MonadPlus m+  => (KeyID+  -> m PublicKey)                     -- ^ Get an RSA 'PublicKey' from somewhere, with the possibility of failing.+  -> MaybeValidResponse a+  -> m Bool                           -- ^ 'True' for a verified signature, 'False' for a verified invalid signature, and 'mzero' for an inability to validate+validateSigKey importKey sar =+    pure . rsaValidate <=< importKey <=< liftMaybe $ sar ^. ucamAKid+  where+    rsaValidate :: PublicKey -> Bool+    rsaValidate key = verify (Just SHA1) key message signature+    message :: ByteString+    message = sar ^. ucamAToSign+    signature :: ByteString+    signature = maybe mempty decodeUcamB64 . view ucamASig $ sar++------------------------------------------------------------------------------+-- ** Issue time++{-|+  Validate the time of issue is within 'syncTimeOut' of the current time.++  TODO Uses 'getCurrentTime'. There may be a better implementation.+-}+validateIssueTime+  :: (MonadIO m)+  => SetWAA a+  -> AuthResponse a+  -> m Bool+validateIssueTime (configWAA -> waa) ar =+  (waa ^. wSet . syncTimeOut >) . flip diffUTCTime (ar ^. ucamAIssue)+  <$> liftIO getCurrentTime++------------------------------------------------------------------------------+-- ** Url++{-|+  Check the url parameter matches that sent in the 'AuthRequest'+-}+validateUrl+  :: SetWAA a+  -> AuthResponse a+  -> Bool+validateUrl (configWAA -> waa) =+  (== waa ^. aReq . ucamQUrl) . view ucamAUrl++------------------------------------------------------------------------------+-- ** Authentication type++{-|+  Check the authentication type matches that sent.++  * If the iact variable is Yes, only return 'True' if the aauth value is acceptable.+  * If the iact variable is No, only return 'True' if sso contains a value that is acceptable.+  * If the iact variable is unset, return 'True' if there is an acceptable value in either field.+-}+validateAuthTypes+  :: forall a f . (Alternative f)+  => SetWAA a+  -> AuthResponse a+  -> f Bool+validateAuthTypes (configWAA -> waa) ar =+    maybe validateAnyAuth validateSpecificAuth $ waa ^. wSet . needReauthentication++  where+    isAcceptableAuth :: AuthType -> Bool+    isAcceptableAuth = flip elem $ waa ^. wSet . authAccepted++    anyAuth :: Maybe AuthType -> Maybe (NonEmpty AuthType) -> Bool+    anyAuth Nothing (Just x) = any isAcceptableAuth x+    anyAuth (Just x) Nothing = isAcceptableAuth x+    anyAuth _ _ = False++    validateAnyAuth :: f Bool+    validateAnyAuth = pure $ (ar ^. ucamAAuth) `anyAuth` (ar ^. ucamASso)++    validateSpecificAuth :: YesNo -> f Bool+    validateSpecificAuth Yes = isAcceptableAuth <$> liftMaybe (ar ^. ucamAAuth)+    validateSpecificAuth _ = any isAcceptableAuth <$> liftMaybe (ar ^. ucamASso)
+ src/UcamWebauth/Internal.hs view
@@ -0,0 +1,23 @@+{-# OPTIONS_HADDOCK hide, not_here #-}+{-# LANGUAGE+    PackageImports+  #-}++{-|+Module      : UcamWebauth.Internal+Description : Ucam-Webauth protocol internals+Maintainer  : David Baynard <ucamwebauth@baynard.me>++-}++module UcamWebauth.Internal+  ( module UcamWebauth.Internal+  ) where++import "base" Control.Applicative (empty, Alternative)++{-|+  Lift a 'Maybe' value.+-}+liftMaybe :: Alternative f => Maybe a -> f a+liftMaybe = maybe empty pure
+ src/UcamWebauth/Parser.hs view
@@ -0,0 +1,218 @@+{-# OPTIONS_HADDOCK hide, not_here #-}+{-# LANGUAGE+    PackageImports+  , ApplicativeDo+  , DataKinds+  , OverloadedStrings+  , RecordWildCards+  , ScopedTypeVariables+  , ViewPatterns+  #-}++{-|+Module      : UcamWebauth.Parser+Description : Parsers for the UCam-Webauth protocol+Maintainer  : David Baynard <ucamwebauth@baynard.me>++-}++module UcamWebauth.Parser+  ( ucamResponseParser+  , ucamAuthResponseParser+  ) where++import           "base"               Control.Applicative+import           "parser-combinators" Control.Applicative.Combinators.NonEmpty+import           "base"               Control.Arrow ((***))+import           "base"               Control.Monad ((<=<))+import           "aeson"              Data.Aeson (FromJSON)+import qualified "aeson"              Data.Aeson as A+import qualified "attoparsec"         Data.Attoparsec.ByteString.Char8 as A+import           "attoparsec"         Data.Attoparsec.ByteString.Char8 hiding (count, take, sepBy1)+import           "attoparsec"         Data.Attoparsec.Combinator (lookAhead)+import           "bytestring"         Data.ByteString (ByteString)+import           "ucam-webauth-types" Data.ByteString.B64+import           "bytestring"         Data.ByteString.Builder (stringUtf8, toLazyByteString)+import qualified "bytestring"         Data.ByteString.Char8 as B8+import qualified "bytestring"         Data.ByteString.Lazy as BSL+import           "base"               Data.Char (isAlphaNum)+import           "base"               Data.List.NonEmpty (NonEmpty)+import           "base"               Data.Maybe+import           "text"               Data.Text (Text)+import           "text"               Data.Text.Encoding+import           "time"               Data.Time (UTCTime)+import           "time"               Data.Time.LocalTime+import           "http-types"         Network.HTTP.Types+import           "ucam-webauth-types" UcamWebauth.Data+import           "ucam-webauth-types" UcamWebauth.Data.Internal++------------------------------------------------------------------------------+-- * 'Parser's++{-|+  Parse the response from the @WLS@++  As a reminder, the 'MaybeValid' symbol indicates the response has not yet been verified.+-}+ucamResponseParser :: FromJSON a => Parser (MaybeValidResponse a)+ucamResponseParser = do+  (_ucamAToSign, _ucamAResponse@AuthResponse{..}) <- noBang . match $ ucamAuthResponseParser+  (_ucamAKid, _ucamASig) <- parseKidSig _ucamAStatus <?> "A Kid signature"+  _ <- endOfInput+  pure SignedAuthResponse{..}++ucamAuthResponseParser :: FromJSON a => Parser (AuthResponse a)+ucamAuthResponseParser = do+  _ucamAVer       <- noBang wlsVersionParser+  _ucamAStatus    <- noBang responseCodeParser+  _ucamAMsg       <- maybeBang . urlWrapText $ (betweenBangs <?> "Msg")+  _ucamAIssue     <- noBang utcTimeParser+  _ucamAId        <- fmap (fromMaybe "") . maybeBang . urlWrapText $ (betweenBangs <?> "Id")+  _ucamAUrl       <- fmap (fromMaybe "") . maybeBang . urlWrapText $ (betweenBangs <?> "Url")+  _ucamAPrincipal <- parsePrincipal _ucamAStatus+  _ucamAPtags     <- parsePtags _ucamAVer+  _ucamAAuth      <- maybeBang $ authTypeParser+  _ucamASso       <- parseSso _ucamAStatus _ucamAAuth+  _ucamALife      <- maybeBang . fmap timePeriodFromSeconds $ decimal+  _ucamAParams    <- A.decodeStrict . decodeUcamB64 . UcamB64 <$> betweenBangs <|> pure empty <?> "Parameters"+  pure AuthResponse{..}++noBang :: Parser b -> Parser b+noBang = (<* "!")++-- urlWrap :: Functor f => f ByteString -> f ByteString+-- urlWrap = fmap (urlDecode False)++urlWrapText :: Parser ByteString -> Parser Text+urlWrapText = (<?> "Url wrapping") . either (fail . show) pure . decodeUtf8' <=< fmap id -- (urlDecode False)++maybeBang :: Parser b -> Parser (Maybe b)+maybeBang = noBang . optional++parsePtags :: WLSVersion -> Parser [Ptag]+parsePtags WLS3 = noBang $ ptagParser `sepBy` ","+parsePtags _    = pure empty++parsePrincipal :: StatusCode -> Parser (Maybe Text)+parsePrincipal (statusCode . getStatus -> 200) = maybeBang . urlWrapText $ (betweenBangs <?> "Principal")+parsePrincipal _                               = noBang empty <?> "Empty principal"++parseSso :: StatusCode -> Maybe AuthType -> Parser (Maybe (NonEmpty AuthType))+parseSso (statusCode . getStatus -> 200) Nothing = (maybeBang $ authTypeParser `sepBy1` ",") <?> "Sso"+parseSso _ _                                     = noBang (pure empty <?> "Empty Sso")++parseKidSig :: StatusCode -> Parser (Maybe KeyID, Maybe UcamBase64BS)+parseKidSig (statusCode . getStatus -> 200) =+  curry (pure *** pure)+    <$> (noBang kidParser <?> "The Kid")+    <*> (ucamB64parser <?> "The signature")+parseKidSig _ = (,)+  <$> (maybeBang kidParser <?> "The Kid")+  <*> (optional ucamB64parser <?> "The signature")++{-|+  The Ucam-Webauth protocol uses @!@ characters to separate the fields in the response. Any @!@+  characters in the data itself must be url encoded. The representations used in this module+  meet this criterion.++  TODO Add tests to verify.+-}+betweenBangs :: Parser ByteString+betweenBangs = takeWhile1 (/= '!')+  <?> "Missing field between !"++------------------------------------------------------------------------------+-- ** Helpers++{-|+  A parser for the 'WLSVersion', as used by the 'AuthResponse' parser.+-}+wlsVersionParser :: Parser WLSVersion+wlsVersionParser = choice+  [ "3" *> pure WLS3+  , "2" *> pure WLS2+  , "1" *> pure WLS1+  ]+  <?> "WLS version"++{-|+  A parser for 'AuthType' data+-}+authTypeParser :: Parser AuthType+authTypeParser = "pwd" *> pure Pwd+  <?> "AuthType"++{-|+  Parser representing a 'Ptag'+-}+ptagParser :: Parser Ptag+ptagParser = "current" *> pure Current+  <?> "PTag"++{-|+  A parser representing a typed 'Status' code within the protocol.+-}+responseCodeParser :: Parser StatusCode+responseCodeParser = toEnum <$> decimal+  <?> "Response code"++{-|+  The 'KeyID' can represent a restricted set of possible 'ByteString's, as per the protocol document,+  and this parser should only allow a valid representation.++  TODO Add tests to verify.+-}+kidParser :: Parser KeyID+kidParser = (<?> "Key id") $ do+  frst <- satisfy . inClass $ "1-9"+  rest <- (fmap catMaybes . A.count 7 . optional $ digit) <* (lookAhead . satisfy $ not . isDigit)+  pure (KeyID . B8.pack $ frst : rest)++{-|+  Using 'ucamTimeParser', work out the actual 'UTCTime' for further processing.++  If 'ucamTimeParser' succeeds it should always produce a valid result for 'parseTimeRFC3339'.+  As a result, 'parseTimeRFC3339' is extracted from the Maybe enviroment using 'fromMaybe' with+  'error'.+-}+utcTimeParser :: Parser UTCTime+utcTimeParser = let er = error "Cannot parse time as RFC3339. There’s a bug in the parser." in+  zonedTimeToUTC . fromMaybe er . zonedUcamTime <$> ucamTimeParser+  <?> "UTCTime"++{-|+  This parses a 'ByteString' into a 'UcamTime'+-}+ucamTimeParser :: Parser UcamTime+ucamTimeParser = (<?> "UcamWebauth Time") $ do+  year   <- stringUtf8 <$> A.count 4 digit+  month  <- stringUtf8 <$> A.count 2 digit+  day    <- stringUtf8 <$> A.count 2 digit <* "T"+  hour   <- stringUtf8 <$> A.count 2 digit+  minute <- stringUtf8 <$> A.count 2 digit+  sec    <- stringUtf8 <$> A.count 2 digit <* "Z"+  either (fail . show) (pure . UcamTime) . decodeUtf8' . BSL.toStrict . toLazyByteString . mconcat $+    [year, "-", month, "-", day, "T", hour, ":", minute, ":", sec, "Z"]++{-|+  A parser to represent a Ucam-Webauth variant base64–encoded 'ByteString' as a 'UcamBase64BS'+-}+ucamB64parser :: Parser UcamBase64BS+ucamB64parser = UcamB64 <$> takeWhile1 (ors [isAlphaNum, inClass "-._"])+  <?> "UcamWebauth Base 64 string"++------------------------------------------------------------------------------+-- * Helper functions++{-|+  Combines a list of predicates into a single predicate. /c.f./ 'any', which applies+  a single predicate to many items in a data structure.++  Simplifies to++  @ors :: ['Char' -> 'Bool'] -> 'Char' -> 'Bool'@+-}+ors+  :: (Traversable t, Applicative f)+  => t (f Bool) -> f Bool+ors = fmap or . sequenceA
+ src/UcamWebauth/WLS.hs view
@@ -0,0 +1,89 @@+{-|+Module      : Protocol.UcamWebauth.WLS+Description : Encoding WLS responses in Ucam-Webauth protocol, from the University of Cambridge+Maintainer  : David Baynard <ucamwebauth@baynard.me>++Encoding the WLS-Response contents.++<https://raven.cam.ac.uk/project/waa2wls-protocol.txt>++This is useful for test suite and more.++-}++{-# LANGUAGE+    PackageImports+  , OverloadedStrings+  , PartialTypeSignatures+  #-}++module UcamWebauth.WLS+  ( wlsEncode+  , wlsEncodeSign+  ) where++import           "aeson"              Data.Aeson (ToJSON)+import qualified "aeson"              Data.Aeson as A+import           "ucam-webauth-types" Data.ByteString.B64+import qualified "bytestring"         Data.ByteString.Builder as B+import qualified "bytestring"         Data.ByteString.Lazy as BSL+import           "base"               Data.Foldable (fold)+import           "base"               Data.List (intersperse)+import qualified "base"               Data.List.NonEmpty as NE (intersperse)+import           "base"               Data.Maybe+import           "text"               Data.Text (Text)+import           "text"               Data.Text.Encoding+import           "text"               Data.Text.Encoding.Error (lenientDecode)+import           "microlens"          Lens.Micro+import           "ucam-webauth-types" UcamWebauth.Data as X+import           "ucam-webauth-types" UcamWebauth.Data.Internal++wlsEncode :: ToJSON a => AuthResponse a -> Text+wlsEncode = textBuilder . wlsEncode'++wlsEncodeSign :: ToJSON a => MaybeValidResponse a -> Text+wlsEncodeSign = textBuilder . wlsEncodeSign'++textBuilder :: B.Builder -> Text+textBuilder = decodeUtf8With lenientDecode . BSL.toStrict . B.toLazyByteString++wlsEncode' :: ToJSON a => AuthResponse a -> B.Builder+wlsEncode' r = mconcat . intersperse "!" $+  [ r ^. ucamAVer       . to displayWLSVersion+  , r ^. ucamAStatus    . to (B.stringUtf8 . show . fromEnum)+  , r ^. ucamAMsg       . mTextEncoded+  , r ^. ucamAIssue     . encoded (unUcamTime . ucamTime)+  , r ^. ucamAId        . textEncoded+  , r ^. ucamAUrl       . textEncoded+  , r ^. ucamAPrincipal . mTextEncoded+  , r ^. ucamAPtags     . to (mconcat . intersperse "," . fmap displayPtag)+  , r ^. ucamAAuth      . to (maybe "" displayAuthType)+  , r ^. ucamASso       . to (maybe "" $ fold . NE.intersperse "," . fmap displayAuthType)+  , r ^. ucamALife      . to (maybe "" $ B.stringUtf8 . show . secondsFromTimePeriod)+  , r ^. ucamAParams    . to (maybe "" $ B.lazyByteString . unUcamB64L . encodeUcamB64L . A.encode)+  ]++wlsEncodeSign' :: ToJSON a => MaybeValidResponse a -> B.Builder+wlsEncodeSign' r = mconcat . intersperse "!" $+  [ r ^. ucamAResponse . to wlsEncode'+  , r ^. ucamAKid      . to (maybe "" $ B.byteString . unKeyID)+  , r ^. ucamASig      . to (maybe "" $ B.byteString . unUcamB64)+  ]++-- orBlank :: Getting r a B.Builder -> Getting r (Maybe a) B.Builder+-- orBlank g bcrb a = _++textEncoded :: Getting r Text B.Builder+textEncoded = encoded id++encoded :: ToJSON a => (a -> Text) -> Getting r a B.Builder+encoded f = to $ encodeUtf8Builder . f++mEncoded :: ToJSON a => (a -> Text) -> Getting r (Maybe a) B.Builder+mEncoded f = to $ encodeUtf8Builder . fromMaybe "" . fmap f++mTextEncoded :: Getting r (Maybe Text) B.Builder+mTextEncoded = mEncoded id++-- ucamTimeFormat :: (UTCTime )+-- ucamTimeFormat = B.stringUtf8 . formatTime defaultTimeLocale "%0Y%m%dT%H%M%SZ"
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/UcamWebauthSpec.hs view
@@ -0,0 +1,182 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE+    PackageImports+  , AllowAmbiguousTypes+  , FlexibleInstances+  , NamedFieldPuns+  , OverloadedLists+  , OverloadedStrings+  , QuasiQuotes+  , RecordWildCards+  , ScopedTypeVariables+  , TypeApplications+  , TypeOperators+  , TypeSynonymInstances+  #-}++module UcamWebauthSpec (spec) where++import           "aeson"                Data.Aeson.Types hiding ((.=))+import           "base"                 Data.Bits+import qualified "bytestring"           Data.ByteString as BS+import           "ucam-webauth-types"   Data.ByteString.B64+import qualified "bytestring"           Data.ByteString.Char8 as B8+import           "base"                 Data.Char+import           "base"                 Data.Maybe+import           "here"                 Data.String.Here+import           "text"                 Data.Text (Text)+import           "text"                 Data.Text.Encoding+import qualified "text"                 Data.Text as T+import           "time"                 Data.Time+import           "time-qq"              Data.Time.QQ as Q+import           "generic-random"       Generic.Random+import           "microlens"            Lens.Micro+import           "microlens-mtl"        Lens.Micro.Mtl+import           "hspec"                Test.Hspec+import           "hspec"                Test.Hspec.QuickCheck+import           "QuickCheck"           Test.QuickCheck+import           "quickcheck-instances" Test.QuickCheck.Instances ()+import           "this"                 UcamWebauth+import           "ucam-webauth-types"   UcamWebauth.Data.Internal+import           "http-api-data"        Web.HttpApiData++spec :: Spec+spec = do+  describe "UcamWebauth" $ do+    it "should parse example response" $ do+      parseQueryParam @(AuthResponse Text) exampleResponseText `shouldBe` Right exampleResponse+    it "should parse example signed response" $ do+      parseQueryParam @(MaybeValidResponse Text) exampleSignedResponseText `shouldBe` Right exampleSignedResponse+    it "should produce example response" $ do+      toQueryParam @(AuthResponse Text) exampleResponse `shouldBe` exampleResponseText+    it "should produce example signed response" $ do+      toQueryParam @(MaybeValidResponse Text) exampleSignedResponse `shouldBe` exampleSignedResponseText+    modifyMaxSuccess (const 1000) $ prop_HttpApiData @(AuthResponse Text)+    modArgs $ prop_HttpApiData @(MaybeValidResponse Text)++modArgs :: SpecWith a -> SpecWith a+modArgs = modifyMaxSuccess (const 100) . modifyMaxDiscardRatio (const 1) . modifyMaxSize (const 1)++prop_HttpApiData+  :: forall a .+    ( Arbitrary a+    , Eq a+    , Show a+    , ToHttpApiData a+    , FromHttpApiData a+    )+  => Spec+prop_HttpApiData = prop "should serialize with HttpApiData correctly" $ \(h :: a) ->+  let qp = toQueryParam h in counterexample (T.unpack qp) $+    parseQueryParam qp === Right h++instance (ToJSON a, FromJSON a, Arbitrary a) => Arbitrary (MaybeValidResponse a) where+  arbitrary = do+    x <- genericArbitraryU `suchThat` \a -> and @[]+      [ notElem @[] (a ^. ucamASig) [Just "", Nothing]+      , isNothing (a ^. ucamASig) || isJust (a ^. ucamAKid)+      ]+    pure $ x &~ do+      ucamAToSign .= encodeUtf8 (toQueryParam (x ^. ucamAResponse))+  shrink = genericShrink++instance (ToJSON a, FromJSON a, Arbitrary a) => Arbitrary (AuthResponse a) where+  arbitrary = do+    x <- genericArbitraryU `suchThat` \a -> and @[]+      [ fromMaybe True $ a ^? ucamAPrincipal . _Just . to (T.all (/= '!'))+      , fromMaybe True $ a ^? ucamAMsg . _Just . to (T.all (/= '!'))+      , a ^. ucamAId . to (T.all (/= '!'))+      , a ^. ucamAUrl . to (T.all (/= '!'))+      , isJust (a ^. ucamAAuth) `xor` isJust (a ^. ucamASso)+      , a ^. ucamAPrincipal /= Just ""+      , a ^. ucamAMsg /= Just ""+      ]+    pure $ x &~ do+      ucamAIssue . dayTime %= fromInteger . round+  shrink = genericShrink++dayTime :: UTCTime `Lens'` DiffTime+dayTime f UTCTime{..} = (\x -> UTCTime{utctDayTime = x, ..}) <$> f utctDayTime+{-# INLINE dayTime #-}++instance Arbitrary KeyID where+  arbitrary = genericArbitraryU `suchThat` \(KeyID a) -> and @[]+    [ BS.length a & \x -> x >= 1 && x <= 8+    , B8.all isDigit a+    , B8.head a /= '0'+    ]+  shrink = genericShrink++instance Arbitrary WLSVersion where+  arbitrary = pure WLS3++instance Arbitrary StatusCode where+  arbitrary = pure Ok200++instance Arbitrary UcamBase64BS where+  arbitrary = encodeUcamB64 <$> arbitrary+  shrink = genericShrink++instance Arbitrary Ptag where+  arbitrary = genericArbitraryU+  shrink = genericShrink++instance Arbitrary AuthType where+  arbitrary = genericArbitraryU+  shrink = genericShrink++instance Arbitrary TimePeriod where+  arbitrary = timePeriodFromSeconds . secondsFromTimePeriod <$> genericArbitraryU `suchThat` (>= 0)++  shrink = genericShrink++exampleSignedResponse :: MaybeValidResponse Text+exampleSignedResponse = SignedAuthResponse+  { _ucamAResponse = AuthResponse+    { _ucamAVer       = WLS3+    , _ucamAStatus    = Ok200+    , _ucamAMsg       = Nothing+    , _ucamAIssue     = [utcIso8601ms| 2017-05-15T17:23:11 |]+    , _ucamAId        = "oANAuhC9fZmMlZUPIm53y5vn"+    , _ucamAUrl       = "http://localhost:3000/foo/query"+    , _ucamAPrincipal = Just "test0244"+    , _ucamAPtags     = [Current]+    , _ucamAAuth      = Nothing+    , _ucamASso       = Just [Pwd]+    , _ucamALife      = Just (timePeriodFromSeconds 30380)+    , _ucamAParams    = Just "This is 100% of the data! And it’s really quite cool"+    }+  , _ucamAToSign = "3!200!!20170515T172311Z!oANAuhC9fZmMlZUPIm53y5vn!http://localhost:3000/foo/query!test0244!current!!pwd!30380!IlRoaXMgaXMgMTAwJSBvZiB0aGUgZGF0YSEgQW5kIGl04oCZcyByZWFsbHkgcXVpdGUgY29vbCI_"+  , _ucamAKid      = Just "901"+  , _ucamASig      = Just+    (UcamB64+      { unUcamB64 = "RzC9KZWALCSeK0n9885X4zzemHizuj8K.NOpt.n1hfRCTE2ZBgvJ-fBvT-PaL80cSFGpyCJgt9LvM4-peJzcidoKC6zhBEvG0QnlqWTLsphbIA0JmBRiOoeqyLYRVGwDEdLdacdsQRM.u7bik.enhbuN1-aIQCOdB5MutxtYiu4_"+      }+    )+  }++exampleResponse :: AuthResponse Text+exampleResponse = AuthResponse+  { _ucamAVer       = WLS3+  , _ucamAStatus    = Ok200+  , _ucamAMsg       = Nothing+  , _ucamAIssue     = [utcIso8601ms| 2017-05-15T17:23:11 |]+  , _ucamAId        = "oANAuhC9fZmMlZUPIm53y5vn"+  , _ucamAUrl       = "http://localhost:3000/foo/query"+  , _ucamAPrincipal = Just "test0244"+  , _ucamAPtags     = [Current]+  , _ucamAAuth      = Nothing+  , _ucamASso       = Just [Pwd]+  , _ucamALife      = Just (timePeriodFromSeconds 30380)+  , _ucamAParams    = Just "This is 100% of the data! And it’s really quite cool"+  }++exampleSignedResponseText :: Text+exampleSignedResponseText = exampleResponseText <> [here|+  !901!RzC9KZWALCSeK0n9885X4zzemHizuj8K.NOpt.n1hfRCTE2ZBgvJ-fBvT-PaL80cSFGpyCJgt9LvM4-peJzcidoKC6zhBEvG0QnlqWTLsphbIA0JmBRiOoeqyLYRVGwDEdLdacdsQRM.u7bik.enhbuN1-aIQCOdB5MutxtYiu4_+|]++exampleResponseText :: Text+exampleResponseText = [here|+  3!200!!20170515T172311Z!oANAuhC9fZmMlZUPIm53y5vn!http://localhost:3000/foo/query!test0244!current!!pwd!30380!IlRoaXMgaXMgMTAwJSBvZiB0aGUgZGF0YSEgQW5kIGl04oCZcyByZWFsbHkgcXVpdGUgY29vbCI_+|]
+ ucam-webauth.cabal view
@@ -0,0 +1,110 @@+cabal-version: 2.2+name: ucam-webauth+version: 0.1.0.0+license: (BSD-3-Clause OR Apache-2.0)+license-file: LICENSE+copyright: 2018 David Baynard+maintainer: David Baynard <ucamwebauth@baynard.me>+author: David Baynard <ucamwebauth@baynard.me>+homepage: https://github.com/dbaynard/UcamWebauth#readme+bug-reports: https://github.com/dbaynard/UcamWebauth/issues+synopsis: The Ucam-Webauth protocol, used by Raven+description:+    An implementation of the Ucam-Webauth protocol, as used by the University of Cambridge’s+    Raven authentication service.+category: Web+build-type: Simple+extra-source-files:+    Changelog.md+    README.md++source-repository head+    type: git+    location: https://github.com/dbaynard/UcamWebauth++flag dev+    description:+        Compile for development+    default: False+    manual: True++library+    exposed-modules:+        UcamWebauth+        UcamWebauth.Internal+        UcamWebauth.Parser+        UcamWebauth.WLS+    hs-source-dirs: src+    default-language: Haskell2010+    ghc-options: -Wall -Wincomplete-uni-patterns+                 -Wincomplete-record-updates -Wcompat -Wnoncanonical-monad-instances+                 -Wnoncanonical-monadfail-instances+    build-depends:+        aeson >=1.2 && <1.5,+        attoparsec >=0.13.2.2 && <0.14,+        base >=4.11.0.0 && <4.12,+        bytestring >=0.10.8.2 && <0.11,+        containers >=0.5.11.0 && <0.6,+        cryptonite ==0.25.*,+        errors >=2.3.0 && <2.4,+        http-api-data >=0.3.8.1 && <0.4,+        http-types >=0.12.1 && <0.13,+        microlens >=0.4.9.1 && <0.5,+        microlens-mtl >=0.1.11.1 && <0.2,+        mtl >=2.2.2 && <2.3,+        parser-combinators >=1.0.0 && <1.1,+        pem >=0.2.4 && <0.3,+        text >=0.11 && <1.2.3.0 || >=1.2.3.1 && <1.3,+        time >=1.9.2 && <1.10,+        ucam-webauth-types >=0.1.0.0 && <0.2,+        x509 >=1.7.4 && <1.8+    +    if flag(dev)+        ghc-options: -ddump-minimal-imports++test-suite test+    type: exitcode-stdio-1.0+    main-is: Spec.hs+    build-tool-depends: hspec-discover:hspec-discover -any+    hs-source-dirs: src test+    other-modules:+        UcamWebauth+        UcamWebauth.Internal+        UcamWebauth.Parser+        UcamWebauth.WLS+        UcamWebauthSpec+        Paths_ucam_webauth+    autogen-modules:+        Paths_ucam_webauth+    default-language: Haskell2010+    ghc-options: -Wall -Wincomplete-uni-patterns+                 -Wincomplete-record-updates -Wcompat -Wnoncanonical-monad-instances+                 -Wnoncanonical-monadfail-instances -Wall+    build-depends:+        QuickCheck >=2.12.4 && <2.13,+        aeson >=1.2 && <1.5,+        attoparsec >=0.13.2.2 && <0.14,+        base >=4.11.0.0 && <4.12,+        bytestring >=0.10.8.2 && <0.11,+        containers >=0.5.11.0 && <0.6,+        cryptonite ==0.25.*,+        errors >=2.3.0 && <2.4,+        generic-random >=1.2.0.0 && <1.3,+        here >=1.2.13 && <1.3,+        hspec >=2.0.0 && <2.6,+        http-api-data >=0.3.8.1 && <0.4,+        http-types >=0.12.1 && <0.13,+        microlens >=0.4.9.1 && <0.5,+        microlens-mtl >=0.1.11.1 && <0.2,+        mtl >=2.2.2 && <2.3,+        parser-combinators >=1.0.0 && <1.1,+        pem >=0.2.4 && <0.3,+        quickcheck-instances >=0.3.19 && <0.4,+        text >=0.11 && <1.2.3.0 || >=1.2.3.1 && <1.3,+        time >=1.9.2 && <1.10,+        time-qq >=0.0.1.0 && <0.1,+        ucam-webauth-types >=0.1.0.0 && <0.2,+        x509 >=1.7.4 && <1.8+    +    if flag(dev)+        ghc-options: -ddump-minimal-imports