SSO assertion contract
Nanaade tenant SSO uses a short-lived JSON assertion protected with HMAC-SHA256. It is designed for a browser handoff from an already-authenticated tenant portal—not as a general API token.
Exchange request
POST /api/v1/sso/exchange HTTP/1.1
Host: api.nanaade.ai
Origin: https://portal.northbridge.edu
Content-Type: application/x-www-form-urlencoded
clientId=tn_...&payload=eyJ...&signature=x9D...
| Form field | Format | Description |
|---|---|---|
clientId | String | Public integration identifier issued by Nanaade. |
payload | Base64URL | UTF-8 JSON assertion encoded without padding. |
signature | Base64URL | HMAC-SHA256 digest of the exact encoded payload. |
All three fields are required and case-sensitive. Use application/x-www-form-urlencoded; JSON is not the browser handoff contract.
Assertion schema
{
"sub": "student_10482",
"email": "amara@northbridge.edu",
"name": "Amara Okafor",
"iat": 1784635200,
"exp": 1784635500,
"nonce": "FnrYfZkFG9gIQSllpQOxBkVUxAL5Vi-A",
"redirectOrigin": "https://northbridge.nanaade.ai",
"launchCode": "job-board.browse",
"portalReturnUrl": "https://portal.northbridge.edu/career-services"
}
| Claim | Rules | Why it exists |
|---|---|---|
sub | Non-empty, stable tenant-side user ID | Permanent identity binding |
email | Non-empty email; normalized for membership lookup | First-launch membership match |
name | Required on a user's first launch; 1–160 characters | Creates the Nanaade profile during JIT provisioning |
iat | Unix seconds; no more than 60 seconds in the future | Assertion creation time |
exp | Unix seconds; not expired; at most 300 seconds after iat | Limits stolen assertion lifetime |
nonce | Unique per assertion; use 24+ random bytes | Single-use replay protection |
redirectOrigin | Exact registered HTTPS origin | Safe post-exchange destination |
launchCode | Nanaade-registered launch code | Requests a module journey without accepting an arbitrary route |
launchContext | Optional object containing a constrained contextId | Identifies a Nanaade record for registered deep-link launches |
portalReturnUrl | Absolute URL on the submitting portal origin | Safe destination if access cannot be completed |
Milliseconds are incorrect. In JavaScript use Math.floor(Date.now() / 1000), not Date.now().
Encoding and signature
json = JSON.stringify(assertion)
payload = base64url(UTF8(json))
mac = HMAC-SHA256(key=tenantSecret, message=payload)
signature = base64url(mac)
Sign the encoded payload string exactly as submitted. Do not sign the original object, standard Base64 text, decoded bytes, or a re-serialized copy.
const payload = Buffer.from(JSON.stringify(assertion), "utf8").toString("base64url");
const signature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("base64url");
Base64URL uses - and _ instead of + and /, and omits = padding.
Validation behavior
Nanaade validates the integration and active tenant, submitting origin, signature, JSON encoding, time window, nonce, identity claims, membership policy, subject binding, redirect origin, portal return URL, and launch code. A nonce is consumed during validation and cannot be submitted again.
For tenants using the default jit provisioning mode, an unknown identity is created during this exchange. Nanaade creates the user and active membership atomically, then derives the Nanaade role from the tenant type. A university provisions a student; employer and partner tenants provision a regular user. Assertion claims never grant Nanaade administrator roles, permissions, or modules.
Launch codes are requests, not grants
Your portal sends the code for the action the user selected. Nanaade owns the registry that maps the code to its required feature, audience, and existing application route.
| Launch code | Licensed feature | Nanaade destination |
|---|---|---|
job-board.browse | job-board | Job Board |
job-board.view-job | job-board | Job details; requires launchContext.contextId |
career-pathway.open | career-pathway | Career Pathway |
career-pathway.roadmap | career-pathway | Generated roadmap |
career-pathway.upskilling | career-pathway.upskill-recommendations | Upskilling recommendations |
student-copilot.open | jobseeker-copilot | Manka Career Copilot |
student-copilot.chat | jobseeker-copilot | Manka chat |
student-copilot.jobs | jobseeker-copilot.job-discovery | Copilot job discovery |
student-copilot.interview-preparation | jobseeker-copilot.interview-preparation | Manka interview preparation |
Launch-code names remain stable integration identifiers even when the underlying product catalog evolves. The portal cannot send a feature grant or a Nanaade path. For example, a signed student-copilot.open request still fails when the tenant has not licensed jobseeker-copilot.
{
"launchCode": "job-board.view-job",
"launchContext": {
"contextId": "66b87b12e78ce43f51a92800"
}
}
Nanaade validates the context format and constructs the destination itself. Values such as ../../admin-dashboard, full URLs, and unregistered launch codes are rejected.
On success Nanaade signs a tenant-scoped session, stores it in an HTTP-only cookie, and returns:
HTTP/1.1 303 See Other
Location: https://northbridge.nanaade.ai/dashboard
Set-Cookie: token=...; HttpOnly; Secure; SameSite=None; Path=/
In production on nanaade.ai hosts, the cookie is scoped to .nanaade.ai. Custom-domain deployments need routing and cookie behavior verified on the final hostname.
First launch and membership conflict
The normal first launch needs no prior Nanaade registration:
Authenticated tenant user
→ tenant backend signs the assertion
→ Nanaade creates the user and active membership if absent
→ Nanaade creates the tenant-scoped session
→ browser enters the licensed workspace
If the email already belongs to a user with active sponsored access under another tenant, Nanaade does not create a duplicate user or a second active membership. The browser is redirected to a branded conflict page. The user may request an audited transfer, contact support, or return to the signed portalReturnUrl.
Origin and return-path rules
Configured origins are exact. These are different:
https://portal.northbridge.edu
https://www.portal.northbridge.edu
https://portal.northbridge.edu:8443
The browser's Origin, redirectOrigin, and the origin of portalReturnUrl must be registered. The portal return origin must also equal the submitting browser origin. Production origins must use HTTPS. Nanaade administrators register destination namespaces such as /dashboard; a launch code may resolve inside that namespace, but the tenant cannot choose the resulting path.
Optional JSON response mode
The exchange controller supports ?mode=json and returns a redirect URL after setting the session cookie:
{ "success": true, "data": { "redirectUrl": "https://northbridge.nanaade.ai/dashboard" } }
For normal cross-origin launches, prefer the documented form submission and 303 flow. JSON mode does not make server-to-server exchange suitable for establishing a browser session.
Rotation
Rotation activates a new secret immediately. The previous secret can remain valid for an administrator-selected overlap from 0 to 60 minutes (15 minutes by default).
- Generate/rotate in Nanaade Admin.
- Copy the reveal-once replacement into your secret manager.
- deploy and test within the overlap.
- Confirm the old secret fails after the overlap.
Never compare signatures with ordinary string equality in your own verifier; use timing-safe comparison. Nanaade already performs timing-safe verification.
Credential and agreement expiry
Nanaade administrators may assign an expiry date to an SSO credential and a separate end date to the tenant agreement. An expired credential cannot be exchanged even when its signature is valid. When a non-renewing agreement ends, Nanaade suspends the tenant and revokes active SSO credentials during the scheduled expiry sweep.
Store these errors as organization-level integration states rather than asking the user to sign in repeatedly:
SSO_CREDENTIAL_EXPIREDTENANT_CONTRACT_EXPIRED