packages feed

yesod-auth-oidc 0.1.3 → 0.1.4

raw patch · 7 files changed

+1/−576 lines, 7 filesdep −blaze-htmldep −brochdep −bytestringdep ~aesondep ~basedep ~base64-bytestringPVP ok

version bump matches the API change (PVP)

Dependencies removed: blaze-html, broch, bytestring, classy-prelude, directory, email-validate, fast-logger, hspec, http-conduit, http-types, lens, lens-regex-pcre, memory, monad-logger, persistent, persistent-sqlite, postgresql-simple, reroute, resource-pool, sqlite-simple, wai-app-static, wai-extra, warp, yesod, yesod-persistent, yesod-test

Dependency ranges changed: aeson, base, base64-bytestring, classy-prelude-yesod, containers, cryptonite, http-client, jose-jwt, oidc-client, shakespeare, text, time, unordered-containers, yesod-auth, yesod-core, yesod-form

API changes (from Hackage documentation)

Files

− test/ExampleApp.hs
@@ -1,165 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE UndecidableInstances #-}--module ExampleApp where--import ClassyPrelude.Yesod-import qualified Data.Aeson as J-import qualified Data.Aeson.KeyMap as HM-import qualified Data.Text as T-import Database.Persist.Sql-import ExampleProviderOpts-import Yesod.Auth-import Yesod.Auth.OIDC-import Yesod.Core.Types (Logger)--data App = App-  { appLogger   :: Logger-  , appHost :: Text-  , appConnPool :: ConnectionPool-  , appHttpManager :: Manager-  , appBrochClientId :: ClientId-  }---share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|-OidcConfig-  clientId Text-  issuer Text-  Primary clientId issuer-  deriving Show--OidcDomain-  domain Text-  clientId Text-  issuer Text-  Primary domain-  Foreign OidcConfig oidc_domain_oidc_config_fk clientId issuer-  deriving Show---- | Users in this example app come exclusively from OIDC--- Providers. Users are created automatically on successful--- authentication-User-  -- | We specifically do not make the email unique per-user, in-  -- accordance with the OIDC spec-  email Email--  -- | The unique issuer + subject pair could be made null-able in an-  -- app that also allows non-OIDC registrations-  issuer Text-  subject Text--  UniqueUser issuer subject-  deriving Show-|]--mkYesod "App" [parseRoutes|-/ HomeR GET-/auth AuthR Auth getAuth-/convenient-test-token CsrfTokenR GET-/protected/resource ProtectedResourceR GET-|]--instance Yesod App where-instance YesodAuth App where-  type AuthId App = Key User-  loginDest _ = HomeR-  logoutDest _ = HomeR-  authPlugins _ = [ authOIDC ]-  getAuthId = return . fromPathPiece . credsIdent-  maybeAuthId = defaultMaybeAuthId---getHomeR :: Handler ()-getHomeR = pure ()--getCsrfTokenR :: Handler Text-getCsrfTokenR = do-  setCsrfCookie-  reqToken <$> getRequest >>= \case-    Nothing -> error "app unexpectedly started without session storage"-    Just t -> pure t--getProtectedResourceR :: Handler J.Value-getProtectedResourceR = do-  uid <- requireAuthId-  pure $ J.String $ tshow uid---instance YesodPersist App where-  type YesodPersistBackend App = SqlBackend--  runDB :: SqlPersistT Handler a -> Handler a-  runDB db = getsYesod appConnPool >>= runSqlPool db---addProvider ::-  ( PersistStoreWrite (YesodPersistBackend (HandlerSite f))-  , YesodPersist (HandlerSite f)-  , MonadHandler f-  , BaseBackend (YesodPersistBackend (HandlerSite f)) ~ SqlBackend)-  => OidcConfig -> [Text] -> f ()-addProvider cfg domains = liftHandler . runDB $ do-  insert_ cfg-  insertMany_ $ flip map domains $ \domain ->-    OidcDomain { oidcDomainDomain = domain-               , oidcDomainClientId = oidcConfigClientId cfg-               , oidcDomainIssuer = oidcConfigIssuer cfg-               }--instance HasHttpManager App where-  getHttpManager = appHttpManager--instance YesodAuthOIDC App where-  getHttpManagerForOidc = Right <$> getsYesod appHttpManager-  getProviderConfig loginHint = do-    let domain = snd $ T.breakOnEnd "@" loginHint-    mConfig <- liftHandler . runDB $ get $ OidcDomainKey domain-    case mConfig of-      Just cfg -> pure ( Right $ oidcDomainIssuer cfg-                       , ClientId $ oidcDomainClientId cfg)-      Nothing -> error "No config for this domain"-  getClientSecret clientId _ = pure $ fakeClientSecret clientId-  onSuccessfulAuthentication _originalLoginHint _clientId _provider tokens mUserInfo = do-    let idTok = idToken tokens-    -- The 'email' is sometimes in the ID Token, and sometimes in the-    -- UserInfo Response. Both are JSON objects.-    let emailVal =-          HM.lookup "email" (otherClaims idTok)-          <|> (mUserInfo >>= HM.lookup "email")-        emailAddr = case emailVal of-          Just (J.String em) -> em-          _ -> error "Spec-conforming email missing from ID token and/or User Info Response"-    mUser <- liftHandler . runDB $ getBy $ UniqueUser (iss idTok) (sub idTok)-    case mUser of-      Just (Entity uid u) -> do-        when (userEmail u /= emailAddr) $-          liftHandler . runDB $ update uid [ UserEmail =. emailAddr ]-        pure $ toPathPiece uid-      Nothing -> do-        fmap toPathPiece $ liftHandler . runDB $ insert $ User-          { userEmail = emailAddr-          , userIssuer = iss idTok-          , userSubject = sub idTok-          }--instance YesodAuthPersist App--instance RenderMessage App FormMessage where-  renderMessage _ _ = defaultFormMessage
− test/ExampleProvider.hs
@@ -1,131 +0,0 @@-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}-{--The code in this module is modified from that found in the-broch-server/broch.hs file in the 'broch' library, which is under the-following copyright and license:--------------------------Copyright (c) 2014, Luke Taylor--All rights reserved.--Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions-are met:--1. Redistributions of source code must retain the above copyright-   notice, this list of conditions and the following disclaimer.-2. Redistributions in binary form must reproduce the above copyright-   notice, this list of conditions and the following disclaimer in the-   documentation and/or other materials provided with the distribution.-3. Neither the name of the author nor the names of his contributors-   may be used to endorse or promote products derived from this software-   without specific prior written permission.--THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED-TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR-PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR-CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,-EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,-PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;-OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,-WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR-OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF-ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.---}-module ExampleProvider where--import ClassyPrelude-import ExampleProviderOpts--import Broch.Model-import Broch.Server-import Broch.Server.Config-import Broch.Server.Internal-import Broch.Server.Session (defaultKey, defaultLoadSession)-import qualified Broch.SQLite as BS-import Broch.URI-import Crypto.KDF.BCrypt (hashPassword)-import Data.Aeson-import qualified Data.Map as M-import Data.Pool (createPool, withResource)-import qualified Database.SQLite.Simple as SQLite-import Network.Wai.Application.Static (defaultWebAppSettings, staticApp)-import Network.Wai.Handler.Warp (run)-import Network.Wai.Middleware.RequestLogger (logStdoutDev)-import System.Directory-import qualified Web.Routing.Combinators as R-import qualified Web.Routing.SafeRouting as R-import Yesod.Auth.OIDC (ClientId(..), ClientSecret(..))---- Adapted from Broch.SQLite-toJSONField :: ToJSON a => Maybe a -> SQLite.SQLData-toJSONField = maybe SQLite.SQLNull (SQLite.SQLText . decodeUtf8 . toStrict . encode)---- Adapted from Broch.SQLite-insertClient :: SQLite.Connection -> Client -> IO ()-insertClient conn Client{..} =-    void $ SQLite.execute conn "INSERT INTO oauth2_client (id, secret, redirect_uri, allowed_scope, authorized_grant_types, access_token_validity, refresh_token_validity, auth_method, auth_alg, keys_uri, keys, id_token_algs, user_info_algs, request_obj_algs, sector_identifier, auto_approve) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" ((clientId, clientSecret, redirectURIs, allowedScope, authorizedGrantTypes, accessTokenValidity, refreshTokenValidity, clientAuthMethodName tokenEndpointAuthMethod, fmap tshow tokenEndpointAuthAlg, clientKeysUri) SQLite.:. (toJSONField clientKeys, toJSONField idTokenAlgs, toJSONField userInfoAlgs, toJSONField requestObjAlgs, sectorIdentifier, autoapprove))--initDB :: BrochOptions -> SQLite.Connection -> IO ()-initDB BrochOptions{..} c = do-  void $ flip M.traverseWithKey boClients $ \(ClientId clientId) (ClientSecret secret, host, callback) ->-    insertClient c $ Client-      { clientId = clientId-      , clientSecret = Just secret-      , authorizedGrantTypes = [AuthorizationCode]-      , redirectURIs = case parseURI callback of-          Right url -> [url]-          Left e -> error $ "Can't initialise tests due to bad callback URL: " <> show e-      , accessTokenValidity = 3600-      , refreshTokenValidity = 7200-      , allowedScope = [OpenID, Profile, Email]-      , autoapprove = False-      , tokenEndpointAuthMethod = ClientSecretPost-      , tokenEndpointAuthAlg    = Nothing -- :: Maybe JwsAlg-      , clientKeysUri  = Nothing -- :: Maybe Text-      , clientKeys     = Just [] -- :: Maybe [Jwk]-      , idTokenAlgs    = Nothing -- :: Maybe AlgPrefs-      , userInfoAlgs   = Nothing -- :: Maybe AlgPrefs-      , requestObjAlgs = Nothing -- :: Maybe AlgPrefs-      , sectorIdentifier = host-      }-  void $ flip M.traverseWithKey boUsers $ \userId (emailAddr, pw) -> do-    pwHash :: ByteString <- hashPassword 6 (encodeUtf8 pw)-    void $ SQLite.execute c "INSERT OR REPLACE INTO op_user VALUES (?, ?, ?, 'key')" ((userId, userId, pwHash))-    void $ SQLite.execute c "INSERT OR REPLACE INTO user_info VALUES (?, 'name', 'first', 'last', 'middle', 'nick', 'name', 'http://placeholder', 'http://placeholder', 'http://placeholder', ?, 0, null, '2000-01-01', 'Europe/Paris', 'en-US', '+33 12 34 56 78', 0, '25 My Street, Village, 1234567, France', '25 My Street', 'Vilage', 'Shire', '1234567', 'EN', datetime('now'))" ((userId, emailAddr))---runBroch :: BrochOptions -> IO ()-runBroch opts@BrochOptions{..} = do-  sessionKey <- defaultKey-  kr <- defaultKeyRing-  rotateKeys kr True-  let dbFile = "broch.sqlite3"-  dbExists <- doesFileExist dbFile-  when dbExists $ removeFile dbFile-  pool <- createPool (SQLite.open dbFile) SQLite.close 1 60 20-  withResource pool BS.createSchema-  config <- BS.sqliteBackend pool <$> inMemoryConfig boIssuerUri kr Nothing-  let app = staticApp (defaultWebAppSettings "webroot")-      baseRouter = brochServer config defaultApprovalPage authenticatedSubject authenticateSubject-      authenticate username password = pure $ M.lookup username boUsers >>= \case-        (_, pw) | pw == password -> Just username-        _ -> Nothing-      extraRoutes =-          [ ("/home",   text "Hello, I'm the home page")-          , ("/login",  passwordLoginHandler defaultLoginPage authenticate)-          , ("/logout", invalidateSession >> text "You have been logged out")-          ]-      router = foldl' (\pathMap (r, h) -> R.insertPathMap' (R.toInternalPath (R.static r)) (const h) pathMap) baseRouter extraRoutes-      broch = routerToMiddleware (defaultLoadSession 3600 sessionKey) boIssuerUri router--  withResource pool $ initDB opts--  run boPort (logStdoutDev (broch app))
− test/ExampleProviderOpts.hs
@@ -1,29 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module ExampleProviderOpts where--import ClassyPrelude-import Yesod.Auth.OIDC---- Quick-and-dirty type synonyms just for testing-type Domain = Text-type Email = Text-type SubjectId = Text-type ClientHost = Text-type CallbackUri = Text-type Password = Text---- | You should store your client secrets as you would for other--- credentials in your app.-fakeClientSecret :: ClientId -> ClientSecret-fakeClientSecret (ClientId cid) =-    -- You should not hardcode it like this:-    ClientSecret $ cid <> "_secret"---- | We use the 'broch' library as a local OIDC Provider. You do not--- need something like this in your client app.-data BrochOptions = BrochOptions-  { boIssuerUri :: Text-  , boPort :: Int-  , boUsers :: Map SubjectId (Email, Password)-  , boClients :: Map ClientId (ClientSecret, ClientHost, CallbackUri)-  }
− test/Spec.hs
@@ -1,1 +0,0 @@-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
− test/TestImport.hs
@@ -1,82 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE RecordWildCards #-}--module TestImport-  ( module TestImport-  , module X-  ) where--import ClassyPrelude as X hiding (Handler, delete, deleteBy)-import Control.Monad.Logger (runLoggingT)-import qualified Data.Map as M-import Database.Persist as X hiding (get)-import Database.Persist.Sql (runMigration, runSqlPool)-import Database.Persist.Sqlite (createSqlitePool)-import ExampleApp as X-import ExampleProvider as Broch-import ExampleProviderOpts as X-import Network.HTTP.Client-import Network.Wai.Handler.Warp-import Network.Wai.Middleware.RequestLogger (logStdoutDev)-import System.Directory-import System.Log.FastLogger (newStdoutLoggerSet)-import Test.Hspec as X-import Yesod hiding (get)-import Yesod.Auth.OIDC as X hiding (exp)-import Yesod.Core as X-import qualified Yesod.Core.Unsafe as Unsafe-import Yesod.Default.Config2 (makeYesodLogger)-import Yesod.Persist.Core as X--type TestArgs = (Async (), Async (), App, BrochOptions)--user1 :: (SubjectId, (Email, Password))-user1 = ("user1", ("user1@example.local", "password1"))--brochUsers :: Map SubjectId (Email, Password)-brochUsers = M.fromList-  [ user1-  , ("user2", ("user2@sub.example.local", "password2"))-  ]--withServers :: SpecWith TestArgs -> Spec-withServers = beforeAll runServers . afterAll stopServers-  where-    appPort = 4049-    appHost = "http://localhost:" <> tshow appPort-    boPort = 4050-    boIssuerUri = "http://localhost:" <> tshow boPort-    boUsers = brochUsers-    appBrochClientId = ClientId "client1"-    boClients = M.fromList-      [ (appBrochClientId, ( fakeClientSecret appBrochClientId-                           , appHost-                           , appHost <> "/auth/page/oidc/callback"))-      ]-    brochOptions = BrochOptions{..}-    runServers = do-      brochA <- async $ Broch.runBroch brochOptions-      appLogger <- newStdoutLoggerSet 1 >>= makeYesodLogger-      appHttpManager <- newManager defaultManagerSettings-      let mkFoundation appConnPool = App {..}-          tempFoundation = mkFoundation $ error "connPool forced in tempFoundation"-          logFunc = messageLoggerSource tempFoundation appLogger-      removeIfExists "auth.sqlite3"-      pool <- flip runLoggingT logFunc $ createSqlitePool "auth.sqlite3" 10-      runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc-      let app = mkFoundation pool-      waiApp <- toWaiAppPlain app-      appA <- async $ run appPort $ logStdoutDev waiApp-      pure (brochA, appA, app, brochOptions)-    stopServers (brochA, appA, _, _) = do-      cancel brochA-      cancel appA--unsafeHandler :: App -> Handler a -> IO a-unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger--removeIfExists :: FilePath -> IO ()-removeIfExists f = do-  fileExists <- doesFileExist f-  when fileExists (removeFile f)
− test/Yesod/Auth/OIDCSpec.hs
@@ -1,105 +0,0 @@-{-# LANGUAGE NumericUnderscores #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE RecordWildCards #-}--module Yesod.Auth.OIDCSpec (spec) where--import Control.Lens ((^.))-import qualified Control.Lens.Regex.Text as Regex-import qualified Data.Map as M-import qualified Data.Set as Set-import qualified Data.Text as T-import Network.HTTP.Client-import Network.HTTP.Types.Status-import TestImport--spec :: Spec-spec = withServers $ do-  describe "login with OpenID Connect" $ do-    it "can login with OIDC" $ \(_, _, app@App{..}, BrochOptions{..}) -> do-      -- In this test, the ExampleProvider has already been populated-      -- with the user, and the ExampleApp is configured to insert new-      -- users into its local database upon successful-      -- authentication. We first configure our client app for this-      -- provider:-      unsafeHandler app $ runDB $ do-          insert_ $ OidcConfig-            { oidcConfigClientId = unClientId appBrochClientId-            , oidcConfigIssuer = boIssuerUri-            }-          let domains = Set.toList . Set.fromList-                $ map (snd . T.breakOnEnd "@" . fst) $ M.elems brochUsers-          insertMany_ $ flip map domains $ \domain -> OidcDomain-            { oidcDomainDomain = domain-            , oidcDomainClientId = unClientId appBrochClientId-            , oidcDomainIssuer = boIssuerUri-            }--      ur <- unsafeHandler app getUrlRender-      let-        mgr = appHttpManager-        mkReq = parseRequest_ . unpack . (appHost <>) . ur-        protectedReq = mkReq ProtectedResourceR-        tokenReq = mkReq CsrfTokenR-        forwardReq = mkReq $ AuthR oidcForwardR--      -- Assert that we can't get the ProtectedResource yet-      protectedRes0 <- httpNoBody protectedReq mgr-      liftIO $ responseStatus protectedRes0 `shouldBe` status403--      -- Get the csrf token (Yesod's default csrf handling is-      -- session-specific rather than request-specific).-      tokenResp <- httpLbs tokenReq mgr-      let token = toStrict $ responseBody tokenResp--      -- POST to the oidcForwardR with correct params.-      let (user1_id, (user1_email, user1_pw)) = user1-      respForwardR <- httpLbs-        (urlEncodedBody-          [ (encodeUtf8 defaultCsrfParamName, token)-          , ("email", encodeUtf8 user1_email)-          ] $ forwardReq { method = "POST"-                         , cookieJar = Just $ responseCookieJar tokenResp })-        mgr-      let-        respForwardR_body = toStrict $ decodeUtf8 (responseBody respForwardR)-        brochCsrfToken = respForwardR_body-          ^. [Regex.regex|name="_rid" value="([^"]*)">|]-          . Regex.group 0--      -- POST the provider's login URL with username and password.-      let loginReq = urlEncodedBody-            [ ("username", encodeUtf8 user1_id)-            , ("password", encodeUtf8 user1_pw)-            , ("_rid", encodeUtf8 brochCsrfToken)-            ] $ (parseRequest_ (unpack $ boIssuerUri <> "/login"))-            { method = "POST"-            , cookieJar = Just $ responseCookieJar respForwardR-            }-      respProviderLogin <- httpLbs loginReq mgr--      -- POST the provider's scope approval form-      let-        expiryParam =-          (toStrict $ decodeUtf8 $ responseBody respProviderLogin)-          ^. [Regex.regex|"expiry"><option value="([^"]*)"|]-          . Regex.group 0-        approvalReq = urlEncodedBody-          [ ("client_id", encodeUtf8 $ unClientId appBrochClientId)-          , ("expiry", encodeUtf8 expiryParam)-          , ("requested_scope", "openid")-          , ("scope", "openid")-          , ("scope", "email")-          ] $ (parseRequest_ (unpack $ boIssuerUri <> "/approval"))-          { method = "POST"-          , cookieJar = Just $ responseCookieJar respProviderLogin-          }-      respApproval <- httpLbs approvalReq mgr--      -- Assert that we can access previously unaccessible protected-      -- resource.-      protectedRes1 <- httpNoBody-        (protectedReq { cookieJar = Just $ responseCookieJar respApproval }) mgr--      liftIO $ responseStatus protectedRes1 `shouldBe` status200
yesod-auth-oidc.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.2 name:               yesod-auth-oidc-version:            0.1.3+version:            0.1.4 build-type:         Simple category:           Web, Yesod extra-source-files: README.md@@ -53,65 +53,3 @@   hs-source-dirs:  src   exposed-modules: Yesod.Auth.OIDC -common test-properties-  default-language:   Haskell2010-  hs-source-dirs:     src test-  ghc-options:        -Wall-  default-extensions: NoImplicitPrelude-  build-depends:-    , aeson-    , base-    , base64-bytestring-    , blaze-html            ^>=0.9.1-    , broch                 ^>=0.1-    , bytestring            ^>=0.10.10-    , classy-prelude        ^>=1.5.0-    , classy-prelude-yesod-    , containers            ^>=0.6.2-    , cryptonite-    , directory             ^>=1.3.6-    , email-validate        ^>=2.3.2-    , fast-logger           >=3.0.5   && <4.0-    , hspec                 >=2.7.10  && <3.0-    , http-client-    , http-conduit          ^>=2.3.8-    , http-types            ^>=0.12.3-    , jose-jwt-    , lens                  >=4.19.2  && <6.0-    , lens-regex-pcre       ^>=1.1.0-    , memory                >=0.15.0  && <1-    , monad-logger          ^>=0.3.36-    , oidc-client-    , persistent            >=2.11.0  && <=3.0.0-    , persistent-sqlite     >=2.11.1  && <=3.0-    , postgresql-simple     ^>=0.6.4-    , reroute               >=0.6.0   && <0.8-    , resource-pool         ^>=0.2.3-    , shakespeare-    , sqlite-simple         ^>=0.4.18-    , text-    , time-    , unordered-containers-    , wai-app-static        ^>=3.1.7-    , wai-extra             ^>=3.1.6-    , warp                  ^>=3.3.15-    , yesod                 ^>=1.6.1-    , yesod-auth-    , yesod-core-    , yesod-form-    , yesod-persistent      ^>=1.6.0-    , yesod-test            ^>=1.6.12--  other-modules:-    ExampleApp-    ExampleProvider-    ExampleProviderOpts-    TestImport-    Yesod.Auth.OIDC-    Yesod.Auth.OIDCSpec--test-suite spec-  import:             test-properties-  type:               exitcode-stdio-1.0-  main-is:            Spec.hs-  build-tool-depends: hspec-discover:hspec-discover