Browse documentation
Integration reference

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).

  1. Generate/rotate in Nanaade Admin.
  2. Copy the reveal-once replacement into your secret manager.
  3. deploy and test within the overlap.
  4. 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.