OIDC directory: Name Claim never populated when email is present in JWT access token

Issue Description

When using an OIDC directory, the Name Claim setting has no effect and the Full Name (description) field is never populated on accounts.

The Name Claim field in the admin UI states: “…to retrieve the user’s display name from the token or user info response”. However, the userinfo endpoint is never called when the access token is a JWT containing an email claim.

Expected Behavior

When a Name Claim is configured (e.g. name or given_name), Stalwart should retrieve the user’s display name from either the JWT claims or the userinfo endpoint, as the UI description states. The Full Name field on the account should be populated after login.

Actual Behavior

The Full Name / description field remains empty after login, regardless of which Name Claim value is configured. Tested with both name and given_name

Reproduction Steps

1 Configure an OIDC directory (I used Rauthy)
2 Set the Name Claim to name or given_name.
3 Log in via the account portal.
4 Check the account in the admin UI, Full Name is empty.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

OIDC

Additional Context

In crates/directory/src/backend/oidc/lookup.rs, authenticate_jwt decides where to source claims:

let (email, claims) = if let Ok(email) = self.resolve_email(&token_data.claims) {
    (email, token_data.claims)          // JWT claims used for everything
} else {
    let claims = self.fetch_userinfo(token).await?;  // only called if email missing
    (self.resolve_email(&claims)?, claims)
};
return self.build_account(email, &claims);

The userinfo endpoint is only called when email cannot be resolved from the JWT. When the access token contains the email claim, the userinfo endpoint is never reached.

build_account then extracts the name from whichever claims object was used, meaning the name claim can only ever be found if it is also present in the access token JWT:

description: self
    .config
    .claim_name
    .as_ref()
    .and_then(|name_claim| claims.get(name_claim))
    .and_then(|v| v.as_str())
    .map(|s| s.to_string()),

Per the OIDC specification, profile claims (name, given_name, family_name) belong in the id_token and userinfo endpoint, not in the access token. Rauthy explicitly follows this and does not include profile claims in the access token. This means the Name Claim feature is broken for any spec-compliant provider: email resolves from the JWT, userinfo is skipped, and the name claim is never found.

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

The analysis in the bug report is correct and confirmed by the source code. Here is a precise summary of the issue and the correct fix:


This is a confirmed bug in crates/directory/src/backend/oidc/lookup.rs. The authenticate_jwt function only calls fetch_userinfo when the email claim is missing from the JWT. Once email is resolved from the JWT, it uses only the JWT claims for build_account, meaning the name/given_name claim is only ever populated if it also happens to be in the access token. Per OIDC spec, profile claims belong in the ID token and at the userinfo endpoint, not the access token – so for any spec-compliant provider (Rauthy, Keycloak in default config, etc.), the name claim will never be found.

The relevant logic (lines 78-86):

let (email, claims) = if let Ok(email) = self.resolve_email(&token_data.claims) {
    (email, token_data.claims)          // <-- userinfo is never reached
} else {
    let claims = self.fetch_userinfo(token).await?;
    (self.resolve_email(&claims)?, claims)
};
return self.build_account(email, &claims);

The fix would be to always call fetch_userinfo (or at least call it as a supplemental source for non-email claims like name) when a claim_name is configured and the JWT doesn’t contain it. For example, one approach:

  1. Resolve email from the JWT as currently done.
  2. If claim_name is configured and not found in the JWT claims, fetch userinfo and merge/prefer the userinfo response for the name field.

Workaround in the meantime: Configure your OIDC provider (Rauthy) to also include the name (or given_name) claim directly in the access token JWT. This is a provider-side workaround and goes against OIDC spec, but it will make the current Stalwart code pick it up.

This is a genuine code defect – the behavior diverges from what the admin UI promises (“retrieve the user’s display name from the token or user info response”). A human maintainer will need to address this in the source.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

This has been fixed in v0.16.12 which will be released this week.