diff --git a/.env.development.local.template b/.env.development.local.template index f6fb67d5..2a010911 100644 --- a/.env.development.local.template +++ b/.env.development.local.template @@ -45,6 +45,13 @@ OIDC_PROVIDERS=cern # Required Scopes: openid, profile, email # Audience: rucio (required by Rucio server) # ============================================ + +# Expected audience claim in OIDC JWT tokens +# This value must match the 'aud' claim in tokens returned by your OIDC provider +# For ATLAS Rucio instance: use "atlas-rucio-oidc-client" +# For standard Rucio instances: use "rucio" (default) +OIDC_EXPECTED_AUDIENCE_CLAIM=atlas-rucio-oidc-client + OIDC_PROVIDER_CERN_CLIENT_ID=atlas-rucio-webui OIDC_PROVIDER_CERN_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx OIDC_PROVIDER_CERN_AUTHORIZATION_URL=https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/auth diff --git a/package.json b/package.json index 26a393d7..52e9c205 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rucio-webui", - "version": "39.1.0", + "version": "39.1.1", "private": true, "scripts": { "clean": "rimraf .next .swc build", diff --git a/src/lib/core/port/secondary/env-config-gateway-output-port.ts b/src/lib/core/port/secondary/env-config-gateway-output-port.ts index 9072fef0..9f3a6772 100644 --- a/src/lib/core/port/secondary/env-config-gateway-output-port.ts +++ b/src/lib/core/port/secondary/env-config-gateway-output-port.ts @@ -80,4 +80,10 @@ export default interface EnvConfigGatewayOutputPort { * @returns whether the query parameters should get URI encoded */ paramsEncodingEnabled(): Promise; + + /** + * @returns the expected OIDC audience claim value for JWT token validation + * @returns "rucio" if not configured (default fallback) + */ + oidcExpectedAudience(): Promise; } diff --git a/src/lib/infrastructure/auth/auth.config.ts b/src/lib/infrastructure/auth/auth.config.ts index 620c2373..89ae9802 100644 --- a/src/lib/infrastructure/auth/auth.config.ts +++ b/src/lib/infrastructure/auth/auth.config.ts @@ -27,6 +27,53 @@ function getSessionUserIndex(allUsers: SessionUser[] | undefined, user: SessionU ); } +/** + * Validates the audience claim in a JWT token payload + * Logs a warning if validation fails but does NOT reject authentication + * + * @param payload - Decoded JWT token payload + * @param expectedAudience - Expected audience value from configuration + * @param providerName - Name of the OIDC provider (for logging) + * @returns true if validation passes, false otherwise + */ +function validateAudienceClaim(payload: any, expectedAudience: string, providerName: string): boolean { + const audience = payload.aud; + + if (!audience) { + console.warn( + `[OIDC] WARNING: No audience (aud) claim found in JWT token from ${providerName}. ` + + `Expected audience: ${expectedAudience}`, + ); + return false; + } + + // Handle both string and array audience claims + // Some OIDC providers return a single string, others return an array + let audienceMatches = false; + if (Array.isArray(audience)) { + audienceMatches = audience.includes(expectedAudience); + if (!audienceMatches) { + console.warn( + `[OIDC] WARNING: Audience claim mismatch for ${providerName}. ` + + `Expected: "${expectedAudience}", Found: [${audience.join(', ')}]`, + ); + } + } else { + audienceMatches = audience === expectedAudience; + if (!audienceMatches) { + console.warn( + `[OIDC] WARNING: Audience claim mismatch for ${providerName}. ` + `Expected: "${expectedAudience}", Found: "${audience}"`, + ); + } + } + + if (audienceMatches) { + console.log(`[OIDC] Audience validation passed for ${providerName}: ${expectedAudience}`); + } + + return audienceMatches; +} + /** * NextAuth configuration for Rucio WebUI * Supports UserPass and x509 authentication with multi-account sessions @@ -135,7 +182,7 @@ export const authConfig: NextAuthConfig = { const rucioAuthToken = account.access_token; const rucioAuthTokenExpires = new Date(account.expires_at! * 1000).toISOString(); - // Decode and log JWT token claims (for debugging) + // Decode and validate JWT token claims try { const tokenParts = rucioAuthToken.split('.'); if (tokenParts.length === 3) { @@ -145,9 +192,24 @@ export const authConfig: NextAuthConfig = { console.log(`[OIDC] Audience (aud) claim: ${JSON.stringify(payload.aud)}`); console.log(`[OIDC] Issuer (iss) claim: ${payload.iss}`); console.log(`[OIDC] Subject (sub) claim: ${payload.sub}`); + + // Validate audience claim + const envConfigGateway = appContainer.get(GATEWAYS.ENV_CONFIG); + const expectedAudience = await envConfigGateway.oidcExpectedAudience(); + + const validationPassed = validateAudienceClaim(payload, expectedAudience, providerName); + + // Note: We log warnings but don't reject authentication + // This allows for gradual rollout and doesn't break existing deployments + if (!validationPassed) { + console.warn( + `[OIDC] Continuing with authentication despite audience validation failure. ` + + `Review your OIDC_EXPECTED_AUDIENCE_CLAIM configuration.`, + ); + } } } catch (e) { - console.error('[OIDC] Failed to decode JWT token:', e); + console.error('[OIDC] Failed to decode or validate JWT token:', e); } // Log the full token for manual testing (on separate line for easy extraction) diff --git a/src/lib/infrastructure/auth/oidc-providers.ts b/src/lib/infrastructure/auth/oidc-providers.ts index a0516a36..74c50b14 100644 --- a/src/lib/infrastructure/auth/oidc-providers.ts +++ b/src/lib/infrastructure/auth/oidc-providers.ts @@ -38,7 +38,10 @@ interface OIDCUser { * @param oidcProvider - OIDC provider configuration from environment * @returns NextAuth OAuth provider configuration */ -export function createNextAuthOAuthProvider(oidcProvider: OIDCProvider): OAuthConfig { +export function createNextAuthOAuthProvider( + oidcProvider: OIDCProvider, + audienceClaim: string +): OAuthConfig { const providerName = oidcProvider.name.toLowerCase(); // Parse scopes (default to 'openid profile email' if not specified) @@ -57,8 +60,8 @@ export function createNextAuthOAuthProvider(oidcProvider: OIDCProvider): OAuthCo params: { scope: scopes, // Audience is required by Rucio for token validation - // Rucio expects tokens to have aud: "rucio" claim - audience: 'rucio', + // Configurable via OIDC_EXPECTED_AUDIENCE_CLAIM env var + audience: audienceClaim, }, }, token: { @@ -125,6 +128,10 @@ export async function loadOIDCProviders(): Promise[]> { return []; } + // Load expected audience claim from environment + const audienceClaim = await envConfigGateway.oidcExpectedAudience(); + console.log(`[OIDC] Using audience claim: ${audienceClaim}`); + // Load OIDC providers from environment variables // This uses the existing EnvConfigGateway.oidcProviders() method // which reads OIDC_PROVIDERS and parses all provider configurations @@ -134,8 +141,8 @@ export async function loadOIDCProviders(): Promise[]> { // Convert each Rucio OIDC provider to NextAuth OAuth provider const nextAuthProviders = oidcProviders.map(provider => { - console.log(`[OIDC] Creating NextAuth provider for: ${provider.name}`); - return createNextAuthOAuthProvider(provider); + console.log(`[OIDC] Creating NextAuth provider for: ${provider.name} with audience: ${audienceClaim}`); + return createNextAuthOAuthProvider(provider, audienceClaim); }); return nextAuthProviders; diff --git a/src/lib/infrastructure/gateway/env-config-gateway.ts b/src/lib/infrastructure/gateway/env-config-gateway.ts index 2cba8994..ee4235fe 100644 --- a/src/lib/infrastructure/gateway/env-config-gateway.ts +++ b/src/lib/infrastructure/gateway/env-config-gateway.ts @@ -205,6 +205,16 @@ class EnvConfigGateway implements EnvConfigGatewayOutputPort { return Promise.resolve(false); } } + + async oidcExpectedAudience(): Promise { + const value = await this.get('OIDC_EXPECTED_AUDIENCE_CLAIM'); + // Default to "rucio" if not configured + // This is the standard audience claim expected by Rucio + if (!value || value.trim() === '') { + return Promise.resolve('rucio'); + } + return Promise.resolve(value.trim()); + } } export default EnvConfigGateway; diff --git a/tools/env-generator/README.md b/tools/env-generator/README.md index 8fcc24a5..420103cb 100644 --- a/tools/env-generator/README.md +++ b/tools/env-generator/README.md @@ -33,6 +33,7 @@ The `PARAMS_ENCODING_ENABLED` in the helm chart config of the Rucio WebUI tells | MULTIVO_ENABLED | RUCIO_WEBUI_MULTIVO_ENABLED | Whether to enable multi-VO config (true or false) | true | | | OIDC_ENABLED | RUCIO_WEBUI_OIDC_ENABLED | Enable or Disable OIDC Authentication (true or false) | true | | | OIDC_PROVIDERS | RUCIO_WEBUI_OIDC_PROVIDERS | CSV string containing names of OIDC Providers | cern, indigo | | +| OIDC_EXPECTED_AUDIENCE_CLAIM | RUCIO_WEBUI_OIDC_EXPECTED_AUDIENCE_CLAIM | Expected audience claim value for JWT token validation. Must match 'aud' claim in tokens. | atlas-rucio-oidc-client | rucio | | RULE_ACTIVITY | RUCIO_WEBUI_RULE_ACTIVITY | The `Activity` to associate with rules created with WebUI | User Subscriptions | User Subscriptions | ### NextAuth Configuration @@ -63,6 +64,8 @@ For each `VO` specified in the `VO_LIST` variable, additional variables need to For each `OIDC Provider` specified in the `OIDC_PROVIDERS` variable, the additional variables need to be specified. The variables should be added in the following format: `export RUCIO_WEBUI_OIDC_PROVIDER__=`. An example for the CERN OIDC provider is shown below: +**Note on Audience Claim:** The `OIDC_EXPECTED_AUDIENCE_CLAIM` is a global setting that applies to all OIDC providers. When registering your application with an OIDC provider, ensure the audience parameter in your OAuth configuration matches this value. For ATLAS Rucio instances, use `atlas-rucio-oidc-client`. For standard Rucio deployments, use `rucio` (default). + | Variable Name | Full Name | Description | Example | Default | | ------------------------------------ | ------------------------------------------------ | --------------------------------------------------------------------- | ------- | ------- | | OIDC_PROVIDER_CERN_CLIENT_ID | RUCIO_WEBUI_OIDC_PROVIDER_CERN_CLIENT_ID | The client id for the webui registered on the OIDC Provider dashboard | | | diff --git a/tools/env-generator/src/api/base.ts b/tools/env-generator/src/api/base.ts index 16fca221..432b7470 100644 --- a/tools/env-generator/src/api/base.ts +++ b/tools/env-generator/src/api/base.ts @@ -60,6 +60,7 @@ export class WebUIEnvTemplateCompiler { 'VO_LIST': 'def', 'VO_DEFAULT': 'def', 'OIDC_ENABLED': 'false', + 'OIDC_EXPECTED_AUDIENCE_CLAIM': 'rucio', 'ENABLE_SSL': 'false', 'PARAMS_ENCODING_ENABLED': 'false', 'RULE_ACTIVITY': 'User Subscriptions', diff --git a/tools/env-generator/src/templates/.env.liquid b/tools/env-generator/src/templates/.env.liquid index 3f7ddc23..d81f1dd1 100644 --- a/tools/env-generator/src/templates/.env.liquid +++ b/tools/env-generator/src/templates/.env.liquid @@ -33,6 +33,9 @@ LIST_DIDS_INITIAL_PATTERN={{ context.LIST_DIDS_INITIAL_PATTERN }} [oidc] OIDC_ENABLED={{ context.OIDC_ENABLED }} +{% if context.OIDC_EXPECTED_AUDIENCE_CLAIM %} +OIDC_EXPECTED_AUDIENCE_CLAIM={{ context.OIDC_EXPECTED_AUDIENCE_CLAIM }} +{% endif %} {% if context.OIDC_PROVIDERS %} OIDC_PROVIDERS={{ context.OIDC_PROVIDERS }} {% assign oidc_providers = context.OIDC_PROVIDERS | split: "," | strip %}