Skip to content

OIDC login fails against Zitadel: ID token verifier rejects multi-audience aud claim (Zitadel always includes project ID) #10

Description

@DaniW42

Summary

ThinkWatch's OIDC ID-token verification rejects any token with more than one entry in the aud claim, even when the extra audience is benign and the client's own client_id is correctly present. This breaks OIDC login for Zitadel out of the box, since Zitadel always includes the project ID alongside the client ID in the aud claim for every project-scoped application — this is documented, intentional Zitadel behavior that cannot be disabled (see zitadel/zitadel#9200 and the related Proxmox VE discussion, which hit the exact same failure mode against a different Rust OIDC client).

Steps to reproduce

  1. Configure a Zitadel OIDC application (any project) as the identity provider in ThinkWatch's Settings → Authentication → OpenID Connect / SSO wizard.
  2. Complete steps 1–3 (issuer verification, redirect URL registration, client credentials).
  3. Run "Run a test login" (step 4).

Actual result

Token exchange failed: ID token verification failed: Invalid audiences: `<zitadel-project-id>` is not a trusted audience

This happens regardless of whether the Zitadel project contains a single app or many — moving the ThinkWatch application into a brand-new, otherwise-empty Zitadel project does not help, because Zitadel puts the project ID itself in aud, not just sibling client IDs.

Root cause

crates/auth/src/oidc.rs, OidcManager::exchange_code:

let verifier = client.id_token_verifier();
let claims = id_token
    .claims(&verifier, nonce)
    .map_err(|e| anyhow::anyhow!("ID token verification failed: {e}"))?;

client.id_token_verifier() returns the openidconnect-rs crate's default CoreIdTokenVerifier, which rejects any aud claim containing an audience other than the configured client_id unless additional trusted audiences are explicitly registered via .set_other_audience_verifier_fn(...). That method is never called here, so any IdP that legitimately emits multiple audiences (Zitadel being the most common OSS example) cannot complete login, no matter how the IdP-side app/project is configured.

Suggested fix

Accept additional audiences at least permissively enough to unblock the common Zitadel case, e.g.:

let verifier = client.id_token_verifier()
    .set_other_audience_verifier_fn(|_aud| true);

or, better for security-conscious deployments, expose a per-provider "additional trusted audiences" field in the setup wizard (Settings → Authentication → OpenID Connect / SSO) and thread it through to set_other_audience_verifier_fn, similar to vaultwarden's SSO_AUDIENCE_TRUSTED setting, which was added to solve this exact Zitadel interop problem.

Environment

  • ThinkWatch version: 1.0.1 (per Admin → Settings → General)
  • Deployment: Docker Compose, ghcr.io/thinkwatchproject/think-watch-server:latest
  • IdP: self-hosted Zitadel v4.17.1
  • Confirmed the Zitadel-side app/project configuration is otherwise correct (issuer discovery succeeds, redirect URI matches, client credentials are valid — token exchange itself succeeds; only ID-token claims validation fails)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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