Install
$ agentstack add skill-thunder-id-skills-integrate-node ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
ThunderID — Node.js Integration
Assumes ThunderID is running at https://localhost:8090. If not, run /setup-thunderid first.
@thunderid/node is the generic Node.js SDK — use it with the built-in http module, Fastify, Hono, Koa, or any other Node.js framework. For Express, use /integrate-express instead.
Step 1 — Register an Application
Ask the developer to create an application in ThunderID and share the Client ID and Client Secret before continuing.
Guide them through these steps:
- Open
https://localhost:8090/consoleand sign in (default:admin/secret) - Navigate to Applications → New Application
- Fill in:
- Name: their app name (e.g.
my-node-app) - Type: Web Application
- Authorized Redirect URL:
http://localhost:3000/callback
- Click Create and copy the Client ID and Client Secret shown on the next screen
Once they paste both values, use them in all subsequent steps. Do not use placeholders — wait for the real values.
Step 2 — Install
Detect the package manager from lockfiles: pnpm-lock.yaml → pnpm add, yarn.lock → yarn add, bun.lockb → bun add, else npm install.
npm install @thunderid/node
Step 3 — Initialize the Client
Create index.js and initialize 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: '',
clientSecret: '',
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
? 'View profile | Sign out'
: 'Sign in'
);
} 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(`
Welcome, ${user.name || user.username}!
Email: ${user.email}
Sign out
`);
} 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();
How the sign-in flow works:
GET /login— generates a session ID cookie and callssignInwith anauthUrlCallbackthat redirects to ThunderID.GET /callback— ThunderID redirects back withcodeandstate; callingsignInagain exchanges the code for tokens.GET /logout— callssignOutto get the OIDC end-session URL, clears the cookie, redirects to complete logout at ThunderID.
Step 4 — Run and Verify
node index.js
Visit http://localhost:3000 — click the sign-in link to authenticate, then view your profile at /profile.
Troubleshooting
Certificate error — Set NODE_TLS_REJECT_UNAUTHORIZED=0 in .env for local development (remove before deploying).
invalid_client — Double-check the Client ID and Client Secret.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: thunder-id
- Source: thunder-id/skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.