lti13 0.2.0.3 → 0.3.0.0
raw patch · 6 files changed
+291/−78 lines, 6 filesdep ~aesondep ~basedep ~bytestringPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: aeson, base, bytestring, http-client, oidc-client, text
API changes (from Hackage documentation)
- Web.LTI13: SessionStore :: m ByteString -> (State -> Nonce -> m ()) -> m (Maybe State, Maybe Nonce) -> m () -> SessionStore (m :: Type -> Type)
+ Web.LTI13: SessionStore :: m ByteString -> (State -> Nonce -> m ()) -> (State -> m (Maybe Nonce)) -> m () -> SessionStore (m :: Type -> Type)
- Web.LTI13: [sessionStoreGet] :: SessionStore (m :: Type -> Type) -> m (Maybe State, Maybe Nonce)
+ Web.LTI13: [sessionStoreGet] :: SessionStore (m :: Type -> Type) -> State -> m (Maybe Nonce)
Files
- CHANGELOG.md +7/−0
- lti13.cabal +86/−52
- src/Web/LTI13.hs +25/−26
- tests/AnonSpec.hs +42/−0
- tests/ParsingSpec.hs +60/−0
- tests/example.json +71/−0
CHANGELOG.md view
@@ -1,5 +1,12 @@ # Revision history for lti13 +## 0.3.0.0 -- 2022-11-23++* Support newer compilers and dependencies+ * aeson >= 2 now required;+ * oidc-client >= 0.7 now required+ * Support ghc-9.4+ ## 0.2.0.3 -- 2021-08-10 * Bump version for yesod-auth-lti13.
lti13.cabal view
@@ -1,57 +1,91 @@-cabal-version: >=1.10+cabal-version: 1.12 -name: lti13-version: 0.2.0.3-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+-- This file has been generated from package.yaml by hpack version 0.34.7.+--+-- see: https://github.com/sol/hpack -test-suite spec- type: exitcode-stdio-1.0- default-language: Haskell2010- hs-source-dirs: tests- ghc-options: -Wall- main-is: Spec.hs- build-depends: base- , hspec- , QuickCheck- , file-embed- , th-utilities- , text- , bytestring- , aeson- , lti13+name: lti13+version: 0.3.0.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>+category: Web+homepage: https://github.com/lf-/lti13#readme+bug-reports: https://github.com/lf-/lti13/issues+author: Jade Lovelace+maintainer: Jade Lovelace <software at lfcode dot ca>+license: LGPL-3+license-file: LICENSE+build-type: Simple+tested-with:+ GHC ==8.10.7 || ==9.2.5 || ==9.4.3+extra-source-files:+ CHANGELOG.md+ README.md+ ./tests/example.json +source-repository head+ type: git+ location: https://github.com/lf-/lti13+ library- exposed-modules: Web.LTI13- -- other-modules:- -- other-extensions:- hs-source-dirs: src- build-depends: aeson >= 1.4.7 && < 1.6- , 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.10- , oidc-client >= 0.5.1 && < 0.7- , safe-exceptions >= 0.1.7 && < 0.2- default-language: Haskell2010- ghc-options: -Wall+ exposed-modules:+ Web.LTI13+ other-modules:+ Paths_lti13+ hs-source-dirs:+ src+ default-extensions:+ NamedFieldPuns+ OverloadedStrings+ RecordWildCards+ DerivingStrategies+ DeriveGeneric+ ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-export-lists -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-missing-local-signatures -Wno-monomorphism-restriction -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-missing-kind-signatures+ build-depends:+ aeson >=2 && <2.2+ , base >=4.12.0 && <5+ , bytestring >=0.10.10 && <0.12+ , containers >=0.6.2 && <0.7+ , http-client >=0.6.4 && <0.8+ , http-types >=0.12.3 && <0.13+ , jose-jwt >=0.8.0 && <0.10+ , oidc-client ==0.7.*+ , safe-exceptions >=0.1.7 && <0.2+ , text >=1.2.4 && <1.3 || >=2.0 && <2.1+ default-language: Haskell2010 -source-repository head- type: git- location: https://github.com/lf-/lti13+test-suite spec+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ AnonSpec+ ParsingSpec+ Paths_lti13+ hs-source-dirs:+ tests+ default-extensions:+ NamedFieldPuns+ OverloadedStrings+ RecordWildCards+ DerivingStrategies+ DeriveGeneric+ ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-export-lists -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-missing-local-signatures -Wno-monomorphism-restriction -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-missing-kind-signatures+ build-tool-depends:+ hspec-discover:hspec-discover+ build-depends:+ QuickCheck+ , aeson >=2 && <2.2+ , base >=4.12.0 && <5+ , bytestring >=0.10.10 && <0.12+ , containers >=0.6.2 && <0.7+ , file-embed+ , hspec+ , http-client >=0.6.4 && <0.8+ , http-types >=0.12.3 && <0.13+ , jose-jwt >=0.8.0 && <0.10+ , lti13+ , oidc-client ==0.7.*+ , safe-exceptions >=0.1.7 && <0.2+ , text >=1.2.4 && <1.3 || >=2.0 && <2.1+ , th-utilities+ default-language: Haskell2010
src/Web/LTI13.hs view
@@ -1,8 +1,4 @@-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}---- | A basic LTI 1.3 library.+-- | A simple LTI 1.3 library. -- It's intended to be used by implementing routes for 'initiate' and -- 'handleAuthResponse', and work out the associated parameters thereof. --@@ -35,7 +31,7 @@ , handleAuthResponse ) where import Control.Exception.Safe (Exception, MonadCatch,- MonadThrow, Typeable,+ MonadThrow, catch, throw, throwM) import Control.Monad (when, (>=>)) import qualified Control.Monad.Fail as Fail@@ -50,6 +46,7 @@ import qualified Data.Aeson as A import Data.Aeson.Types (Parser) import qualified Data.Map.Strict as Map+import Data.String (IsString) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8, encodeUtf8)@@ -59,6 +56,7 @@ httpLbs, parseRequest, responseBody) import qualified Network.HTTP.Types.URI as URI+import Prelude import qualified Web.OIDC.Client.Discovery.Provider as P import Web.OIDC.Client.IdTokenFlow (getValidIdTokenClaims) import qualified Web.OIDC.Client.Settings as O@@ -67,7 +65,7 @@ import Web.OIDC.Client.Types (Nonce, SessionStore (..)) -- | 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 :: (FromJSON a, Eq a, Show a) => Object -> A.Key -> a -> Parser a parseFixed obj field fixedVal = obj .: field >>= \v -> if v == fixedVal then@@ -85,7 +83,7 @@ | Learner | Mentor | Other Text- deriving (Show, Eq)+ deriving stock (Show, Eq) roleFromString :: Text -> Role roleFromString "http://purl.imsglobal.org/vocab/lis/v2/membership#Administrator"@@ -127,7 +125,7 @@ , resultSourcedId :: Maybe Text -- ^ An identifier for the position in the gradebook associated with the -- assignment being viewed.- } deriving (Show, Eq)+ } deriving stock (Show, Eq) instance FromJSON LisClaim where parseJSON = withObject "LisClaim" $ \v ->@@ -166,7 +164,7 @@ , contextLabel :: Maybe Text , contextTitle :: Maybe Text }- deriving (Show, Eq)+ deriving stock (Show, Eq) instance FromJSON ContextClaim where parseJSON = withObject "ContextClaim" $ \v ->@@ -204,16 +202,16 @@ , lastName :: Maybe Text , context :: Maybe ContextClaim , lis :: Maybe LisClaim- } deriving (Show, Eq)+ } deriving stock (Show, Eq) -- | An object representing in the type system a token whose claims have been -- validated. newtype LtiTokenClaims = LtiTokenClaims { unLtiTokenClaims :: UncheckedLtiTokenClaims }- deriving (Show, Eq)+ deriving stock (Show, Eq) -- | LTI token claims from which all student data has been removed. For logging. newtype AnonymizedLtiTokenClaims = AnonymizedLtiTokenClaims UncheckedLtiTokenClaims- deriving (Show, Eq)+ deriving stock (Show, Eq) limitLength :: (Fail.MonadFail m) => Int -> Text -> m Text limitLength len string@@ -221,19 +219,19 @@ = return string limitLength _ _ = fail "String is too long" -claimMessageType :: Text+claimMessageType :: IsString t => t claimMessageType = "https://purl.imsglobal.org/spec/lti/claim/message_type"-claimVersion :: Text+claimVersion :: IsString t => t claimVersion = "https://purl.imsglobal.org/spec/lti/claim/version"-claimDeploymentId :: Text+claimDeploymentId :: IsString t => t claimDeploymentId = "https://purl.imsglobal.org/spec/lti/claim/deployment_id"-claimTargetLinkUri :: Text+claimTargetLinkUri :: IsString t => t claimTargetLinkUri = "https://purl.imsglobal.org/spec/lti/claim/target_link_uri"-claimRoles :: Text+claimRoles :: IsString t => t claimRoles = "https://purl.imsglobal.org/spec/lti/claim/roles"-claimContext :: Text+claimContext :: IsString t => t claimContext = "https://purl.imsglobal.org/spec/lti/claim/context"-claimLis :: Text+claimLis :: IsString t => t claimLis = "https://purl.imsglobal.org/spec/lti/claim/lis" instance FromJSON UncheckedLtiTokenClaims where@@ -345,7 +343,7 @@ | 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)+ deriving stock (Show) instance Exception LTI13Exception -- | @client_id@, one or more per platform; <https://www.imsglobal.org/spec/lti/v1p3/#tool-deployment LTI spec § 3.1.3>@@ -420,9 +418,10 @@ 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 (`lookupOrThrow` params) ["iss", "login_hint", "target_link_uri"]- -- not actually fallible- let [iss, loginHint, _] = res+ iss <- liftIO $ lookupOrThrow "iss" params+ loginHint <- liftIO $ lookupOrThrow "login_hint" params+ _targetLinkUri <- liftIO $ lookupOrThrow "target_link_uri" params+ 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."@@ -493,8 +492,8 @@ -> PlatformInfo -> m (Text, IdTokenClaims LtiTokenClaims) handleAuthResponse mgr cfg params pinfo = do- params' <- liftIO $ mapM (`lookupOrThrow` params) ["state", "id_token"]- let [state, idToken] = params'+ state <- liftIO $ lookupOrThrow "state" params+ idToken <- liftIO $ lookupOrThrow "id_token" params let PlatformInfo { jwksUrl } = pinfo jwkSet <- liftIO $ getJwkSet mgr jwksUrl
+ tests/AnonSpec.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Tests the various parsers in lti13 round-trip and parse the samples.+module AnonSpec (spec) where++import Test.Hspec hiding (context)+import Prelude+import Web.LTI13++exampleToken :: UncheckedLtiTokenClaims+exampleToken = UncheckedLtiTokenClaims+ { messageType = "LtiResourceLinkRequest"+ , ltiVersion = "1.3.0"+ , deploymentId = "07940580-b309-415e-a37c-914d387c1150"+ , targetLinkUri = "https://tool.example.com/lti/48320/ruix8782rs"+ , roles = [Other "http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student",Learner,Mentor]+ , email = secret+ , displayName = secret+ , firstName = secret+ , lastName = secret+ , context = Just (+ ContextClaim+ { contextId = "c1d887f0-a1a3-4bca-ae25-c375edcc131a"+ , contextLabel = Just "ECON 1010"+ , contextTitle = Just "Economics as a Social Science"}+ )+ , lis = Just (+ LisClaim {+ personSourcedId = secret+ , outcomeServiceUrl = Nothing+ , courseOfferingSourcedId = Just "example.edu:SI182-F16"+ , courseSectionSourcedId = Just "example.edu:SI182-001-F16"+ , resultSourcedId = Nothing+ }+ )}+ where+ secret = Just "secret"++spec :: Spec+spec = do+ it "removes secrets" $ do+ show exampleToken `shouldContain` "secret"+ (show . anonymizeLtiTokenForLogging $ exampleToken) `shouldNotContain` "secret"
+ tests/ParsingSpec.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE TemplateHaskell, OverloadedStrings #-}+-- | Tests the various parsers in lti13 round-trip and parse the samples.+module ParsingSpec (spec) where++import Test.Hspec+import Test.Hspec.QuickCheck+import Data.FileEmbed+import TH.RelativePaths+import Web.LTI13+import qualified Web.LTI13 as LTI+import Data.Aeson+import Prelude+import qualified Data.ByteString as BS++-- | This is lifted directly from section E of the LTI 1.3 spec.+ltiSpecExample :: BS.ByteString+ltiSpecExample = $(embedFile =<< pathRelativeToCabalPackage "tests/example.json")++specExampleOutput :: UncheckedLtiTokenClaims+specExampleOutput = UncheckedLtiTokenClaims {+ messageType = "LtiResourceLinkRequest"+ , ltiVersion = "1.3.0"+ , deploymentId = "07940580-b309-415e-a37c-914d387c1150"+ , targetLinkUri = "https://tool.example.com/lti/48320/ruix8782rs"+ , roles = [Other "http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student",Learner,Mentor]+ , email = Just "jane@platform.example.edu"+ , displayName = Just "Ms Jane Marie Doe"+ , firstName = Just "Jane"+ , lastName = Just "Doe"+ , LTI.context = Just (+ ContextClaim+ { contextId = "c1d887f0-a1a3-4bca-ae25-c375edcc131a"+ , contextLabel = Just "ECON 1010"+ , contextTitle = Just "Economics as a Social Science"}+ )+ , lis = Just (+ LisClaim {+ personSourcedId = Just "example.edu:71ee7e42-f6d2-414a-80db-b69ac2defd4"+ , outcomeServiceUrl = Nothing+ , courseOfferingSourcedId = Just "example.edu:SI182-F16"+ , courseSectionSourcedId = Just "example.edu:SI182-001-F16"+ , resultSourcedId = Nothing+ }+ )}++spec :: Spec+spec = do+ describe "id" $ do+ prop "id" $ \x y ->+ add x y == add y x+ describe "main parser" $ do+ it "parses the file from section E of the lti spec" $ do+ let v = decodeStrict ltiSpecExample :: Maybe UncheckedLtiTokenClaims+ -- (putStrLn . show $ v) `shouldReturn` ()+ v `shouldBe` Just specExampleOutput+ let roundTripped = decode . encode =<< v+ roundTripped `shouldBe` Just specExampleOutput++add :: Int -> Int -> Int+add x y = x + y
+ tests/example.json view
@@ -0,0 +1,71 @@+{+ "iss": "https://platform.example.edu",+ "sub": "a6d5c443-1f51-4783-ba1a-7686ffe3b54a",+ "aud": ["962fa4d8-bcbf-49a0-94b2-2de05ad274af"],+ "exp": 1510185728,+ "iat": 1510185228,+ "azp": "962fa4d8-bcbf-49a0-94b2-2de05ad274af",+ "nonce": "fc5fdc6d-5dd6-47f4-b2c9-5d1216e9b771",+ "name": "Ms Jane Marie Doe",+ "given_name": "Jane",+ "family_name": "Doe",+ "middle_name": "Marie",+ "picture": "https://platform.example.edu/jane.jpg",+ "email": "jane@platform.example.edu",+ "locale": "en-US",+ "https://purl.imsglobal.org/spec/lti/claim/deployment_id":+ "07940580-b309-415e-a37c-914d387c1150",+ "https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiResourceLinkRequest",+ "https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",+ "https://purl.imsglobal.org/spec/lti/claim/roles": [+ "http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student",+ "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner",+ "http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor"+ ],+ "https://purl.imsglobal.org/spec/lti/claim/role_scope_mentor": [+ "fad5fb29-a91c-770-3c110-1e687120efd9",+ "5d7373de-c76c-e2b-01214-69e487e2bd33",+ "d779cfd4-bc7b-019-9bf1a-04bf1915d4d0"+ ],+ "https://purl.imsglobal.org/spec/lti/claim/context": {+ "id": "c1d887f0-a1a3-4bca-ae25-c375edcc131a",+ "label": "ECON 1010",+ "title": "Economics as a Social Science",+ "type": ["http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering"]+ },+ "https://purl.imsglobal.org/spec/lti/claim/resource_link": {+ "id": "200d101f-2c14-434a-a0f3-57c2a42369fd",+ "description": "Assignment to introduce who you are",+ "title": "Introduction Assignment"+ },+ "https://purl.imsglobal.org/spec/lti/claim/tool_platform": {+ "guid": "ex/48bbb541-ce55-456e-8b7d-ebc59a38d435",+ "contact_email": "support@platform.example.edu",+ "description": "An Example Tool Platform",+ "name": "Example Tool Platform",+ "url": "https://platform.example.edu",+ "product_family_code": "ExamplePlatformVendor-Product",+ "version": "1.0"+ },+ "https://purl.imsglobal.org/spec/lti/claim/target_link_uri":+ "https://tool.example.com/lti/48320/ruix8782rs",+ "https://purl.imsglobal.org/spec/lti/claim/launch_presentation": {+ "document_target": "iframe",+ "height": 320,+ "width": 240,+ "return_url": "https://platform.example.edu/terms/201601/courses/7/sections/1/resources/2"+ },+ "https://purl.imsglobal.org/spec/lti/claim/custom": {+ "xstart": "2017-04-21T01:00:00Z",+ "request_url": "https://tool.com/link/123"+ },+ "https://purl.imsglobal.org/spec/lti/claim/lis": {+ "person_sourcedid": "example.edu:71ee7e42-f6d2-414a-80db-b69ac2defd4",+ "course_offering_sourcedid": "example.edu:SI182-F16",+ "course_section_sourcedid": "example.edu:SI182-001-F16"+ },+ "http://www.ExamplePlatformVendor.com/session": {+ "id": "89023sj890dju080"+}++}