Skip to content

SAML and the identity provider

For the operator. Registering the grader with an institution's identity provider, what it asks for, and what it does with the answer.

The grader is a SAML 2.0 service provider. It asks the institution one question — who is this person — and takes a NetID from the answer. Roles, courses and permissions are its own business; see How sign-in works.

What to give your identity team

Three values, plus a metadata URL that supplies everything else. The service publishes its metadata in every auth mode, so you can deploy first and register second.

Value
Identifier (entity id) https://grader.example.edu
Reply URL (assertion consumer service) https://grader.example.edu/api/v1/auth/saml/acs
SP metadata https://grader.example.edu/api/v1/auth/saml/metadata
Requested attribute eduPersonPrincipalName (urn:oid:1.3.6.1.4.1.5923.1.1.1.6)
Optional displayName, uid, eduPersonAffiliation

A ready-to-send draft is in deploy/saml-registration.md in the repository.

Staging registers separately, with its own entity id and its own certificate. Student notebooks embed the server they were published from, so nothing published on staging can submit to production by accident.

How the NetID is derived

From eduPersonPrincipalName, taking the part before the @ and lowercasing it — f00abc1@example.edu becomes f00abc1. If the assertion carries no EPPN, the service falls back to uid and then to the NameID.

This is the assumption most worth checking

Enrollments are keyed on the NetID that came from your Canvas roster's SIS Login ID. If the identity provider's EPPN local part is not that same NetID, every enrollment lookup misses silently: people sign in successfully and then appear to be enrolled in nothing at all.

Verify it with one real account before a class depends on it.

Attributes are read by friendly name first and by OID second, because identity providers differ in which they release; either works.

The security settings

These are set by the service and are not configurable, because each one is a decision rather than a preference:

Setting Value Why
authnRequestsSigned on the identity provider can verify the request came from this service
wantAssertionsSigned on the assertion carrying the identity must be signed
wantMessagesSigned off many identity providers sign the assertion but not the response envelope; requiring both rejects perfectly valid logins
requestedAuthnContext off asking for a specific authentication context makes some identity providers refuse; the institution decides how it authenticates, including its second factor
Signature / digest algorithm RSA-SHA256 —
Clock skew 120 seconds GRADER_SAML_CLOCK_SKEW_SECONDS

The second factor — Duo, or whatever the institution uses — happens inside the identity provider. The grader never sees it, and never needs to.

Configuration

Setting Meaning
GRADER_SAML_SP_ENTITY_ID the identifier you registered; defaults to the base URL
GRADER_SAML_IDP_ENTITY_ID the identity provider's entity id
GRADER_SAML_IDP_SSO_URL its single sign-on endpoint (HTTP-POST binding)
GRADER_SAML_IDP_LOGOUT_URL where sign-out sends the browser
GRADER_SAML_IDP_CERT the identity provider's signing certificate, PEM or bare base64
GRADER_SAML_SP_CERT, GRADER_SAML_SP_KEY this service's own certificate and key

The service's certificate is self-signed and exists only to sign AuthnRequests and identify the service in metadata; generate it with a long life and treat rotating it as a re-registration:

openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
  -keyout sp.key -out sp.crt -subj "/CN=grader.example.edu"

Multi-line PEM values go into .env on one line with \n separators — see Deploy.

The Dartmouth configuration

Dartmouth moved from Apero CAS to Microsoft EntraID in 2026. The entity id and sign-on URL are already the defaults in config.py, so only the signing certificate has to be supplied:

GRADER_AUTH_MODE=saml
GRADER_SAML_SP_ENTITY_ID=https://grader.dartbrains.org
GRADER_SAML_IDP_ENTITY_ID=https://sts.windows.net/995b0936-48d6-40e5-a31e-bf689ec9446f/
GRADER_SAML_IDP_SSO_URL=https://login.microsoftonline.com/995b0936-48d6-40e5-a31e-bf689ec9446f/saml2
GRADER_SAML_IDP_LOGOUT_URL=https://logout.dartmouth.edu
GRADER_SAML_IDP_CERT=<X509Certificate body from the metadata below>

The IdP side comes from ITC's published metadata at https://dartmouth.github.io/dartmouth-idp-metadata/metadata.xml. Verify the certificate you paste against the thumbprint ITC publishes rather than trusting the copy you happen to have:

curl -s https://dartmouth.github.io/dartmouth-idp-metadata/metadata.xml \
  | sed -n 's:.*<X509Certificate>\(.*\)</X509Certificate>.*:\1:p' | tr -d ' \n' \
  | base64 -d | openssl x509 -inform der -noout -fingerprint -sha1

EntraID sources eduPersonPrincipalName from userPrincipalName, so the NetID rule above holds while Dartmouth accounts are netid@dartmouth.edu with name forms as aliases. ITC releases each attribute under both its OID and its friendly name.

Before registration is complete

Run with GRADER_AUTH_MODE=disabled. Students can open assignments and press Check all term; Submit tells them sign-in is not available yet and opens the moment you switch to saml and redeploy. No notebook changes.

Meanwhile, two paths let named people in: grader login-link for anyone with shell access to mint, and email sign-in links for self-service. Publishing an assignment does not need a browser at all — grader token mints a credential for the CLI.

When something is wrong

Symptom Usually
The identity provider refuses the request outright the service is not registered yet, or the entity id does not match exactly
SAML error: invalid_response the IdP signing certificate in .env is not the one the IdP is using — re-fetch the metadata
Signature validation fails intermittently clock drift; check the host's time sync before raising the skew
Sign-in succeeds, but the person is enrolled in nothing the NetID rule — see the warning above
Sign-in loops back to the login page cookies are being dropped: GRADER_COOKIE_SECURE=true without HTTPS end to end, or a frontend URL on a different site than the API

The service's own view of its configuration is at /api/health, which reports the environment and the auth mode without requiring a session.