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