oidc-client (empty) → 0.1.0.0
raw patch · 10 files changed
+703/−0 lines, 10 filesdep +aesondep +attoparsecdep +basesetup-changed
Dependencies added: aeson, attoparsec, base, base32-bytestring, blaze-html, bytestring, containers, cprng-aes, crypto-random, exceptions, hspec, http-client, http-client-tls, http-types, jose-jwt, network, network-uri, oidc-client, scotty, scotty-cookie, text, time, tls, transformers, wai-extra
Files
- LICENSE +20/−0
- Setup.hs +2/−0
- examples/scotty/Main.hs +99/−0
- oidc-client.cabal +91/−0
- src/Web/OIDC/Client.hs +233/−0
- src/Web/OIDC/Client/Internal.hs +30/−0
- src/Web/OIDC/Discovery.hs +47/−0
- src/Web/OIDC/Discovery/Issuers.hs +14/−0
- src/Web/OIDC/Types.hs +158/−0
- test/Spec.hs +9/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2015 Sho Kuroda++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/scotty/Main.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main where++import Control.Applicative ((<$>))+import Control.Monad.IO.Class (liftIO)+import Crypto.Random.AESCtr (makeSystem)+import Crypto.Random.API (CPRG, cprgGenBytes)+import Data.ByteString (ByteString)+import Data.ByteString.Base32 (encode)+import qualified Data.ByteString.Char8 as B+import Data.IORef (IORef, newIORef, atomicModifyIORef', readIORef)+import Data.Map (Map)+import qualified Data.Map as M+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8)+import Data.Text.Lazy (pack)+import Data.Tuple (swap)+import Network.HTTP.Client (newManager, Manager)+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types (badRequest400)+import Network.Wai.Middleware.RequestLogger (logStdoutDev)+import System.Environment (getEnv)+import Text.Blaze.Html.Renderer.Text (renderHtml)+import Text.Blaze.Html5 ((!))+import qualified Text.Blaze.Html5 as H+import qualified Text.Blaze.Html5.Attributes as A+import qualified Web.OIDC.Client as O+import qualified Web.OIDC.Discovery as O+import Web.Scotty (scotty, middleware, get, param, post, redirect, html, status, text)+import Web.Scotty.Cookie (setSimpleCookie, getCookie)++type SessionStateMap = Map Text O.State++redirectUri :: ByteString+redirectUri = "http://localhost:3000/callback"++main :: IO ()+main = do+ clientId <- B.pack <$> getEnv "OPENID_CLIENT_ID"+ clientSecret <- B.pack <$> getEnv "OPENID_CLIENT_SECRET"++ cprg <- makeSystem >>= newIORef+ ssm <- newIORef M.empty+ mgr <- newManager tlsManagerSettings+ prov <- O.discover O.google mgr+ let oidc = O.setCredentials clientId clientSecret redirectUri $ O.setProvider prov $ O.newOIDC cprg++ run oidc cprg ssm mgr++run :: CPRG g => O.OIDC -> IORef g -> IORef SessionStateMap -> Manager -> IO ()+run oidc cprg ssm mgr = scotty 3000 $ do+ middleware logStdoutDev++ get "/login" $+ blaze $ do+ H.h1 "Login"+ H.form ! A.method "post" ! A.action "/login" $+ H.button ! A.type_ "submit" $ "login"++ post "/login" $ do+ state <- genState+ loc <- liftIO $ O.getAuthenticationRequestUrl oidc [O.Email] (Just state) []+ sid <- genSessionId+ saveState sid state+ setSimpleCookie "test-session" sid+ redirect $ pack . show $ loc++ get "/callback" $ do+ code :: O.Code <- param "code"+ state :: O.State <- param "state"+ cookie <- getCookie "test-session"+ case cookie of+ Nothing -> status401+ Just sid -> do+ sst <- getStateBy sid+ if state == sst+ then do+ tokens <- liftIO $ O.requestTokens oidc code mgr+ blaze $ do+ H.h1 "Result"+ H.pre . H.toHtml . show . O.claims . O.idToken $ tokens+ else status401++ where+ blaze = html . renderHtml+ status401 = status badRequest400 >> text "cookie not found"++ gen = encode <$> atomicModifyIORef' cprg (swap . cprgGenBytes 64)+ genSessionId = liftIO $ decodeUtf8 <$> gen+ genState = liftIO gen+ saveState sid st = liftIO $ atomicModifyIORef' ssm $ \m -> (M.insert sid st m, ())+ getStateBy sid = liftIO $ do+ m <- readIORef ssm+ case M.lookup sid m of+ Just st -> return st+ Nothing -> return ""+
+ oidc-client.cabal view
@@ -0,0 +1,91 @@+name: oidc-client+version: 0.1.0.0+synopsis: OpenID Connect 1.0 library for RP+homepage: https://github.com/krdlab/haskell-oidc-client+stability: experimental+license: MIT+license-file: LICENSE+author: Sho Kuroda+maintainer: Sho Kuroda <krdlab@gmail.com>+copyright: (c) 2015 Sho Kuroda+category: Web+build-type: Simple+cabal-version: >=1.10+description:+ This package supports implementing of an OpenID Connect 1.0 Relying Party.+ .+ Examples: <https://github.com/krdlab/haskell-oidc-client/tree/master/examples>++source-repository head+ type: git+ location: git://github.com/krdlab/haskell-oidc-client.git++flag network-uri+ default: True++flag build-examples+ default: False++library+ hs-source-dirs: src+ default-language: Haskell2010+ exposed-modules:+ Web.OIDC.Client+ , Web.OIDC.Discovery+ , Web.OIDC.Discovery.Issuers+ other-modules:+ Web.OIDC.Client.Internal+ , Web.OIDC.Types+ build-depends:+ base >=4.7 && <5+ , bytestring >=0.10 && <0.11+ , text >=1.2 && <1.3+ , aeson >=0.9+ , attoparsec >=0.12+ , exceptions+ , http-client+ , tls >=1.3.2+ , http-client-tls+ , jose-jwt >=0.6.2+ , time+ , crypto-random+ if flag(network-uri)+ build-depends: network-uri >=2.6, network >=2.6+ else+ build-depends: network-uri <2.6, network <2.6++test-suite oidc-client-spec+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ default-language: Haskell2010+ ghc-options: -Wall+ main-is: Spec.hs+ build-depends:+ base+ , hspec+ , oidc-client++executable scotty-example+ main-is: Main.hs+ hs-source-dirs: examples/scotty/+ if flag(build-examples)+ build-depends:+ base >=4.7 && <5+ , oidc-client+ , bytestring+ , text+ , containers+ , transformers+ , wai-extra+ , scotty+ , scotty-cookie+ , blaze-html+ , cprng-aes+ , crypto-random+ , base32-bytestring+ , http-types+ , http-client+ , tls >=1.3.2+ , http-client-tls+ else+ buildable: False
+ src/Web/OIDC/Client.hs view
@@ -0,0 +1,233 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GADTs #-}+{-|+Module: Web.OIDC.Client+Maintainer: krdlab@gmail.com+Stability: experimental+-}+module Web.OIDC.Client+ (+ -- * Client Obtains ID Token and Access Token+ OIDC+ , newOIDC+ , newOIDC'+ , setProvider+ , setCredentials+ , getAuthenticationRequestUrl+ , requestTokens++ -- * Types+ , Provider+ , Scope, ScopeValue(..)+ , Code, State+ , Parameters+ , Tokens(..), IdToken(..), IdTokenClaims(..)++ -- * Exception+ , OpenIdException(..)++ -- * Re-exports+ , module Jose.Jwt+ ) where++import Control.Applicative ((<$>))+import Control.Monad (unless)+import Control.Monad.Catch (MonadThrow, throwM, MonadCatch, catch)+import Crypto.Random (CPRG)+import Data.Aeson (decode)+import qualified Data.ByteString.Char8 as B+import Data.ByteString (ByteString)+import Data.IORef (IORef, atomicModifyIORef')+import Data.List (nub)+import Data.Maybe (fromMaybe, fromJust)+import Data.Text (pack)+import Data.Text.Encoding (decodeUtf8)+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Tuple (swap)+import qualified Jose.Jwk as Jwk+import Jose.Jwt (Jwt)+import qualified Jose.Jwt as Jwt+import Network.HTTP.Client (parseUrl, getUri, setQueryString, applyBasicAuth, urlEncodedBody, Request(..), Manager, httpLbs, responseBody)+import Network.URI (URI)+import Prelude hiding (exp)++import qualified Web.OIDC.Client.Internal as I+import qualified Web.OIDC.Types as OT+import Web.OIDC.Types (Provider, Scope, ScopeValue(..), Code, State, Parameters, Tokens(..), IdToken(..), IdTokenClaims(..), OpenIdException(..))++-- | This data type represents information needed in the OpenID flow.+data OIDC = OIDC+ { authorizationSeverUrl :: String+ , tokenEndpoint :: String+ , clientId :: ByteString+ , clientSecret :: ByteString+ , redirectUri :: ByteString+ , provider :: Provider+ , cprgRef :: CPRGRef+ }++data CPRGRef where+ Ref :: (CPRG g) => IORef g -> CPRGRef+ NoRef :: CPRGRef++def :: OIDC+def = OIDC+ { authorizationSeverUrl = error "You must specify authorizationSeverUrl"+ , tokenEndpoint = error "You must specify tokenEndpoint"+ , clientId = error "You must specify clientId"+ , clientSecret = error "You must specify clientSecret"+ , redirectUri = error "You must specify redirectUri"+ , provider = error "You must specify provider"+ , cprgRef = NoRef+ }++-- | Create OIDC.+--+-- First argument is used in a token decoding on ID Token Validation.+newOIDC :: CPRG g => IORef g -> OIDC+newOIDC ref = def { cprgRef = Ref ref }++newOIDC' :: OIDC+newOIDC' = def++setProvider+ :: Provider -- ^ OP's information (obtain by 'discover')+ -> OIDC+ -> OIDC+setProvider p oidc =+ oidc { authorizationSeverUrl = OT.authorizationEndpoint . OT.configuration $ p+ , tokenEndpoint = OT.tokenEndpoint . OT.configuration $ p+ , provider = p+ }++setCredentials+ :: ByteString -- ^ client ID+ -> ByteString -- ^ client secret+ -> ByteString -- ^ redirect URI+ -> OIDC+ -> OIDC+setCredentials cid secret redirect oidc =+ oidc { clientId = cid+ , clientSecret = secret+ , redirectUri = redirect+ }++getAuthenticationRequestUrl :: (MonadThrow m, MonadCatch m) => OIDC -> Scope -> Maybe State -> Parameters -> m URI+getAuthenticationRequestUrl oidc scope state params = do+ req <- parseUrl endpoint `catch` OT.rethrow+ return $ getUri $ setQueryString query req+ where+ endpoint = authorizationSeverUrl oidc+ query = requireds ++ state' ++ params+ requireds =+ [ ("response_type", Just "code")+ , ("client_id", Just $ clientId oidc)+ , ("redirect_uri", Just $ redirectUri oidc)+ , ("scope", Just $ B.pack . unwords . nub . map show $ OpenId:scope)+ ]+ state' =+ case state of+ Just _ -> [("state", state)]+ Nothing -> []++-- TODO: error response++-- | Request and obtain valid tokens.+--+-- This function requests ID Token and Access Token to a OP's token endpoint, and validates the received ID Token.+-- Returned value is a valid tokens.+requestTokens :: OIDC -> Code -> Manager -> IO Tokens+requestTokens oidc code manager = do+ json <- getTokensJson `catch` OT.rethrow+ case decode json of+ Just ts -> validate oidc ts+ Nothing -> error "failed to decode tokens json" -- TODO+ where+ getTokensJson = do+ req <- parseUrl endpoint+ let req' = applyBasicAuth cid sec $ urlEncodedBody body $ req { method = "POST" }+ res <- httpLbs req' manager+ return $ responseBody res+ endpoint = tokenEndpoint oidc+ cid = clientId oidc+ sec = clientSecret oidc+ redirect = redirectUri oidc+ body =+ [ ("grant_type", "authorization_code")+ , ("code", code)+ , ("redirect_uri", redirect)+ ]++validate :: OIDC -> I.TokensResponse -> IO Tokens+validate oidc tres = do+ let jwt' = I.idToken tres+ claims' <- validateIdToken oidc jwt'+ let tokens = Tokens {+ accessToken = I.accessToken tres+ , tokenType = I.tokenType tres+ , idToken = IdToken { claims = OT.toIdTokenClaims claims', jwt = jwt' }+ , expiresIn = I.expiresIn tres+ , refreshToken = I.refreshToken tres+ }+ return tokens++validateIdToken :: OIDC -> Jwt -> IO Jwt.JwtClaims+validateIdToken oidc jwt' = do+ case cprgRef oidc of+ Ref crpg -> do+ decoded <- case Jwt.decodeClaims (Jwt.unJwt jwt') of+ Left cause -> throwM $ JwtExceptoin cause+ Right (jwth, _) ->+ case jwth of+ (Jwt.JwsH jws) -> do+ let kid = Jwt.jwsKid jws+ alg = Jwt.jwsAlg jws+ jwk = getJwk kid (OT.jwkSet . provider $ oidc)+ atomicModifyIORef' crpg $ \g -> swap (Jwt.decode g [jwk] (Just $ Jwt.JwsEncoding alg) (Jwt.unJwt jwt'))+ (Jwt.JweH jwe) -> do+ let kid = Jwt.jweKid jwe+ alg = Jwt.jweAlg jwe+ enc = Jwt.jweEnc jwe+ jwk = getJwk kid (OT.jwkSet . provider $ oidc)+ atomicModifyIORef' crpg $ \g -> swap (Jwt.decode g [jwk] (Just $ Jwt.JweEncoding alg enc) (Jwt.unJwt jwt'))+ _ -> error "not supported"+ case decoded of+ Left err -> throwM $ JwtExceptoin err+ Right _ -> return ()+ NoRef -> error "not implemented" -- TODO: request tokeninfo++ claims' <- getClaims++ unless (getIss claims' == issuer')+ $ throwM $ ValidationException "issuer"++ unless (clientId' `elem` getAud claims')+ $ throwM $ ValidationException "audience"++ expire <- getExp claims'+ now <- getCurrentTime+ unless (now < expire)+ $ throwM $ ValidationException "expire"++ return claims'+ where+ getJwk kid jwks = head $ case kid of+ Just keyId -> filter (eq keyId) jwks+ Nothing -> jwks+ where+ eq e jwk = fromMaybe False ((==) e <$> Jwk.jwkId jwk)++ getClaims = case Jwt.decodeClaims (Jwt.unJwt jwt') of+ Right (_, c) -> return c+ Left cause -> throwM $ JwtExceptoin cause++ issuer' = pack . OT.issuer . OT.configuration . provider $ oidc+ clientId' = decodeUtf8 . clientId $ oidc++ getIss c = fromJust (Jwt.jwtIss c)+ getAud c = fromJust (Jwt.jwtAud c)+ getExp c = case Jwt.jwtExp c of+ Just e -> return e+ Nothing -> throwM $ ValidationException "exp claim was not found"+ getCurrentTime = Jwt.IntDate <$> getPOSIXTime+
+ src/Web/OIDC/Client/Internal.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}+{-|+Module: Web.OIDC.Client.Internal+Maintainer: krdlab@gmail.com+Stability: experimental+-}+module Web.OIDC.Client.Internal where++import Control.Applicative ((<$>), (<*>))+import Control.Monad (mzero)+import Data.Aeson (FromJSON, parseJSON, Value(..), (.:), (.:?))+import Jose.Jwt (Jwt)++data TokensResponse = TokensResponse+ { accessToken :: !String+ , tokenType :: !String+ , idToken :: !Jwt+ , expiresIn :: !(Maybe Integer)+ , refreshToken :: !(Maybe String)+ }+ deriving (Show, Eq)++instance FromJSON TokensResponse where+ parseJSON (Object o) = TokensResponse+ <$> o .: "access_token"+ <*> o .: "token_type"+ <*> o .: "id_token"+ <*> o .:? "expires_in"+ <*> o .:? "refresh_token"+ parseJSON _ = mzero
+ src/Web/OIDC/Discovery.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE OverloadedStrings #-}+{-|+Module: Web.OIDC.Discovery+Maintainer: krdlab@gmail.com+Stability: experimental+-}+module Web.OIDC.Discovery+ ( discover+ , IssuerLocation+ , Provider+ , module I+ ) where++import Control.Applicative ((<$>))+import Control.Monad.Catch (throwM, catch)+import Data.Aeson (decode)+import Data.Maybe (fromMaybe)+import Data.Monoid (mempty)+import qualified Jose.Jwk as Jwk+import Network.HTTP.Client (Manager, parseUrl, httpLbs, responseBody)+import Web.OIDC.Types+import Web.OIDC.Discovery.Issuers as I++-- | This function obtains OpenID Provider configuration and JWK set.+discover+ :: IssuerLocation -- ^ OpenID Provider's Issuer location+ -> Manager+ -> IO Provider+discover location manager = do+ conf <- getConfiguration `catch` rethrow+ case conf of+ Just c -> Provider c . jwks <$> getJwkSetJson (jwksUri c) `catch` rethrow+ Nothing -> throwM $ DiscoveryException "failed to decode configuration"+ where+ getConfiguration = do+ req <- parseUrl (location ++ "/.well-known/openid-configuration")+ res <- httpLbs req manager+ return $ decode $ responseBody res+ getJwkSetJson url = do+ req <- parseUrl url+ res <- httpLbs req manager+ return $ responseBody res+ jwks j = fromMaybe single (Jwk.keys <$> decode j)+ where+ single = case decode j of+ Just k -> return k+ Nothing -> mempty
+ src/Web/OIDC/Discovery/Issuers.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE OverloadedStrings #-}+{-|+Module: Web.OIDC.Discovery.Issuers+Maintainer: krdlab@gmail.com+Stability: experimental+-}+module Web.OIDC.Discovery.Issuers+ ( google+ ) where++import Web.OIDC.Types (IssuerLocation)++google :: IssuerLocation+google = "https://accounts.google.com"
+ src/Web/OIDC/Types.hs view
@@ -0,0 +1,158 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-|+Module: Web.OIDC.Types+Maintainer: krdlab@gmail.com+Stability: experimental+-}+module Web.OIDC.Types where++import Control.Applicative ((<$>), (<*>), (<*), (*>), (<|>))+import Control.Exception (Exception)+import Control.Monad (mzero)+import Control.Monad.Catch (throwM, MonadCatch)+import Data.Aeson (FromJSON, parseJSON, withText, Value(..), (.:))+import Data.Attoparsec.Text (parseOnly, endOfInput, string)+import Data.ByteString (ByteString)+import Data.List (isPrefixOf)+import Data.Maybe (fromJust)+import Data.Text (unpack, pack)+import Data.Typeable (Typeable)+import Jose.Jwk (Jwk)+import Jose.Jwt (Jwt, JwtClaims(..), JwtError, IntDate)+import Network.HTTP.Client (HttpException)+import Prelude hiding (exp)++type IssuerLocation = String++-- | An OpenID Provider information+data Provider = Provider { configuration :: Configuration, jwkSet :: [Jwk] }++-- | An OpenID Provider Configuration+data Configuration = Configuration+ { issuer :: IssuerLocation+ , authorizationEndpoint :: String+ , tokenEndpoint :: String+ , userinfoEndpoint :: String+ , revocationEndpoint :: String+ , jwksUri :: String+ , responseTypesSupported :: [String]+ , subjectTypesSupported :: [String]+ , idTokenSigningAlgValuesSupported :: [String]+ , scopesSupported :: [ScopeValue]+ , tokenEndpointAuthMethodsSupported :: [String]+ , claimsSupported :: [String]+ }+ deriving (Show, Eq)++instance FromJSON Configuration where+ parseJSON (Object o) = Configuration+ <$> o .: "issuer"+ <*> o .: "authorization_endpoint"+ <*> o .: "token_endpoint"+ <*> o .: "userinfo_endpoint"+ <*> o .: "revocation_endpoint"+ <*> o .: "jwks_uri"+ <*> o .: "response_types_supported"+ <*> o .: "subject_types_supported"+ <*> o .: "id_token_signing_alg_values_supported"+ <*> o .: "scopes_supported"+ <*> o .: "token_endpoint_auth_methods_supported"+ <*> o .: "claims_supported"+ parseJSON _ = mzero++data ScopeValue =+ OpenId+ | Profile+ | Email+ | Address+ | Phone+ | OfflineAccess+ deriving (Eq)++instance Show ScopeValue where+ show OpenId = "openid"+ show Profile = "profile"+ show Email = "email"+ show Address = "address"+ show Phone = "phone"+ show OfflineAccess = "offline_access"++instance Read ScopeValue where+ readsPrec _ s+ | "openid" `isPrefixOf` s = [(OpenId, drop 6 s)]+ | "profile" `isPrefixOf` s = [(Profile, drop 7 s)]+ | "email" `isPrefixOf` s = [(Email, drop 5 s)]+ | "address" `isPrefixOf` s = [(Address, drop 7 s)]+ | "phone" `isPrefixOf` s = [(Phone, drop 5 s)]+ | "offline_access" `isPrefixOf` s = [(OfflineAccess, drop 14 s)]+ | otherwise = []++instance FromJSON ScopeValue where+ parseJSON = withText "ScopeValue" (run parser)+ where+ run p t =+ case parseOnly (p <* endOfInput) t of+ Right r -> return r+ Left err -> fail $ "could not parse scope value: " ++ err+ parser = parser' OpenId+ <|> parser' Profile+ <|> parser' Email+ <|> parser' Address+ <|> parser' Phone+ <|> parser' OfflineAccess+ parser' v = string (pack . show $ v) *> return v++type Scope = [ScopeValue]++type State = ByteString++type Parameters = [(ByteString, Maybe ByteString)]++type Code = ByteString++data Tokens = Tokens+ { accessToken :: String+ , tokenType :: String+ , idToken :: IdToken+ , expiresIn :: Maybe Integer+ , refreshToken :: Maybe String+ }+ deriving (Show, Eq)++data IdToken = IdToken+ { claims :: IdTokenClaims+ , jwt :: Jwt+ }+ deriving (Show, Eq)++data IdTokenClaims = IdTokenClaims+ { iss :: String+ , sub :: String+ , aud :: [String]+ , exp :: IntDate+ , iat :: IntDate+ -- TODO: optional+ }+ deriving (Show, Eq)++toIdTokenClaims :: JwtClaims -> IdTokenClaims+toIdTokenClaims c = IdTokenClaims+ { iss = unpack $ fromJust (jwtIss c)+ , sub = unpack $ fromJust (jwtSub c)+ , aud = map unpack $ fromJust (jwtAud c)+ , exp = fromJust (jwtExp c)+ , iat = fromJust (jwtIat c)+ }++data OpenIdException =+ DiscoveryException String+ | InternalHttpException HttpException+ | JwtExceptoin JwtError+ | ValidationException String+ deriving (Show, Typeable)++instance Exception OpenIdException++rethrow :: (MonadCatch m) => HttpException -> m a+rethrow = throwM . InternalHttpException
+ test/Spec.hs view
@@ -0,0 +1,9 @@+{-# LANGUAGE OverloadedStrings #-}++import Test.Hspec++main :: IO ()+main = hspec $+ describe "dummy test" $+ it "dummy" $+ True `shouldBe` True