docs: clarify DAT usage and middleware configuration

Updated middleware example to use 'global' for OIDC routes.
Noted that ID token verification is distinct from DAT minting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
Guillermo Pages 2 months ago
parent 8c114f7a61
commit b3554c1310

@ -6,7 +6,7 @@ SwissOID authentication backend for Node.js applications. Provides reusable OIDC
- 🔐 Full OIDC Authorization Code Flow implementation
- 🍪 Session management with Redis
- 🔑 JWT verification with JWKS
- 🔑 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
@ -27,7 +27,7 @@ npm install swissoid-back
### With express-knifey / graphql-knifey (Recommended)
swissoid-back integrates seamlessly with express-knifey's middleware system:
`swissoid-back` integrates with `express-knifey` middleware handles so gateways/standalone services can share the same DI wiring used across Cronide:
```typescript
import DiContainer, { mergeLDs } from 'di-why';
@ -36,16 +36,15 @@ import { swissoidAuthLoadDict } from 'swissoid-back';
// Define middleware configuration including OIDC routes
const middlewareConfig = {
'/graphql': [
global: [
{ name: 'expressCorsMiddleware', priority: 90 },
{ name: 'expressCookieParserMiddleware', priority: 80 },
{ name: 'expressBodyParserMiddleware', priority: 70 },
{ name: 'oidcStandardRoutesMiddleware', priority: 60 },
],
'/graphql': [
{ name: 'expressGraphqlMiddleware', required: true, priority: -100 },
],
'*': [
// Add OIDC routes as global middleware
{ name: 'oidcStandardRoutesMiddleware', priority: 50 },
]
};
const diContainer = new DiContainer({
@ -117,7 +116,7 @@ The package provides the following OIDC routes when loaded:
- `GET /login` - Initiates OIDC authorization flow
- `POST /oidc/callback` - Handles OIDC callback from SwissOID
- `POST /oidc/finalize` - Completes authentication and sets session
- `POST /oidc/finalize` - Completes authentication and sets session (gateway subsequently mints a DAT for subgraphs)
- `GET /auth/status` - Returns current authentication status
- `POST /auth/logout` - Logs out the user
@ -152,4 +151,4 @@ import {
## License
MIT
MIT

@ -1,96 +0,0 @@
# STEP 01 Wire project dependencies and environment for swissoid-back
Updated `biblio-stats-graphql` to depend on `swissoid-back` and its runtime requirements (express-knifey, ioredis, redis) while aligning Express to v4 for compatibility. Also documented the new SwissOID and Redis environment variables the service must expose.
```diff
--- a/package.json
+++ b/package.json
@@
- "express": "^5.1.0",
- "ghooks": "^2.0.4",
- "graphql-knifey": "^7.1.2",
- "jose": "^6.1.0",
- "mysql-oh-wait-utils": "^0.5.1",
- "saylo": "0.6.3",
- "swiss-army-knifey": "^1.36.4",
- "typescript": "5.9.2",
- "uuid": "^13.0.0"
+ "express": "^4.21.2",
+ "express-knifey": "^1.1.2",
+ "ghooks": "^2.0.4",
+ "graphql-knifey": "^7.1.2",
+ "ioredis": "^5.7.0",
+ "mysql-oh-wait-utils": "^0.5.1",
+ "redis": "^5.8.2",
+ "saylo": "0.6.3",
+ "swiss-army-knifey": "^1.36.4",
+ "swissoid-back": "^2.2.1",
+ "typescript": "5.9.2",
+ "uuid": "^13.0.0"
```
```diff
--- a/.env
+++ b/.env
@@
-# SwissOID Configuration
-SWISSOID_CLIENT_ID=biblio-stats
-SWISSOID_ISSUER=https://api.swissoid.com
-SWISSOID_JWKS_URI=https://api.swissoid.com/.well-known/jwks.json
-ACCESS_TOKEN_COOKIE=swissoid_access_token
-REFRESH_TOKEN_COOKIE=swissoid_refresh_token
-COOKIE_DOMAIN=.biblio-stats.meow.ch
+# SwissOID Configuration for swissoid-back
+SWISSOID_CLIENT_ID=biblio-stats
+SWISSOID_ISSUER=https://api.swissoid.com
+SWISSOID_JWKS_URI=https://api.swissoid.com/.well-known/jwks.json
+SWISSOID_TOKEN_ENDPOINT=https://api.swissoid.com/token
+SWISSOID_AUTHORIZE_ENDPOINT=https://api.swissoid.com/authorize
+
+# Relying party (this service) OIDC + session configuration
+OIDC_REDIRECT_BASE_URL=http://localhost:3666
+RP_FRONTEND_URL=http://localhost:5173
+RP_COOKIE_DOMAIN=.biblio-stats.meow.ch
+SESSION_COOKIE_NAME=biblio_stats_session
+REFRESH_COOKIE_NAME=biblio_stats_refresh
+SESSION_SECRET=dev-super-secret-change-me
+STATE_SIGNING_SECRET=dev-super-secret-change-me-state
+SESSION_TTL=7200
+REFRESH_TTL=604800
+
+# Redis connection for session storage
+REDIS_URL=redis://localhost:6379
```
```diff
--- a/.env.prod
+++ b/.env.prod
@@
-# SwissOID Configuration
-SWISSOID_CLIENT_ID=biblio-stats
-SWISSOID_ISSUER=https://api.swissoid.com
-SWISSOID_JWKS_URI=https://api.swissoid.com/.well-known/jwks.json
-ACCESS_TOKEN_COOKIE=swissoid_access_token
-REFRESH_TOKEN_COOKIE=swissoid_refresh_token
-COOKIE_DOMAIN=.biblio-stats.meow.ch
+# SwissOID Configuration for swissoid-back
+SWISSOID_CLIENT_ID=biblio-stats
+SWISSOID_ISSUER=https://api.swissoid.com
+SWISSOID_JWKS_URI=https://api.swissoid.com/.well-known/jwks.json
+SWISSOID_TOKEN_ENDPOINT=https://api.swissoid.com/token
+SWISSOID_AUTHORIZE_ENDPOINT=https://api.swissoid.com/authorize
+
+# Relying party (this service) OIDC + session configuration
+OIDC_REDIRECT_BASE_URL=https://graphql.biblio-stats.meow.ch
+RP_FRONTEND_URL=https://biblio-stats.meow.ch
+RP_COOKIE_DOMAIN=.biblio-stats.meow.ch
+SESSION_COOKIE_NAME=biblio_stats_session
+REFRESH_COOKIE_NAME=biblio_stats_refresh
+SESSION_SECRET=prod-super-secret-change-me
+STATE_SIGNING_SECRET=prod-super-secret-change-me-state
+SESSION_TTL=7200
+REFRESH_TTL=604800
+
+# Redis connection for session storage
+REDIS_URL=redis://redis:6379
```

@ -1,72 +0,0 @@
# STEP 02 Replace local auth wiring with swissoid-back loaders
Integrated the shared `swissoidAuthLoadDict` and express middleware handles, rebuilding the DI container so the service mounts SwissOID routes through express-knifey. This removes the bespoke loaders and ensures session/cookie services are injected into the Apollo context.
## Loader rewrite (`biblio-stats-graphql/src/loaders/index.ts`)
```ts
import 'dotenv/config';
import statModel from './statsModel';
import statService from './statsService';
import userModel from './userModel';
import userService from './userService';
import localAppConfigMap from '../config/appConfig';
import { mysqlReqLoader as mysqlReq, mysqlMultipleReqLoader as mysqlMultipleReq } from 'mysql-oh-wait-utils';
import resolvers from '../graphql/resolvers';
import graphqlSchema from '../graphql/schema';
import coreToDomain from './coreToDomain';
import logger from './logger';
import DiContainer, { addMergeableConfigMap, AppConfigNamespace, LoadDict, mergeLDs } from 'di-why';
import { apolloContextLDEGen, apolloStandaloneServerModularLDGen, createGraphqlMiddlewareConfig } from 'graphql-knifey';
import { swissoidAuthLoadDict, SWISSOID_MIDDLEWARE } from 'swissoid-back';
const appConfigMapNamespace: AppConfigNamespace = {
namespace: 'biblioStatsAppConfigMap',
priority: 61,
};
const middlewareConfig = createGraphqlMiddlewareConfig({
global: [SWISSOID_MIDDLEWARE.oidcStandardRoutes],
});
const loadDict: LoadDict = mergeLDs(
swissoidAuthLoadDict,
apolloStandaloneServerModularLDGen({
resolvers,
typeDefs: graphqlSchema,
middlewareConfig,
}),
{
mysqlReq,
mysqlMultipleReq,
logger,
statModel,
statService,
coreToDomain,
userModel,
userService,
apolloContext: apolloContextLDEGen({
statService: 'statService',
userService: 'userService',
coreToDomain: 'coreToDomain',
sessionService: 'sessionService',
cookieManager: 'cookieManager',
appConfig: 'appConfig',
logger: 'logger',
}),
...addMergeableConfigMap(localAppConfigMap, appConfigMapNamespace),
}
);
const diContainer = new DiContainer({ load: loadDict });
export default diContainer;
```
## Express launcher (`biblio-stats-graphql/src/index.ts`)
```ts
await di.load('expressLauncher');
```
Deleted the redundant SwissOID stubs (`src/loaders/tokenAuthService.ts`, `src/loaders/swissoidAuthMiddleware.ts`, `src/loaders/swissoidOidcRoutes.ts`, `src/loaders/apolloContextWithSwissoidAuth.ts`, `src/middleware/swissoidAuth.ts`, `src/setupAuth.ts`) since the shared package now owns the flow.

@ -1,154 +0,0 @@
# STEP 03 Adapt GraphQL context, authorization, and docs to SwissOID sessions
Aligned the GraphQL layer with the session-based flow exposed by `swissoid-back`, eliminating the old JWT token helpers and ensuring resolvers consume the shared session service.
## Updated types (`biblio-stats-graphql/src/generalTypes.ts`)
```ts
import { ActionOutcomeError, ActionOutcomeFail, ActionOutcomeSuccess, ActionStatus, UUIDProp } from 'graphql-knifey';
import type { Request } from 'express';
import type { CookieManager, SwissOIDSessionService } from 'swissoid-back';
import UserService from './services/UserService';
import StatService from './services/StatService';
import type { AppConfig } from './config/appConfig';
export type BiblioStatsGraphQLContext = {
statService: StatService;
userService: UserService;
coreToDomain: (core: string) => string;
sessionService?: SwissOIDSessionService;
cookieManager?: CookieManager;
appConfig?: AppConfig;
logger?: { log: (...args: any[]) => void };
token?: string;
req?: Request & { cookies?: Record<string, string> };
user?: {
UUID?: string;
email?: string;
username?: string;
sub?: string;
};
};
```
## Session-backed authorization (`biblio-stats-graphql/src/utils/authorize.ts`)
```ts
import type { SessionData } from 'swissoid-back';
import { BiblioStatsGraphQLContext, UserProp } from '../generalTypes';
import { getFailOutcomeFromError } from 'graphql-knifey';
import { ActionOutcomeFail, ActionOutcomeForbidden, ActionStatus, GqlResolversContextParams } from 'graphql-knifey/build/src/generalTypes';
const unauthenticatedOutcome: ActionOutcomeFail<'UNAUTHENTICATED'> = {
status: ActionStatus.fail,
code: 'UNAUTHENTICATED',
message: 'Authentication required',
};
function buildUserFromSession(session: SessionData) {
return {
UUID: session.sub,
email: session.email,
username: session.metadata?.username ?? session.name ?? session.email,
sub: session.sub,
};
}
async function resolveSession(context: GqlResolversContextParams<BiblioStatsGraphQLContext>) {
const { sessionService, cookieManager, req } = context;
if (!sessionService || !req) {
return null;
}
const cookieName = context.appConfig?.sessionCookieName || 'sid';
let sessionId = cookieManager?.getSessionId?.(req) || req.cookies?.[cookieName];
if (!sessionId) {
const rawCookies = req.headers?.cookie ?? req.headers?.Cookie;
const cookieHeader = Array.isArray(rawCookies) ? rawCookies.join(';') : rawCookies;
if (cookieHeader) {
sessionId = cookieHeader
.split(';')
.map((cookie) => cookie.trim().split('=') as [string, string])
.find(([name]) => name === cookieName)?.[1];
}
}
if (!sessionId) {
return null;
}
const session = await sessionService.getSession(sessionId);
return session ?? null;
}
export async function authorized<T extends {}>(
context: GqlResolversContextParams<BiblioStatsGraphQLContext>,
input: T
): Promise<{ shouldReturn: true; value: ActionOutcomeForbidden | ActionOutcomeFail<'ERROR'> | ActionOutcomeFail<'UNAUTHENTICATED'> } | { shouldReturn: false; value: UserProp & T }>
{
try {
const session = await resolveSession(context);
if (!session) {
return { shouldReturn: true, value: unauthenticatedOutcome };
}
const props = {
...input,
user: buildUserFromSession(session),
};
context.user = props.user;
const isAllowed = await context.userService.existsOrRegisterAndIfExistsIsAllowed({ user: props.user });
if (!isAllowed) {
return {
shouldReturn: true,
value: {
status: ActionStatus.fail,
code: 'FORBIDDEN',
message: "You don't have permission to perform this request",
},
};
}
return {
shouldReturn: false,
value: props,
};
} catch (err) {
return {
shouldReturn: true,
value: getFailOutcomeFromError(err as Error),
};
}
}
```
## Resolver integration (`biblio-stats-graphql/src/graphql/resolvers/index.ts`)
```ts
import { GQLResolverDict, getFailOutcomeFromError } from 'graphql-knifey';
// ...
registerUser: async (_, __, context) => {
try {
const authResult = await authorized(context, {});
if (authResult.shouldReturn) return authResult.value;
return await context.userService.registerUser(authResult.value);
} catch (err) {
return getFailOutcomeFromError(err as Error);
}
},
```
## Documentation (`biblio-stats-graphql/README.md`)
```md
Authentication is handled by the shared SwissOID backend (`swissoid-back`). The service mounts the reusable OIDC routes so `/login`, `/oidc/callback`, `/auth/status`, and `/auth/logout` follow the exact same flow as cronide-user. Sessions are stored in Redis and materialised as HttpOnly cookies on the relying party domain.
Resolvers read the active session via `sessionService` and `cookieManager` from `swissoid-back`. When a valid session cookie is present the user UUID/email is injected into the GraphQL context and used for downstream authorization.
```
Legacy JWT helpers were deleted (`src/loaders/tokenAuthService.ts`, `src/middleware/swissoidAuth.ts`, `src/setupAuth.ts`, and related middleware wrappers) because the SwissOID middleware now covers the full flow.

@ -1,70 +0,0 @@
# STEP 04 Provide Redis runtime for SwissOID sessions
Added a Redis service to biblio-stats-graphqls docker compose files so swissoid-backs session store has a local endpoint, and wired the required environment variables through the containers.
## docker-compose.dev.yml
```yaml
services:
redis:
image: redis:7-alpine
container_name: "${APP_DIRNAME}_redis"
command: ["redis-server", "--save", "", "--appendonly", "no"]
restart: always
garphql-server-stats:
environment:
LOGGER_DEBUG: "${LOGGER_DEBUG}"
SWISSOID_CLIENT_ID: "${SWISSOID_CLIENT_ID}"
SWISSOID_ISSUER: "${SWISSOID_ISSUER}"
SWISSOID_JWKS_URI: "${SWISSOID_JWKS_URI}"
SWISSOID_TOKEN_ENDPOINT: "${SWISSOID_TOKEN_ENDPOINT}"
SWISSOID_AUTHORIZE_ENDPOINT: "${SWISSOID_AUTHORIZE_ENDPOINT}"
OIDC_REDIRECT_BASE_URL: "${OIDC_REDIRECT_BASE_URL}"
RP_FRONTEND_URL: "${RP_FRONTEND_URL}"
RP_COOKIE_DOMAIN: "${RP_COOKIE_DOMAIN}"
SESSION_COOKIE_NAME: "${SESSION_COOKIE_NAME}"
REFRESH_COOKIE_NAME: "${REFRESH_COOKIE_NAME}"
SESSION_SECRET: "${SESSION_SECRET}"
STATE_SIGNING_SECRET: "${STATE_SIGNING_SECRET}"
SESSION_TTL: "${SESSION_TTL}"
REFRESH_TTL: "${REFRESH_TTL}"
REDIS_URL: "${REDIS_URL}"
depends_on:
- "${DB_HOST}"
- redis
```
## docker-compose.yml
```yaml
services:
redis:
image: redis:7-alpine
container_name: "${REVERSE_DOMAIN}_redis"
command: ["redis-server", "--save", "", "--appendonly", "no"]
restart: always
networks:
- app_network
biblio-stats-graphql:
environment:
SWISSOID_CLIENT_ID: "${SWISSOID_CLIENT_ID}"
SWISSOID_ISSUER: "${SWISSOID_ISSUER}"
SWISSOID_JWKS_URI: "${SWISSOID_JWKS_URI}"
SWISSOID_TOKEN_ENDPOINT: "${SWISSOID_TOKEN_ENDPOINT}"
SWISSOID_AUTHORIZE_ENDPOINT: "${SWISSOID_AUTHORIZE_ENDPOINT}"
OIDC_REDIRECT_BASE_URL: "${OIDC_REDIRECT_BASE_URL}"
RP_FRONTEND_URL: "${RP_FRONTEND_URL}"
RP_COOKIE_DOMAIN: "${RP_COOKIE_DOMAIN}"
SESSION_COOKIE_NAME: "${SESSION_COOKIE_NAME}"
REFRESH_COOKIE_NAME: "${REFRESH_COOKIE_NAME}"
SESSION_SECRET: "${SESSION_SECRET}"
STATE_SIGNING_SECRET: "${STATE_SIGNING_SECRET}"
SESSION_TTL: "${SESSION_TTL}"
REFRESH_TTL: "${REFRESH_TTL}"
REDIS_URL: "${REDIS_URL}"
depends_on:
- "${DB_HOST}"
- redis
```
This ensures local and production stacks both ship with Redis so `swissoid-back` can persist sessions without additional manual setup.
Loading…
Cancel
Save