@camstack/addon-auth-oidc 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,8 +50,8 @@ class AuthOidcAddon extends types.BaseAddon {
50
50
  }
51
51
  async onInitialize() {
52
52
  Object.assign(this, {
53
- displayName: this.config.values.displayName || DEFAULT_CONFIG.displayName,
54
- icon: this.config.values.icon || DEFAULT_CONFIG.icon
53
+ displayName: this.config.displayName || DEFAULT_CONFIG.displayName,
54
+ icon: this.config.icon || DEFAULT_CONFIG.icon
55
55
  });
56
56
  const authProvider = {
57
57
  validateCredentials: async () => null,
@@ -90,7 +90,7 @@ class AuthOidcAddon extends types.BaseAddon {
90
90
  this.ctx.logger.info("OIDC auth provider initialized", {
91
91
  meta: {
92
92
  displayName: this.displayName,
93
- issuerUrl: this.config.values.issuerUrl || "(not configured)"
93
+ issuerUrl: this.config.issuerUrl || "(not configured)"
94
94
  }
95
95
  });
96
96
  return [
@@ -151,7 +151,7 @@ class AuthOidcAddon extends types.BaseAddon {
151
151
  if (this.discovery && Date.now() - this.discoveryFetchedAt < 60 * 6e4) {
152
152
  return this.discovery;
153
153
  }
154
- const issuer = this.config.values.issuerUrl?.replace(/\/+$/, "");
154
+ const issuer = this.config.issuerUrl?.replace(/\/+$/, "");
155
155
  if (!issuer) throw new Error("OIDC issuerUrl not configured");
156
156
  const url = `${issuer}/.well-known/openid-configuration`;
157
157
  const res = await fetch(url);
@@ -167,7 +167,7 @@ class AuthOidcAddon extends types.BaseAddon {
167
167
  return doc;
168
168
  }
169
169
  buildRedirectUri() {
170
- const configured = this.config.values.redirectUri?.trim();
170
+ const configured = this.config.redirectUri?.trim();
171
171
  if (configured) return configured;
172
172
  const origin = process.env["CAMSTACK_PUBLIC_ORIGIN"] || "https://localhost:4443";
173
173
  return `${origin.replace(/\/+$/, "")}/addon/auth-oidc/callback`;
@@ -180,9 +180,9 @@ class AuthOidcAddon extends types.BaseAddon {
180
180
  this.pendingStates.set(state, { codeVerifier, createdAt: Date.now() });
181
181
  const params = new URLSearchParams({
182
182
  response_type: "code",
183
- client_id: this.config.values.clientId,
183
+ client_id: this.config.clientId,
184
184
  redirect_uri: this.buildRedirectUri(),
185
- scope: this.config.values.scopes || DEFAULT_CONFIG.scopes,
185
+ scope: this.config.scopes || DEFAULT_CONFIG.scopes,
186
186
  state,
187
187
  code_challenge: codeChallenge,
188
188
  code_challenge_method: "S256"
@@ -213,8 +213,8 @@ class AuthOidcAddon extends types.BaseAddon {
213
213
  grant_type: "authorization_code",
214
214
  code,
215
215
  redirect_uri: this.buildRedirectUri(),
216
- client_id: this.config.values.clientId,
217
- client_secret: this.config.values.clientSecret,
216
+ client_id: this.config.clientId,
217
+ client_secret: this.config.clientSecret,
218
218
  code_verifier: pending.codeVerifier
219
219
  })
220
220
  });
@@ -236,7 +236,7 @@ class AuthOidcAddon extends types.BaseAddon {
236
236
  if (!claims?.sub) {
237
237
  throw new Error("OIDC callback: id_token missing required `sub` claim");
238
238
  }
239
- const usernameClaim = this.config.values.usernameClaim;
239
+ const usernameClaim = this.config.usernameClaim;
240
240
  const username = usernameClaim === "preferred_username" && claims.preferred_username || usernameClaim === "email" && claims.email || claims.sub;
241
241
  const userId = `oidc:${claims.sub}`;
242
242
  return {
@@ -244,7 +244,7 @@ class AuthOidcAddon extends types.BaseAddon {
244
244
  username: String(username),
245
245
  ...claims.email ? { email: claims.email } : {},
246
246
  ...claims.name ? { displayName: claims.name } : {},
247
- roles: [this.config.values.defaultRole]
247
+ roles: [this.config.defaultRole]
248
248
  };
249
249
  }
250
250
  validateLocalSessionToken(_token) {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-oidc.addon.js","sources":["../src/auth-oidc.addon.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return -- BaseAddon<TConfig>.config.values is typed-erased in the consumer's type tree under projectService context (the cap-router union resolves the generic at the call site, not in the addon's own dist). All sites flagged here are reads/writes of well-typed local fields; runtime contract is validated by Zod via globalSettingsSchema. */\n/**\n * Generic OpenID Connect (OIDC) authentication provider.\n *\n * Registers two capabilities:\n * - `auth-provider` (collection) — the standard CamStack auth surface.\n * `getLoginUrl({state})` returns the IdP's authorization URL with\n * state + PKCE; `handleCallback({code, state})` exchanges the code\n * for tokens and returns AuthResult.\n * - `addon-routes` (collection) — `/start` (302 to IdP) and\n * `/callback` (token exchange + redirect to admin UI with the\n * local session token in the URL fragment).\n *\n * Flow (login):\n * 1. Browser hits `/api/auth/sso/auth-oidc/start` (proxy to\n * `/addon/auth-oidc/start`)\n * 2. Addon redirects to `<issuer>/authorize?...&state=...&code_challenge=...`\n * 3. User authenticates at the IdP\n * 4. IdP redirects back to `/addon/auth-oidc/callback?code=...&state=...`\n * 5. Addon exchanges code for id_token + access_token, decodes claims,\n * mints local CamStack session, redirects to `/admin#token=...`\n *\n * Configuration: per-instance via global addon settings (issuer URL,\n * client ID/secret, scopes, displayName). Multiple OIDC providers can\n * coexist by installing this addon multiple times under different IDs.\n *\n * Production-readiness gaps (documented for follow-up):\n * - ID-token signature is NOT verified against JWKS. The token is\n * decoded as base64-url-JSON and trusted because the code came\n * from the IdP redirect (state param is verified). Add\n * `jose.jwtVerify(idToken, JWKS)` for full RFC 7515 compliance.\n * - State + PKCE are stored in-memory keyed by state value with a\n * 5-minute TTL. Survives addon restart only if the user retries\n * within the window. For production multi-replica deploys, move\n * to a settings-backed store.\n * - `nonce` parameter not yet implemented. Mitigate replay by\n * validating `aud` and `iss` from id_token claims.\n */\nimport * as crypto from 'node:crypto'\nimport {\n BaseAddon,\n authProviderCapability,\n addonRoutesCapability,\n errMsg,\n type ProviderRegistration,\n type IAuthProvider,\n type IAddonRouteProvider,\n type IAddonHttpRoute,\n type AddonHttpRequest,\n type AddonHttpReply,\n} from '@camstack/types'\n\ninterface OidcConfig {\n /** Display name shown on the login button + admin UI (\"Log in with Acme\"). */\n readonly displayName: string\n /** Operator-facing icon hint (lucide-react icon name). Defaults to 'shield-check'. */\n readonly icon: string\n /**\n * Issuer base URL — discovery document MUST live at\n * `<issuer>/.well-known/openid-configuration`. Examples:\n * - https://accounts.google.com\n * - https://login.microsoftonline.com/<tenant>/v2.0\n * - https://your-keycloak.example.com/realms/master\n */\n readonly issuerUrl: string\n /** OAuth2 client ID issued by the IdP. */\n readonly clientId: string\n /** OAuth2 client secret. Server-side only — never exposed to the browser. */\n readonly clientSecret: string\n /**\n * Public callback URL the IdP will redirect to. Must match what's\n * configured at the IdP. Default: `<server-origin>/addon/auth-oidc/callback`.\n * Override when the server is behind a reverse proxy with a different\n * public-facing origin (e.g. `https://hub.example.com/addon/auth-oidc/callback`).\n */\n readonly redirectUri: string\n /** Space-separated OAuth scopes. Default: 'openid profile email'. */\n readonly scopes: string\n /**\n * Scopes / claims used to derive the local username. The first\n * matching claim wins. Default order: preferred_username → email → sub.\n */\n readonly usernameClaim: 'preferred_username' | 'email' | 'sub'\n /** Default role assigned to first-time SSO users. */\n readonly defaultRole: 'viewer' | 'admin' | 'super_admin'\n}\n\nconst DEFAULT_CONFIG: OidcConfig = {\n displayName: 'OpenID Connect',\n icon: 'shield-check',\n issuerUrl: '',\n clientId: '',\n clientSecret: '',\n redirectUri: '',\n scopes: 'openid profile email',\n usernameClaim: 'preferred_username',\n defaultRole: 'viewer',\n}\n\ninterface DiscoveryDocument {\n readonly issuer: string\n readonly authorization_endpoint: string\n readonly token_endpoint: string\n readonly userinfo_endpoint?: string\n readonly jwks_uri?: string\n}\n\ninterface PendingState {\n readonly codeVerifier: string\n readonly createdAt: number\n}\n\ninterface OidcTokenResponse {\n readonly access_token: string\n readonly id_token?: string\n readonly token_type?: string\n readonly expires_in?: number\n readonly refresh_token?: string\n}\n\ninterface OidcClaims {\n readonly sub: string\n readonly iss?: string\n readonly aud?: string | readonly string[]\n readonly email?: string\n readonly preferred_username?: string\n readonly name?: string\n}\n\nconst STATE_TTL_MS = 5 * 60_000\n\nexport class AuthOidcAddon extends BaseAddon<OidcConfig> {\n private discovery: DiscoveryDocument | null = null\n private discoveryFetchedAt = 0\n private readonly pendingStates = new Map<string, PendingState>()\n\n // Provider metadata — read by the auth-orchestrator when the admin\n // UI calls `authentication.listProviders`. Static fields on the\n // provider object so the orchestrator's coercion picks them up.\n readonly displayName: string\n readonly kind = 'oidc' as const\n readonly icon: string\n readonly hasRedirectFlow = true\n readonly hasCredentialFlow = false\n\n constructor() {\n super({ ...DEFAULT_CONFIG })\n this.displayName = DEFAULT_CONFIG.displayName\n this.icon = DEFAULT_CONFIG.icon\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n // Override the static metadata with the configured displayName so\n // the orchestrator picks up the operator-chosen label.\n Object.assign(this, {\n displayName: this.config.values.displayName || DEFAULT_CONFIG.displayName,\n icon: this.config.values.icon || DEFAULT_CONFIG.icon,\n })\n\n const authProvider: IAuthProvider = {\n validateCredentials: async () => null, // OIDC has no credential flow\n validateToken: async ({ token }) => this.validateLocalSessionToken(token),\n getLoginUrl: async ({ state }) => this.buildLoginUrl(state),\n handleCallback: async (params) => {\n const code = params['code']\n const state = params['state']\n if (!code || !state) {\n throw new Error('Missing code or state in OIDC callback')\n }\n return this.handleCallback(code, state)\n },\n }\n\n const routes: IAddonHttpRoute[] = [\n {\n method: 'GET',\n path: '/start',\n access: 'public',\n description: 'Begin OIDC redirect login flow',\n handler: async (req, reply) => this.handleStart(req, reply),\n },\n {\n method: 'GET',\n path: '/callback',\n access: 'public',\n description: 'OIDC redirect callback — exchanges code for tokens',\n handler: async (req, reply) => this.handleCallbackRoute(req, reply),\n },\n ]\n const routeProvider: IAddonRouteProvider = {\n id: 'auth-oidc',\n getRoutes: () => routes,\n }\n\n this.ctx.logger.info('OIDC auth provider initialized', {\n meta: {\n displayName: this.displayName,\n issuerUrl: this.config.values.issuerUrl || '(not configured)',\n },\n })\n\n return [\n { capability: authProviderCapability, provider: authProvider as never },\n { capability: addonRoutesCapability, provider: routeProvider as never },\n ]\n }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'oidc',\n title: 'OIDC Provider Settings',\n description: 'Configure your IdP. The redirect URI must be registered at the IdP exactly as shown below.',\n columns: 1,\n fields: [\n this.field({ type: 'text', key: 'displayName', label: 'Display Name', description: 'Shown on the login button (e.g., \"Continue with Google\").', default: DEFAULT_CONFIG.displayName }),\n this.field({ type: 'text', key: 'icon', label: 'Icon', description: 'lucide-react icon name. Default: shield-check.', default: DEFAULT_CONFIG.icon }),\n this.field({ type: 'text', key: 'issuerUrl', label: 'Issuer URL', description: 'IdP base URL — discovery doc lives at <issuer>/.well-known/openid-configuration.', default: '' }),\n this.field({ type: 'text', key: 'clientId', label: 'Client ID', description: 'OAuth2 client ID issued by the IdP.', default: '' }),\n this.field({ type: 'text', key: 'clientSecret', label: 'Client Secret', description: 'OAuth2 client secret. Server-side only.', default: '' }),\n this.field({ type: 'text', key: 'redirectUri', label: 'Redirect URI', description: 'Public callback URL. Default: <origin>/addon/auth-oidc/callback. Must match the IdP-registered URI.', default: '' }),\n this.field({ type: 'text', key: 'scopes', label: 'Scopes', description: 'Space-separated OAuth scopes.', default: DEFAULT_CONFIG.scopes }),\n this.field({\n type: 'select',\n key: 'usernameClaim',\n label: 'Username Claim',\n description: 'Which OIDC claim to use as the local username.',\n default: DEFAULT_CONFIG.usernameClaim,\n options: [\n { value: 'preferred_username', label: 'preferred_username' },\n { value: 'email', label: 'email' },\n { value: 'sub', label: 'sub (user id)' },\n ],\n }),\n this.field({\n type: 'select',\n key: 'defaultRole',\n label: 'Default Role',\n description: 'Role assigned to first-time SSO users.',\n default: DEFAULT_CONFIG.defaultRole,\n options: [\n { value: 'viewer', label: 'Viewer' },\n { value: 'admin', label: 'Admin' },\n { value: 'super_admin', label: 'Super Admin' },\n ],\n }),\n ],\n }],\n })\n }\n\n // ─── OIDC discovery + URL building ─────────────────────────────────\n\n /**\n * Fetch + cache the IdP's discovery document. Refreshes every hour\n * (issuer config rarely changes; refresh window keeps us correct\n * without hammering on every login).\n */\n private async getDiscovery(): Promise<DiscoveryDocument> {\n if (this.discovery && Date.now() - this.discoveryFetchedAt < 60 * 60_000) {\n return this.discovery\n }\n const issuer = this.config.values.issuerUrl?.replace(/\\/+$/, '')\n if (!issuer) throw new Error('OIDC issuerUrl not configured')\n const url = `${issuer}/.well-known/openid-configuration`\n const res = await fetch(url)\n if (!res.ok) {\n throw new Error(`OIDC discovery failed: ${res.status} ${res.statusText}`)\n }\n const doc = (await res.json()) as DiscoveryDocument\n if (!doc.authorization_endpoint || !doc.token_endpoint) {\n throw new Error('OIDC discovery response missing required endpoints')\n }\n this.discovery = doc\n this.discoveryFetchedAt = Date.now()\n return doc\n }\n\n private buildRedirectUri(): string {\n const configured = this.config.values.redirectUri?.trim()\n if (configured) return configured\n // Fallback: server origin from env. In a multi-host deploy the\n // operator MUST set redirectUri explicitly.\n const origin = process.env['CAMSTACK_PUBLIC_ORIGIN'] || 'https://localhost:4443'\n return `${origin.replace(/\\/+$/, '')}/addon/auth-oidc/callback`\n }\n\n private async buildLoginUrl(state: string): Promise<string> {\n const doc = await this.getDiscovery()\n\n // PKCE: random code_verifier, S256 hash → code_challenge.\n const codeVerifier = crypto.randomBytes(32).toString('base64url')\n const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url')\n\n this.pruneExpiredStates()\n this.pendingStates.set(state, { codeVerifier, createdAt: Date.now() })\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.values.clientId,\n redirect_uri: this.buildRedirectUri(),\n scope: this.config.values.scopes || DEFAULT_CONFIG.scopes,\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n })\n return `${doc.authorization_endpoint}?${params.toString()}`\n }\n\n private pruneExpiredStates(): void {\n const now = Date.now()\n for (const [k, v] of this.pendingStates.entries()) {\n if (now - v.createdAt > STATE_TTL_MS) this.pendingStates.delete(k)\n }\n }\n\n // ─── Callback / token exchange ─────────────────────────────────────\n\n private async handleCallback(code: string, state: string): Promise<{\n userId: string\n username: string\n email?: string\n displayName?: string\n roles: readonly string[]\n }> {\n const pending = this.pendingStates.get(state)\n if (!pending) {\n throw new Error('Unknown or expired OIDC state — login session timed out, please retry')\n }\n this.pendingStates.delete(state)\n\n const doc = await this.getDiscovery()\n const tokenRes = await fetch(doc.token_endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.buildRedirectUri(),\n client_id: this.config.values.clientId,\n client_secret: this.config.values.clientSecret,\n code_verifier: pending.codeVerifier,\n }),\n })\n if (!tokenRes.ok) {\n const text = await tokenRes.text().catch(() => '')\n throw new Error(`OIDC token exchange failed: ${tokenRes.status} ${tokenRes.statusText} — ${text.slice(0, 200)}`)\n }\n const tokens = (await tokenRes.json()) as OidcTokenResponse\n\n // Decode id_token claims (no signature verification — see header\n // doc comment). For now we trust the token because it arrived via\n // the back-channel from a verified state-bound exchange. PRODUCTION:\n // call `jose.jwtVerify(id_token, await jose.createRemoteJWKSet(jwks_uri))`.\n let claims: OidcClaims | null = null\n if (tokens.id_token) {\n claims = decodeJwtPayload(tokens.id_token) as OidcClaims | null\n }\n // Fallback: query userinfo_endpoint with the access token\n if (!claims && doc.userinfo_endpoint) {\n const ui = await fetch(doc.userinfo_endpoint, {\n headers: { Authorization: `Bearer ${tokens.access_token}` },\n })\n if (ui.ok) claims = (await ui.json()) as OidcClaims\n }\n if (!claims?.sub) {\n throw new Error('OIDC callback: id_token missing required `sub` claim')\n }\n\n const usernameClaim = this.config.values.usernameClaim\n const username =\n (usernameClaim === 'preferred_username' && claims.preferred_username) ||\n (usernameClaim === 'email' && claims.email) ||\n claims.sub\n\n const userId = `oidc:${claims.sub}`\n return {\n userId,\n username: String(username),\n ...(claims.email ? { email: claims.email } : {}),\n ...(claims.name ? { displayName: claims.name } : {}),\n roles: [this.config.values.defaultRole],\n }\n }\n\n private validateLocalSessionToken(_token: string): null {\n // The local CamStack session token is minted by the host's auth\n // subsystem after `handleCallback` returns. Token verification\n // remains the responsibility of `local-auth` / the auth-manager;\n // this provider only mints AuthResult.\n return null\n }\n\n // ─── HTTP route handlers ───────────────────────────────────────────\n\n private async handleStart(_req: AddonHttpRequest, reply: AddonHttpReply): Promise<void> {\n const state = crypto.randomBytes(16).toString('base64url')\n try {\n const url = await this.buildLoginUrl(state)\n reply.code(302)\n reply.header('Location', url)\n reply.send('')\n } catch (err) {\n reply.code(500)\n reply.send({ error: 'OIDC start failed', message: errMsg(err) })\n }\n }\n\n private async handleCallbackRoute(req: AddonHttpRequest, reply: AddonHttpReply): Promise<void> {\n const query = req.query as Record<string, string | undefined>\n const code = query['code']\n const state = query['state']\n const errorParam = query['error']\n\n if (errorParam) {\n reply.code(400)\n reply.send({ error: 'OIDC error', detail: errorParam, description: query['error_description'] })\n return\n }\n if (!code || !state) {\n reply.code(400)\n reply.send({ error: 'Missing code or state' })\n return\n }\n\n try {\n const result = await this.handleCallback(code, state)\n // The host's auth subsystem owns local session minting. We\n // signal success by redirecting to a known admin endpoint with\n // the OIDC result encoded in the query — the server's auth\n // route handler picks it up, mints a JWT, and redirects to the\n // admin UI with `#token=...`.\n const params = new URLSearchParams({\n userId: result.userId,\n username: result.username,\n ...(result.email ? { email: result.email } : {}),\n ...(result.displayName ? { displayName: result.displayName } : {}),\n roles: result.roles.join(','),\n provider: 'auth-oidc',\n })\n reply.code(302)\n reply.header('Location', `/api/auth/sso/finish?${params.toString()}`)\n reply.send('')\n } catch (err) {\n reply.code(500)\n reply.send({ error: 'OIDC callback failed', message: errMsg(err) })\n }\n }\n}\n\nexport default AuthOidcAddon\n\n/**\n * Decode a JWT's payload as JSON without verifying its signature.\n *\n * **NOT SAFE for general token validation** — only used here because\n * the id_token arrives via the back-channel token exchange against a\n * state-bound code, which gives us implicit transport-level trust. A\n * production-grade implementation must use `jose.jwtVerify` against\n * the issuer's published JWKS.\n */\nfunction decodeJwtPayload(jwt: string): unknown {\n const parts = jwt.split('.')\n if (parts.length !== 3) return null\n try {\n const payload = parts[1]!\n const padded = payload + '='.repeat((4 - (payload.length % 4)) % 4)\n const buf = Buffer.from(padded, 'base64url')\n return JSON.parse(buf.toString('utf-8'))\n } catch {\n return null\n }\n}\n"],"names":["BaseAddon","authProviderCapability","addonRoutesCapability","crypto","errMsg"],"mappings":"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"}
1
+ {"version":3,"file":"auth-oidc.addon.js","sources":["../src/auth-oidc.addon.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return -- BaseAddon<TConfig>.config.values is typed-erased in the consumer's type tree under projectService context (the cap-router union resolves the generic at the call site, not in the addon's own dist). All sites flagged here are reads/writes of well-typed local fields; runtime contract is validated by Zod via globalSettingsSchema. */\n/**\n * Generic OpenID Connect (OIDC) authentication provider.\n *\n * Registers two capabilities:\n * - `auth-provider` (collection) — the standard CamStack auth surface.\n * `getLoginUrl({state})` returns the IdP's authorization URL with\n * state + PKCE; `handleCallback({code, state})` exchanges the code\n * for tokens and returns AuthResult.\n * - `addon-routes` (collection) — `/start` (302 to IdP) and\n * `/callback` (token exchange + redirect to admin UI with the\n * local session token in the URL fragment).\n *\n * Flow (login):\n * 1. Browser hits `/api/auth/sso/auth-oidc/start` (proxy to\n * `/addon/auth-oidc/start`)\n * 2. Addon redirects to `<issuer>/authorize?...&state=...&code_challenge=...`\n * 3. User authenticates at the IdP\n * 4. IdP redirects back to `/addon/auth-oidc/callback?code=...&state=...`\n * 5. Addon exchanges code for id_token + access_token, decodes claims,\n * mints local CamStack session, redirects to `/admin#token=...`\n *\n * Configuration: per-instance via global addon settings (issuer URL,\n * client ID/secret, scopes, displayName). Multiple OIDC providers can\n * coexist by installing this addon multiple times under different IDs.\n *\n * Production-readiness gaps (documented for follow-up):\n * - ID-token signature is NOT verified against JWKS. The token is\n * decoded as base64-url-JSON and trusted because the code came\n * from the IdP redirect (state param is verified). Add\n * `jose.jwtVerify(idToken, JWKS)` for full RFC 7515 compliance.\n * - State + PKCE are stored in-memory keyed by state value with a\n * 5-minute TTL. Survives addon restart only if the user retries\n * within the window. For production multi-replica deploys, move\n * to a settings-backed store.\n * - `nonce` parameter not yet implemented. Mitigate replay by\n * validating `aud` and `iss` from id_token claims.\n */\nimport * as crypto from 'node:crypto'\nimport {\n BaseAddon,\n authProviderCapability,\n addonRoutesCapability,\n errMsg,\n type ProviderRegistration,\n type IAuthProvider,\n type IAddonRouteProvider,\n type IAddonHttpRoute,\n type AddonHttpRequest,\n type AddonHttpReply,\n} from '@camstack/types'\n\ninterface OidcConfig {\n /** Display name shown on the login button + admin UI (\"Log in with Acme\"). */\n readonly displayName: string\n /** Operator-facing icon hint (lucide-react icon name). Defaults to 'shield-check'. */\n readonly icon: string\n /**\n * Issuer base URL — discovery document MUST live at\n * `<issuer>/.well-known/openid-configuration`. Examples:\n * - https://accounts.google.com\n * - https://login.microsoftonline.com/<tenant>/v2.0\n * - https://your-keycloak.example.com/realms/master\n */\n readonly issuerUrl: string\n /** OAuth2 client ID issued by the IdP. */\n readonly clientId: string\n /** OAuth2 client secret. Server-side only — never exposed to the browser. */\n readonly clientSecret: string\n /**\n * Public callback URL the IdP will redirect to. Must match what's\n * configured at the IdP. Default: `<server-origin>/addon/auth-oidc/callback`.\n * Override when the server is behind a reverse proxy with a different\n * public-facing origin (e.g. `https://hub.example.com/addon/auth-oidc/callback`).\n */\n readonly redirectUri: string\n /** Space-separated OAuth scopes. Default: 'openid profile email'. */\n readonly scopes: string\n /**\n * Scopes / claims used to derive the local username. The first\n * matching claim wins. Default order: preferred_username → email → sub.\n */\n readonly usernameClaim: 'preferred_username' | 'email' | 'sub'\n /** Default role assigned to first-time SSO users. */\n readonly defaultRole: 'viewer' | 'admin' | 'super_admin'\n}\n\nconst DEFAULT_CONFIG: OidcConfig = {\n displayName: 'OpenID Connect',\n icon: 'shield-check',\n issuerUrl: '',\n clientId: '',\n clientSecret: '',\n redirectUri: '',\n scopes: 'openid profile email',\n usernameClaim: 'preferred_username',\n defaultRole: 'viewer',\n}\n\ninterface DiscoveryDocument {\n readonly issuer: string\n readonly authorization_endpoint: string\n readonly token_endpoint: string\n readonly userinfo_endpoint?: string\n readonly jwks_uri?: string\n}\n\ninterface PendingState {\n readonly codeVerifier: string\n readonly createdAt: number\n}\n\ninterface OidcTokenResponse {\n readonly access_token: string\n readonly id_token?: string\n readonly token_type?: string\n readonly expires_in?: number\n readonly refresh_token?: string\n}\n\ninterface OidcClaims {\n readonly sub: string\n readonly iss?: string\n readonly aud?: string | readonly string[]\n readonly email?: string\n readonly preferred_username?: string\n readonly name?: string\n}\n\nconst STATE_TTL_MS = 5 * 60_000\n\nexport class AuthOidcAddon extends BaseAddon<OidcConfig> {\n private discovery: DiscoveryDocument | null = null\n private discoveryFetchedAt = 0\n private readonly pendingStates = new Map<string, PendingState>()\n\n // Provider metadata — read by the auth-orchestrator when the admin\n // UI calls `authentication.listProviders`. Static fields on the\n // provider object so the orchestrator's coercion picks them up.\n readonly displayName: string\n readonly kind = 'oidc' as const\n readonly icon: string\n readonly hasRedirectFlow = true\n readonly hasCredentialFlow = false\n\n constructor() {\n super({ ...DEFAULT_CONFIG })\n this.displayName = DEFAULT_CONFIG.displayName\n this.icon = DEFAULT_CONFIG.icon\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n // Override the static metadata with the configured displayName so\n // the orchestrator picks up the operator-chosen label.\n Object.assign(this, {\n displayName: this.config.displayName || DEFAULT_CONFIG.displayName,\n icon: this.config.icon || DEFAULT_CONFIG.icon,\n })\n\n const authProvider: IAuthProvider = {\n validateCredentials: async () => null, // OIDC has no credential flow\n validateToken: async ({ token }) => this.validateLocalSessionToken(token),\n getLoginUrl: async ({ state }) => this.buildLoginUrl(state),\n handleCallback: async (params) => {\n const code = params['code']\n const state = params['state']\n if (!code || !state) {\n throw new Error('Missing code or state in OIDC callback')\n }\n return this.handleCallback(code, state)\n },\n }\n\n const routes: IAddonHttpRoute[] = [\n {\n method: 'GET',\n path: '/start',\n access: 'public',\n description: 'Begin OIDC redirect login flow',\n handler: async (req, reply) => this.handleStart(req, reply),\n },\n {\n method: 'GET',\n path: '/callback',\n access: 'public',\n description: 'OIDC redirect callback — exchanges code for tokens',\n handler: async (req, reply) => this.handleCallbackRoute(req, reply),\n },\n ]\n const routeProvider: IAddonRouteProvider = {\n id: 'auth-oidc',\n getRoutes: () => routes,\n }\n\n this.ctx.logger.info('OIDC auth provider initialized', {\n meta: {\n displayName: this.displayName,\n issuerUrl: this.config.issuerUrl || '(not configured)',\n },\n })\n\n return [\n { capability: authProviderCapability, provider: authProvider as never },\n { capability: addonRoutesCapability, provider: routeProvider as never },\n ]\n }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'oidc',\n title: 'OIDC Provider Settings',\n description: 'Configure your IdP. The redirect URI must be registered at the IdP exactly as shown below.',\n columns: 1,\n fields: [\n this.field({ type: 'text', key: 'displayName', label: 'Display Name', description: 'Shown on the login button (e.g., \"Continue with Google\").', default: DEFAULT_CONFIG.displayName }),\n this.field({ type: 'text', key: 'icon', label: 'Icon', description: 'lucide-react icon name. Default: shield-check.', default: DEFAULT_CONFIG.icon }),\n this.field({ type: 'text', key: 'issuerUrl', label: 'Issuer URL', description: 'IdP base URL — discovery doc lives at <issuer>/.well-known/openid-configuration.', default: '' }),\n this.field({ type: 'text', key: 'clientId', label: 'Client ID', description: 'OAuth2 client ID issued by the IdP.', default: '' }),\n this.field({ type: 'text', key: 'clientSecret', label: 'Client Secret', description: 'OAuth2 client secret. Server-side only.', default: '' }),\n this.field({ type: 'text', key: 'redirectUri', label: 'Redirect URI', description: 'Public callback URL. Default: <origin>/addon/auth-oidc/callback. Must match the IdP-registered URI.', default: '' }),\n this.field({ type: 'text', key: 'scopes', label: 'Scopes', description: 'Space-separated OAuth scopes.', default: DEFAULT_CONFIG.scopes }),\n this.field({\n type: 'select',\n key: 'usernameClaim',\n label: 'Username Claim',\n description: 'Which OIDC claim to use as the local username.',\n default: DEFAULT_CONFIG.usernameClaim,\n options: [\n { value: 'preferred_username', label: 'preferred_username' },\n { value: 'email', label: 'email' },\n { value: 'sub', label: 'sub (user id)' },\n ],\n }),\n this.field({\n type: 'select',\n key: 'defaultRole',\n label: 'Default Role',\n description: 'Role assigned to first-time SSO users.',\n default: DEFAULT_CONFIG.defaultRole,\n options: [\n { value: 'viewer', label: 'Viewer' },\n { value: 'admin', label: 'Admin' },\n { value: 'super_admin', label: 'Super Admin' },\n ],\n }),\n ],\n }],\n })\n }\n\n // ─── OIDC discovery + URL building ─────────────────────────────────\n\n /**\n * Fetch + cache the IdP's discovery document. Refreshes every hour\n * (issuer config rarely changes; refresh window keeps us correct\n * without hammering on every login).\n */\n private async getDiscovery(): Promise<DiscoveryDocument> {\n if (this.discovery && Date.now() - this.discoveryFetchedAt < 60 * 60_000) {\n return this.discovery\n }\n const issuer = this.config.issuerUrl?.replace(/\\/+$/, '')\n if (!issuer) throw new Error('OIDC issuerUrl not configured')\n const url = `${issuer}/.well-known/openid-configuration`\n const res = await fetch(url)\n if (!res.ok) {\n throw new Error(`OIDC discovery failed: ${res.status} ${res.statusText}`)\n }\n const doc = (await res.json()) as DiscoveryDocument\n if (!doc.authorization_endpoint || !doc.token_endpoint) {\n throw new Error('OIDC discovery response missing required endpoints')\n }\n this.discovery = doc\n this.discoveryFetchedAt = Date.now()\n return doc\n }\n\n private buildRedirectUri(): string {\n const configured = this.config.redirectUri?.trim()\n if (configured) return configured\n // Fallback: server origin from env. In a multi-host deploy the\n // operator MUST set redirectUri explicitly.\n const origin = process.env['CAMSTACK_PUBLIC_ORIGIN'] || 'https://localhost:4443'\n return `${origin.replace(/\\/+$/, '')}/addon/auth-oidc/callback`\n }\n\n private async buildLoginUrl(state: string): Promise<string> {\n const doc = await this.getDiscovery()\n\n // PKCE: random code_verifier, S256 hash → code_challenge.\n const codeVerifier = crypto.randomBytes(32).toString('base64url')\n const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url')\n\n this.pruneExpiredStates()\n this.pendingStates.set(state, { codeVerifier, createdAt: Date.now() })\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.clientId,\n redirect_uri: this.buildRedirectUri(),\n scope: this.config.scopes || DEFAULT_CONFIG.scopes,\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n })\n return `${doc.authorization_endpoint}?${params.toString()}`\n }\n\n private pruneExpiredStates(): void {\n const now = Date.now()\n for (const [k, v] of this.pendingStates.entries()) {\n if (now - v.createdAt > STATE_TTL_MS) this.pendingStates.delete(k)\n }\n }\n\n // ─── Callback / token exchange ─────────────────────────────────────\n\n private async handleCallback(code: string, state: string): Promise<{\n userId: string\n username: string\n email?: string\n displayName?: string\n roles: readonly string[]\n }> {\n const pending = this.pendingStates.get(state)\n if (!pending) {\n throw new Error('Unknown or expired OIDC state — login session timed out, please retry')\n }\n this.pendingStates.delete(state)\n\n const doc = await this.getDiscovery()\n const tokenRes = await fetch(doc.token_endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.buildRedirectUri(),\n client_id: this.config.clientId,\n client_secret: this.config.clientSecret,\n code_verifier: pending.codeVerifier,\n }),\n })\n if (!tokenRes.ok) {\n const text = await tokenRes.text().catch(() => '')\n throw new Error(`OIDC token exchange failed: ${tokenRes.status} ${tokenRes.statusText} — ${text.slice(0, 200)}`)\n }\n const tokens = (await tokenRes.json()) as OidcTokenResponse\n\n // Decode id_token claims (no signature verification — see header\n // doc comment). For now we trust the token because it arrived via\n // the back-channel from a verified state-bound exchange. PRODUCTION:\n // call `jose.jwtVerify(id_token, await jose.createRemoteJWKSet(jwks_uri))`.\n let claims: OidcClaims | null = null\n if (tokens.id_token) {\n claims = decodeJwtPayload(tokens.id_token) as OidcClaims | null\n }\n // Fallback: query userinfo_endpoint with the access token\n if (!claims && doc.userinfo_endpoint) {\n const ui = await fetch(doc.userinfo_endpoint, {\n headers: { Authorization: `Bearer ${tokens.access_token}` },\n })\n if (ui.ok) claims = (await ui.json()) as OidcClaims\n }\n if (!claims?.sub) {\n throw new Error('OIDC callback: id_token missing required `sub` claim')\n }\n\n const usernameClaim = this.config.usernameClaim\n const username =\n (usernameClaim === 'preferred_username' && claims.preferred_username) ||\n (usernameClaim === 'email' && claims.email) ||\n claims.sub\n\n const userId = `oidc:${claims.sub}`\n return {\n userId,\n username: String(username),\n ...(claims.email ? { email: claims.email } : {}),\n ...(claims.name ? { displayName: claims.name } : {}),\n roles: [this.config.defaultRole],\n }\n }\n\n private validateLocalSessionToken(_token: string): null {\n // The local CamStack session token is minted by the host's auth\n // subsystem after `handleCallback` returns. Token verification\n // remains the responsibility of `local-auth` / the auth-manager;\n // this provider only mints AuthResult.\n return null\n }\n\n // ─── HTTP route handlers ───────────────────────────────────────────\n\n private async handleStart(_req: AddonHttpRequest, reply: AddonHttpReply): Promise<void> {\n const state = crypto.randomBytes(16).toString('base64url')\n try {\n const url = await this.buildLoginUrl(state)\n reply.code(302)\n reply.header('Location', url)\n reply.send('')\n } catch (err) {\n reply.code(500)\n reply.send({ error: 'OIDC start failed', message: errMsg(err) })\n }\n }\n\n private async handleCallbackRoute(req: AddonHttpRequest, reply: AddonHttpReply): Promise<void> {\n const query = req.query as Record<string, string | undefined>\n const code = query['code']\n const state = query['state']\n const errorParam = query['error']\n\n if (errorParam) {\n reply.code(400)\n reply.send({ error: 'OIDC error', detail: errorParam, description: query['error_description'] })\n return\n }\n if (!code || !state) {\n reply.code(400)\n reply.send({ error: 'Missing code or state' })\n return\n }\n\n try {\n const result = await this.handleCallback(code, state)\n // The host's auth subsystem owns local session minting. We\n // signal success by redirecting to a known admin endpoint with\n // the OIDC result encoded in the query — the server's auth\n // route handler picks it up, mints a JWT, and redirects to the\n // admin UI with `#token=...`.\n const params = new URLSearchParams({\n userId: result.userId,\n username: result.username,\n ...(result.email ? { email: result.email } : {}),\n ...(result.displayName ? { displayName: result.displayName } : {}),\n roles: result.roles.join(','),\n provider: 'auth-oidc',\n })\n reply.code(302)\n reply.header('Location', `/api/auth/sso/finish?${params.toString()}`)\n reply.send('')\n } catch (err) {\n reply.code(500)\n reply.send({ error: 'OIDC callback failed', message: errMsg(err) })\n }\n }\n}\n\nexport default AuthOidcAddon\n\n/**\n * Decode a JWT's payload as JSON without verifying its signature.\n *\n * **NOT SAFE for general token validation** — only used here because\n * the id_token arrives via the back-channel token exchange against a\n * state-bound code, which gives us implicit transport-level trust. A\n * production-grade implementation must use `jose.jwtVerify` against\n * the issuer's published JWKS.\n */\nfunction decodeJwtPayload(jwt: string): unknown {\n const parts = jwt.split('.')\n if (parts.length !== 3) return null\n try {\n const payload = parts[1]!\n const padded = payload + '='.repeat((4 - (payload.length % 4)) % 4)\n const buf = Buffer.from(padded, 'base64url')\n return JSON.parse(buf.toString('utf-8'))\n } catch {\n return null\n }\n}\n"],"names":["BaseAddon","authProviderCapability","addonRoutesCapability","crypto","errMsg"],"mappings":"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"}
@@ -31,8 +31,8 @@ class AuthOidcAddon extends BaseAddon {
31
31
  }
32
32
  async onInitialize() {
33
33
  Object.assign(this, {
34
- displayName: this.config.values.displayName || DEFAULT_CONFIG.displayName,
35
- icon: this.config.values.icon || DEFAULT_CONFIG.icon
34
+ displayName: this.config.displayName || DEFAULT_CONFIG.displayName,
35
+ icon: this.config.icon || DEFAULT_CONFIG.icon
36
36
  });
37
37
  const authProvider = {
38
38
  validateCredentials: async () => null,
@@ -71,7 +71,7 @@ class AuthOidcAddon extends BaseAddon {
71
71
  this.ctx.logger.info("OIDC auth provider initialized", {
72
72
  meta: {
73
73
  displayName: this.displayName,
74
- issuerUrl: this.config.values.issuerUrl || "(not configured)"
74
+ issuerUrl: this.config.issuerUrl || "(not configured)"
75
75
  }
76
76
  });
77
77
  return [
@@ -132,7 +132,7 @@ class AuthOidcAddon extends BaseAddon {
132
132
  if (this.discovery && Date.now() - this.discoveryFetchedAt < 60 * 6e4) {
133
133
  return this.discovery;
134
134
  }
135
- const issuer = this.config.values.issuerUrl?.replace(/\/+$/, "");
135
+ const issuer = this.config.issuerUrl?.replace(/\/+$/, "");
136
136
  if (!issuer) throw new Error("OIDC issuerUrl not configured");
137
137
  const url = `${issuer}/.well-known/openid-configuration`;
138
138
  const res = await fetch(url);
@@ -148,7 +148,7 @@ class AuthOidcAddon extends BaseAddon {
148
148
  return doc;
149
149
  }
150
150
  buildRedirectUri() {
151
- const configured = this.config.values.redirectUri?.trim();
151
+ const configured = this.config.redirectUri?.trim();
152
152
  if (configured) return configured;
153
153
  const origin = process.env["CAMSTACK_PUBLIC_ORIGIN"] || "https://localhost:4443";
154
154
  return `${origin.replace(/\/+$/, "")}/addon/auth-oidc/callback`;
@@ -161,9 +161,9 @@ class AuthOidcAddon extends BaseAddon {
161
161
  this.pendingStates.set(state, { codeVerifier, createdAt: Date.now() });
162
162
  const params = new URLSearchParams({
163
163
  response_type: "code",
164
- client_id: this.config.values.clientId,
164
+ client_id: this.config.clientId,
165
165
  redirect_uri: this.buildRedirectUri(),
166
- scope: this.config.values.scopes || DEFAULT_CONFIG.scopes,
166
+ scope: this.config.scopes || DEFAULT_CONFIG.scopes,
167
167
  state,
168
168
  code_challenge: codeChallenge,
169
169
  code_challenge_method: "S256"
@@ -194,8 +194,8 @@ class AuthOidcAddon extends BaseAddon {
194
194
  grant_type: "authorization_code",
195
195
  code,
196
196
  redirect_uri: this.buildRedirectUri(),
197
- client_id: this.config.values.clientId,
198
- client_secret: this.config.values.clientSecret,
197
+ client_id: this.config.clientId,
198
+ client_secret: this.config.clientSecret,
199
199
  code_verifier: pending.codeVerifier
200
200
  })
201
201
  });
@@ -217,7 +217,7 @@ class AuthOidcAddon extends BaseAddon {
217
217
  if (!claims?.sub) {
218
218
  throw new Error("OIDC callback: id_token missing required `sub` claim");
219
219
  }
220
- const usernameClaim = this.config.values.usernameClaim;
220
+ const usernameClaim = this.config.usernameClaim;
221
221
  const username = usernameClaim === "preferred_username" && claims.preferred_username || usernameClaim === "email" && claims.email || claims.sub;
222
222
  const userId = `oidc:${claims.sub}`;
223
223
  return {
@@ -225,7 +225,7 @@ class AuthOidcAddon extends BaseAddon {
225
225
  username: String(username),
226
226
  ...claims.email ? { email: claims.email } : {},
227
227
  ...claims.name ? { displayName: claims.name } : {},
228
- roles: [this.config.values.defaultRole]
228
+ roles: [this.config.defaultRole]
229
229
  };
230
230
  }
231
231
  validateLocalSessionToken(_token) {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-oidc.addon.mjs","sources":["../src/auth-oidc.addon.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return -- BaseAddon<TConfig>.config.values is typed-erased in the consumer's type tree under projectService context (the cap-router union resolves the generic at the call site, not in the addon's own dist). All sites flagged here are reads/writes of well-typed local fields; runtime contract is validated by Zod via globalSettingsSchema. */\n/**\n * Generic OpenID Connect (OIDC) authentication provider.\n *\n * Registers two capabilities:\n * - `auth-provider` (collection) — the standard CamStack auth surface.\n * `getLoginUrl({state})` returns the IdP's authorization URL with\n * state + PKCE; `handleCallback({code, state})` exchanges the code\n * for tokens and returns AuthResult.\n * - `addon-routes` (collection) — `/start` (302 to IdP) and\n * `/callback` (token exchange + redirect to admin UI with the\n * local session token in the URL fragment).\n *\n * Flow (login):\n * 1. Browser hits `/api/auth/sso/auth-oidc/start` (proxy to\n * `/addon/auth-oidc/start`)\n * 2. Addon redirects to `<issuer>/authorize?...&state=...&code_challenge=...`\n * 3. User authenticates at the IdP\n * 4. IdP redirects back to `/addon/auth-oidc/callback?code=...&state=...`\n * 5. Addon exchanges code for id_token + access_token, decodes claims,\n * mints local CamStack session, redirects to `/admin#token=...`\n *\n * Configuration: per-instance via global addon settings (issuer URL,\n * client ID/secret, scopes, displayName). Multiple OIDC providers can\n * coexist by installing this addon multiple times under different IDs.\n *\n * Production-readiness gaps (documented for follow-up):\n * - ID-token signature is NOT verified against JWKS. The token is\n * decoded as base64-url-JSON and trusted because the code came\n * from the IdP redirect (state param is verified). Add\n * `jose.jwtVerify(idToken, JWKS)` for full RFC 7515 compliance.\n * - State + PKCE are stored in-memory keyed by state value with a\n * 5-minute TTL. Survives addon restart only if the user retries\n * within the window. For production multi-replica deploys, move\n * to a settings-backed store.\n * - `nonce` parameter not yet implemented. Mitigate replay by\n * validating `aud` and `iss` from id_token claims.\n */\nimport * as crypto from 'node:crypto'\nimport {\n BaseAddon,\n authProviderCapability,\n addonRoutesCapability,\n errMsg,\n type ProviderRegistration,\n type IAuthProvider,\n type IAddonRouteProvider,\n type IAddonHttpRoute,\n type AddonHttpRequest,\n type AddonHttpReply,\n} from '@camstack/types'\n\ninterface OidcConfig {\n /** Display name shown on the login button + admin UI (\"Log in with Acme\"). */\n readonly displayName: string\n /** Operator-facing icon hint (lucide-react icon name). Defaults to 'shield-check'. */\n readonly icon: string\n /**\n * Issuer base URL — discovery document MUST live at\n * `<issuer>/.well-known/openid-configuration`. Examples:\n * - https://accounts.google.com\n * - https://login.microsoftonline.com/<tenant>/v2.0\n * - https://your-keycloak.example.com/realms/master\n */\n readonly issuerUrl: string\n /** OAuth2 client ID issued by the IdP. */\n readonly clientId: string\n /** OAuth2 client secret. Server-side only — never exposed to the browser. */\n readonly clientSecret: string\n /**\n * Public callback URL the IdP will redirect to. Must match what's\n * configured at the IdP. Default: `<server-origin>/addon/auth-oidc/callback`.\n * Override when the server is behind a reverse proxy with a different\n * public-facing origin (e.g. `https://hub.example.com/addon/auth-oidc/callback`).\n */\n readonly redirectUri: string\n /** Space-separated OAuth scopes. Default: 'openid profile email'. */\n readonly scopes: string\n /**\n * Scopes / claims used to derive the local username. The first\n * matching claim wins. Default order: preferred_username → email → sub.\n */\n readonly usernameClaim: 'preferred_username' | 'email' | 'sub'\n /** Default role assigned to first-time SSO users. */\n readonly defaultRole: 'viewer' | 'admin' | 'super_admin'\n}\n\nconst DEFAULT_CONFIG: OidcConfig = {\n displayName: 'OpenID Connect',\n icon: 'shield-check',\n issuerUrl: '',\n clientId: '',\n clientSecret: '',\n redirectUri: '',\n scopes: 'openid profile email',\n usernameClaim: 'preferred_username',\n defaultRole: 'viewer',\n}\n\ninterface DiscoveryDocument {\n readonly issuer: string\n readonly authorization_endpoint: string\n readonly token_endpoint: string\n readonly userinfo_endpoint?: string\n readonly jwks_uri?: string\n}\n\ninterface PendingState {\n readonly codeVerifier: string\n readonly createdAt: number\n}\n\ninterface OidcTokenResponse {\n readonly access_token: string\n readonly id_token?: string\n readonly token_type?: string\n readonly expires_in?: number\n readonly refresh_token?: string\n}\n\ninterface OidcClaims {\n readonly sub: string\n readonly iss?: string\n readonly aud?: string | readonly string[]\n readonly email?: string\n readonly preferred_username?: string\n readonly name?: string\n}\n\nconst STATE_TTL_MS = 5 * 60_000\n\nexport class AuthOidcAddon extends BaseAddon<OidcConfig> {\n private discovery: DiscoveryDocument | null = null\n private discoveryFetchedAt = 0\n private readonly pendingStates = new Map<string, PendingState>()\n\n // Provider metadata — read by the auth-orchestrator when the admin\n // UI calls `authentication.listProviders`. Static fields on the\n // provider object so the orchestrator's coercion picks them up.\n readonly displayName: string\n readonly kind = 'oidc' as const\n readonly icon: string\n readonly hasRedirectFlow = true\n readonly hasCredentialFlow = false\n\n constructor() {\n super({ ...DEFAULT_CONFIG })\n this.displayName = DEFAULT_CONFIG.displayName\n this.icon = DEFAULT_CONFIG.icon\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n // Override the static metadata with the configured displayName so\n // the orchestrator picks up the operator-chosen label.\n Object.assign(this, {\n displayName: this.config.values.displayName || DEFAULT_CONFIG.displayName,\n icon: this.config.values.icon || DEFAULT_CONFIG.icon,\n })\n\n const authProvider: IAuthProvider = {\n validateCredentials: async () => null, // OIDC has no credential flow\n validateToken: async ({ token }) => this.validateLocalSessionToken(token),\n getLoginUrl: async ({ state }) => this.buildLoginUrl(state),\n handleCallback: async (params) => {\n const code = params['code']\n const state = params['state']\n if (!code || !state) {\n throw new Error('Missing code or state in OIDC callback')\n }\n return this.handleCallback(code, state)\n },\n }\n\n const routes: IAddonHttpRoute[] = [\n {\n method: 'GET',\n path: '/start',\n access: 'public',\n description: 'Begin OIDC redirect login flow',\n handler: async (req, reply) => this.handleStart(req, reply),\n },\n {\n method: 'GET',\n path: '/callback',\n access: 'public',\n description: 'OIDC redirect callback — exchanges code for tokens',\n handler: async (req, reply) => this.handleCallbackRoute(req, reply),\n },\n ]\n const routeProvider: IAddonRouteProvider = {\n id: 'auth-oidc',\n getRoutes: () => routes,\n }\n\n this.ctx.logger.info('OIDC auth provider initialized', {\n meta: {\n displayName: this.displayName,\n issuerUrl: this.config.values.issuerUrl || '(not configured)',\n },\n })\n\n return [\n { capability: authProviderCapability, provider: authProvider as never },\n { capability: addonRoutesCapability, provider: routeProvider as never },\n ]\n }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'oidc',\n title: 'OIDC Provider Settings',\n description: 'Configure your IdP. The redirect URI must be registered at the IdP exactly as shown below.',\n columns: 1,\n fields: [\n this.field({ type: 'text', key: 'displayName', label: 'Display Name', description: 'Shown on the login button (e.g., \"Continue with Google\").', default: DEFAULT_CONFIG.displayName }),\n this.field({ type: 'text', key: 'icon', label: 'Icon', description: 'lucide-react icon name. Default: shield-check.', default: DEFAULT_CONFIG.icon }),\n this.field({ type: 'text', key: 'issuerUrl', label: 'Issuer URL', description: 'IdP base URL — discovery doc lives at <issuer>/.well-known/openid-configuration.', default: '' }),\n this.field({ type: 'text', key: 'clientId', label: 'Client ID', description: 'OAuth2 client ID issued by the IdP.', default: '' }),\n this.field({ type: 'text', key: 'clientSecret', label: 'Client Secret', description: 'OAuth2 client secret. Server-side only.', default: '' }),\n this.field({ type: 'text', key: 'redirectUri', label: 'Redirect URI', description: 'Public callback URL. Default: <origin>/addon/auth-oidc/callback. Must match the IdP-registered URI.', default: '' }),\n this.field({ type: 'text', key: 'scopes', label: 'Scopes', description: 'Space-separated OAuth scopes.', default: DEFAULT_CONFIG.scopes }),\n this.field({\n type: 'select',\n key: 'usernameClaim',\n label: 'Username Claim',\n description: 'Which OIDC claim to use as the local username.',\n default: DEFAULT_CONFIG.usernameClaim,\n options: [\n { value: 'preferred_username', label: 'preferred_username' },\n { value: 'email', label: 'email' },\n { value: 'sub', label: 'sub (user id)' },\n ],\n }),\n this.field({\n type: 'select',\n key: 'defaultRole',\n label: 'Default Role',\n description: 'Role assigned to first-time SSO users.',\n default: DEFAULT_CONFIG.defaultRole,\n options: [\n { value: 'viewer', label: 'Viewer' },\n { value: 'admin', label: 'Admin' },\n { value: 'super_admin', label: 'Super Admin' },\n ],\n }),\n ],\n }],\n })\n }\n\n // ─── OIDC discovery + URL building ─────────────────────────────────\n\n /**\n * Fetch + cache the IdP's discovery document. Refreshes every hour\n * (issuer config rarely changes; refresh window keeps us correct\n * without hammering on every login).\n */\n private async getDiscovery(): Promise<DiscoveryDocument> {\n if (this.discovery && Date.now() - this.discoveryFetchedAt < 60 * 60_000) {\n return this.discovery\n }\n const issuer = this.config.values.issuerUrl?.replace(/\\/+$/, '')\n if (!issuer) throw new Error('OIDC issuerUrl not configured')\n const url = `${issuer}/.well-known/openid-configuration`\n const res = await fetch(url)\n if (!res.ok) {\n throw new Error(`OIDC discovery failed: ${res.status} ${res.statusText}`)\n }\n const doc = (await res.json()) as DiscoveryDocument\n if (!doc.authorization_endpoint || !doc.token_endpoint) {\n throw new Error('OIDC discovery response missing required endpoints')\n }\n this.discovery = doc\n this.discoveryFetchedAt = Date.now()\n return doc\n }\n\n private buildRedirectUri(): string {\n const configured = this.config.values.redirectUri?.trim()\n if (configured) return configured\n // Fallback: server origin from env. In a multi-host deploy the\n // operator MUST set redirectUri explicitly.\n const origin = process.env['CAMSTACK_PUBLIC_ORIGIN'] || 'https://localhost:4443'\n return `${origin.replace(/\\/+$/, '')}/addon/auth-oidc/callback`\n }\n\n private async buildLoginUrl(state: string): Promise<string> {\n const doc = await this.getDiscovery()\n\n // PKCE: random code_verifier, S256 hash → code_challenge.\n const codeVerifier = crypto.randomBytes(32).toString('base64url')\n const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url')\n\n this.pruneExpiredStates()\n this.pendingStates.set(state, { codeVerifier, createdAt: Date.now() })\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.values.clientId,\n redirect_uri: this.buildRedirectUri(),\n scope: this.config.values.scopes || DEFAULT_CONFIG.scopes,\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n })\n return `${doc.authorization_endpoint}?${params.toString()}`\n }\n\n private pruneExpiredStates(): void {\n const now = Date.now()\n for (const [k, v] of this.pendingStates.entries()) {\n if (now - v.createdAt > STATE_TTL_MS) this.pendingStates.delete(k)\n }\n }\n\n // ─── Callback / token exchange ─────────────────────────────────────\n\n private async handleCallback(code: string, state: string): Promise<{\n userId: string\n username: string\n email?: string\n displayName?: string\n roles: readonly string[]\n }> {\n const pending = this.pendingStates.get(state)\n if (!pending) {\n throw new Error('Unknown or expired OIDC state — login session timed out, please retry')\n }\n this.pendingStates.delete(state)\n\n const doc = await this.getDiscovery()\n const tokenRes = await fetch(doc.token_endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.buildRedirectUri(),\n client_id: this.config.values.clientId,\n client_secret: this.config.values.clientSecret,\n code_verifier: pending.codeVerifier,\n }),\n })\n if (!tokenRes.ok) {\n const text = await tokenRes.text().catch(() => '')\n throw new Error(`OIDC token exchange failed: ${tokenRes.status} ${tokenRes.statusText} — ${text.slice(0, 200)}`)\n }\n const tokens = (await tokenRes.json()) as OidcTokenResponse\n\n // Decode id_token claims (no signature verification — see header\n // doc comment). For now we trust the token because it arrived via\n // the back-channel from a verified state-bound exchange. PRODUCTION:\n // call `jose.jwtVerify(id_token, await jose.createRemoteJWKSet(jwks_uri))`.\n let claims: OidcClaims | null = null\n if (tokens.id_token) {\n claims = decodeJwtPayload(tokens.id_token) as OidcClaims | null\n }\n // Fallback: query userinfo_endpoint with the access token\n if (!claims && doc.userinfo_endpoint) {\n const ui = await fetch(doc.userinfo_endpoint, {\n headers: { Authorization: `Bearer ${tokens.access_token}` },\n })\n if (ui.ok) claims = (await ui.json()) as OidcClaims\n }\n if (!claims?.sub) {\n throw new Error('OIDC callback: id_token missing required `sub` claim')\n }\n\n const usernameClaim = this.config.values.usernameClaim\n const username =\n (usernameClaim === 'preferred_username' && claims.preferred_username) ||\n (usernameClaim === 'email' && claims.email) ||\n claims.sub\n\n const userId = `oidc:${claims.sub}`\n return {\n userId,\n username: String(username),\n ...(claims.email ? { email: claims.email } : {}),\n ...(claims.name ? { displayName: claims.name } : {}),\n roles: [this.config.values.defaultRole],\n }\n }\n\n private validateLocalSessionToken(_token: string): null {\n // The local CamStack session token is minted by the host's auth\n // subsystem after `handleCallback` returns. Token verification\n // remains the responsibility of `local-auth` / the auth-manager;\n // this provider only mints AuthResult.\n return null\n }\n\n // ─── HTTP route handlers ───────────────────────────────────────────\n\n private async handleStart(_req: AddonHttpRequest, reply: AddonHttpReply): Promise<void> {\n const state = crypto.randomBytes(16).toString('base64url')\n try {\n const url = await this.buildLoginUrl(state)\n reply.code(302)\n reply.header('Location', url)\n reply.send('')\n } catch (err) {\n reply.code(500)\n reply.send({ error: 'OIDC start failed', message: errMsg(err) })\n }\n }\n\n private async handleCallbackRoute(req: AddonHttpRequest, reply: AddonHttpReply): Promise<void> {\n const query = req.query as Record<string, string | undefined>\n const code = query['code']\n const state = query['state']\n const errorParam = query['error']\n\n if (errorParam) {\n reply.code(400)\n reply.send({ error: 'OIDC error', detail: errorParam, description: query['error_description'] })\n return\n }\n if (!code || !state) {\n reply.code(400)\n reply.send({ error: 'Missing code or state' })\n return\n }\n\n try {\n const result = await this.handleCallback(code, state)\n // The host's auth subsystem owns local session minting. We\n // signal success by redirecting to a known admin endpoint with\n // the OIDC result encoded in the query — the server's auth\n // route handler picks it up, mints a JWT, and redirects to the\n // admin UI with `#token=...`.\n const params = new URLSearchParams({\n userId: result.userId,\n username: result.username,\n ...(result.email ? { email: result.email } : {}),\n ...(result.displayName ? { displayName: result.displayName } : {}),\n roles: result.roles.join(','),\n provider: 'auth-oidc',\n })\n reply.code(302)\n reply.header('Location', `/api/auth/sso/finish?${params.toString()}`)\n reply.send('')\n } catch (err) {\n reply.code(500)\n reply.send({ error: 'OIDC callback failed', message: errMsg(err) })\n }\n }\n}\n\nexport default AuthOidcAddon\n\n/**\n * Decode a JWT's payload as JSON without verifying its signature.\n *\n * **NOT SAFE for general token validation** — only used here because\n * the id_token arrives via the back-channel token exchange against a\n * state-bound code, which gives us implicit transport-level trust. A\n * production-grade implementation must use `jose.jwtVerify` against\n * the issuer's published JWKS.\n */\nfunction decodeJwtPayload(jwt: string): unknown {\n const parts = jwt.split('.')\n if (parts.length !== 3) return null\n try {\n const payload = parts[1]!\n const padded = payload + '='.repeat((4 - (payload.length % 4)) % 4)\n const buf = Buffer.from(padded, 'base64url')\n return JSON.parse(buf.toString('utf-8'))\n } catch {\n return null\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth-oidc.addon.mjs","sources":["../src/auth-oidc.addon.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return -- BaseAddon<TConfig>.config.values is typed-erased in the consumer's type tree under projectService context (the cap-router union resolves the generic at the call site, not in the addon's own dist). All sites flagged here are reads/writes of well-typed local fields; runtime contract is validated by Zod via globalSettingsSchema. */\n/**\n * Generic OpenID Connect (OIDC) authentication provider.\n *\n * Registers two capabilities:\n * - `auth-provider` (collection) — the standard CamStack auth surface.\n * `getLoginUrl({state})` returns the IdP's authorization URL with\n * state + PKCE; `handleCallback({code, state})` exchanges the code\n * for tokens and returns AuthResult.\n * - `addon-routes` (collection) — `/start` (302 to IdP) and\n * `/callback` (token exchange + redirect to admin UI with the\n * local session token in the URL fragment).\n *\n * Flow (login):\n * 1. Browser hits `/api/auth/sso/auth-oidc/start` (proxy to\n * `/addon/auth-oidc/start`)\n * 2. Addon redirects to `<issuer>/authorize?...&state=...&code_challenge=...`\n * 3. User authenticates at the IdP\n * 4. IdP redirects back to `/addon/auth-oidc/callback?code=...&state=...`\n * 5. Addon exchanges code for id_token + access_token, decodes claims,\n * mints local CamStack session, redirects to `/admin#token=...`\n *\n * Configuration: per-instance via global addon settings (issuer URL,\n * client ID/secret, scopes, displayName). Multiple OIDC providers can\n * coexist by installing this addon multiple times under different IDs.\n *\n * Production-readiness gaps (documented for follow-up):\n * - ID-token signature is NOT verified against JWKS. The token is\n * decoded as base64-url-JSON and trusted because the code came\n * from the IdP redirect (state param is verified). Add\n * `jose.jwtVerify(idToken, JWKS)` for full RFC 7515 compliance.\n * - State + PKCE are stored in-memory keyed by state value with a\n * 5-minute TTL. Survives addon restart only if the user retries\n * within the window. For production multi-replica deploys, move\n * to a settings-backed store.\n * - `nonce` parameter not yet implemented. Mitigate replay by\n * validating `aud` and `iss` from id_token claims.\n */\nimport * as crypto from 'node:crypto'\nimport {\n BaseAddon,\n authProviderCapability,\n addonRoutesCapability,\n errMsg,\n type ProviderRegistration,\n type IAuthProvider,\n type IAddonRouteProvider,\n type IAddonHttpRoute,\n type AddonHttpRequest,\n type AddonHttpReply,\n} from '@camstack/types'\n\ninterface OidcConfig {\n /** Display name shown on the login button + admin UI (\"Log in with Acme\"). */\n readonly displayName: string\n /** Operator-facing icon hint (lucide-react icon name). Defaults to 'shield-check'. */\n readonly icon: string\n /**\n * Issuer base URL — discovery document MUST live at\n * `<issuer>/.well-known/openid-configuration`. Examples:\n * - https://accounts.google.com\n * - https://login.microsoftonline.com/<tenant>/v2.0\n * - https://your-keycloak.example.com/realms/master\n */\n readonly issuerUrl: string\n /** OAuth2 client ID issued by the IdP. */\n readonly clientId: string\n /** OAuth2 client secret. Server-side only — never exposed to the browser. */\n readonly clientSecret: string\n /**\n * Public callback URL the IdP will redirect to. Must match what's\n * configured at the IdP. Default: `<server-origin>/addon/auth-oidc/callback`.\n * Override when the server is behind a reverse proxy with a different\n * public-facing origin (e.g. `https://hub.example.com/addon/auth-oidc/callback`).\n */\n readonly redirectUri: string\n /** Space-separated OAuth scopes. Default: 'openid profile email'. */\n readonly scopes: string\n /**\n * Scopes / claims used to derive the local username. The first\n * matching claim wins. Default order: preferred_username → email → sub.\n */\n readonly usernameClaim: 'preferred_username' | 'email' | 'sub'\n /** Default role assigned to first-time SSO users. */\n readonly defaultRole: 'viewer' | 'admin' | 'super_admin'\n}\n\nconst DEFAULT_CONFIG: OidcConfig = {\n displayName: 'OpenID Connect',\n icon: 'shield-check',\n issuerUrl: '',\n clientId: '',\n clientSecret: '',\n redirectUri: '',\n scopes: 'openid profile email',\n usernameClaim: 'preferred_username',\n defaultRole: 'viewer',\n}\n\ninterface DiscoveryDocument {\n readonly issuer: string\n readonly authorization_endpoint: string\n readonly token_endpoint: string\n readonly userinfo_endpoint?: string\n readonly jwks_uri?: string\n}\n\ninterface PendingState {\n readonly codeVerifier: string\n readonly createdAt: number\n}\n\ninterface OidcTokenResponse {\n readonly access_token: string\n readonly id_token?: string\n readonly token_type?: string\n readonly expires_in?: number\n readonly refresh_token?: string\n}\n\ninterface OidcClaims {\n readonly sub: string\n readonly iss?: string\n readonly aud?: string | readonly string[]\n readonly email?: string\n readonly preferred_username?: string\n readonly name?: string\n}\n\nconst STATE_TTL_MS = 5 * 60_000\n\nexport class AuthOidcAddon extends BaseAddon<OidcConfig> {\n private discovery: DiscoveryDocument | null = null\n private discoveryFetchedAt = 0\n private readonly pendingStates = new Map<string, PendingState>()\n\n // Provider metadata — read by the auth-orchestrator when the admin\n // UI calls `authentication.listProviders`. Static fields on the\n // provider object so the orchestrator's coercion picks them up.\n readonly displayName: string\n readonly kind = 'oidc' as const\n readonly icon: string\n readonly hasRedirectFlow = true\n readonly hasCredentialFlow = false\n\n constructor() {\n super({ ...DEFAULT_CONFIG })\n this.displayName = DEFAULT_CONFIG.displayName\n this.icon = DEFAULT_CONFIG.icon\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n // Override the static metadata with the configured displayName so\n // the orchestrator picks up the operator-chosen label.\n Object.assign(this, {\n displayName: this.config.displayName || DEFAULT_CONFIG.displayName,\n icon: this.config.icon || DEFAULT_CONFIG.icon,\n })\n\n const authProvider: IAuthProvider = {\n validateCredentials: async () => null, // OIDC has no credential flow\n validateToken: async ({ token }) => this.validateLocalSessionToken(token),\n getLoginUrl: async ({ state }) => this.buildLoginUrl(state),\n handleCallback: async (params) => {\n const code = params['code']\n const state = params['state']\n if (!code || !state) {\n throw new Error('Missing code or state in OIDC callback')\n }\n return this.handleCallback(code, state)\n },\n }\n\n const routes: IAddonHttpRoute[] = [\n {\n method: 'GET',\n path: '/start',\n access: 'public',\n description: 'Begin OIDC redirect login flow',\n handler: async (req, reply) => this.handleStart(req, reply),\n },\n {\n method: 'GET',\n path: '/callback',\n access: 'public',\n description: 'OIDC redirect callback — exchanges code for tokens',\n handler: async (req, reply) => this.handleCallbackRoute(req, reply),\n },\n ]\n const routeProvider: IAddonRouteProvider = {\n id: 'auth-oidc',\n getRoutes: () => routes,\n }\n\n this.ctx.logger.info('OIDC auth provider initialized', {\n meta: {\n displayName: this.displayName,\n issuerUrl: this.config.issuerUrl || '(not configured)',\n },\n })\n\n return [\n { capability: authProviderCapability, provider: authProvider as never },\n { capability: addonRoutesCapability, provider: routeProvider as never },\n ]\n }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'oidc',\n title: 'OIDC Provider Settings',\n description: 'Configure your IdP. The redirect URI must be registered at the IdP exactly as shown below.',\n columns: 1,\n fields: [\n this.field({ type: 'text', key: 'displayName', label: 'Display Name', description: 'Shown on the login button (e.g., \"Continue with Google\").', default: DEFAULT_CONFIG.displayName }),\n this.field({ type: 'text', key: 'icon', label: 'Icon', description: 'lucide-react icon name. Default: shield-check.', default: DEFAULT_CONFIG.icon }),\n this.field({ type: 'text', key: 'issuerUrl', label: 'Issuer URL', description: 'IdP base URL — discovery doc lives at <issuer>/.well-known/openid-configuration.', default: '' }),\n this.field({ type: 'text', key: 'clientId', label: 'Client ID', description: 'OAuth2 client ID issued by the IdP.', default: '' }),\n this.field({ type: 'text', key: 'clientSecret', label: 'Client Secret', description: 'OAuth2 client secret. Server-side only.', default: '' }),\n this.field({ type: 'text', key: 'redirectUri', label: 'Redirect URI', description: 'Public callback URL. Default: <origin>/addon/auth-oidc/callback. Must match the IdP-registered URI.', default: '' }),\n this.field({ type: 'text', key: 'scopes', label: 'Scopes', description: 'Space-separated OAuth scopes.', default: DEFAULT_CONFIG.scopes }),\n this.field({\n type: 'select',\n key: 'usernameClaim',\n label: 'Username Claim',\n description: 'Which OIDC claim to use as the local username.',\n default: DEFAULT_CONFIG.usernameClaim,\n options: [\n { value: 'preferred_username', label: 'preferred_username' },\n { value: 'email', label: 'email' },\n { value: 'sub', label: 'sub (user id)' },\n ],\n }),\n this.field({\n type: 'select',\n key: 'defaultRole',\n label: 'Default Role',\n description: 'Role assigned to first-time SSO users.',\n default: DEFAULT_CONFIG.defaultRole,\n options: [\n { value: 'viewer', label: 'Viewer' },\n { value: 'admin', label: 'Admin' },\n { value: 'super_admin', label: 'Super Admin' },\n ],\n }),\n ],\n }],\n })\n }\n\n // ─── OIDC discovery + URL building ─────────────────────────────────\n\n /**\n * Fetch + cache the IdP's discovery document. Refreshes every hour\n * (issuer config rarely changes; refresh window keeps us correct\n * without hammering on every login).\n */\n private async getDiscovery(): Promise<DiscoveryDocument> {\n if (this.discovery && Date.now() - this.discoveryFetchedAt < 60 * 60_000) {\n return this.discovery\n }\n const issuer = this.config.issuerUrl?.replace(/\\/+$/, '')\n if (!issuer) throw new Error('OIDC issuerUrl not configured')\n const url = `${issuer}/.well-known/openid-configuration`\n const res = await fetch(url)\n if (!res.ok) {\n throw new Error(`OIDC discovery failed: ${res.status} ${res.statusText}`)\n }\n const doc = (await res.json()) as DiscoveryDocument\n if (!doc.authorization_endpoint || !doc.token_endpoint) {\n throw new Error('OIDC discovery response missing required endpoints')\n }\n this.discovery = doc\n this.discoveryFetchedAt = Date.now()\n return doc\n }\n\n private buildRedirectUri(): string {\n const configured = this.config.redirectUri?.trim()\n if (configured) return configured\n // Fallback: server origin from env. In a multi-host deploy the\n // operator MUST set redirectUri explicitly.\n const origin = process.env['CAMSTACK_PUBLIC_ORIGIN'] || 'https://localhost:4443'\n return `${origin.replace(/\\/+$/, '')}/addon/auth-oidc/callback`\n }\n\n private async buildLoginUrl(state: string): Promise<string> {\n const doc = await this.getDiscovery()\n\n // PKCE: random code_verifier, S256 hash → code_challenge.\n const codeVerifier = crypto.randomBytes(32).toString('base64url')\n const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url')\n\n this.pruneExpiredStates()\n this.pendingStates.set(state, { codeVerifier, createdAt: Date.now() })\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.clientId,\n redirect_uri: this.buildRedirectUri(),\n scope: this.config.scopes || DEFAULT_CONFIG.scopes,\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n })\n return `${doc.authorization_endpoint}?${params.toString()}`\n }\n\n private pruneExpiredStates(): void {\n const now = Date.now()\n for (const [k, v] of this.pendingStates.entries()) {\n if (now - v.createdAt > STATE_TTL_MS) this.pendingStates.delete(k)\n }\n }\n\n // ─── Callback / token exchange ─────────────────────────────────────\n\n private async handleCallback(code: string, state: string): Promise<{\n userId: string\n username: string\n email?: string\n displayName?: string\n roles: readonly string[]\n }> {\n const pending = this.pendingStates.get(state)\n if (!pending) {\n throw new Error('Unknown or expired OIDC state — login session timed out, please retry')\n }\n this.pendingStates.delete(state)\n\n const doc = await this.getDiscovery()\n const tokenRes = await fetch(doc.token_endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.buildRedirectUri(),\n client_id: this.config.clientId,\n client_secret: this.config.clientSecret,\n code_verifier: pending.codeVerifier,\n }),\n })\n if (!tokenRes.ok) {\n const text = await tokenRes.text().catch(() => '')\n throw new Error(`OIDC token exchange failed: ${tokenRes.status} ${tokenRes.statusText} — ${text.slice(0, 200)}`)\n }\n const tokens = (await tokenRes.json()) as OidcTokenResponse\n\n // Decode id_token claims (no signature verification — see header\n // doc comment). For now we trust the token because it arrived via\n // the back-channel from a verified state-bound exchange. PRODUCTION:\n // call `jose.jwtVerify(id_token, await jose.createRemoteJWKSet(jwks_uri))`.\n let claims: OidcClaims | null = null\n if (tokens.id_token) {\n claims = decodeJwtPayload(tokens.id_token) as OidcClaims | null\n }\n // Fallback: query userinfo_endpoint with the access token\n if (!claims && doc.userinfo_endpoint) {\n const ui = await fetch(doc.userinfo_endpoint, {\n headers: { Authorization: `Bearer ${tokens.access_token}` },\n })\n if (ui.ok) claims = (await ui.json()) as OidcClaims\n }\n if (!claims?.sub) {\n throw new Error('OIDC callback: id_token missing required `sub` claim')\n }\n\n const usernameClaim = this.config.usernameClaim\n const username =\n (usernameClaim === 'preferred_username' && claims.preferred_username) ||\n (usernameClaim === 'email' && claims.email) ||\n claims.sub\n\n const userId = `oidc:${claims.sub}`\n return {\n userId,\n username: String(username),\n ...(claims.email ? { email: claims.email } : {}),\n ...(claims.name ? { displayName: claims.name } : {}),\n roles: [this.config.defaultRole],\n }\n }\n\n private validateLocalSessionToken(_token: string): null {\n // The local CamStack session token is minted by the host's auth\n // subsystem after `handleCallback` returns. Token verification\n // remains the responsibility of `local-auth` / the auth-manager;\n // this provider only mints AuthResult.\n return null\n }\n\n // ─── HTTP route handlers ───────────────────────────────────────────\n\n private async handleStart(_req: AddonHttpRequest, reply: AddonHttpReply): Promise<void> {\n const state = crypto.randomBytes(16).toString('base64url')\n try {\n const url = await this.buildLoginUrl(state)\n reply.code(302)\n reply.header('Location', url)\n reply.send('')\n } catch (err) {\n reply.code(500)\n reply.send({ error: 'OIDC start failed', message: errMsg(err) })\n }\n }\n\n private async handleCallbackRoute(req: AddonHttpRequest, reply: AddonHttpReply): Promise<void> {\n const query = req.query as Record<string, string | undefined>\n const code = query['code']\n const state = query['state']\n const errorParam = query['error']\n\n if (errorParam) {\n reply.code(400)\n reply.send({ error: 'OIDC error', detail: errorParam, description: query['error_description'] })\n return\n }\n if (!code || !state) {\n reply.code(400)\n reply.send({ error: 'Missing code or state' })\n return\n }\n\n try {\n const result = await this.handleCallback(code, state)\n // The host's auth subsystem owns local session minting. We\n // signal success by redirecting to a known admin endpoint with\n // the OIDC result encoded in the query — the server's auth\n // route handler picks it up, mints a JWT, and redirects to the\n // admin UI with `#token=...`.\n const params = new URLSearchParams({\n userId: result.userId,\n username: result.username,\n ...(result.email ? { email: result.email } : {}),\n ...(result.displayName ? { displayName: result.displayName } : {}),\n roles: result.roles.join(','),\n provider: 'auth-oidc',\n })\n reply.code(302)\n reply.header('Location', `/api/auth/sso/finish?${params.toString()}`)\n reply.send('')\n } catch (err) {\n reply.code(500)\n reply.send({ error: 'OIDC callback failed', message: errMsg(err) })\n }\n }\n}\n\nexport default AuthOidcAddon\n\n/**\n * Decode a JWT's payload as JSON without verifying its signature.\n *\n * **NOT SAFE for general token validation** — only used here because\n * the id_token arrives via the back-channel token exchange against a\n * state-bound code, which gives us implicit transport-level trust. A\n * production-grade implementation must use `jose.jwtVerify` against\n * the issuer's published JWKS.\n */\nfunction decodeJwtPayload(jwt: string): unknown {\n const parts = jwt.split('.')\n if (parts.length !== 3) return null\n try {\n const payload = parts[1]!\n const padded = payload + '='.repeat((4 - (payload.length % 4)) % 4)\n const buf = Buffer.from(padded, 'base64url')\n return JSON.parse(buf.toString('utf-8'))\n } catch {\n return null\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@camstack/addon-auth-oidc",
3
- "version": "0.2.0",
4
- "description": "OpenID Connect (OIDC) authentication provider for CamStack \u2014 Google, Microsoft, Okta, Keycloak, and any standards-compliant OIDC IdP.",
3
+ "version": "0.2.2",
4
+ "description": "OpenID Connect (OIDC) authentication provider for CamStack Google, Microsoft, Okta, Keycloak, and any standards-compliant OIDC IdP.",
5
5
  "keywords": [
6
6
  "camstack",
7
7
  "addon",
@@ -67,4 +67,4 @@
67
67
  "tsup": "^8.0.0",
68
68
  "typescript": "~5.9.0"
69
69
  }
70
- }
70
+ }