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/nodepackage
- Add working sign-in and sign-out routes
- Protect routes and display the signed-in user's profile
Prerequisites
- About 15 minutes
- Node.js 18+ installed on your system
- npm, yarn, or pnpm
- Your preferred code editor
Run ThunderID
Start a local ThunderID instance. Pick the method that works best for you:
Requires Node.js 18+
Full install guide →Once it's running, the console is available at https://localhost:8090/console.
Create an Application
-
Sign in to the Console.
Test UserIf you used the default setup, sign in to the Console as
adminwith the password generated during setup and printed to the setup output (unless you supplied your own). -
Navigate to Applications, and click Add Application.
-
Under Technology, select Node.js.
-
Enter a name (e.g.
My Node.js App) and create an application. The rest of the settings can stay at their defaults. -
Copy both the Client ID and Client Secret from the window that pops up. The Client ID can also be found in the General tab.
-
Under General, add
http://localhost:3000/callbackto the list of Authorized Redirect URIs.
Create a Node.js Project
Initialize a new Node.js project:
npm
Yarn
pnpm
mkdir my-node-app
cd my-node-app
npm init -y
mkdir my-node-app
cd my-node-app
yarn init -y
mkdir my-node-app
cd my-node-app
pnpm init
Install @thunderid/node
Install the ThunderID Node.js SDK:
npm
Yarn
pnpm
npm install @thunderid/node
yarn add @thunderid/node
pnpm add @thunderid/node
Initialize the Client
Create an index.js file and initialize the ThunderIDNodeClient with your application credentials:
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();
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
| Parameter | Description |
|---|---|
clientId | The Client ID from your ThunderID application |
clientSecret | The Client Secret from your ThunderID application |
baseUrl | Your ThunderID instance URL (e.g., https://localhost:8090) |
afterSignInUrl | The callback URL ThunderID redirects to after sign-in |
afterSignOutUrl | The URL to redirect to after sign-out |
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:
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:
GET /login: generates a session ID, stores it in a cookie, and callssignInwith anauthUrlCallback. The callback receives the ThunderID authorization URL and redirects the user's browser there.GET /callback: ThunderID redirects back withcodeandstatequery parameters. CallingsignInagain with those values exchanges the code for tokens and stores the session.GET /logout: callssignOutto get the OIDC end-session URL, clears the local cookie, then redirects the browser to complete the logout at ThunderID.
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 }:
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') {
Complete index.js
Here is the full file for reference:
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();
Run Your App
Start the server:
npm
Yarn
pnpm
node index.js
yarn node index.js
pnpm node index.js
Open http://localhost:3000.
You'll need a user to sign in with. If you haven't created one yet, open https://localhost:8090/console, navigate to Users, and add a test user with an email and password.
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.