{"id":6,"date":"2026-03-08T15:46:42","date_gmt":"2026-03-08T15:46:42","guid":{"rendered":"http:\/\/localhost:8088\/blog\/lti-1-3-oidc-with-anthology-end-to-end-sso-integration-guide\/"},"modified":"2026-03-08T23:10:21","modified_gmt":"2026-03-08T23:10:21","slug":"lti-1-3-oidc-with-anthology-end-to-end-sso-integration-guide","status":"publish","type":"post","link":"https:\/\/www.bonuspoint.info\/blog\/lti-1-3-oidc-with-anthology-end-to-end-sso-integration-guide\/","title":{"rendered":"LTI 1.3 + OIDC with Anthology: End-to-End SSO Integration Guide"},"content":{"rendered":"<div class=\"post-full\" style=\"max-width: 920px; margin: 0 auto; line-height: 1.7;\">\n<h2>Introduction<\/h2>\n<p>This article explains a real example of the SSO flow from Anthology Blackboard to a Custom Learning Tool (BonuspointLearningTool.sg). The SSO flow follows OpenID Connect using JWT. The same pattern can be applied to any Custom Learning Tool that is SSO-integrated with Blackboard.<\/p>\n<p>For security, sensitive encoded strings and key values are masked while preserving original length.<\/p>\n<hr \/>\n<h2>0) Entry and callback URLs in this flow<\/h2>\n<ul>\n<li>BonuspointLearningTool OIDC login entry:<\/li>\n<li><code>https:\/\/api.learningtool.bonuspoint.info\/lti\/oidc\/login<\/code><\/li>\n<li>BonuspointLearningTool frontend callback:<\/li>\n<li><code>https:\/\/learningtool.bonuspoint.info\/sso-callback?token=&lt;BonuspointLearningToolSessionToken&gt;<\/code><\/li>\n<\/ul>\n<p>These two are in different stages:<br \/>\n&#8211; <code>\/lti\/oidc\/login<\/code> is LMS-to-tool OIDC initiation<br \/>\n&#8211; <code>\/sso-callback?token=...<\/code> is tool backend redirect to frontend after launch verification<\/p>\n<hr \/>\n<h2>1) Token structure and cryptography clarification<\/h2>\n<p>Original point kept: \u201ctoken has 3 parts separated by dot (<code>.<\/code>)\u201d \u2014 correct.<\/p>\n<p>For a JWT:<br \/>\n&#8211; Part 1: header<br \/>\n&#8211; Part 2: payload<br \/>\n&#8211; Part 3: signature<\/p>\n<p>Important correction:<br \/>\n&#8211; These parts are <strong>Base64URL<\/strong> encoded (not standard Base64 wording in strict JWT terms).<br \/>\n&#8211; Signature segment is also Base64URL text of signature bytes.<\/p>\n<h3>Example session token<\/h3>\n<p><code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImRvbmd6aGkueWFuZ0Bib251c3BvaW50LmluZm8iLCJyb2xlIjoiYWRtaW4iLCJpZCI6IjY5NTdiOGY5NTRmOGQ0MDA0NjhlYmU1OSIsInNjaG9vbElkIjoiNjkyZDk0YzllYmEyZDgwMDE5YmY0NjliIiwiY291cnNlSWQiOiI2OTU3YjA1M2E0MWVkZjAwNDc4MjQ1NzciLCJjb3Vyc2VOYW1lIjoiTklFX1RFU1RfQVBQTE******************************************************************************************************************************<\/code><\/p>\n<p>Decoded header:<br \/>\n&#8211; <code>{\"alg\":\"HS256\",\"typ\":\"JWT\"}<\/code><\/p>\n<p>Decoded payload:<br \/>\n&#8211; <code>{\"email\":\"dongzhi.yang@bonuspoint.info\",\"role\":\"admin\",\"id\":\"6957b8f954f8d400468ebe59\",\"schoolId\":\"692d94c9eba2d80019bf469b\",\"courseId\":\"6957b053a41edf0047824577\",\"courseName\":\"NIE_TEST_APPLETREE\",\"time\":1772912607514,\"iat\":1772912607,\"exp\":1773344607}<\/code><\/p>\n<h3>Signature math<\/h3>\n<p>For HS256 token signing:<\/p>\n<p><code>signature = HMACSHA256(base64url(header) + \".\" + base64url(payload), secret)<\/code><\/p>\n<p>Then JWT stores <code>base64url(signature)<\/code> as segment 3.<\/p>\n<hr \/>\n<h2>2) Two JWT contexts in this integration<\/h2>\n<h3>A) Platform launch <code>id_token<\/code> (Blackboard\/Anthology -&gt; BonuspointLearningTool)<\/h3>\n<ul>\n<li>Verified by BonuspointLearningTool backend using platform JWKS public keys<\/li>\n<li>RS256 verification flow in code path<\/li>\n<li>Validation includes issuer, audience, nonce, expiry, etc.<\/li>\n<\/ul>\n<h3>B) BonuspointLearningTool app session token (BonuspointLearningTool backend -&gt; frontend\/API)<\/h3>\n<ul>\n<li>Generated by BonuspointLearningTool backend using session secret (<code>helpers\/jwt.js<\/code>)<\/li>\n<li>Verified by BonuspointLearningTool backend on API requests (<code>middlewares\/authentication.js<\/code>)<\/li>\n<li>This is issuer-and-verifier both BonuspointLearningTool (shared secret model)<\/li>\n<\/ul>\n<p>Original point kept and corrected:<br \/>\n&#8211; This is different from RS256\/ES256 asymmetric verification.<br \/>\n&#8211; Verification is signature checking, not decryption.<\/p>\n<hr \/>\n<h2>3) Credential mapping<\/h2>\n<ul>\n<li><code>LTI_CLIENT_ID = Application ID<\/code><\/li>\n<li><code>ANTHOLOGY_APPLICATION_KEY = Application Key<\/code><\/li>\n<li><code>ANTHOLOGY_APPLICATION_SECRET = LTI_CLIENT_SECRET = Application Secret<\/code><\/li>\n<\/ul>\n<p>Values currently listed in original source:<br \/>\n&#8211; Application ID: <code>68c75d43-9477-4060-8750-3***********<\/code><br \/>\n&#8211; Application Key: <code>691c7d94-09a2-4714-9329-6***********<\/code><br \/>\n&#8211; Secret: <code>EOU9tV95HjgBZkMWwUubeH**********<\/code><\/p>\n<p>Security note:<br \/>\n&#8211; Keep these in secret storage\/env vars; avoid public publication.<\/p>\n<hr \/>\n<h2>4) End-to-End SSO Sequence<\/h2>\n<h3>Step 1 \u2014 User clicks tool in Blackboard<\/h3>\n<ul>\n<li>Browser opens Blackboard tool link (launch link \/ placement page context)<\/li>\n<li>Blackboard prepares LTI launch context<\/li>\n<\/ul>\n<h3>Step 2 \u2014 Blackboard calls BonuspointLearningTool OIDC login endpoint<\/h3>\n<ul>\n<li>Request to:<\/li>\n<li><code>https:\/\/api.learningtool.bonuspoint.info\/lti\/oidc\/login<\/code><\/li>\n<li>Blackboard provides parameters such as:<\/li>\n<li><code>iss<\/code><\/li>\n<li><code>client_id<\/code><\/li>\n<li><code>login_hint<\/code><\/li>\n<li><code>lti_message_hint<\/code><\/li>\n<li><code>target_link_uri<\/code><\/li>\n<li><code>lti_deployment_id<\/code><\/li>\n<\/ul>\n<h3>Step 3 \u2014 BonuspointLearningTool backend validates and creates temporary launch session<\/h3>\n<ul>\n<li>Validates issuer and client id against configured values<\/li>\n<li>Generates:<\/li>\n<li><code>state<\/code><\/li>\n<li><code>nonce<\/code><\/li>\n<li>Stores state\/nonce in temporary LTI session storage<\/li>\n<\/ul>\n<h3>Step 4 \u2014 BonuspointLearningTool redirects browser to Anthology OIDC auth endpoint<\/h3>\n<ul>\n<li>302 redirect to:<\/li>\n<li><code>https:\/\/developer.anthology.com\/api\/v1\/gateway\/oidcauth<\/code><\/li>\n<li>Includes:<\/li>\n<li><code>response_type=id_token<\/code><\/li>\n<li><code>scope=openid<\/code><\/li>\n<li><code>response_mode=form_post<\/code><\/li>\n<li><code>client_id<\/code><\/li>\n<li><code>redirect_uri=https:\/\/api.learningtool.bonuspoint.info\/lti\/oidc\/launch<\/code><\/li>\n<li><code>state<\/code><\/li>\n<li><code>nonce<\/code><\/li>\n<li><code>prompt=none<\/code><\/li>\n<li>forwarded <code>login_hint<\/code><\/li>\n<li>forwarded <code>lti_message_hint<\/code> (if present)<\/li>\n<\/ul>\n<h3>Step 5 \u2014 Anthology\/Blackboard returns launch result to BonuspointLearningTool<\/h3>\n<ul>\n<li>Browser receives an auto-submitting form (<code>response_mode=form_post<\/code>)<\/li>\n<li>POST to:<\/li>\n<li><code>https:\/\/api.learningtool.bonuspoint.info\/lti\/oidc\/launch<\/code><\/li>\n<li>Form fields include:<\/li>\n<li><code>id_token<\/code><\/li>\n<li><code>state<\/code><\/li>\n<\/ul>\n<h3>Step 6 \u2014 BonuspointLearningTool verifies platform id_token<\/h3>\n<ul>\n<li>Decodes token header, reads <code>kid<\/code><\/li>\n<li>Gets platform keys from JWKS endpoint:<\/li>\n<li><code>https:\/\/developer.anthology.com\/.well-known\/jwks.json<\/code><\/li>\n<li>Selects matching key by <code>kid<\/code><\/li>\n<li>Verifies RS256 signature and validates claims (<code>iss<\/code>, <code>aud<\/code>, nonce, expiry)<\/li>\n<\/ul>\n<h3>Step 7 \u2014 BonuspointLearningTool user\/course processing and sync<\/h3>\n<ul>\n<li>Finds\/creates user<\/li>\n<li>Updates LTI context<\/li>\n<li>Finds\/creates class\/course mapping<\/li>\n<li>If needed, calls Blackboard token endpoint for API-based membership sync:<\/li>\n<li><code>https:\/\/ntulearntst.ntu.edu.sg\/learn\/api\/public\/v1\/oauth2\/token<\/code><\/li>\n<li>Uses application key + secret for that server-to-server API token<\/li>\n<\/ul>\n<h3>Step 8 \u2014 BonuspointLearningTool generates BonuspointLearningTool session token<\/h3>\n<ul>\n<li>Uses backend session secret (<code>helpers\/jwt.js<\/code>)<\/li>\n<li>Sets cookie and redirects user to frontend callback:<\/li>\n<li><code>https:\/\/learningtool.bonuspoint.info\/sso-callback?token=&lt;BonuspointLearningToolSessionToken&gt;<\/code><\/li>\n<\/ul>\n<h3>Step 9 \u2014 Frontend callback and API authentication<\/h3>\n<ul>\n<li>Frontend decodes token payload for context routing<\/li>\n<li>Frontend stores auth context\/token and sends GraphQL\/API requests with bearer token<\/li>\n<li>Backend verifies bearer token in middleware<\/li>\n<li>Frontend navigates user to role-appropriate page (<code>home<\/code>, <code>admin-dashboard<\/code>, <code>school-dashboard<\/code>, etc.)<\/li>\n<\/ul>\n<hr \/>\n<h2>5) Query\/Form Parameter Purpose Table<\/h2>\n<ul>\n<li><code>iss<\/code>: identifies trusted platform issuer<\/li>\n<li><code>client_id<\/code>: identifies BonuspointLearningTool tool registration in LMS<\/li>\n<li><code>login_hint<\/code>: opaque LMS-provided hint for launch\/user continuity<\/li>\n<li><code>lti_message_hint<\/code>: opaque LMS launch context hint<\/li>\n<li><code>target_link_uri<\/code>: intended tool launch target URL<\/li>\n<li><code>lti_deployment_id<\/code>: specific LMS deployment identifier for this tool install<\/li>\n<li><code>state<\/code>: anti-CSRF + transaction correlation<\/li>\n<li><code>nonce<\/code>: replay protection for id_token<\/li>\n<li><code>id_token<\/code>: signed platform assertion containing launch\/user\/role\/context claims<\/li>\n<li><code>kid<\/code>: key id in JWT header used to choose proper verification key from JWKS<\/li>\n<\/ul>\n<hr \/>\n<h2>6) Original conversational summary<\/h2>\n<ul>\n<li>Browser\/User -&gt; Blackboard: user wants to open BonuspointLearningTool tool<\/li>\n<li>Blackboard -&gt; BonuspointLearningTool: sends OIDC launch initiation with known client id and hints<\/li>\n<li>BonuspointLearningTool -&gt; Blackboard\/Browser: returns redirect to Anthology OIDC with generated state\/nonce<\/li>\n<li>Browser -&gt; Anthology: continues authentication launch<\/li>\n<li>Anthology + Blackboard runtime prepare signed <code>id_token<\/code><\/li>\n<li>Browser -&gt; BonuspointLearningTool launch callback: submits <code>id_token<\/code> + <code>state<\/code><\/li>\n<li>BonuspointLearningTool -&gt; Anthology runtime: fetches JWKS public keys and verifies signature<\/li>\n<li>BonuspointLearningTool -&gt; Browser: after validation, returns BonuspointLearningTool app session token and redirects to frontend<\/li>\n<li>BonuspointLearningTool frontend -&gt; Browser: initializes authenticated experience<\/li>\n<\/ul>\n<p>Corrections applied to original wording:<br \/>\n&#8211; Not \u201cdecryption\u201d of JWT signature; this is signature verification<br \/>\n&#8211; JWKS keys are platform-published rotating keys, not per-user \u201cdynamic session key\u201d semantics<br \/>\n&#8211; BonuspointLearningTool app session token is issued by BonuspointLearningTool backend and then used for BonuspointLearningTool API authorization<\/p>\n<hr \/>\n<h2>7) Mermaid sequence<\/h2>\n<p><img decoding=\"async\" style=\"max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 8px;\" src=\"http:\/\/localhost:8088\/wp-content\/uploads\/2026\/03\/full-sequence.png\" alt=\"Full SSO sequence chart\" \/><\/p>\n<hr \/>\n<h2>8) Additional implementation notes from code<\/h2>\n<ul>\n<li>Backend sets <code>httpOnly<\/code> cookie for token during redirect flow.<\/li>\n<li>Frontend callback also processes token from query and stores token\/context for app usage.<\/li>\n<li>Membership sync logic can use Blackboard REST API and token endpoint credentials.<\/li>\n<\/ul>\n<hr \/>\n<h2>9) Final concise takeaway<\/h2>\n<ul>\n<li>Platform launch token validation (LTI\/OIDC): asymmetric verification via JWKS (RS256 path)<\/li>\n<li>BonuspointLearningTool app token validation: shared-secret signing\/verification within BonuspointLearningTool<\/li>\n<li>Security of this flow depends on strict validation of state, nonce, issuer, audience, signature key id (<code>kid<\/code>), and expiry.<\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Introduction This article explains a real example of the SSO flow from Anthology Blackboard to a Custom Learning Tool (BonuspointLearningTool.sg). The SSO flow follows OpenID Connect using JWT. The same pattern can be applied to any Custom Learning Tool that is SSO-integrated with Blackboard. For security, sensitive encoded strings and key values are masked while [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":7,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[4,5,9,8,7,2,3,6],"class_list":["post-6","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-anthology","tag-blackboard","tag-edtech-integration","tag-jwks","tag-jwt","tag-lti-1-3","tag-oidc","tag-sso"],"_links":{"self":[{"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/posts\/6","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/comments?post=6"}],"version-history":[{"count":5,"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/posts\/6\/revisions"}],"predecessor-version":[{"id":30,"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/posts\/6\/revisions\/30"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/media\/7"}],"wp:attachment":[{"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/media?parent=6"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/categories?post=6"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.bonuspoint.info\/blog\/wp-json\/wp\/v2\/tags?post=6"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}