packages feed

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