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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions