JavaScript / TypeScript SDK

Integrate Signward authentication into Node.js, Express, and browser apps with the @signward/idserver-client package.

JavaScript / TypeScript SDK

The @signward/idserver-client package adds Signward OIDC authentication to Node.js, Express, and the browser. Open source (MIT), no Microsoft-stack dependency.

  • OIDC Authorization Code flow with PKCE (S256)
  • Local JWT validation via the server's JWKS, with discovery + JWKS caching
  • Typed user model with built-in and per-tenant custom roles
  • Universal: works in Node 18+ and modern browsers (native fetch + Web Crypto)
  • First-class Express middleware

Install

npm install @signward/idserver-client

Peer dependency for the Express middleware: express >= 4.18.

Quickstart — Express (protecting an API)

import express from 'express';
import { IdServerClient } from '@signward/idserver-client';
import { requireAuth, requireRole } from '@signward/idserver-client/express';

const app = express();

const idserver = new IdServerClient({
  authority: 'https://mytenant.signward.com',
  clientId: 'my-api',
  clientSecret: process.env.IDSERVER_CLIENT_SECRET,
});

// Build auth middleware once, reuse on any route
const auth = requireAuth(idserver);

app.get('/me', auth, (req, res) => {
  res.json({
    id: req.user!.userId,
    email: req.user!.email,
    tenantId: req.user!.tenantId,
    roles: req.user!.roles,
    customRoles: req.user!.customRoles,
  });
});

app.get('/admin', auth, requireRole('admin'), (req, res) => {
  res.json({ ok: true });
});

app.get('/editors', auth, requireRole('admin', 'editor'), (req, res) => {
  res.json({ ok: true });
});

app.get(
  '/billing-admin',
  auth,
  requireRole('admin', 'billing', { requireAll: true }),
  (req, res) => {
    res.json({ ok: true });
  },
);

app.listen(3000);

Clients call your API with a bearer token obtained from Signward:

GET /me
Authorization: Bearer eyJhbGciOi...

Quickstart — server-side login flow (Express / Node)

import { IdServerClient } from '@signward/idserver-client';

const client = new IdServerClient({
  authority: 'https://mytenant.signward.com',
  clientId: 'my-webapp',
  clientSecret: process.env.IDSERVER_CLIENT_SECRET,
});

// 1) Build the login URL (with PKCE) and redirect
app.get('/login', async (req, res) => {
  const { url, verifier } = await client.authorizeUrlWithPkce({
    redirectUri: 'https://myapp.com/callback',
    state: req.query.returnTo as string ?? '/',
  });
  // Store the verifier in the session so you can pass it back later
  req.session!.pkceVerifier = verifier;
  res.redirect(url);
});

// 2) Handle the callback
app.get('/callback', async (req, res) => {
  const code = req.query.code as string;
  const tokens = await client.exchangeCode(code, {
    redirectUri: 'https://myapp.com/callback',
    codeVerifier: req.session!.pkceVerifier,
  });

  // Validate locally to get the user
  const user = await client.validateToken(tokens.access_token);

  req.session!.user = { email: user.email, roles: user.roles };
  req.session!.tokens = tokens;

  res.redirect((req.query.state as string) ?? '/');
});

// 3) Refresh an expired access token
app.get('/refresh', async (req, res) => {
  const fresh = await client.refreshToken(req.session!.tokens.refresh_token);
  req.session!.tokens = fresh;
  res.json({ ok: true });
});

// 4) Logout
app.get('/logout', async (req, res) => {
  const url = await client.endSessionUrl({
    idTokenHint: req.session!.tokens?.id_token,
    postLogoutRedirectUri: 'https://myapp.com/',
  });
  req.session!.destroy?.(() => res.redirect(url));
});

Quickstart — browser (SPA)

The same IdServerClient works in the browser using native fetch + Web Crypto:

import { IdServerClient } from '@signward/idserver-client';

const client = new IdServerClient({
  authority: 'https://mytenant.signward.com',
  clientId: 'my-spa',
  // No clientSecret in the browser — use PKCE
});

// Login button handler
async function login() {
  const { url, verifier } = await client.authorizeUrlWithPkce({
    redirectUri: window.location.origin + '/callback',
  });
  sessionStorage.setItem('pkce', verifier);
  window.location.href = url;
}

// Callback page
async function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code')!;
  const verifier = sessionStorage.getItem('pkce')!;
  sessionStorage.removeItem('pkce');

  const tokens = await client.exchangeCode(code, {
    redirectUri: window.location.origin + '/callback',
    codeVerifier: verifier,
  });

  const user = await client.validateToken(tokens.access_token);
  console.log('Logged in as', user.email, user.roles);

  // Store tokens — consider using HttpOnly cookies via your backend instead
  sessionStorage.setItem('access_token', tokens.access_token);
}

Security tip: for browser apps, prefer storing tokens in HttpOnly cookies set by your backend rather than in sessionStorage / localStorage. Never embed a client secret in browser code.

User model

interface User {
  userId: string | null;        // sub claim
  email: string | null;
  emailVerified: boolean | null;
  name: string | null;
  givenName: string | null;
  familyName: string | null;
  tenantId: string | null;
  roles: string[];              // built-in roles
  customRoles: string[];        // per-tenant RBAC roles
  claims: Record<string, unknown>;

  hasRole(role: string): boolean;
  hasCustomRole(role: string): boolean;
  hasAnyRole(...roles: string[]): boolean;
  hasAllRoles(...roles: string[]): boolean;
}

Role checks are case-insensitive.

Error handling

All errors derive from IdServerError:

import {
  IdServerError,
  InvalidTokenError,
  TokenExchangeError,
  DiscoveryError,
  UserInfoError,
} from '@signward/idserver-client';

try {
  const user = await client.validateToken(token);
} catch (err) {
  if (err instanceof InvalidTokenError) { /* 401 */ }
  else if (err instanceof TokenExchangeError) { console.log(err.error, err.description, err.statusCode); }
  else if (err instanceof DiscoveryError) { /* can't reach IdServer */ }
}

Other SDKs