diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/examples/scotty/Main.hs b/examples/scotty/Main.hs
new file mode 100644
--- /dev/null
+++ b/examples/scotty/Main.hs
@@ -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 ""
+
diff --git a/oidc-client.cabal b/oidc-client.cabal
new file mode 100644
--- /dev/null
+++ b/oidc-client.cabal
@@ -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
diff --git a/src/Web/OIDC/Client.hs b/src/Web/OIDC/Client.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/OIDC/Client.hs
@@ -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
+
diff --git a/src/Web/OIDC/Client/Internal.hs b/src/Web/OIDC/Client/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/OIDC/Client/Internal.hs
@@ -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
diff --git a/src/Web/OIDC/Discovery.hs b/src/Web/OIDC/Discovery.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/OIDC/Discovery.hs
@@ -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
diff --git a/src/Web/OIDC/Discovery/Issuers.hs b/src/Web/OIDC/Discovery/Issuers.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/OIDC/Discovery/Issuers.hs
@@ -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"
diff --git a/src/Web/OIDC/Types.hs b/src/Web/OIDC/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/OIDC/Types.hs
@@ -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
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,9 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Test.Hspec
+
+main :: IO ()
+main = hspec $
+    describe "dummy test" $
+        it "dummy" $
+            True `shouldBe` True
