Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.development.local.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rucio-webui",
"version": "39.1.0",
"version": "39.1.1",
"private": true,
"scripts": {
"clean": "rimraf .next .swc build",
Expand Down
6 changes: 6 additions & 0 deletions src/lib/core/port/secondary/env-config-gateway-output-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,10 @@ export default interface EnvConfigGatewayOutputPort {
* @returns whether the query parameters should get URI encoded
*/
paramsEncodingEnabled(): Promise<boolean>;

/**
* @returns the expected OIDC audience claim value for JWT token validation
* @returns "rucio" if not configured (default fallback)
*/
oidcExpectedAudience(): Promise<string>;
}
66 changes: 64 additions & 2 deletions src/lib/infrastructure/auth/auth.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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<EnvConfigGatewayOutputPort>(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)
Expand Down
17 changes: 12 additions & 5 deletions src/lib/infrastructure/auth/oidc-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ interface OIDCUser {
* @param oidcProvider - OIDC provider configuration from environment
* @returns NextAuth OAuth provider configuration
*/
export function createNextAuthOAuthProvider(oidcProvider: OIDCProvider): OAuthConfig<OIDCProfile> {
export function createNextAuthOAuthProvider(
oidcProvider: OIDCProvider,
audienceClaim: string
): OAuthConfig<OIDCProfile> {
const providerName = oidcProvider.name.toLowerCase();

// Parse scopes (default to 'openid profile email' if not specified)
Expand All @@ -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: {
Expand Down Expand Up @@ -125,6 +128,10 @@ export async function loadOIDCProviders(): Promise<OAuthConfig<OIDCProfile>[]> {
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
Expand All @@ -134,8 +141,8 @@ export async function loadOIDCProviders(): Promise<OAuthConfig<OIDCProfile>[]> {

// 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;
Expand Down
10 changes: 10 additions & 0 deletions src/lib/infrastructure/gateway/env-config-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ class EnvConfigGateway implements EnvConfigGatewayOutputPort {
return Promise.resolve(false);
}
}

async oidcExpectedAudience(): Promise<string> {
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;
3 changes: 3 additions & 0 deletions tools/env-generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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_<PROVIDER_NAME>_<VARIABLE_NAME>=<VARIABLE_VALUE>`. 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 | | |
Expand Down
1 change: 1 addition & 0 deletions tools/env-generator/src/api/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions tools/env-generator/src/templates/.env.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
Loading