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 fieldFormatDescription
clientIdStringPublic integration identifier issued by Nanaade.
payloadBase64URLUTF-8 JSON assertion encoded without padding.
signatureBase64URLHMAC-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"
}
ClaimRulesWhy it exists
subNon-empty, stable tenant-side user IDPermanent identity binding
emailNon-empty email; normalized for membership lookupFirst-launch membership match
nameRequired on a user's first launch; 1–160 charactersCreates the Nanaade profile during JIT provisioning
iatUnix seconds; no more than 60 seconds in the futureAssertion creation time
expUnix seconds; not expired; at most 300 seconds after iatLimits stolen assertion lifetime
nonceUnique per assertion; use 24+ random bytesSingle-use replay protection
redirectOriginExact registered HTTPS originSafe post-exchange destination
launchCodeNanaade-registered launch codeRequests a module journey without accepting an arbitrary route
launchContextOptional object containing a constrained contextIdIdentifies a Nanaade record for registered deep-link launches
portalReturnUrlAbsolute URL on the submitting portal originSafe 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 codeLicensed featureNanaade destination
job-board.browsejob-boardJob Board
job-board.view-jobjob-boardJob details; requires launchContext.contextId
career-pathway.opencareer-pathwayCareer Pathway
career-pathway.roadmapcareer-pathwayGenerated roadmap
career-pathway.upskillingcareer-pathway.upskill-recommendationsUpskilling recommendations
student-copilot.openjobseeker-copilotManka Career Copilot
student-copilot.chatjobseeker-copilotManka chat
student-copilot.jobsjobseeker-copilot.job-discoveryCopilot job discovery
student-copilot.interview-preparationjobseeker-copilot.interview-preparationManka 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).

  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.

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_EXPIRED
  • TENANT_CONTRACT_EXPIRED