swissoid-back
v2.3.3
Published
SwissOID authentication package for Node.js backends
Maintainers
Readme
swissoid-back
SwissOID authentication backend for Node.js applications. Provides reusable OIDC authentication components for integrating with SwissOID (Swiss eID).
Features
- 🔐 Full OIDC Authorization Code Flow implementation
- 🍪 Session management with Redis
- 🔑 JWT verification with JWKS (for validating SwissOID ID tokens and optional DATs)
- 🎯 Built for di-why dependency injection
- 📦 Reusable authentication components
- 🔄 Configurable via environment variables or appConfig
Installation
npm install swissoid-backRequirements
- Node.js >= 18
- Redis server for session storage
- SwissOID client credentials
Usage
With express-knifey / graphql-knifey (Recommended)
swissoid-back integrates with express-knifey middleware handles so gateways and standalone services can share the same DI wiring:
import DiContainer, { mergeLDs } from 'di-why';
import { apolloSubgraphServerModularLDGen } from 'graphql-knifey';
import { swissoidAuthLoadDict } from 'swissoid-back';
// Define middleware configuration including OIDC routes
const middlewareConfig = {
global: [
{ name: 'expressCorsMiddleware', priority: 90 },
{ name: 'expressCookieParserMiddleware', priority: 80 },
{ name: 'expressBodyParserMiddleware', priority: 70 },
{ name: 'oidcStandardRoutesMiddleware', priority: 60 },
],
'/graphql': [
{ name: 'expressGraphqlMiddleware', required: true, priority: -100 },
],
};
const diContainer = new DiContainer({
load: mergeLDs(
// GraphQL server with middleware config
apolloSubgraphServerModularLDGen({
resolvers,
typeDefs,
middlewareConfig
}),
// SwissOID authentication components
swissoidAuthLoadDict,
// Your other loaders...
)
});Direct Express Usage
import { swissoidAuthLoadDict } from 'swissoid-back';
// Load OIDC routes directly (this will mount them on the express app)
await diContainer.load('oidcStandardRoutes');AppConfig Integration
swissoid-back provides an appConfigMap that can be merged with your application's configuration:
import { swissoidMergeAppConfigMap } from 'swissoid-back';
const merged = swissoidMergeAppConfigMap(yourAppConfigMap);
export type AppConfig = ReturnType<typeof merged>;Configuration
Create a .env file with the required configuration (see .env.example for all options):
# SwissOID Configuration
SWISSOID_ISSUER=https://api.swissoid.com
SWISSOID_CLIENT_ID=your-client-id
SWISSOID_CLIENT_SECRET=your-client-secret
SWISSOID_TOKEN_ENDPOINT=https://api.swissoid.com/token
SWISSOID_JWKS_URI=https://api.swissoid.com/.well-known/jwks.json
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
# Session Configuration
SESSION_COOKIE_NAME=connect.sid
SESSION_SECRET=your-session-secret
# Optional: override derived state signing secret
# STATE_SIGNING_SECRET=your-state-secret
# RP Configuration
RP_FRONTEND_URL=http://localhost:3000
COOKIE_DOMAIN=localhost
OIDC_REDIRECT_BASE_URL=http://localhost:3668Generating strong secrets
Use the helper script to produce high-entropy values for
SESSION_SECRET and STATE_SIGNING_SECRET:
npm run generate:secretsYou can also invoke the packaged CLI from your own project:
npx swissoid-back-generate-secretsExample output:
SESSION_SECRET=8Qd8d...snipped
STATE_SIGNING_SECRET=Ob7v3...snipped
# Copy the values above into your deployment secret store (.env, Vault, etc.).
# Keep them private and rotate on a regular schedule.- Pass
--derive-stateto derive the state secret from the session secret (mirrors the default behaviour whenSTATE_SIGNING_SECRETis omitted). - Adjust entropy with
--session-bytes=<n>or--state-bytes=<n>if you need different lengths (defaults: 48 bytes for the session secret and 32 for the state secret).
Routes
The package provides the following OIDC routes when loaded:
GET /login- Initiates OIDC authorization flowPOST /oidc/callback- Handles OIDC callback from SwissOIDGET /oidc/finalize- Completes authentication and sets session (gateway subsequently mints a DAT for subgraphs)GET /auth/status- Returns current authentication statusPOST /auth/logout- Logs out the userGET /auth/debug- Debug endpoint to check session and cookie statusGET /auth/ping- Connectivity test endpoint
Components
The swissoidAuthLoadDict includes:
- redisClient: Redis connection for session storage
- sessionService: Session management service
- cookieManager: Cookie handling utilities
- oidcStandardRoutes: Express router with OIDC endpoints
- oidcStandardRoutesMiddleware: Middleware-compatible version for express-knifey integration
- swissoidAppConfigMap: AppConfig map with priority 60
Exports
// Main LoadDict for di-why
import { swissoidAuthLoadDict } from 'swissoid-back';
// AppConfig utilities
import { swissoidMergeAppConfigMap } from 'swissoid-back';
// Individual loaders if needed
import {
sessionService,
cookieManager,
oidcStandardRoutes,
oidcStandardRoutesMiddleware
} from 'swissoid-back';Integration Patterns
The following patterns are observed across working implementations:
Pattern 1: LoadDict Merging
Always merge swissoidAuthLoadDict with your application's loaders:
import { swissoidAuthLoadDict } from 'swissoid-back';
const loadDict: LoadDict = mergeLDs(
expressLoadDict, // Express foundation
graphqlKnifeyLoadDict, // GraphQL (if applicable)
swissoidAuthLoadDict, // ⬅️ SwissOID authentication
{
// Your loaders...
}
);Pattern 2: AppConfig Merging
Use swissoidMergeAppConfigMap() to combine your app's config with SwissOID's:
import { swissoidMergeAppConfigMap } from 'swissoid-back';
import { gkMergeAppConfigMap } from 'graphql-knifey';
const myAppConfigMap = function (env) {
return {
applicationName: 'My App',
// ... your config
};
};
// Merge with swissoid-back config, then graphql-knifey (if applicable)
const merged = gkMergeAppConfigMap(swissoidMergeAppConfigMap(myAppConfigMap));
export type AppConfig = ReturnType<typeof merged>;This provides type-safe access to all SwissOID config fields like swissoidClientId, rpCallbackUrl, etc.
Pattern 3: Middleware Configuration
For standalone services:
import { EXPRESS_MIDDLEWARE } from 'express-knifey';
import { SWISSOID_MIDDLEWARE } from 'swissoid-back';
const middlewareConfig = createGraphqlMiddlewareConfig({
global: [
EXPRESS_MIDDLEWARE.cors,
SWISSOID_MIDDLEWARE.oidcStandardRoutes // ⬅️ OIDC routes
],
});For gateways with priority-based middleware:
const middlewareConfig: MiddlewarePathConfig = {
'*': [
{ name: EXPRESS_MIDDLEWARE.trustProxy.name, priority: 100 },
{ name: EXPRESS_MIDDLEWARE.cors.name, priority: 90 },
{ name: EXPRESS_MIDDLEWARE.cookieParser.name, priority: 80 },
{ name: SWISSOID_MIDDLEWARE.oidcStandardRoutes.name, priority: 50 },
],
'/graphql': [
{ name: GRAPHQL_MIDDLEWARE.graphql.name, priority: 100 },
],
};Pattern 4: User Registration Hook (Optional)
To register users after OIDC authentication, provide an oidcUserRegistrar loader:
// src/loaders/oidcUserRegistrar.ts
const oidcUserRegistrar: LoadDictElement<(args: OnUserAuthenticatedArgs) => Promise<void>> = {
factory: ({ userService, logger }) => {
return async ({ claims }) => {
// Register/upsert user from OIDC claims
await userService.upsertFromOidc({
externalIssuer: claims.iss,
externalSubject: claims.sub,
email: claims.email,
username: claims.name || claims.email?.split('@')[0]
});
};
},
locateDeps: {
userService: 'userService',
logger: 'logger'
}
};
// Add to your loadDict
const loadDict = mergeLDs(
swissoidAuthLoadDict,
{ oidcUserRegistrar } // ⬅️ Hook will be called after successful auth
);The hook receives { claims, tokenResponse, request } and is called after id_token verification succeeds.
Pattern 5: GraphQL Context Enrichment
Include SwissOID services in your Apollo context for authenticated GraphQL resolvers:
import { apolloContextLDEGen } from 'graphql-knifey';
const loadDict = mergeLDs(
swissoidAuthLoadDict,
{
apolloContext: apolloContextLDEGen({
sessionService: 'sessionService', // ⬅️ Access sessions
cookieManager: 'cookieManager', // ⬅️ Cookie helpers
tokenAuthService: 'tokenAuthService',// ⬅️ Token validation (if using DATs)
appConfig: 'appConfig',
// ... your services
})
}
);Then in resolvers:
const resolvers = {
Query: {
me: async (_: any, __: any, context: AppContext) => {
const sessionId = context.cookieManager.extractSessionId(context.req);
if (!sessionId) throw new Error('Not authenticated');
const session = await context.sessionService.getSession(sessionId);
return session.sub; // User UUID
}
}
};License
MIT
