Skip to main content

Node.js / Express SDK

Complete guide to integrating Signia authentication in Node.js and Express backend applications.

Installation​

npm install @getsignia/signia-auth-sdk express express-session

Setup​

1. Initialize OIDC Client​

Create the OIDC client with your configuration:

import { OIDCClient } from '@getsignia/signia-auth-sdk';

export const oidcClient = new OIDCClient({
clientId: 'my-backend-api-abc123', // Copy from Signia ID Dashboard
clientSecret: 'YOUR_CLIENT_SECRET', // Copy from Signia ID Dashboard (shown once!)
redirectUri: 'http://localhost:3001/auth/callback',
issuer: 'https://YOUR_TENANT.signiaauth.com',
scopes: ['openid', 'profile', 'email']
});
Client Secret

Unlike frontend apps, backend applications use a client secret for secure token exchange. Never expose this secret to clients.

2. Configure Express Session​

import express from 'express';
import session from 'express-session';

const app = express();

app.use(session({
secret: 'your-session-secret', // Change in production
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));

Authentication Routes​

Login Route​

app.get('/login', async (req, res) => {
try {
const authUrl = await oidcClient.getAuthorizationUrl();
res.redirect(authUrl);
} catch (error) {
console.error('Login error:', error);
res.status(500).send('Authentication failed');
}
});

Callback Route​

app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;

if (!code) {
return res.status(400).send('Missing authorization code');
}

try {
// Exchange code for tokens
const tokens = await oidcClient.exchangeCodeForTokens(code as string);

// Verify and decode ID token
const userInfo = await oidcClient.verifyIdToken(tokens.id_token);

// Store user info in session
req.session.user = userInfo;
req.session.tokens = tokens;

res.redirect('/dashboard');
} catch (error) {
console.error('Callback error:', error);
res.status(500).send('Authentication failed');
}
});

Logout Route​

app.get('/logout', async (req, res) => {
const idToken = req.session.tokens?.id_token;

// Clear session
req.session.destroy((err) => {
if (err) {
console.error('Session destroy error:', err);
}
});

// Redirect to OIDC logout endpoint
const logoutUrl = await oidcClient.getLogoutUrl(idToken);
res.redirect(logoutUrl);
});

Middleware​

Authentication Middleware​

Protect routes that require authentication:

function requireAuth(req: express.Request, res: express.Response, next: express.NextFunction) {
if (!req.session.user) {
return res.redirect('/login');
}
next();
}

// Usage
app.get('/dashboard', requireAuth, (req, res) => {
res.render('dashboard', { user: req.session.user });
});

Token Refresh Middleware​

Automatically refresh expired tokens:

async function refreshTokenMiddleware(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
const tokens = req.session.tokens;

if (!tokens?.refresh_token) {
return next();
}

// Check if access token is expired or about to expire
const decoded = oidcClient.decodeToken(tokens.access_token);
const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);

if (expiresIn < 300) { // Refresh if less than 5 minutes remaining
try {
const newTokens = await oidcClient.refreshAccessToken(tokens.refresh_token);
req.session.tokens = newTokens;
} catch (error) {
console.error('Token refresh failed:', error);
return res.redirect('/login');
}
}

next();
}

Making Authenticated API Calls​

Using the HTTP Client​

app.get('/api/user-data', requireAuth, async (req, res) => {
try {
const httpClient = await oidcClient.getAuthHttpClient(
req.session.tokens.access_token
);

const response = await httpClient.get('https://api.example.com/user');
res.json(response.data);
} catch (error) {
console.error('API call failed:', error);
res.status(500).json({ error: 'Failed to fetch user data' });
}
});

Manual Token Usage​

import axios from 'axios';

app.get('/api/protected', requireAuth, async (req, res) => {
const accessToken = req.session.tokens.access_token;

try {
const response = await axios.get('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});

res.json(response.data);
} catch (error) {
res.status(500).json({ error: 'API request failed' });
}
});

User Information​

Access User Profile​

app.get('/api/me', requireAuth, (req, res) => {
res.json({
sub: req.session.user.sub,
email: req.session.user.email,
name: req.session.user.name,
email_verified: req.session.user.email_verified
});
});

Fetch User Info from OIDC Endpoint​

app.get('/api/userinfo', requireAuth, async (req, res) => {
try {
const userInfo = await oidcClient.getUserInfo(
req.session.tokens.access_token
);
res.json(userInfo);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch user info' });
}
});

Complete Example​

import express from 'express';
import session from 'express-session';
import { OIDCClient } from '@getsignia/signia-auth-sdk';

const app = express();
const port = 3001;

// Initialize OIDC client
const oidcClient = new OIDCClient({
clientId: process.env.OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET!,
redirectUri: `http://localhost:${port}/auth/callback`,
issuer: process.env.OIDC_ISSUER!,
scopes: ['openid', 'profile', 'email']
});

// Session configuration
app.use(session({
secret: process.env.SESSION_SECRET || 'dev-secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000
}
}));

// Middleware
function requireAuth(req: express.Request, res: express.Response, next: express.NextFunction) {
if (!req.session.user) {
return res.redirect('/login');
}
next();
}

// Routes
app.get('/', (req, res) => {
if (req.session.user) {
res.redirect('/dashboard');
} else {
res.send(`
<h1>Welcome</h1>
<a href="/login">Login with Signia</a>
`);
}
});

app.get('/login', async (req, res) => {
const authUrl = await oidcClient.getAuthorizationUrl();
res.redirect(authUrl);
});

app.get('/auth/callback', async (req, res) => {
const { code } = req.query;

try {
const tokens = await oidcClient.exchangeCodeForTokens(code as string);
const userInfo = await oidcClient.verifyIdToken(tokens.id_token);

req.session.user = userInfo;
req.session.tokens = tokens;

res.redirect('/dashboard');
} catch (error) {
console.error('Authentication error:', error);
res.status(500).send('Authentication failed');
}
});

app.get('/dashboard', requireAuth, (req, res) => {
res.send(`
<h1>Dashboard</h1>
<p>Welcome, ${req.session.user.email}</p>
<p>User ID: ${req.session.user.sub}</p>
<a href="/logout">Logout</a>
`);
});

app.get('/logout', (req, res) => {
req.session.destroy(() => {
res.redirect('/');
});
});

app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});

TypeScript Support​

Type Declarations​

import { Request } from 'express';

declare module 'express-session' {
interface SessionData {
user: {
sub: string;
email?: string;
name?: string;
email_verified?: boolean;
};
tokens: {
access_token: string;
id_token: string;
refresh_token?: string;
expires_in: number;
};
}
}

Security Best Practices​

1. Use Environment Variables​

# .env file
OIDC_CLIENT_ID=my-backend-api-abc123 # From Signia ID Dashboard
OIDC_CLIENT_SECRET=your-client-secret # From Signia ID Dashboard (shown once!)
OIDC_ISSUER=https://tenant.signiaauth.com
SESSION_SECRET=your-random-secret
NODE_ENV=production

2. Secure Session Configuration​

app.use(session({
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Prevent XSS
sameSite: 'lax', // CSRF protection
maxAge: 3600000 // 1 hour
}
}));

3. Validate Redirect URIs​

const ALLOWED_REDIRECT_URIS = [
'http://localhost:3001/auth/callback',
'https://myapp.com/auth/callback'
];

app.get('/auth/callback', async (req, res) => {
const redirectUri = req.query.redirect_uri as string;

if (!ALLOWED_REDIRECT_URIS.includes(redirectUri)) {
return res.status(400).send('Invalid redirect URI');
}

// ... rest of callback logic
});

4. Use PKCE (Proof Key for Code Exchange)​

const oidcClient = new OIDCClient({
clientId: process.env.OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET!,
redirectUri: 'http://localhost:3001/auth/callback',
issuer: process.env.OIDC_ISSUER!,
scopes: ['openid', 'profile', 'email'],
usePKCE: true // Enable PKCE
});

Troubleshooting​

"Invalid client" error​

Make sure your client secret is correct and matches what's configured in Signia dashboard.

Session not persisting​

Check that your session secret is set and cookies are being sent:

// Check cookie configuration
cookie: {
secure: process.env.NODE_ENV === 'production', // false for local dev
sameSite: 'lax' // not 'strict' which can break redirects
}

Token expired errors​

Implement token refresh middleware to automatically renew tokens before they expire.

Next Steps​