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",
"iat": 1784635200,
"exp": 1784635500,
"nonce": "FnrYfZkFG9gIQSllpQOxBkVUxAL5Vi-A",
"redirectOrigin": "https://northbridge.nanaade.ai",
"returnPath": "/dashboard"
}
| 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 |
| 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 |
| returnPath | Registered safe relative path | Destination inside the workspace |
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, active membership, subject binding, redirect origin, and return path. A nonce is consumed during validation and cannot be submitted again.
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.
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 must be registered when present, and redirectOrigin must also be in the integration allowlist. Production origins must use HTTPS. Return paths must begin with one /, cannot begin with //, and must use safe relative-path characters.
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.