Skip to main content

OIDC Node.js Example

This guide demonstrates how to integrate NetActuate OIDC authentication into a Node.js application using the openid-client library with TypeScript.

Prerequisites

  • Node.js 18 or later
  • A NetActuate OIDC client ID and client secret (configured at Account → Settings → OIDC)

Project Setup

Initialize a new project and install dependencies:

mkdir netactuate-oidc-node && cd netactuate-oidc-node
npm init -y
npm install openid-client express express-session
npm install -D typescript @types/express @types/express-session ts-node

Create a tsconfig.json:

{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist"
}
}

Configuration

Create a config.ts file:

export const oidcConfig = {
issuerUrl: "https://portal.netactuate.com",
clientId: process.env.OIDC_CLIENT_ID || "your-client-id",
clientSecret: process.env.OIDC_CLIENT_SECRET || "your-client-secret",
redirectUri: "http://localhost:3000/callback",
scope: "openid profile email",
};

Note: Always use environment variables for secrets in production. Never commit credentials to version control.


Full Implementation

Create an app.ts file:

import express from "express";
import session from "express-session";
import { Issuer, Client, generators } from "openid-client";
import { oidcConfig } from "./config";

const app = express();

// Configure session middleware
app.use(
session({
secret: "replace-with-secure-random-string",
resave: false,
saveUninitialized: false,
})
);

let oidcClient: Client;

async function initializeOIDC(): Promise<void> {
const issuer = await Issuer.discover(oidcConfig.issuerUrl);
console.log("Discovered issuer:", issuer.metadata.issuer);

oidcClient = new issuer.Client({
client_id: oidcConfig.clientId,
client_secret: oidcConfig.clientSecret,
redirect_uris: [oidcConfig.redirectUri],
response_types: ["code"],
});
}

// Home page
app.get("/", (req, res) => {
res.send('<html><body><a href="/login">Log in with NetActuate</a></body></html>');
});

// Initiate OIDC login
app.get("/login", (req, res) => {
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);

// Store code verifier in session for PKCE
(req.session as any).codeVerifier = codeVerifier;

const authUrl = oidcClient.authorizationUrl({
scope: oidcConfig.scope,
code_challenge: codeChallenge,
code_challenge_method: "S256",
state: generators.state(),
});

res.redirect(authUrl);
});

// Handle callback
app.get("/callback", async (req, res) => {
try {
const params = oidcClient.callbackParams(req);
const codeVerifier = (req.session as any).codeVerifier;

const tokenSet = await oidcClient.callback(oidcConfig.redirectUri, params, {
code_verifier: codeVerifier,
});

// Get user info
const userInfo = await oidcClient.userinfo(tokenSet.access_token!);

res.json({
message: "Authentication successful",
user: {
sub: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
},
tokens: {
accessTokenExpires: tokenSet.expires_at,
hasRefreshToken: !!tokenSet.refresh_token,
},
});
} catch (err) {
console.error("Callback error:", err);
res.status(500).json({ error: "Authentication failed" });
}
});

// Token refresh endpoint
app.get("/refresh", async (req, res) => {
try {
const refreshToken = (req.session as any).refreshToken;
if (!refreshToken) {
res.status(401).json({ error: "No refresh token available" });
return;
}

const tokenSet = await oidcClient.refresh(refreshToken);
(req.session as any).refreshToken = tokenSet.refresh_token;

res.json({
message: "Token refreshed",
accessTokenExpires: tokenSet.expires_at,
});
} catch (err) {
console.error("Refresh error:", err);
res.status(500).json({ error: "Token refresh failed" });
}
});

// Start server
const PORT = 3000;
initializeOIDC().then(() => {
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
});

Running the Application

Set your credentials as environment variables and start the server:

export OIDC_CLIENT_ID="your-client-id"
export OIDC_CLIENT_SECRET="your-client-secret"
npx ts-node app.ts

Open your browser and navigate to http://localhost:3000. Click the login link to initiate the OIDC flow.


Security Features

This example includes several security best practices:

  • PKCE (Proof Key for Code Exchange): Protects the authorization code flow against interception attacks
  • Server-side sessions: Tokens are stored server-side, not in the browser
  • Token refresh: Demonstrates how to refresh expired access tokens

Production Considerations

  • Use a production-grade session store (e.g., Redis)
  • Enable HTTPS for all endpoints
  • Implement CSRF protection
  • Add proper logging and error handling
  • Set secure cookie options (secure: true, httpOnly: true, sameSite: 'strict')

Need Help?

If you run into issues, contact NetActuate Support.