Skip to content
TDACorpIDENTITYSign in

Next.js quickstart

Register a client, install the SDK, mount four routes in a Next.js app and sign in once.

On this page

This page takes a Next.js application from nothing to one working sign-in. It uses the relying-party module of the identity SDK, which does discovery, PKCE, the pushed authorization request, client authentication and token checks for you.

Before you start#

You needWhy
Node 22.12 or laterThe SDK requires it.
The SDK@tdacorp/identity-sdk, from the public npm registry. See the note below.
A registered clientIdentity only signs people in to clients it knows. Step 1.

Note

Install the SDK with pnpm add @tdacorp/identity-sdk. The examples import from that name. The package is published by TDACorp under the @tdacorp scope; if a copy of an older private tarball is pinned in your project, repack against 2.0.0 (the server now requires DPoP for clients that hold authorization codes).

1. Register a client#

Generate the key pair on your machine. You keep the private key and send only the public half, because identity authenticates your application with a signed assertion (private_key_jwt) and has no client secret to leak.

import { generateKeyPairSync } from 'node:crypto'// Run once on your machine. Ed25519 and P-256 are the two key types identity accepts.const { publicKey, privateKey } = generateKeyPairSync('ed25519')// The key id names this key in every client assertion you sign, and in the request to register it.export const kid = 'app-key-1'// Send this and the kid to whoever registers your client. It is public.export const publicJwk = { ...publicKey.export({ format: 'jwk' }), kid }// Keep this in your secret manager. It never leaves your infrastructure.export const privateJwk = privateKey.export({ format: 'jwk' })

An administrator registers the client for you. What to send, and the rules your redirect addresses must meet, are on Get a client registered. For this quickstart the values are:

ValueQuickstart value
Typeweb
Redirect URIhttps://app.example.com/api/auth/callback
Scopesopenid, profile, email
Grantsauthorization_code and refresh_token
Public keyThe publicJwk from the script, with its kid

You get back a client id that starts with tdaid_c_. A client that holds a code or refresh grant is registered with DPoP required by default, so the SDK needs a DPoP key too: see DPoP.

2. Configure the relying party#

Put the client id, the client key and a DPoP key in your secret store and read them from the environment. createRelyingParty reads discovery from the issuer on first use and keeps it for five minutes.

import { createRelyingParty, importDpopKeyPair } from '@tdacorp/identity-sdk/rp'// Secrets come from your secret store, never from source. The values are placeholders.const clientKey = JSON.parse(process.env.TDAID_CLIENT_KEY_JWK ?? '{}')const dpopKey = JSON.parse(process.env.TDAID_DPOP_KEY_JWK ?? '{}')export const rp = createRelyingParty({  issuer: 'https://identity.tdacorp.in',  clientId: process.env.TDAID_CLIENT_ID ?? '',  redirectUri: 'https://app.example.com/api/auth/callback',  // The private half of the key whose public JWK was registered with the client.  key: { kid: process.env.TDAID_CLIENT_KID ?? '', privateKey: clientKey },  // Required for any client registered with require_dpop. Keep it stable: refresh tokens are bound to it.  dpop: await importDpopKeyPair(dpopKey),})

redirectUri must equal the registered value exactly, and issuer must equal the issuer string identity publishes, byte for byte.

3. Mount the routes#

createNextAuthHandlers returns one GET and one POST that dispatch on the last path segment of the request. Put them in a catch-all route file.

// app/api/auth/[action]/route.tsimport { signSession } from '@tdacorp/identity-sdk/sealed'import { createNextAuthHandlers } from '@tdacorp/identity-sdk/rp/next'import { rp } from './relying-party'const cookieSecret = process.env.APP_COOKIE_SECRET ?? '' // at least 32 bytes// One process only. A shared store replaces this when the app runs on several instances.const endedSessions = new Set<string>()export const { GET, POST } = createNextAuthHandlers({  rp,  appOrigin: 'https://app.example.com',  cookieSecret,  scope: 'openid profile email offline_access',  defaultReturnTo: '/dashboard',  // Runs after the SDK has checked the callback and verified the ID token.  onLogin: async (login) => {    // Persist login.tokens.refreshToken on your server before answering: it rotates on every refresh.    const session = await signSession(      { sub: login.idToken.sub, ...(login.idToken.sid === null ? {} : { sid: login.idToken.sid }) },      { secret: cookieSecret, ttlSeconds: 3600, purpose: 'app:session' },    )    return new Response(null, {      status: 303,      headers: {        location: login.redirectTo,        'set-cookie': `app_session=${session.value}; Path=/; Max-Age=${String(session.maxAge)}; HttpOnly; SameSite=Lax; Secure`,      },    })  },  logout: {    postLogoutRedirectUri: 'https://app.example.com/signed-out',    clearSessionCookies: () => ['app_session=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax; Secure'],  },  backchannelLogout: {    // Make this idempotent: the issuer retries when you answer with an error.    onLogout: (token) => {      if (token.sid !== null) endedSessions.add(token.sid)    },  },})
PathMethodWhat it does
/api/auth/loginGETPushes the authorization request and redirects the browser to identity.
/api/auth/callbackGETChecks the response, exchanges the code, verifies the ID token and calls your onLogin.
/api/auth/logoutPOSTEnds your session and redirects to identity's end-session address. Only mounted when you pass logout.
/api/auth/backchannel-logoutPOSTReceives logout tokens from identity. Only mounted when you pass backchannelLogout.

An unknown last segment answers 404. The helpers read the path of the request only, never its host: every absolute address they produce comes from appOrigin and the registered redirect URI.

4. Sign in once#

  1. Start your app and open /api/auth/login?returnTo=/dashboard. The returnTo value must be a path on your own app; anything else is ignored.
  2. Identity shows its own sign-in page. A third-party client then shows a consent screen the first time, listing the scopes you asked for. A first-party client skips it.
  3. Identity redirects back to /api/auth/callback. If every check passes, onLogin runs with the verified ID token and the tokens. In the sample it sets a session cookie and redirects to returnTo.

If the callback answers with an error code (400, or 500 for internal_error), the code names the step that failed (for example state_mismatch or token_verification_failed). The default body never repeats text from identity.

What to do next#

Verified against commit 5d85077