Skip to content

Allow <token> user-directory to authenticate against multiple token_processors (side-by-side OIDC IdPs) #1928

Description

@BorisTyshkevich

Summary

A <token> user-directory can only authenticate JWTs from a single token_processor, so a deployment that wants multiple OIDC IdPs side-by-side (e.g. Google and an Auth0/Keycloak tenant) cannot do it — even though the per-request multi-processor verification logic already exists in ExternalAuthenticators::checkTokenCredentials.

Observed on v26.3.10.20001.altinityantalya.

Why it can't be done today

  • Only one <token> user-directory is allowed — AccessControl.cpp:514 throws "Only one token section can be defined."
  • The <token> directory requires a single, non-empty <processor>TokenAccessStorage.cpp:215-217:
    provider_name = config.getString(prefix_str + "processor");
    if (provider_name.empty())
        throw Exception(ErrorCodes::BAD_ARGUMENTS, "'processor' must be specified for Token user directory");
  • It then authenticates against only that processor — TokenAccessStorage.cpp:595:
    if (!external_authenticators.checkTokenCredentials(token_credentials, provider_name))

The capability already exists

ExternalAuthenticators::checkTokenCredentials already iterates all configured processors when processor_name is empty (ExternalAuthenticators.cpp:855-868):

if (processor_name.empty())
{
    for (const auto & [name, proc] : processors_snapshot)
        if (auto result = try_processor(proc); result.has_value())
            return *result;   // first processor that validates wins
}
else
{
    const auto it = processors_snapshot.find(processor_name);  // single, pinned
    ...
}

Each JwksJwtProcessor independently verifies the JWT against its own JWKS (keyed by the token's kid) plus optional expected_issuer/expected_audience. With distinct IdPs that means a given token validates under exactly one processor (the others fail at signature/issuer), so first-match is deterministic. The processor-aware token cache already handles two processors resolving different username_claims from the same token (ExternalAuthenticators.cpp:~709).

So the only thing missing is letting the <token> directory opt into that auto-discovery path.

Proposed change

Let the <token> user-directory authenticate against all configured token_processors (or an explicit subset). Either:

  1. Make <processor> optional — when omitted/empty, pass an empty processor_name to checkTokenCredentials (the existing auto-discovery branch), or
  2. Accept a <processors> list (or repeated <processor>).

MultipleAccessStorage/<token> plumbing is otherwise unchanged; this is a config-surface + one parameter change.

Repro

<token_processors>
  <google_oauth><type>jwt_dynamic_jwks</type><jwks_uri>https://www.googleapis.com/oauth2/v3/certs</jwks_uri><username_claim>email</username_claim></google_oauth>
  <auth0><type>jwt_dynamic_jwks</type><jwks_uri>https://TENANT.auth0.com/.well-known/jwks.json</jwks_uri>
         <expected_issuer>https://TENANT.auth0.com/</expected_issuer><expected_audience>CLIENT_ID</expected_audience><username_claim>email</username_claim></auth0>
</token_processors>
<user_directories>
  <token><processor>google_oauth</processor><common_roles>...</common_roles></token>
</user_directories>

A valid Google id_token authenticates. A valid Auth0 id_token (correct sig/iss/aud) is rejected — There is no user '<name>' in 'token' (UNKNOWN_USER) — because the directory only consults google_oauth. Pinning to auth0 instead just inverts which IdP works.

Safety note

With multiple processors, each should set expected_issuer + expected_audience so a token binds to exactly one IdP (an un-pinned processor accepts any token its JWKS can verify — the server already warns about this). First-match-wins is safe given distinct JWKS/issuers.


Filed via Claude Code while wiring multi-IdP login into a ClickHouse-served SQL browser.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions