Skip to main content

Node.js Quickstart

Use this guide to add ThunderID authentication to a vanilla Node.js application using the @thunderid/node SDK and the built-in http module — no framework required.

What You Will Learn
  • Create a Node.js project
  • Install the @thunderid/node package
  • Add working sign-in and sign-out routes
  • Protect routes and display the signed-in user's profile
Prerequisites
  • About 15 minutes
  • Steps 1–3 complete: ThunderID running, an application registered, and a sign-in flow built. Start at Get ThunderID if you haven't already.
  • Node.js 18+ installed on your system
  • npm, yarn, or pnpm
  • Your preferred code editor
1

Create a Node.js Project

Initialize a new Node.js project:

mkdir my-node-app
cd my-node-app
npm init -y
2

Install @thunderid/node

Install the ThunderID Node.js SDK:

npm install @thunderid/node
3

Initialize the Client

Create an index.js file and initialize the ThunderIDNodeClient with your application credentials:

index.js
const http = require('http');
const { URL } = require('url');
const { randomUUID } = require('crypto');
const { ThunderIDNodeClient } = require('@thunderid/node');

const PORT = 3000;
const SESSION_COOKIE = 'tid_session';

const auth = new ThunderIDNodeClient();

function getSessionId(req) {
const cookieHeader = req.headers.cookie ?? '';
for (const part of cookieHeader.split(';')) {
const [name, value] = part.trim().split('=');
if (name === SESSION_COOKIE) return decodeURIComponent(value);
}
return null;
}

async function main() {
await auth.initialize({
clientId: '<your-client-id>',
clientSecret: '<your-client-secret>',
baseUrl: 'https://localhost:8090',
afterSignInUrl: 'http://localhost:3000/callback',
afterSignOutUrl: 'http://localhost:3000',
});

const server = http.createServer(async (req, res) => {
// routes added in the next step
});

server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
}

main();
Configuration

Replace <your-client-id> and <your-client-secret> with the values from your ThunderID application. Set the authorized redirect URL in your application settings to http://localhost:3000/callback.

Configuration Parameters

ParameterDescription
clientIdThe Client ID from your ThunderID application
clientSecretThe Client Secret from your ThunderID application
baseUrlYour ThunderID instance URL (e.g., https://localhost:8090)
afterSignInUrlThe callback URL ThunderID redirects to after sign-in
afterSignOutUrlThe URL to redirect to after sign-out
4

Add Sign-In and Sign-Out Routes

The signIn method works in two phases: it first redirects the user to ThunderID, then handles the authorization code on the callback. Session state is tied to a session ID stored in a cookie.

Replace the // routes added in the next step comment with:

index.js
    const url = new URL(req.url, `http://localhost:${PORT}`);

try {
if (url.pathname === '/login') {
let sessionId = getSessionId(req);
const extraHeaders = {};
if (!sessionId) {
sessionId = randomUUID();
extraHeaders['Set-Cookie'] =
`${SESSION_COOKIE}=${sessionId}; HttpOnly; SameSite=Lax; Path=/`;
}
await auth.signIn((authUrl) => {
res.writeHead(302, { ...extraHeaders, Location: authUrl });
res.end();
}, sessionId);

} else if (url.pathname === '/callback') {
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const sessionState = url.searchParams.get('session_state');
const sessionId = getSessionId(req);

if (!sessionId || !code || !state) {
res.writeHead(400);
return res.end('Bad request');
}

await auth.signIn(() => {}, sessionId, code, sessionState, state);
res.writeHead(302, { Location: '/profile' });
res.end();

} else if (url.pathname === '/logout') {
const sessionId = getSessionId(req);
if (!sessionId) {
res.writeHead(302, { Location: '/' });
return res.end();
}
const signOutUrl = await auth.signOut(sessionId);
res.writeHead(302, {
Location: signOutUrl,
'Set-Cookie': `${SESSION_COOKIE}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`,
});
res.end();

}
} catch {
res.writeHead(500);
res.end('Internal server error');
}

How the sign-in flow works:

  1. GET /login — generates a session ID, stores it in a cookie, and calls signIn with an authUrlCallback. The callback receives the ThunderID authorization URL and redirects the user's browser there.
  2. GET /callbackThunderID redirects back with code and state query parameters. Calling signIn again with those values exchanges the code for tokens and stores the session.
  3. GET /logout — calls signOut to get the OIDC end-session URL, clears the local cookie, then redirects the browser to complete the logout at ThunderID.
5

Protect a Route and Display User Info

Use isSignedIn to guard routes and getUser to retrieve the authenticated user's profile. Add these inside the same try block, before the closing }:

index.js
      if (url.pathname === '/') {
const sessionId = getSessionId(req);
const signedIn = sessionId && (await auth.isSignedIn(sessionId));
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(signedIn
? '<a href="/profile">View profile</a> | <a href="/logout">Sign out</a>'
: '<a href="/login">Sign in</a>'
);

} else if (url.pathname === '/profile') {
const sessionId = getSessionId(req);
if (!sessionId || !(await auth.isSignedIn(sessionId))) {
res.writeHead(302, { Location: '/login' });
return res.end();
}
const user = await auth.getUser(sessionId);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<h1>Welcome, ${user.name || user.username}!</h1>
<p><strong>Email:</strong> ${user.email}</p>
<p><strong>First name:</strong> ${user.given_name}</p>
<p><strong>Last name:</strong> ${user.family_name}</p>
<a href="/logout">Sign out</a>
`);

} else if (url.pathname === '/login') {
6

Complete index.js

Here is the full file for reference:

index.js
const http = require('http');
const { URL } = require('url');
const { randomUUID } = require('crypto');
const { ThunderIDNodeClient } = require('@thunderid/node');

const PORT = 3000;
const SESSION_COOKIE = 'tid_session';

const auth = new ThunderIDNodeClient();

function getSessionId(req) {
const cookieHeader = req.headers.cookie ?? '';
for (const part of cookieHeader.split(';')) {
const [name, value] = part.trim().split('=');
if (name === SESSION_COOKIE) return decodeURIComponent(value);
}
return null;
}

async function main() {
await auth.initialize({
clientId: '<your-client-id>',
clientSecret: '<your-client-secret>',
baseUrl: 'https://localhost:8090',
afterSignInUrl: 'http://localhost:3000/callback',
afterSignOutUrl: 'http://localhost:3000',
});

const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`);

try {
if (url.pathname === '/') {
const sessionId = getSessionId(req);
const signedIn = sessionId && (await auth.isSignedIn(sessionId));
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(signedIn
? '<a href="/profile">View profile</a> | <a href="/logout">Sign out</a>'
: '<a href="/login">Sign in</a>'
);

} else if (url.pathname === '/profile') {
const sessionId = getSessionId(req);
if (!sessionId || !(await auth.isSignedIn(sessionId))) {
res.writeHead(302, { Location: '/login' });
return res.end();
}
const user = await auth.getUser(sessionId);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<h1>Welcome, ${user.name || user.username}!</h1>
<p><strong>Email:</strong> ${user.email}</p>
<p><strong>First name:</strong> ${user.given_name}</p>
<p><strong>Last name:</strong> ${user.family_name}</p>
<a href="/logout">Sign out</a>
`);

} else if (url.pathname === '/login') {
let sessionId = getSessionId(req);
const extraHeaders = {};
if (!sessionId) {
sessionId = randomUUID();
extraHeaders['Set-Cookie'] =
`${SESSION_COOKIE}=${sessionId}; HttpOnly; SameSite=Lax; Path=/`;
}
await auth.signIn((authUrl) => {
res.writeHead(302, { ...extraHeaders, Location: authUrl });
res.end();
}, sessionId);

} else if (url.pathname === '/callback') {
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const sessionState = url.searchParams.get('session_state');
const sessionId = getSessionId(req);

if (!sessionId || !code || !state) {
res.writeHead(400);
return res.end('Bad request');
}

await auth.signIn(() => {}, sessionId, code, sessionState, state);
res.writeHead(302, { Location: '/profile' });
res.end();

} else if (url.pathname === '/logout') {
const sessionId = getSessionId(req);
if (!sessionId) {
res.writeHead(302, { Location: '/' });
return res.end();
}
const signOutUrl = await auth.signOut(sessionId);
res.writeHead(302, {
Location: signOutUrl,
'Set-Cookie': `${SESSION_COOKIE}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`,
});
res.end();

} else {
res.writeHead(404);
res.end('Not found');
}
} catch {
res.writeHead(500);
res.end('Internal server error');
}
});

server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
}

main();
7

Run Your App

Start the server:

node index.js

Open http://localhost:3000.

Success

You should see the sign-in link. Click it to be redirected to the ThunderID-hosted sign-in page. After authenticating with your test user, you'll return to the /profile route with your user profile displayed.

You're Done

You have completed the full getting started sequence:

  1. ThunderID running
  2. ✅ Application registered with Client ID and Client Secret
  3. ✅ Sign-in flow built in the Flow Designer
  4. ✅ Node.js app integrated and authenticating

What's Next

ThunderID LogoThunderID Logo

Product

DocsAPIsSDKs
© WSO2 LLC. All rights reserved.Privacy PolicyCookie Policy