yesod-auth-lti13 (empty) → 0.1.1.0
raw patch · 7 files changed
+746/−0 lines, 7 filesdep +aesondep +aeson-prettydep +basesetup-changed
Dependencies added: aeson, aeson-pretty, base, base64-bytestring, bytestring, containers, cryptonite, http-client, http-conduit, jose-jwt, load-env, lti13, microlens, oidc-client, random, safe-exceptions, text, time, warp, yesod, yesod-auth, yesod-auth-lti13, yesod-core
Files
- CHANGELOG.md +10/−0
- LICENSE +165/−0
- README.md +24/−0
- Setup.hs +2/−0
- example/Main.hs +139/−0
- src/Yesod/Auth/LTI13.hs +326/−0
- yesod-auth-lti13.cabal +80/−0
+ CHANGELOG.md view
@@ -0,0 +1,10 @@+# Revision history for lti13++## 0.1.0.0 -- 2020-08-13++* Unreleased++## 0.1.1.0 -- 2020-09-15++* Handle Canvas Cloud setting all their issuers the same.+* Remove dependency on changed jose-jwt. Thanks @tekul for the help on this.
+ LICENSE view
@@ -0,0 +1,165 @@+ GNU LESSER GENERAL PUBLIC LICENSE+ Version 3, 29 June 2007++ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>+ Everyone is permitted to copy and distribute verbatim copies+ of this license document, but changing it is not allowed.+++ This version of the GNU Lesser General Public License incorporates+the terms and conditions of version 3 of the GNU General Public+License, supplemented by the additional permissions listed below.++ 0. Additional Definitions.++ As used herein, "this License" refers to version 3 of the GNU Lesser+General Public License, and the "GNU GPL" refers to version 3 of the GNU+General Public License.++ "The Library" refers to a covered work governed by this License,+other than an Application or a Combined Work as defined below.++ An "Application" is any work that makes use of an interface provided+by the Library, but which is not otherwise based on the Library.+Defining a subclass of a class defined by the Library is deemed a mode+of using an interface provided by the Library.++ A "Combined Work" is a work produced by combining or linking an+Application with the Library. The particular version of the Library+with which the Combined Work was made is also called the "Linked+Version".++ The "Minimal Corresponding Source" for a Combined Work means the+Corresponding Source for the Combined Work, excluding any source code+for portions of the Combined Work that, considered in isolation, are+based on the Application, and not on the Linked Version.++ The "Corresponding Application Code" for a Combined Work means the+object code and/or source code for the Application, including any data+and utility programs needed for reproducing the Combined Work from the+Application, but excluding the System Libraries of the Combined Work.++ 1. Exception to Section 3 of the GNU GPL.++ You may convey a covered work under sections 3 and 4 of this License+without being bound by section 3 of the GNU GPL.++ 2. Conveying Modified Versions.++ If you modify a copy of the Library, and, in your modifications, a+facility refers to a function or data to be supplied by an Application+that uses the facility (other than as an argument passed when the+facility is invoked), then you may convey a copy of the modified+version:++ a) under this License, provided that you make a good faith effort to+ ensure that, in the event an Application does not supply the+ function or data, the facility still operates, and performs+ whatever part of its purpose remains meaningful, or++ b) under the GNU GPL, with none of the additional permissions of+ this License applicable to that copy.++ 3. Object Code Incorporating Material from Library Header Files.++ The object code form of an Application may incorporate material from+a header file that is part of the Library. You may convey such object+code under terms of your choice, provided that, if the incorporated+material is not limited to numerical parameters, data structure+layouts and accessors, or small macros, inline functions and templates+(ten or fewer lines in length), you do both of the following:++ a) Give prominent notice with each copy of the object code that the+ Library is used in it and that the Library and its use are+ covered by this License.++ b) Accompany the object code with a copy of the GNU GPL and this license+ document.++ 4. Combined Works.++ You may convey a Combined Work under terms of your choice that,+taken together, effectively do not restrict modification of the+portions of the Library contained in the Combined Work and reverse+engineering for debugging such modifications, if you also do each of+the following:++ a) Give prominent notice with each copy of the Combined Work that+ the Library is used in it and that the Library and its use are+ covered by this License.++ b) Accompany the Combined Work with a copy of the GNU GPL and this license+ document.++ c) For a Combined Work that displays copyright notices during+ execution, include the copyright notice for the Library among+ these notices, as well as a reference directing the user to the+ copies of the GNU GPL and this license document.++ d) Do one of the following:++ 0) Convey the Minimal Corresponding Source under the terms of this+ License, and the Corresponding Application Code in a form+ suitable for, and under terms that permit, the user to+ recombine or relink the Application with a modified version of+ the Linked Version to produce a modified Combined Work, in the+ manner specified by section 6 of the GNU GPL for conveying+ Corresponding Source.++ 1) Use a suitable shared library mechanism for linking with the+ Library. A suitable mechanism is one that (a) uses at run time+ a copy of the Library already present on the user's computer+ system, and (b) will operate properly with a modified version+ of the Library that is interface-compatible with the Linked+ Version.++ e) Provide Installation Information, but only if you would otherwise+ be required to provide such information under section 6 of the+ GNU GPL, and only to the extent that such information is+ necessary to install and execute a modified version of the+ Combined Work produced by recombining or relinking the+ Application with a modified version of the Linked Version. (If+ you use option 4d0, the Installation Information must accompany+ the Minimal Corresponding Source and Corresponding Application+ Code. If you use option 4d1, you must provide the Installation+ Information in the manner specified by section 6 of the GNU GPL+ for conveying Corresponding Source.)++ 5. Combined Libraries.++ You may place library facilities that are a work based on the+Library side by side in a single library together with other library+facilities that are not Applications and are not covered by this+License, and convey such a combined library under terms of your+choice, if you do both of the following:++ a) Accompany the combined library with a copy of the same work based+ on the Library, uncombined with any other library facilities,+ conveyed under the terms of this License.++ b) Give prominent notice with the combined library that part of it+ is a work based on the Library, and explaining where to find the+ accompanying uncombined form of the same work.++ 6. Revised Versions of the GNU Lesser General Public License.++ The Free Software Foundation may publish revised and/or new versions+of the GNU Lesser General Public License from time to time. Such new+versions will be similar in spirit to the present version, but may+differ in detail to address new problems or concerns.++ Each version is given a distinguishing version number. If the+Library as you received it specifies that a certain numbered version+of the GNU Lesser General Public License "or any later version"+applies to it, you have the option of following the terms and+conditions either of that published version or of any later version+published by the Free Software Foundation. If the Library as you+received it does not specify a version number of the GNU Lesser+General Public License, you may choose any version of the GNU Lesser+General Public License ever published by the Free Software Foundation.++ If the Library as you received it specifies that a proxy can decide+whether future versions of the GNU Lesser General Public License shall+apply, that proxy's public statement of acceptance of any version is+permanent authorization for you to choose that version for the+Library.
+ README.md view
@@ -0,0 +1,24 @@+# yesod-auth-lti13++A [lti13](https://hackage.haskell.org/package/lti13) based authentication+provider for Yesod.++## Usage++Implement an `instance YesodAuthLTI13 App` for your Yesod site, using your+persistence mechanisms. See the example for details.++To build the example, pass `-f example` with your cabal commands. You can also+`cabal configure -f example` to make it apply to commands by default (and also+enable it for haskell-language-server).++A sample configuration of the LTI 1.3 reference implementation for a site using+this library is available here: https://lti-ri.imsglobal.org/platforms/1255/++The following configuration is used on the provider (LMS) side, assuming your+`AuthR` is `/auth`:++* `oidc_initiation_url`: https://YOURAPPROOT/auth/page/lti13/initiate+* `target_link_uri`: https://YOURAPPROOT+* Public JWK URL: https://YOURAPPROOT/auth/page/lti13/jwks+* Redirect URLs: https://YOURAPPROOT/auth/page/lti13/authenticate
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ example/Main.hs view
@@ -0,0 +1,139 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE LambdaCase #-}++import qualified Data.ByteString as BS+import qualified Data.Map as M+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Set as Set+import Data.IORef (readIORef, atomicModifyIORef', newIORef, IORef)+import Data.Text.Encoding (decodeUtf8)+import LoadEnv+import Network.HTTP.Conduit+import Network.Wai.Handler.Warp (runEnv)+import Yesod+import Yesod.Auth+import Yesod.Auth.LTI13+import System.IO.Unsafe (unsafePerformIO)+++type SeenNonces = Set.Set Nonce++data App = App+ { appHttpManager :: Manager+ , appAuthPlugins :: [AuthPlugin App]+ }++mkYesod "App" [parseRoutes|+ / RootR GET+ /auth AuthR Auth getAuth+|]++instance Yesod App where+ approot = ApprootStatic "http://localhost:3000"+ shouldLogIO _ _ level = return $ level >= LevelDebug++instance YesodAuth App where+ type AuthId App = Text+ loginDest _ = RootR+ logoutDest _ = RootR++ -- Disable any attempt to read persisted authenticated state+ maybeAuthId = return Nothing++ -- Copy the Creds response into the session for viewing after+ authenticate c = do+ mapM_ (uncurry setSession) $+ [ ("credsIdent", credsIdent c)+ , ("credsPlugin", credsPlugin c)+ ] ++ credsExtra c++ return $ Authenticated "1"++ authPlugins = appAuthPlugins++instance RenderMessage App FormMessage where+ renderMessage _ _ = defaultFormMessage++-- brittany-disable-next-binding++getRootR :: Handler Html+getRootR = do+ sess <- getSession++ let+ mCredsIdent = decodeUtf8 <$> M.lookup "credsIdent" sess+ mCredsPlugin = decodeUtf8 <$> M.lookup "credsPlugin" sess+ mIss = getLtiIss sess+ mSub = getLtiSub sess+ mTok = getLtiToken sess++ defaultLayout [whamlet|+ <h1>Yesod Auth LTI1.3 Example+ <h2>Credentials++ <h3>Plugin / Ident+ <p>#{show mCredsPlugin} / #{show mCredsIdent}++ <h3>Issuer+ <p>#{show mIss}++ <h3>Subject+ <p>#{show mSub}++ <h3>Token info+ <p>#{show mTok}++ <h3>The entire session+ <p>#{show $ M.toList sess}+ |]++-- This is strictly wrong but I have no idea how to properly get the state into+-- the `instance YesodAuthLTI13 App` if I put it in the App. PRs welcome.+seenNonces :: IORef SeenNonces+seenNonces = unsafePerformIO $ newIORef Set.empty++jwks :: IORef (Maybe BS.ByteString)+jwks = unsafePerformIO $ newIORef Nothing++instance YesodAuthLTI13 App where+ checkSeenNonce nonce = do+ -- how do I get the App in this function?! literally WHAT+ seen <- liftIO $ atomicModifyIORef' seenNonces (\s -> (Set.insert nonce s, Set.member nonce s))+ return seen++ -- You should actually put a database here+ retrievePlatformInfo ("aaaaa", Just "abcde") = return $ PlatformInfo {+ platformIssuer = "aaaaa"+ , platformClientId = "abcde"+ , platformOidcAuthEndpoint = "https://lti-ri.imsglobal.org/platforms/1255/authorizations/new"+ , jwksUrl = "https://lti-ri.imsglobal.org/platforms/1255/platform_keys/1248.json"+ }+ retrievePlatformInfo (iss, cid) = do+ $logWarn $ "unknown platform " <> iss <> " with client id " <> (T.pack $ show cid)+ liftIO $ fail "unknown platform"++ retrieveOrInsertJwks new = do+ -- possibly not thread safe. Also you should actually persist this.+ cur <- liftIO $ readIORef jwks+ makeIt <- liftIO $ maybe new pure cur+ liftIO $ atomicModifyIORef' jwks+ (\case+ Nothing -> (Just makeIt, makeIt)+ Just j -> (Just j, j))++mkFoundation :: IO App+mkFoundation = do+ loadEnv+ appHttpManager <- newManager tlsManagerSettings+ appAuthPlugins <- return $ [ authLTI13 ]+ return App {..}++main :: IO ()+main = runEnv 3000 =<< toWaiApp =<< mkFoundation
+ src/Yesod/Auth/LTI13.hs view
@@ -0,0 +1,326 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE TemplateHaskell #-}+++-- | A Yesod authentication module for LTI 1.3+-- See @example/Main.hs@ for a sample implementation.+--+-- Configuration:+--+-- * Login initiation URL: http://localhost:3000/auth/page/lti13/initiate+--+-- * JWKs URL: http://localhost:3000/auth/page/lti13/jwks+--+-- * Tool link URL: http://localhost:3000+module Yesod.Auth.LTI13 (+ PlatformInfo(..)+ , Issuer+ , ClientId+ , Nonce+ , authLTI13+ , YesodAuthLTI13(..)+ , getLtiIss+ , getLtiSub+ , getLtiToken+ , LtiTokenClaims(..)+ , UncheckedLtiTokenClaims(..)+ , ContextClaim(..)+ , Role(..)+ ) where++import Yesod.Core.Widget+import Yesod.Auth (Route(PluginR), setCredsRedirect, Creds(..), authHttpManager, AuthHandler, AuthPlugin(..), YesodAuth)+import Web.LTI13+import qualified Data.Aeson as A+import Data.Text (Text)+import qualified Data.Map.Strict as Map+import Crypto.Random (getRandomBytes)+import qualified Crypto.PubKey.RSA as RSA+import Yesod.Core.Types (TypedContent)+import Yesod.Core (toTypedContent, permissionDenied, setSession, lookupSession, redirect,+ deleteSession, lookupSessionBS, setSessionBS, runRequestBody,+ getRequest, MonadHandler, SessionMap, notFound, getUrlRender)+import qualified Data.ByteString.Base64.URL as B64+import Web.OIDC.Client.Tokens (IdTokenClaims(..))+import Yesod.Core (YesodRequest(reqGetParams))+import Control.Exception.Safe (Exception, throwIO)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Web.OIDC.Client (Nonce)+import Yesod.Core.Handler (getRouteToParent)+import qualified Data.Text.Encoding as E+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString as BS+import Jose.Jwk (JwkSet(..), Jwk(..), generateRsaKeyPair, KeyUse(Sig))+import Data.Time (getCurrentTime)+import Jose.Jwt (KeyId(UTCKeyId))+import Jose.Jwa (Alg(Signed), JwsAlg(RS256))++data YesodAuthLTI13Exception+ = LTIException Text LTI13Exception+ -- ^ Issue with the token+ -- Plugin name and the original exception+ | BadRequest Text Text+ -- ^ Issue with the request+ -- Plugin name and an error message+ | CorruptJwks Text Text+ -- ^ The jwks stored in the database are corrupt. Wat.+ deriving (Show)++instance Exception YesodAuthLTI13Exception++dispatchAuthRequest+ :: YesodAuthLTI13 master+ => PluginName+ -- ^ Name of the auth provider+ -> Text+ -- ^ Method+ -> [Text]+ -- ^ Path parts+ -> AuthHandler master TypedContent+dispatchAuthRequest name "GET" ["initiate"] =+ unifyParams GET >>= dispatchInitiate name+dispatchAuthRequest name "POST" ["initiate"] =+ unifyParams POST >>= dispatchInitiate name+dispatchAuthRequest name "POST" ["authenticate"] =+ dispatchAuthenticate name+dispatchAuthRequest name "GET" ["jwks"] =+ dispatchJwks name+dispatchAuthRequest _ _ _ = notFound++-- | HTTP method for @unifyParams@+data Method = GET+ | POST++-- | Turns parameters from their respective request type to a simple map.+unifyParams+ :: MonadHandler m+ => Method+ -> m RequestParams+unifyParams GET = do+ req <- getRequest+ return $ Map.fromList $ reqGetParams req+unifyParams POST = do+ (params, _) <- runRequestBody+ return $ Map.fromList params++-- | Makes a name for a saved session piece+prefixSession :: Text -> Text -> Text+prefixSession name datum =+ "_lti13_" <> name <> "_" <> datum++-- | Makes the name for the @clientId@ cookie+myCid :: Text -> Text+myCid = flip prefixSession $ "clientId"++-- | Makes the name for the @iss@ cookie+myIss :: Text -> Text+myIss = flip prefixSession $ "iss"++-- | Makes the name for the @state@ cookie+myState :: Text -> Text+myState = flip prefixSession $ "state"++-- | Makes the name for the @nonce@ cookie+myNonce :: Text -> Text+myNonce = flip prefixSession $ "nonce"++mkSessionStore :: MonadHandler m => Text -> SessionStore m+mkSessionStore name =+ SessionStore+ { sessionStoreGenerate = gen+ , sessionStoreSave = sessionSave+ , sessionStoreGet = sessionGet+ , sessionStoreDelete = sessionDelete+ }+ where+ -- we make only url safe stuff to not cause chaos elsewhere+ gen = liftIO $ (B64.encode <$> getRandomBytes 33)+ sname = myState name+ nname = myNonce name+ sessionSave state nonce = do+ setSessionBS sname state+ setSessionBS nname nonce+ return ()+ sessionGet = do+ state <- lookupSessionBS sname+ nonce <- lookupSessionBS nname+ return (state, nonce)+ sessionDelete = do+ deleteSession sname+ deleteSession nname++type PluginName = Text++makeCfg+ :: MonadHandler m+ => PluginName+ -> ((Issuer, Maybe ClientId) -> m PlatformInfo)+ -> (Nonce -> m Bool)+ -> Text+ -> AuthFlowConfig m+makeCfg name pinfo seenNonce callback =+ AuthFlowConfig+ { getPlatformInfo = pinfo+ , haveSeenNonce = seenNonce+ , myRedirectUri = callback+ , sessionStore = mkSessionStore name+ }++createNewJwk :: IO Jwk+createNewJwk = do+ kid <- UTCKeyId <$> getCurrentTime+ let use = Sig+ alg = Signed RS256+ (_, priv) <- generateRsaKeyPair 256 kid use $ Just alg+ return priv++dispatchJwks+ :: YesodAuthLTI13 master+ => PluginName+ -> AuthHandler master TypedContent+dispatchJwks name = do+ jwks <- retrieveOrInsertJwks makeJwks+ JwkSet privs <- maybe (liftIO $ throwIO $ CorruptJwks name "json decode failed")+ pure (A.decodeStrict jwks)+ let pubs = JwkSet $ map rsaPrivToPub privs+ return $ toTypedContent $ A.toJSON pubs+ where makeJwks = (LBS.toStrict . A.encode) <$> makeJwkSet+ makeJwkSet = fmap (\jwk -> JwkSet {keys = [jwk]}) createNewJwk++rsaPrivToPub :: Jwk -> Jwk+rsaPrivToPub (RsaPrivateJwk privKey mId mUse mAlg) =+ RsaPublicJwk (RSA.private_pub privKey) mId mUse mAlg+rsaPrivToPub _ = error "rsaPrivToPub called on a Jwk that's not a RsaPrivateJwk"++dispatchInitiate+ :: YesodAuthLTI13 master+ => PluginName+ -- ^ Name of the provider+ -> RequestParams+ -- ^ Request parameters+ -> AuthHandler master TypedContent+dispatchInitiate name params = do+ -- TODO: this should be refactored into a function but I don't know how+ let url = PluginR name ["authenticate"]+ tm <- getRouteToParent+ render <- getUrlRender+ let authUrl = render $ tm url++ let cfg = makeCfg name retrievePlatformInfo checkSeenNonce authUrl+ (iss, cid, redir) <- initiate cfg params+ setSession (myIss name) iss+ setSession (myCid name) cid+ redirect redir++type State = Text++checkCSRFToken :: MonadHandler m => State -> Maybe State -> m ()+checkCSRFToken state state' = do+ -- they do not match or the state is wrong+ if state' /= Just state then do+ permissionDenied "Bad CSRF token"+ else+ return ()++-- | Makes a user ID that is not an email address (and should thus be safe from+-- [possible security problem] collisions with email based auth systems)+makeUserId :: Issuer -> Text -> Text+makeUserId iss name = name <> "@@" <> iss++dispatchAuthenticate :: YesodAuthLTI13 m => PluginName -> AuthHandler m TypedContent+dispatchAuthenticate name = do+ mgr <- authHttpManager+ -- first, find who the issuer was+ -- this is safe, least of which because Yesod has encrypted session cookies+ maybeIss <- lookupSession $ myIss name+ iss <- maybe (liftIO . throwIO $ BadRequest name "missing `iss` cookie")+ pure+ maybeIss+ cid <- lookupSession $ myCid name+ deleteSession $ myIss name+ deleteSession $ myCid name++ state' <- lookupSession $ myState name++ pinfo <- retrievePlatformInfo (iss, cid)++ -- we don't care about having a callback URL here since we *are* the callback+ let cfg = makeCfg name retrievePlatformInfo checkSeenNonce undefined+ (params', _) <- runRequestBody+ let params = Map.fromList params'+ (state, tok) <- handleAuthResponse mgr cfg params pinfo++ -- check CSRF token against the state in the request+ checkCSRFToken state state'++ let LtiTokenClaims ltiClaims = otherClaims tok+ ltiClaimsJson = E.decodeUtf8 $ LBS.toStrict $ A.encode ltiClaims++ let IdTokenClaims { sub } = tok+ myCreds = Creds {+ credsPlugin = name+ , credsIdent = makeUserId iss sub+ , credsExtra = [("ltiIss", iss), ("ltiSub", sub), ("ltiToken", ltiClaimsJson)]+ }++ setCredsRedirect myCreds++-- | Gets the @iss@ for the given sesssion+getLtiIss :: SessionMap -> Maybe Issuer+getLtiIss sess =+ E.decodeUtf8 <$> Map.lookup "ltiIss" sess++-- | Gets the @sub@ for the given session+getLtiSub :: SessionMap -> Maybe Issuer+getLtiSub sess =+ E.decodeUtf8 <$> Map.lookup "ltiSub" sess++-- | Gets and decodes the extra token claims with the full LTI launch+-- information from a session+--+-- Signature slightly inaccurate: the claims have been checked at this stage.+getLtiToken :: SessionMap -> Maybe UncheckedLtiTokenClaims+getLtiToken sess =+ (Map.lookup "ltiToken" sess)+ >>= A.decodeStrict++-- | Callbacks into your site for LTI 1.3+class (YesodAuth site)+ => YesodAuthLTI13 site where+ -- | Check if a nonce has been seen in the last validity period. It is+ -- expected that nonces given to this function are stored somewhere,+ -- returning False, then when seen again, True should be returned.+ -- See the <http://www.imsglobal.org/spec/security/v1p0/#authentication-response-validation+ -- relevant section of the IMS security specification> for details.+ checkSeenNonce :: Nonce -> AuthHandler site (Bool)++ -- | Get the configuration for the given platform.+ --+ -- It is possible that the relation between Issuer and ClientId is 1+ -- to n rather than 1 to 1, for instance in the case of cloud hosted+ -- Canvas. You *must* therefore key your 'PlatformInfo' retrieval+ -- with the pair of both and throw an error if there are multiple+ -- 'ClientId' for the given 'Issuer' and the 'ClientId' is 'Nothing'.+ retrievePlatformInfo :: (Issuer, Maybe ClientId) -> AuthHandler site (PlatformInfo)++ -- | Retrieve JWKs list from the database or other store. If not+ -- present, please create a new one by evaluating the given 'IO', store+ -- it, and return it.+ retrieveOrInsertJwks+ :: (IO BS.ByteString)+ -- ^ an 'IO' which, if evaluated, will make a new 'Jwk' set+ -> AuthHandler site (BS.ByteString)++-- | Auth plugin. Add this to @appAuthPlugins@ to enable this plugin.+authLTI13 :: YesodAuthLTI13 m => AuthPlugin m+authLTI13 = do+ AuthPlugin name (dispatchAuthRequest name) login+ where+ name = "lti13"+ login _ = [whamlet|Login via your Learning Management System|]+
+ yesod-auth-lti13.cabal view
@@ -0,0 +1,80 @@+cabal-version: >=1.10++name: yesod-auth-lti13+version: 0.1.1.0+synopsis: A yesod-auth plugin for LTI 1.3+description: A plugin using <https://hackage.haskell.org/package/lti13>+ to implement IMS Global LTI 1.3 authentication for+ yesod-auth.+bug-reports: https://github.com/lf-/lti13/issues+license: LGPL-3+author: Jade+maintainer: Jade <software at lfcode dot ca>+-- copyright:+category: Web, Yesod+license-file: LICENSE+build-type: Simple+extra-source-files: CHANGELOG.md+ README.md++library+ hs-source-dirs: src/+ exposed-modules: Yesod.Auth.LTI13+ ghc-options: -Wall+ -- other-modules:+ -- other-extensions:+ build-depends: base >=4.12 && <5+ , lti13+ , base64-bytestring >= 1.0.0 && < 1.1+ , bytestring >= 0.10.10 && < 0.11+ , containers >= 0.6.2 && < 0.7+ , cryptonite >= 0.26 && < 0.27+ , http-client >= 0.6.4 && < 0.7+ , text >= 1.2.4 && < 1.3+ , random >= 1.1 && < 1.2+ , microlens >= 0.4.11 && < 0.5+ , oidc-client >= 0.5.1 && < 0.6+ , aeson >= 1.4.7 && < 1.5+ , safe-exceptions >= 0.1.7 && < 0.2+ , yesod-auth >= 1.6.10 && < 1.7+ , http-conduit >= 2.3.7 && < 2.4+ , yesod-core >= 1.6.18 && < 1.7+ , warp >= 3.3.13 && < 3.4+ , aeson-pretty >= 0.8.8 && < 0.9+ , load-env >= 0.2.1 && < 0.3+ , yesod >= 1.6.1 && < 1.7+ , jose-jwt >= 0.8.0 && < 0.9.0+ , time >= 1.0.0 && < 1.11+ -- hs-source-dirs:+ default-language: Haskell2010++flag example+ description: "Should I build the Yesod example?"+ manual: False+ default: False++executable yesod-lti13-example+ main-is: Main.hs+ hs-source-dirs:+ example+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ aeson+ , aeson-pretty+ , base >=4.9.0.0 && <5+ , bytestring+ , containers+ , http-conduit+ , load-env+ , text+ , warp+ , yesod+ , yesod-auth+ , yesod-auth-lti13+ if !(flag(example))+ buildable: False+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/lf-/lti13