Build a complete Node.js launch route
This example uses a Next.js route handler, but the signing function works in Express, Fastify, NestJS, or any Node server. The route assumes your application has already authenticated the user.
1. Configure environment variables
NANAADE_API_ORIGIN=https://api.nanaade.ai
NANAADE_TENANT_ORIGIN=https://northbridge.nanaade.ai
NANAADE_RETURN_PATH=/dashboard
NANAADE_SSO_CLIENT_ID=tn_replace_with_client_id
NANAADE_SSO_SECRET=replace_with_reveal_once_secret
Do not prefix the secret with NEXT_PUBLIC_. Validate these variables during application startup so a missing secret fails deployment rather than a user's launch.
2. Create the signing function
import crypto from "node:crypto";
type NanaadeIdentity = {
id: string; // permanent ID in your system
email: string;
};
export function createNanaadeAssertion(user: NanaadeIdentity) {
const secret = process.env.NANAADE_SSO_SECRET;
const clientId = process.env.NANAADE_SSO_CLIENT_ID;
const redirectOrigin = process.env.NANAADE_TENANT_ORIGIN;
if (!secret || !clientId || !redirectOrigin) {
throw new Error("Nanaade SSO configuration is incomplete");
}
const now = Math.floor(Date.now() / 1000);
const assertion = {
sub: user.id,
email: user.email.trim().toLowerCase(),
iat: now,
exp: now + 300,
nonce: crypto.randomBytes(24).toString("base64url"),
redirectOrigin: redirectOrigin.replace(/\/$/, ""),
returnPath: process.env.NANAADE_RETURN_PATH || "/dashboard",
};
const payload = Buffer
.from(JSON.stringify(assertion), "utf8")
.toString("base64url");
const signature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("base64url");
return { clientId, payload, signature };
}Generate this object per click. Never cache it: the nonce is single-use and the time window is short.
3. Escape form values
Because values are inserted into HTML, escape them even though Nanaade-generated identifiers are constrained:
function escapeHtml(value: string) {
return value.replace(/[&<>"']/g, (character) => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[character]!);
}4. Add the launch route
import { NextResponse } from "next/server";
import { createNanaadeAssertion } from "@/lib/nanaade-sso";
import { getAuthenticatedUser } from "@/lib/auth";
export async function GET() {
const user = await getAuthenticatedUser();
if (!user) {
return NextResponse.redirect(new URL("/login", process.env.PORTAL_ORIGIN));
}
const fields = createNanaadeAssertion({ id: user.id, email: user.email });
const endpoint = new URL(
"/api/v1/sso/exchange",
process.env.NANAADE_API_ORIGIN,
).toString();
const inputs = Object.entries(fields).map(([name, value]) =>
`<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}">`
).join("");
const html = `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Opening Nanaade</title></head>
<body>
<p>Opening your Nanaade workspace…</p>
<form id="nanaade" method="post" action="${escapeHtml(endpoint)}">
${inputs}
<noscript><button type="submit">Continue to Nanaade</button></noscript>
</form>
<script>document.getElementById("nanaade").submit()</script>
</body></html>`;
return new NextResponse(html, {
headers: {
"content-type": "text/html; charset=utf-8",
"cache-control": "no-store",
"content-security-policy":
`default-src 'none'; script-src 'unsafe-inline'; form-action ${new URL(endpoint).origin}`,
"referrer-policy": "no-referrer",
},
});
}
Link to the route from your authenticated UI:
<a href="/api/nanaade/launch">Open Nanaade career workspace</a>
Express equivalent
app.get("/integrations/nanaade/launch", requireUser, (req, res) => {
const fields = createNanaadeAssertion({
id: req.user.id,
email: req.user.email,
});
// Render the same escaped, no-store auto-submit form shown above.
res.status(200).type("html").set("Cache-Control", "no-store").send(renderForm(fields));
});
Common implementation mistakes
| Mistake | Result |
|---|---|
| Signing JSON.stringify(assertion) instead of encoded payload | SSO_SIGNATURE_INVALID |
| Using Date.now() directly | Timestamp is milliseconds; assertion rejected |
| Reusing a generated form | SSO_ASSERTION_REPLAYED or expiry |
| Using email as sub | Identity breaks after email changes |
| Calling exchange with backend fetch() | User browser does not receive the intended Nanaade session |
| Putting secret in client code | Credential compromise; rotate immediately |
The repository's tenant_system application is the runnable reference implementation for valid, expired, replayed, invalid-signature, inactive-member, and unknown-member scenarios.