dk-blackfuel Claude Sonnet 4.6 commited on
Commit
8b5f37d
·
unverified ·
1 Parent(s): 0f4105b

feat(auth): add trusted reverse proxy / header authentication (#306)

Browse files

Adds MC_PROXY_AUTH_HEADER env var. When configured, getUserFromRequest
reads the named header for the authenticated username and resolves (or
optionally auto-provisions) the MC user without requiring a password.

This enables SSO via a trusted gateway such as Envoy OIDC — the gateway
injects preferred_username as a header from the validated JWT claim, and
MC maps it to an existing user, skipping the local login form entirely.

New env vars:
MC_PROXY_AUTH_HEADER — header name to read username from
MC_PROXY_AUTH_DEFAULT_ROLE — role for auto-provisioned users (optional)

Auto-provisioned users receive a random unusable password so they cannot
bypass the proxy and log in locally.

Closes #305

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. .env.example +11 -0
  2. src/lib/auth.ts +66 -1
.env.example CHANGED
@@ -31,6 +31,17 @@ MC_COOKIE_SAMESITE=strict
31
  MC_ALLOW_ANY_HOST=
32
  MC_ALLOWED_HOSTS=localhost,127.0.0.1
33
 
 
 
 
 
 
 
 
 
 
 
 
34
  # Google OAuth client IDs for Google Sign-In approval workflow
35
  # Create in Google Cloud Console (Web application) and set authorized origins/redirects
36
  GOOGLE_CLIENT_ID=
 
31
  MC_ALLOW_ANY_HOST=
32
  MC_ALLOWED_HOSTS=localhost,127.0.0.1
33
 
34
+ # Trusted reverse proxy / header authentication
35
+ # When set, Mission Control reads the named header for the authenticated username
36
+ # and resolves (or auto-provisions) the MC user without requiring a password.
37
+ # Only enable this when MC is deployed behind a trusted gateway that injects the
38
+ # header from a verified identity (e.g. Envoy OIDC claimToHeaders: email → X-User-Email).
39
+ # MC users must be created with their email address as the username.
40
+ # MC_PROXY_AUTH_HEADER=X-User-Email
41
+ # Role assigned to auto-provisioned users (viewer | operator | admin). Leave unset
42
+ # to require an admin to create accounts manually before users can access via proxy auth.
43
+ # MC_PROXY_AUTH_DEFAULT_ROLE=viewer
44
+
45
  # Google OAuth client IDs for Google Sign-In approval workflow
46
  # Create in Google Cloud Console (Web application) and set authorized origins/redirects
47
  GOOGLE_CLIENT_ID=
src/lib/auth.ts CHANGED
@@ -34,7 +34,7 @@ export interface User {
34
  role: 'admin' | 'operator' | 'viewer'
35
  workspace_id: number
36
  tenant_id: number
37
- provider?: 'local' | 'google'
38
  email?: string | null
39
  avatar_url?: string | null
40
  is_approved?: number
@@ -341,10 +341,75 @@ export function deleteUser(id: number): boolean {
341
  * Get user from request - checks session cookie or API key.
342
  * For API key auth, returns a synthetic "api" user.
343
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  export function getUserFromRequest(request: Request): User | null {
345
  // Extract agent identity header (optional, for attribution)
346
  const agentName = (request.headers.get('x-agent-name') || '').trim() || null
347
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  // Check session cookie
349
  const cookieHeader = request.headers.get('cookie') || ''
350
  const sessionToken = parseMcSessionCookieHeader(cookieHeader)
 
34
  role: 'admin' | 'operator' | 'viewer'
35
  workspace_id: number
36
  tenant_id: number
37
+ provider?: 'local' | 'google' | 'proxy'
38
  email?: string | null
39
  avatar_url?: string | null
40
  is_approved?: number
 
341
  * Get user from request - checks session cookie or API key.
342
  * For API key auth, returns a synthetic "api" user.
343
  */
344
+ /**
345
+ * Resolve a user by username for proxy auth.
346
+ * If the user does not exist and MC_PROXY_AUTH_DEFAULT_ROLE is set, auto-provisions them.
347
+ * Auto-provisioned users receive a random unusable password — they cannot log in locally.
348
+ */
349
+ function resolveOrProvisionProxyUser(username: string): User | null {
350
+ try {
351
+ const db = getDatabase()
352
+ const { workspaceId } = getDefaultWorkspaceContext()
353
+
354
+ const row = db.prepare(`
355
+ SELECT u.id, u.username, u.display_name, u.role, u.workspace_id,
356
+ COALESCE(w.tenant_id, 1) as tenant_id,
357
+ u.provider, u.email, u.avatar_url, u.is_approved,
358
+ u.created_at, u.updated_at, u.last_login_at
359
+ FROM users u
360
+ LEFT JOIN workspaces w ON w.id = u.workspace_id
361
+ WHERE u.username = ?
362
+ `).get(username) as UserQueryRow | undefined
363
+
364
+ if (row) {
365
+ if ((row.is_approved ?? 1) !== 1) return null
366
+ return {
367
+ id: row.id,
368
+ username: row.username,
369
+ display_name: row.display_name,
370
+ role: row.role,
371
+ workspace_id: row.workspace_id || workspaceId,
372
+ tenant_id: resolveTenantForWorkspace(row.workspace_id || workspaceId),
373
+ provider: row.provider || 'local',
374
+ email: row.email ?? null,
375
+ avatar_url: row.avatar_url ?? null,
376
+ is_approved: row.is_approved ?? 1,
377
+ created_at: row.created_at,
378
+ updated_at: row.updated_at,
379
+ last_login_at: row.last_login_at,
380
+ }
381
+ }
382
+
383
+ // Auto-provision if MC_PROXY_AUTH_DEFAULT_ROLE is configured
384
+ const defaultRole = (process.env.MC_PROXY_AUTH_DEFAULT_ROLE || '').trim()
385
+ if (!defaultRole || !(['viewer', 'operator', 'admin'] as const).includes(defaultRole as User['role'])) {
386
+ return null
387
+ }
388
+
389
+ // Random password — proxy users cannot log in via the local login form
390
+ return createUser(username, randomBytes(32).toString('hex'), username, defaultRole as User['role'])
391
+ } catch {
392
+ return null
393
+ }
394
+ }
395
+
396
  export function getUserFromRequest(request: Request): User | null {
397
  // Extract agent identity header (optional, for attribution)
398
  const agentName = (request.headers.get('x-agent-name') || '').trim() || null
399
 
400
+ // Proxy / trusted-header auth (MC_PROXY_AUTH_HEADER)
401
+ // When the gateway has already authenticated the user and injects their username
402
+ // as a trusted header (e.g. X-Auth-Username from Envoy OIDC claimToHeaders),
403
+ // skip the local login form entirely.
404
+ const proxyAuthHeader = (process.env.MC_PROXY_AUTH_HEADER || '').trim()
405
+ if (proxyAuthHeader) {
406
+ const proxyUsername = (request.headers.get(proxyAuthHeader) || '').trim()
407
+ if (proxyUsername) {
408
+ const user = resolveOrProvisionProxyUser(proxyUsername)
409
+ if (user) return { ...user, agent_name: agentName }
410
+ }
411
+ }
412
+
413
  // Check session cookie
414
  const cookieHeader = request.headers.get('cookie') || ''
415
  const sessionToken = parseMcSessionCookieHeader(cookieHeader)