Skip to content

feat: add school-scoped learner saml single sign-on - #879

Merged
s3rot0n1n merged 1 commit into
platform-based-rewritefrom
feat/learner-sso
Nov 14, 2026
Merged

s3rot0n1n merged 1 commit into
platform-based-rewritefrom
feat/learner-sso

Conversation

@clbotdev

Copy link
Copy Markdown
Contributor

Lets a school's learners sign in to its storefront through the school's own SAML identity provider (Okta, Entra ID, Google Workspace, Keycloak and similar). SSO is for learners only. It never signs anyone in to the admin or to CourseLit as a platform.

What's in it

API

  • SP-initiated SAML only. The assertion must be signed, with RSA-SHA256. The subject must be stable: a persistent NameID, or an attribute the school picks. Requests and assertions can't be replayed. The ACS body is limited to 256 KB and rate limited.
  • Each failure is recorded with a reason (signature_invalid, wrong_audience, unstable_subject, ...), and the admin shows a hint for it.
  • A connection is draft, active, suspended or disabled. Changing what it trusts (certificates, sign-in URL, subject attribute) sends it back to draft and clears its last test, including when it's disabled, so it can be tested and turned on again.
  • Email domains are verified with a TXT record at _courselit-verification.<domain> and rechecked weekly by the worker. After three failed checks in a row, the domain becomes unverified and the owner gets an email.
  • Schools can require SSO for their domains. Email codes for those addresses are then refused.
  • Turning a sign-in method off, or turning a connection off, ends the sessions made with it right away.
  • An SSO session ends at the earlier of the connection's longest-session setting and the provider's SessionNotOnOrAfter.
  • SSO learners belong to shadow users that can never hold a CourseLit-wide Better Auth session.
  • Gated by the learner_sso plan feature, through billing.hasFeature (always on in oss mode). Login-methods settings are reachable over REST and MCP. The MCP tool is high-impact and needs confirmation.

Admin: Settings → Login providers (email codes, Google, SSO), and an SSO connection page with:

  • setup from metadata;
  • the values to give the identity provider;
  • domains with their DNS records;
  • a test sign-in;
  • enforcement and longest-session settings;
  • turn on and off.

Storefront: a "Sign in with " button on the sign-in and checkout forms. Addresses on domains that require SSO are sent to the button.

Dependencies: @codelitdev/platform and @codelitdev/billing 0.11.0 (codelitdev/platform#37), for shared domain verification and plan features. The SAML libraries (@node-saml/node-saml, xml-crypto, @xmldom/xmldom) are pinned exactly, with overrides, and have their own Dependabot group.

Docs:

  • frontlit/docs/self-hosting/single-sign-on.md;
  • learner sign-in notes;
  • a BILLING_MODE note;
  • docs/wip/rewrite-sso-implementation.md;
  • a project skill, .claude/skills/test-sso-with-keycloak, to browser-test against a real Keycloak.

The migration change rewrites the baseline, so reset the database after pulling.

Testing

  • Unit and integration tests: API SAML protocol, flow, admin, jobs, sign-in policy and login methods; admin lib/sso; storefront lib/login-methods. Admin has 121 tests passing. The workspace type-check passes.
  • I ran the API suite with a local .env that has Google keys: 578 of 582 passed.
    • Three failures came from tests that expected no Google keys. They now clear the keys themselves.
    • The fourth, ui-surface.test.ts, already fails on platform-based-rewrite: it reads section-authoring.tsx, which 2c77a4b9 deleted.
  • Browser-tested end to end against Keycloak 26.4, by me and manually by the reviewer over Tailscale:
    • setup from metadata, a test sign-in and turning the connection on;
    • learner sign-in to /dashboard, using the same identity again on the next sign-in;
    • requiring SSO refuses email codes;
    • turning the connection off ends the session;
    • response-only signing is refused with a hint.
    • This testing found the disabled-connection deadlock fixed here.

@gitguardian

gitguardian Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Schools on a plan with the learner_sso feature (every school in oss mode)
can let learners sign in through their own SAML identity provider.

- api: SP-initiated SAML with a signed assertion, RSA-SHA256, a stable
  subject, replay protection and audited failures; connections move
  through draft, active, suspended and disabled, and a change to what a
  connection trusts sends it back to draft to be tested again
- api: email domains are verified by DNS TXT and rechecked weekly; schools
  can require SSO for their domains; turning a method off ends its sessions
- admin: login providers settings, SSO connection setup with a test
  sign-in and hints for common provider mistakes
- storefront: "Sign in with <label>" on the sign-in and checkout forms
- uses @codelitdev/platform and @codelitdev/billing 0.11.0 for domain
  verification and plan features
- docs: self-hosting SSO guide and learner sign-in notes; a project skill
  to browser-test against Keycloak
@s3rot0n1n
s3rot0n1n self-requested a review November 14, 2026 08:37
@s3rot0n1n
s3rot0n1n merged commit f9c0e03 into platform-based-rewrite Nov 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants