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
- Configure a Zitadel OIDC application (any project) as the identity provider in ThinkWatch's Settings → Authentication → OpenID Connect / SSO wizard.
- Complete steps 1–3 (issuer verification, redirect URL registration, client credentials).
- 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)
Summary
ThinkWatch's OIDC ID-token verification rejects any token with more than one entry in the
audclaim, even when the extra audience is benign and the client's ownclient_idis correctly present. This breaks OIDC login for Zitadel out of the box, since Zitadel always includes the project ID alongside the client ID in theaudclaim 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
Actual result
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:client.id_token_verifier()returns theopenidconnect-rscrate's defaultCoreIdTokenVerifier, which rejects anyaudclaim containing an audience other than the configuredclient_idunless 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.:
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'sSSO_AUDIENCE_TRUSTEDsetting, which was added to solve this exact Zitadel interop problem.Environment
ghcr.io/thinkwatchproject/think-watch-server:latest