packages feed

lti13 (empty) → 0.1.1.0

raw patch · 6 files changed

+655/−0 lines, 6 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, containers, http-client, http-types, jose-jwt, oidc-client, safe-exceptions, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,9 @@+# 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.
+ 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,17 @@+# lti13++This is a minimal implementation of LTI 1.3 authentication for Haskell. It+supports performing LTI launches and getting most of the interesting fields of+the [resource link request](http://www.imsglobal.org/spec/lti/v1p3/#examplelinkrequest).++This library is intended to be used in developing integrations with web+frameworks, although it can be used directly. A sample integration is+[yesod-auth-lti13](https://hackage.haskell.org/package/yesod-auth-lti13).++## Correct usage++Client code is expected to maintain a CSRF token, the `state` parameter, in+session storage and check it is the same as the one from `handleAuthResponse`,+failing authentication if it is not. Future versions of the library may+introduce a mandatory callback to ensure clients do this. For an example of+this, see the yesod-auth-lti13 sources.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ lti13.cabal view
@@ -0,0 +1,41 @@+cabal-version:       >=1.10++name:                lti13+version:             0.1.1.0+synopsis:            Core functionality for LTI 1.3.+description:         A library implementing the core+                     <http://www.imsglobal.org/spec/lti/v1p3/ LTI 1.3> authentication protocol,+                     suitable for use in implementing libraries for any web framework. An example+                     use is <https://hackage.haskell.org/package/yesod-auth-lti13>+bug-reports:         https://github.com/lf-/lti13/issues+license:             LGPL-3+author:              Jade+maintainer:          Jade <software at lfcode dot ca>+-- copyright:+category:            Web+license-file:        LICENSE+build-type:          Simple+extra-source-files:  CHANGELOG.md+                     README.md++library+    exposed-modules:     Web.LTI13+    -- other-modules:+    -- other-extensions:+    hs-source-dirs:      src+    build-depends:       aeson                 >= 1.4.7 && < 1.5+                       , base                  >= 4.12.0 && < 5+                       , bytestring            >= 0.10.10 && < 0.11+                       , containers            >= 0.6.2 && < 0.7+                       , text                  >= 1.2.4 && < 1.3+                       , http-client           >= 0.6.4 && < 0.7+                       , http-types            >= 0.12.3 && < 0.13+                       , jose-jwt              >= 0.8.0 && < 0.9+                       , oidc-client           >= 0.5.1 && < 0.6+                       , safe-exceptions       >= 0.1.7 && < 0.2+    default-language:   Haskell2010+    ghc-options:         -Wall++source-repository head+    type: git+    location: https://github.com/lf-/lti13
+ src/Web/LTI13.hs view
@@ -0,0 +1,421 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NamedFieldPuns #-}++-- | A basic LTI 1.3 library.+--   It's intended to be used by implementing routes for 'initiate' and+--   'handleAuthResponse', and work out the associated parameters thereof.+module Web.LTI13 (+        Role(..)+      , ContextClaim(..)+      , UncheckedLtiTokenClaims(..)+      , LtiTokenClaims(..)+      , validateLtiToken+      , LTI13Exception(..)+      , PlatformInfo(..)+      , Issuer+      , ClientId+      , SessionStore(..)+      , AuthFlowConfig(..)+      , RequestParams+      , initiate+      , handleAuthResponse+    ) where+import qualified Web.OIDC.Client.Settings as O+import qualified Web.OIDC.Client.Discovery.Provider as P+import Web.OIDC.Client.Tokens (nonce, aud, otherClaims, iss, IdTokenClaims)+import Web.OIDC.Client.IdTokenFlow (getValidIdTokenClaims)+import Web.OIDC.Client.Types (Nonce, SessionStore(..))+import Jose.Jwa (JwsAlg(RS256))+import qualified Jose.Jwk as Jwk+import Control.Monad (when, (>=>))+import Control.Exception.Safe (MonadCatch, catch, throwM, Typeable, Exception, MonadThrow, throw)+import Control.Monad.IO.Class (liftIO, MonadIO)+import Data.Aeson (eitherDecode, FromJSON (parseJSON), ToJSON(toJSON, toEncoding), Object,+                   object, pairs, withObject, withText, (.:), (.:?), (.=))+import qualified Data.Aeson as A+import Data.Aeson.Types (Parser)+import Data.Text (Text)+import qualified Network.HTTP.Types.URI as URI+import Network.HTTP.Client (responseBody, Manager, HttpException, parseRequest, httpLbs)+import qualified Data.Map.Strict as Map+import qualified Data.Text as T+import Data.Text.Encoding (encodeUtf8, decodeUtf8)++-- | Parses a JSON text field to a fixed expected value, failing otherwise+parseFixed :: (FromJSON a, Eq a, Show a) => Object -> Text -> a -> Parser a+parseFixed obj field fixedVal =+    obj .: field >>= \v ->+        if v == fixedVal then+            return v+        else+            fail $ "field " ++ (show field) ++ " was not the required value " ++ (show fixedVal)++-- | Roles in the target context (≈ course/section); see+--   <http://www.imsglobal.org/spec/lti/v1p3/#lis-vocabulary-for-institution-roles LTI spec § A.2.2>+--   and <http://www.imsglobal.org/spec/lti/v1p3/#roles-claim LTI spec § 5.3.7>+--   for details+data Role = Administrator+          | ContentDeveloper+          | Instructor+          | Learner+          | Mentor+          | Other (Text)+          deriving (Show)++roleFromString :: Text -> Role+roleFromString "http://purl.imsglobal.org/vocab/lis/v2/membership#Administrator"+    = Administrator+roleFromString "http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper"+    = ContentDeveloper+roleFromString "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor"+    = Instructor+roleFromString "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner"+    = Learner+roleFromString "http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor"+    = Mentor+roleFromString s = Other s++roleToString :: Role -> Text+roleToString Administrator = "http://purl.imsglobal.org/vocab/lis/v2/membership#Administrator"+roleToString ContentDeveloper = "http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper"+roleToString Instructor = "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor"+roleToString Learner = "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner"+roleToString Mentor = "http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor"+roleToString (Other s) = s++instance FromJSON Role where+    parseJSON = withText "Role" $ return . roleFromString++instance ToJSON Role where+    toJSON = A.String . roleToString++-- | <http://www.imsglobal.org/spec/lti/v1p3/#context-claim LTI spec § 5.4.1> context claim+data ContextClaim = ContextClaim+    { contextId :: Text+    , contextLabel :: Maybe Text+    , contextTitle :: Maybe Text+    }+    deriving (Show)++instance FromJSON ContextClaim where+    parseJSON = withObject "ContextClaim" $ \v ->+        ContextClaim+            <$> (v .: "id" >>= limitLength 255)+            <*> v .:? "label"+            <*> v .:? "title"++instance ToJSON ContextClaim where+    toJSON (ContextClaim {contextId, contextLabel, contextTitle}) =+        object [+            "id" .= contextId+          , "label" .= contextLabel+          , "title" .= contextTitle+          ]+    toEncoding (ContextClaim {contextId, contextLabel, contextTitle}) =+        pairs (+            "id" .= contextId <>+            "label" .= contextLabel <>+            "title" .= contextTitle+        )++-- | LTI specific claims on a token. You should not accept this type, and+--   instead prefer the @newtype@ 'LtiTokenClaims' which has had checking+--   performed on it.+data UncheckedLtiTokenClaims = UncheckedLtiTokenClaims+    { messageType :: Text+    , ltiVersion :: Text+    , deploymentId :: Text+    , targetLinkUri :: Text+    , roles :: [Role]+    , email :: Maybe Text+    , context :: Maybe ContextClaim+    } deriving (Show)++-- | An object representing in the type system a token whose claims have been+--   validated.+newtype LtiTokenClaims = LtiTokenClaims UncheckedLtiTokenClaims+    deriving (Show)++limitLength :: (MonadFail m) => Int -> Text -> m Text+limitLength len string+    | (T.length string) <= len+    = return string+limitLength _ _ = fail "String is too long"++claimMessageType :: Text+claimMessageType = "https://purl.imsglobal.org/spec/lti/claim/message_type"+claimVersion :: Text+claimVersion = "https://purl.imsglobal.org/spec/lti/claim/version"+claimDeploymentId :: Text+claimDeploymentId = "https://purl.imsglobal.org/spec/lti/claim/deployment_id"+claimTargetLinkUri :: Text+claimTargetLinkUri = "https://purl.imsglobal.org/spec/lti/claim/target_link_uri"+claimRoles :: Text+claimRoles = "https://purl.imsglobal.org/spec/lti/claim/roles"+claimContext :: Text+claimContext = "https://purl.imsglobal.org/spec/lti/claim/context"++instance FromJSON UncheckedLtiTokenClaims where+    parseJSON = withObject "LtiTokenClaims" $ \v ->+        UncheckedLtiTokenClaims+            <$> (parseFixed v claimMessageType "LtiResourceLinkRequest")+            <*> (parseFixed v claimVersion "1.3.0")+            <*> (v .: claimDeploymentId >>= limitLength 255)+            <*> v .: claimTargetLinkUri+            <*> v .: claimRoles+            <*> v .:? "email"+            <*> v .:? claimContext++instance ToJSON UncheckedLtiTokenClaims where+    toJSON (UncheckedLtiTokenClaims {+              messageType, ltiVersion, deploymentId+            , targetLinkUri, roles, email, context}) =+        object [+              claimMessageType .= messageType+            , claimVersion .= ltiVersion+            , claimDeploymentId .= deploymentId+            , claimTargetLinkUri .= targetLinkUri+            , claimRoles .= roles+            , "email" .= email+            , claimContext .= context+          ]+    toEncoding (UncheckedLtiTokenClaims {+              messageType, ltiVersion, deploymentId+            , targetLinkUri, roles, email, context}) =+        pairs (+               claimMessageType .= messageType+            <> claimVersion .= ltiVersion+            <> claimDeploymentId .= deploymentId+            <> claimTargetLinkUri .= targetLinkUri+            <> claimRoles .= roles+            <> "email" .= email+            <> claimContext .= context+          )++-- | A direct implementation of <http://www.imsglobal.org/spec/security/v1p0/#authentication-response-validation Security § 5.1.3>+validateLtiToken+    :: PlatformInfo+    -> IdTokenClaims UncheckedLtiTokenClaims+    -> Either Text (IdTokenClaims LtiTokenClaims)+validateLtiToken pinfo claims =+    valid .+        (issuerMatches+         >=> audContainsClientId+         >=> hasNonce) $ claims+    where+        -- step 1 handled before we are called+        -- step 2+        issuerMatches c+            | iss c == platformIssuer pinfo+                = Right claims+            | otherwise+                = Left "issuer does not match platform issuer"+        -- step 3+        audContainsClientId c+            -- "The Tool MUST reject the ID Token if it does not list the+            -- client_id as a valid audience, or if it contains additional+            -- audiences not trusted by the Tool."+            -- Game on, I don't trust anyone else.+            | (length $ aud c) == 1 && (platformClientId pinfo) `elem` (aud c)+                = Right claims+            | otherwise+                = Left "aud is invalid"+        -- step 4 and 5 elided -> we can ignore azp because we don't accept >1 aud entries+        -- step 6 is performed elsewhere, probably+        -- step 7 elided because it is handled by 'validateClaims'+        -- step 8 optional+        -- step 9 nonce checking "The ID Token MUST contain a nonce Claim."+        hasNonce c =+            case nonce c of+                Just _  -> Right claims+                Nothing -> Left "nonce missing"+        valid :: Either Text (IdTokenClaims UncheckedLtiTokenClaims) -> Either Text (IdTokenClaims LtiTokenClaims)+        -- unwrap a validated token and rewrap it as a valid token+        valid (Left e) = Left e+        valid (Right tok) =+            Right tok { otherClaims = (LtiTokenClaims $ otherClaims tok) }+++-----------------------------------------------------------+-- Helpers for the endpoints you have to implement+-----------------------------------------------------------++-- | (most of) the exceptions that can arise in LTI 1.3 handling. Some may have+--   been forgotten, and this is a bug that should be fixed.+data LTI13Exception+    = InvalidHandshake Text+    -- ^ Error in the handshake format+    | DiscoveryException Text+    | GotHttpException HttpException+    | InvalidLtiToken Text+    -- ^ Token validation error. Per <http://www.imsglobal.org/spec/security/v1p0/#authentication-response-validation Security § 5.1.3>+    --   if you get this, you should return a 401.+    deriving (Show, Typeable)+instance Exception LTI13Exception++-- | @client_id@, one or more per platform; <https://www.imsglobal.org/spec/lti/v1p3/#tool-deployment LTI spec § 3.1.3>+type ClientId = Text++-- | Preregistered information about a learning platform+data PlatformInfo = PlatformInfo+    {+    -- | Issuer value+      platformIssuer :: Issuer+    -- | @client_id@+    , platformClientId :: ClientId+    -- | URL the client is redirected to for <http://www.imsglobal.org/spec/security/v1p0/#step-3-authentication-response auth stage 2>.+    --   See also <http://www.imsglobal.org/spec/security/v1p0/#openid_connect_launch_flow Security spec § 5.1.1>+    , platformOidcAuthEndpoint :: Text+    -- | URL for a JSON object containing the JWK signing keys for the platform+    , jwksUrl :: String+    }++-- | Issuer/@iss@ field+type Issuer = Text++-- | Object you have to provide defining integration points with your app+data AuthFlowConfig m = AuthFlowConfig+    { getPlatformInfo       :: (Issuer, Maybe ClientId) -> m PlatformInfo+    -- ^ Access some persistent storage of the configured platforms and return the+    --   PlatformInfo for a given platform by name+    , haveSeenNonce         :: Nonce -> m Bool+    , myRedirectUri         :: Text+    , sessionStore          :: SessionStore m+    -- ^ Note that as in the example for haskell-oidc-client, this is intended to+    --   be partially parameterized already with some separate cookie you give+    --   the browser. You should also store the @iss@ from the 'initiate' stage+    --   in the session somewhere for the 'handleAuthResponse' stage.+    }++rethrow :: (MonadCatch m) => HttpException -> m a+rethrow = throwM . GotHttpException++-- | Grab the JWK set from a URL+getJwkSet+    :: Manager+    -> String+    -> IO [Jwk.Jwk]+getJwkSet manager fromUrl = do+    json <- getJwkSetJson fromUrl `catch` rethrow+    case jwks json of+        Right keys -> return keys+        Left  err  -> throwM $ DiscoveryException ("Failed to decode JwkSet: " <> T.pack err)+  where+    getJwkSetJson url = do+        req <- parseRequest url+        res <- httpLbs req manager+        return $ responseBody res++    jwks j = Jwk.keys <$> eitherDecode j++lookupOrThrow :: (MonadThrow m) => Text -> Map.Map Text Text -> m Text+lookupOrThrow name map_ =+    case Map.lookup name map_ of+        Nothing -> throw $ InvalidHandshake $ "Missing `" <> name <> "`"+        Just a -> return a++-- | Parameters to a request, either in the URL with a @GET@ or in the body+--   with a @POST@+type RequestParams = Map.Map Text Text++-- | Makes the URL for <http://www.imsglobal.org/spec/security/v1p0/#step-1-third-party-initiated-login IMS Security spec § 5.1.1.2>+--   upon the § 5.1.1.1 request coming in+--+--   Returns @(Issuer, RedirectURL)@.+initiate :: (MonadIO m) => AuthFlowConfig m -> RequestParams -> m (Issuer, ClientId, Text)+initiate cfg params = do+    -- we don't care about target link uri since we only support one endpoint+    res <- liftIO $ mapM (flip lookupOrThrow params) ["iss", "login_hint", "target_link_uri"]+    -- not actually fallible+    let [iss, loginHint, _] = res+    let messageHint = Map.lookup "lti_message_hint" params+    -- "This allows for a platform to support multiple registrations from a+    -- single issuer, without relying on the initiate_login_uri as a key."+    --+    -- Canvas puts the same issuer on all their messages (wat)+    -- (https://community.canvaslms.com/thread/36682-lti13-how-to-identify-clientid-and-deploymentid-on-launch)+    -- so we need to be able to distinguish these. Our client code must+    -- therefore key its platform info store by @(Issuer, Maybe ClientId)@+    let gotCid = Map.lookup "client_id" params+    PlatformInfo+        { platformOidcAuthEndpoint = endpoint+        , platformClientId = clientId } <- (getPlatformInfo cfg) (iss, gotCid)++    let ss = sessionStore cfg+    nonce <- sessionStoreGenerate ss+    state <- sessionStoreGenerate ss+    sessionStoreSave ss state nonce++    let query = URI.simpleQueryToQuery $+                [ ("scope", "openid")+                , ("response_type", "id_token")+                , ("client_id", encodeUtf8 clientId)+                , ("redirect_uri", encodeUtf8 $ myRedirectUri cfg)+                , ("login_hint", encodeUtf8 loginHint)+                , ("state", state)+                , ("response_mode", "form_post")+                , ("nonce", nonce)+                , ("prompt", "none")+                ] ++ maybe [] (\mh -> [("lti_message_hint", encodeUtf8 mh)]) messageHint+    return $ (iss, clientId, endpoint <> (decodeUtf8 . URI.renderQuery True) query)++-- | Makes a fake OIDC object with the bare minimum attributes to hand to+--   verification library functions+fakeOidc :: [Jwk.Jwk] -> O.OIDC+fakeOidc jset = O.OIDC+        { O.oidcProvider = P.Provider+                { P.configuration = P.Configuration+                    { P.idTokenSigningAlgValuesSupported = [ P.JwsAlgJson RS256 ]+                    , P.issuer = undefined+                    , P.authorizationEndpoint = undefined+                    , P.tokenEndpoint = undefined+                    , P.userinfoEndpoint = undefined+                    , P.revocationEndpoint = undefined+                    , P.jwksUri = undefined+                    , P.responseTypesSupported = undefined+                    , P.subjectTypesSupported = undefined+                    , P.scopesSupported = undefined+                    , P.tokenEndpointAuthMethodsSupported = undefined+                    , P.claimsSupported = undefined+                    }+                , P.jwkSet = jset+                }+        , O.oidcAuthorizationServerUrl = undefined+        , O.oidcTokenEndpoint = undefined+        , O.oidcClientId = undefined+        , O.oidcClientSecret = undefined+        , O.oidcRedirectUri = undefined+        }++-- | Handle the <http://www.imsglobal.org/spec/security/v1p0/#step-3-authentication-response § 5.1.1.3 Step 3>+--   response sent to the 'AuthFlowConfig.myRedirectUri'+--+--   Returns @(State, Token)@+handleAuthResponse :: (MonadIO m)+    => Manager+    -> AuthFlowConfig m+    -> RequestParams+    -> PlatformInfo+    -> m (Text, IdTokenClaims LtiTokenClaims)+handleAuthResponse mgr cfg params pinfo = do+    params' <- liftIO $ mapM (flip lookupOrThrow params) ["state", "id_token"]+    let [state, idToken] = params'++    let PlatformInfo { jwksUrl } = pinfo+    jwkSet <- liftIO $ getJwkSet mgr jwksUrl++    let ss = sessionStore cfg+        oidc = fakeOidc jwkSet+    toCheck <- getValidIdTokenClaims ss oidc (encodeUtf8 state) (pure $ encodeUtf8 idToken)++    -- present nonce but seen -> error+    -- present nonce unseen -> good+    -- absent nonce -> different error+    nonceSeen <- case nonce toCheck of+        Just n  -> haveSeenNonce cfg n+        Nothing -> liftIO $ throw $ InvalidLtiToken "missing nonce"+    when nonceSeen (liftIO $ throw $ InvalidLtiToken "nonce seen before")++    case validateLtiToken pinfo toCheck of+        Left err  -> liftIO $ throw $ InvalidLtiToken err+        Right tok -> return (state, tok)