@camstack/addon-auth-oidc 0.2.1 → 0.2.3
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.
package/dist/auth-oidc.addon.js
CHANGED
|
@@ -50,8 +50,8 @@ class AuthOidcAddon extends types.BaseAddon {
|
|
|
50
50
|
}
|
|
51
51
|
async onInitialize() {
|
|
52
52
|
Object.assign(this, {
|
|
53
|
-
displayName: this.config.
|
|
54
|
-
icon: this.config.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
183
|
+
client_id: this.config.clientId,
|
|
184
184
|
redirect_uri: this.buildRedirectUri(),
|
|
185
|
-
scope: this.config.
|
|
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.
|
|
217
|
-
client_secret: this.config.
|
|
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.
|
|
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.
|
|
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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"}
|
package/dist/auth-oidc.addon.mjs
CHANGED
|
@@ -31,8 +31,8 @@ class AuthOidcAddon extends BaseAddon {
|
|
|
31
31
|
}
|
|
32
32
|
async onInitialize() {
|
|
33
33
|
Object.assign(this, {
|
|
34
|
-
displayName: this.config.
|
|
35
|
-
icon: this.config.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
164
|
+
client_id: this.config.clientId,
|
|
165
165
|
redirect_uri: this.buildRedirectUri(),
|
|
166
|
-
scope: this.config.
|
|
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.
|
|
198
|
-
client_secret: this.config.
|
|
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.
|
|
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.
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-auth-oidc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
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",
|
|
@@ -32,11 +32,8 @@
|
|
|
32
32
|
{
|
|
33
33
|
"id": "auth-oidc",
|
|
34
34
|
"name": "OpenID Connect",
|
|
35
|
-
"description": "Standards-compliant OIDC authentication. Configure issuer URL + client credentials to enable SSO via Google, Microsoft, Okta, Keycloak, etc.",
|
|
35
|
+
"description": "Standards-compliant OIDC authentication. Configure issuer URL + client credentials to enable SSO via Google, Microsoft, Okta, Keycloak, etc. In-process on hub root — pure HTTP/JWT, no native deps.",
|
|
36
36
|
"entry": "./dist/auth-oidc.addon.js",
|
|
37
|
-
"execution": {
|
|
38
|
-
"placement": "hub-only"
|
|
39
|
-
},
|
|
40
37
|
"capabilities": [
|
|
41
38
|
{
|
|
42
39
|
"name": "auth-provider"
|