Support multiple OIDC clients in a single external directory

Issue Description

I checked the documentation and existing topics, but I could not find a good way to handle this setup in the OSS version.

Environment

  • Stalwart v0.16.5
  • External OIDC provider: Kanidm
  • Multiple OIDC clients using the same identity provider

Problem

I have a setup where multiple clients authenticate through the same external OIDC provider, but they represent different login flows and application contexts.

In practice, this means I need to use the same external identity source for more than one client, while still keeping those client flows distinguishable and reliable.

With the current OSS behavior, I was not able to connect multiple clients to one external OIDC directory in a way that felt correct and predictable. The configuration model seems too coarse for this kind of setup, because different clients may share the same external provider but still need to be treated differently during discovery and authentication.

Why This Matters

This seems like a common self-hosted scenario: one central identity provider such as Kanidm, but multiple applications or frontends that should authenticate against it as separate clients.

Expected Behavior

Stalwart should make it possible to use one external OIDC provider for multiple distinct clients in a practical and predictable way.

If several clients rely on the same external IdP, they should not be forced into a single indistinguishable authentication flow. Instead, Stalwart should behave in a way that allows these client flows to remain separable and reliable during discovery and authentication.

From a user perspective, the expected behavior is that a shared external OIDC setup can support multiple clients cleanly, without ambiguous matching, fragile behavior, or the need to model each client as a completely separate identity source.

Actual Behavior

In the OSS version, I could not find a clean and predictable way to use multiple distinct OIDC clients with one shared external OIDC directory.

In practice, the setup feels too coarse-grained for this scenario. Different clients can share the same external provider, but they still need to remain distinguishable during discovery and authentication. I was not able to model that cleanly with the currently available external OIDC directory behavior.

Reproduction Steps

  1. Configure Stalwart OSS with an external OIDC directory backed by Kanidm.
  2. Use the same external identity provider for multiple separate OIDC clients.
  3. Try to use those clients as distinct login flows or application contexts against the same external directory.
  4. Observe that this is not practical to model cleanly and predictably with the current OSS behavior.

Relevant Log Output

I do not currently have a concise log excerpt that demonstrates the issue better than the behavioral description above.

If needed, I can provide trace-level logs for a concrete test setup.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

OIDC

Additional Context

I am currently evaluating Stalwart as part of a migration away from Mailcow, and this limitation came up during that process.

For additional context, I documented some of the implementation work I had to do downstream while trying to support this setup:

That downstream patch does work for my use case, but I am not linking it as the required upstream solution. I am only including it as evidence that this scenario led to non-trivial customization work on my side.

The Stalwart UI was not adapted as part of that downstream work, so the related configuration currently has to be managed through the CLI rather than through the web interface.

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

Based on my research, here is what the current documentation and source tell us about your scenario.

The current OIDC directory model in Stalwart is designed as a single active directory pointed at one issuer URL, with one requireAudience (client ID / resource identifier) and one set of requireScopes. The Authentication singleton holds a single directoryId, so there is no built-in concept of routing authentication decisions across multiple OIDC directory objects simultaneously, nor of differentiating clients that share the same issuer but have distinct client IDs.

In other words, the configuration model the docs describe is:

  • One OidcDirectory object per external provider configuration
  • One issuerUrl, one requireAudience (maps to a single client/resource), one requireScopes set
  • A single active directory selected via directoryId on the Authentication singleton

There is no documented mechanism to attach multiple OIDC directory entries that all point at the same issuer but with different requireAudience values and have Stalwart route or fan-out across them. Your finding – that you needed a downstream patch to make this work – is consistent with this being a gap in the current OSS feature set rather than a misconfiguration on your part.

The closest workaround within the current documented model would be to configure separate OIDC directory objects each with a distinct requireAudience matching the respective client ID, but since only one directoryId can be active at a time, this does not cleanly solve the multi-client case without the kind of changes you described in your patch.

Relevant docs:

A human staff member will follow up on whether multi-client OIDC support is planned or whether there is a supported path forward.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

On OSS, requireAudience is a single string, so you can’t match more than one audience in one OIDC directory. Per-domain dispatch by Domain.directoryId is the feature you’d want here, but only takes effect on the Enterprise build; on OSS the global Authentication.directoryId is consulted for everyone.

A few options short of moving to Enterprise: if both clients can share the same audience, leave requireAudience empty and rely on the IdP to gate access (you lose the per-client distinction inside Stalwart); or unify the clients in the IdP under one client_id that emits one consistent aud claim.

I can’t help but notice that it would be easier for us to specify the clientId for the IdP ourselves, instead of using a strictly defined name - stalwart-webui.

Thanks. My issue is broader than requireAudience being a single string.

requireAudience is only one possible discriminator. In practice, selecting the correct external OIDC flow for a shared IdP may depend on more than a single aud value, for example:

  • issuer
  • aud
  • azp
  • required scopes
  • request context / referer during discovery
  • fallback behavior when stronger hints do not uniquely identify one flow

That is why the suggested workarounds do not really solve the problem for me.

Leaving requireAudience empty removes the client-specific distinction inside Stalwart entirely, and unifying everything under one client_id changes the IdP model instead of letting Stalwart distinguish multiple client flows that share one issuer.

To make the change concrete, the downstream patch was not just “allow another audience”. It changed the OIDC directory model from a single-provider shape to a multi-rule provider-selection shape inside one shared OIDC directory.

Before, the effective model was roughly:

  • one OidcDirectory
  • one issuerUrl
  • one optional requireAudience
  • one requireScopes set

After the patch, one OidcDirectory can contain multiple provider match rules, effectively a providers[] array, where each rule can define for example:

  • issuerUrl
  • requireAudience
  • requireScopes
  • refererPrefixes
  • isFallback

The authentication and discovery paths then use those rules to select the correct provider deterministically. In particular, the patch adds provider selection based on issuer, token claims such as aud / azp, scopes, and request context, instead of collapsing everything into one coarse external OIDC configuration.

So from my perspective, the missing OSS capability is not just “multiple audiences”, but deterministic provider selection within one shared external OIDC directory.

Submit this as a feature idea please.