Categories
Artificial Intelligence Dovecot Keycloak Mail OIDC Postfix SSO

How many OIDC clients does an OAuth-enabled MCP mail server need?

One, two, three or four? The number of OIDC client registrations an MCP server for OAuth-enabled IMAP/SMTP needs depends on the architecture. Five scenarios compared: token passthrough, single-client broker, split broker, IdP as authorization server with token exchange, and hosted providers.

In my last post about the MCP mail server I ended up with four Keycloak client registrations for one working demo. That raised an obvious follow-up question: is four the number, or just the number I ended up with? Short answer: the minimum is one, the common setups need two, and the mail server may add one more. This post walks through the scenarios and what each one costs.

DRAFT. This post was drafted by Claude and still needs review.

The two legs

Every MCP server that talks to an OAuth-protected mailbox sits between two separate OAuth relationships:

MCP client  ──(leg 1: token for the MCP server)──►  MCP server  ──(leg 2: XOAUTH2/OAUTHBEARER token)──►  IMAP / SMTP
  • Leg 1 answers “may this user call these MCP tools?”. Per the MCP authorization spec, the MCP server is an OAuth 2.1 resource server, and the token it receives must be issued for it (audience binding via RFC 8707 resource indicators).
  • Leg 2 answers “may this process read and send mail as this user?”. The mail server is the resource server here. Dovecot checks the bearer token from the SASL XOAUTH2 or OAUTHBEARER (RFC 7628) exchange.

Every OAuth token is requested by some client, so each leg needs at least one client identity. The question is whether those identities can be the same and where they get registered.

In the flow diagrams below, the shading shows how each leg gets its token: blue for leg 1, orange for leg 2, and green where a single login yields the tokens for both legs.

One thing that does not add a client: IMAP vs. SMTP. Postfix usually hands SASL to Dovecot (smtpd_sasl_type = dovecot), so both protocols validate tokens through the same code path. One token, or one audience covering both, is enough.

How MCP clients get a client_id at all

Before counting, one detail that changes the count: MCP clients (Claude, Open WebUI, IDEs, …) are not known to your IdP in advance. The spec offers three ways for them to obtain a client_id:

  • Pre-registration: you create a client by hand and configure its ID in the MCP client. Fine for one or two known clients, but it doesn’t scale.
  • Dynamic Client Registration (DCR, RFC 7591): the MCP client registers itself at runtime. Zero admin effort, but every client instance creates a registration, and the authorization server has to allow anonymous registration in some form.
  • Client ID Metadata Documents (CIMD, added in the 2025-11 spec revision): the client_id is an HTTPS URL pointing to a JSON document describing the client. Nothing is stored at the authorization server, but the authorization server has to support it.

The important question is which authorization server MCP clients register with: your IdP, or the MCP server itself. That decides most of the count.

Scenario 0: Token passthrough (1 client, not allowed)

The tempting shortcut: the MCP client gets a token from the IdP whose audience covers both the MCP server and the mail server. The MCP server validates it and forwards the same token to Dovecot. One client, done.

The MCP spec explicitly forbids this (“token passthrough”). The MCP server must not accept tokens that weren’t issued for it, and must not forward the token it received to upstream APIs. The reasons are real, not just formal:

  • Any token that is valid for IMAP is also valid for your MCP tools, and the other way round. A token leaked from either side unlocks both.
  • The mail server can’t tell whether it is talking to the user’s own mail client or to an LLM-driven tool, so audit logs and rate limits lose that information.
  • The MCP server becomes a confused deputy: it acts with whatever the token carries, not with what it was meant to do.

Pros: one client, no tokens stored in the MCP server, no second authorization step, and nothing to configure for token exchange.
Cons: besides the security problems above, it also breaks in practice. A spec-compliant MCP client requests its token with an RFC 8707 resource parameter for the MCP server, so the token is bound to the MCP server and Dovecot shouldn’t accept it.

So the theoretical minimum of one client is off the table if you follow the spec. Every scenario below keeps the two legs apart.

Scenario 1: Single-client broker (1 IdP client)

The MCP server acts as an authorization server of its own towards MCP clients and as an ordinary OIDC client towards the IdP. FastMCP’s OAuthProxy/OIDCProxy works this way:

  1. The MCP client discovers the MCP server’s authorization metadata and registers via DCR at the MCP server. The IdP never sees these registrations.
  2. The MCP server redirects the user to the IdP with its own confidential client, requesting the mail scopes plus offline_access.
  3. The MCP server keeps the upstream tokens (per user, keyed by sub) and issues its own token to the MCP client. That token is valid only for the MCP server, which takes care of leg 1.
  4. For leg 2, the tool uses the stored upstream access token, refreshed as needed, as the XOAUTH2 credential.

The complete flow, step by step:

Dovecot / PostfixIdP(Keycloak)MCP server(OAuth proxy)MCP clientDovecot / PostfixIdP(Keycloak)MCP server(OAuth proxy)MCP clientLeg 1 + leg 2: one login yields the MCP server token and the mailbox tokenStore upstream tokens per user (sub)On expiry: refresh_token grant with mcp-mailUser (browser)Tool call without token1401 + resource metadata (AS = MCP server)2Dynamic client registration (at the MCP server)3Open authorization URL (PKCE)4/authorize5Redirect to IdP (client mcp-mail, mail scopes + offline_access)6Log in and consent7Authorization code (callback)8Code for tokens (confidential client mcp-mail)9Access token (aud includes mail) + refresh token10Own token (aud = MCP server) via code exchange11Tool call + Bearer (MCP server token)12IMAP/SMTP AUTH XOAUTH2 with stored upstream token13Introspection (client dovecot-introspect) or local JWT check14active: true15OK + mail data16Tool result17User (browser)
Scenario 1: the MCP server is the authorization server for MCP clients and a single OIDC client at the IdP. It stores the upstream tokens and uses them for IMAP/SMTP.

IdP clients: 1 (plus the mail server’s introspection client, see below).

Pros: one login, one consent, no DCR at the IdP, and it works with any IdP that does plain authorization code flow, including Google and Microsoft.
Cons: you run a small authorization server and you store refresh tokens for mailboxes, so the MCP server becomes a valuable target. The upstream token carries both “user logged in to the MCP server” and “may access the mailbox”. That is still spec-compliant, because the MCP client never sees it, but there is no privilege separation on the IdP side.

Scenario 2: Split broker (2 IdP clients)

This is what I built with Claude in the previous post. Same broker as in scenario 1, but the mailbox token comes from a second IdP client:

  • mcp-mail: the broker’s upstream client. It authenticates the user and carries the MCP tool scopes (mcp:mail/read, mcp:mail/send).
  • mcp-mail-backend: requests the mailbox token in a separate authorization code flow. Its audience is set up for Dovecot.

The complete flow, step by step:

Dovecot / PostfixIdP(Keycloak)MCP server(OAuth proxy)MCP clientDovecot / PostfixIdP(Keycloak)MCP server(OAuth proxy)MCP clientLeg 1: as in scenario 1, but client mcp-mail only gets tool scopesLeg 2: separate flow with client mcp-mail-backend(silent with prompt=none if the IdP session is alive)Store mailbox token per user (sub)User (browser)DCR + authorization (PKCE) at the MCP server1Login via client mcp-mail2Token with mcp:mail/read, mcp:mail/send3Own token (aud = MCP server)4list_mails + Bearer (MCP server token)5No mailbox token for this user: call authorize_mail6authorize_mail7Authorization link8Show link9Authorize (client mcp-mail-backend)10Authorization code (callback)11Code for tokens (confidential client mcp-mail-backend)12Mailbox token (aud: dovecot-introspect) + refresh token13list_mails + Bearer (MCP server token)14AUTH XOAUTH2 with mailbox token15Introspection (client dovecot-introspect)16active: true17OK + mail data18Tool result19User (browser)
Scenario 2: same broker, but the mailbox token comes from a second client (mcp-mail-backend) in a separate authorization flow, triggered by the authorize_mail tool.

IdP clients: 2 (plus introspection).

Pros: clean least privilege. A leaked mailbox token carries no tool scopes and vice versa, and you can revoke mailbox access without touching the MCP session.
Cons: a second authorization flow. A generic MCP client only knows about leg 1, so the second flow needs an out-of-band step (my authorize_mail tool plus a link to click). A silent prompt=none request right after the first login can hide that step when the IdP session is still alive.

Scenario 3: IdP as authorization server + token exchange (2 clients, or 1 + n with DCR)

The “textbook” layout: the MCP server does not issue tokens at all. Its protected resource metadata (RFC 9728) points MCP clients straight to the IdP.

  1. The MCP client registers at the IdP, either as one pre-registered public client (PKCE, loopback redirect URIs) or as many clients via DCR/CIMD.
  2. The MCP client gets a token with aud = the MCP server and calls the tools.
  3. The MCP server, using its own confidential client, exchanges that token at the IdP for a token with the mail audience. This is OAuth 2.0 Token Exchange (RFC 8693); Microsoft calls its variant “on-behalf-of”. Keycloak has supported standard token exchange since 26.2.
  4. The exchanged token goes to Dovecot via XOAUTH2.

The complete flow, step by step:

Dovecot / PostfixIdP(Keycloak)MCP server(resource server)MCP clientDovecot / PostfixIdP(Keycloak)MCP server(resource server)MCP clientLeg 1: MCP client gets a token for the MCP server directly from the IdPUse configured client_idalt[pre-registered public client][DCR / CIMD]Leg 2: token exchange, no user interactionNothing stored: exchange again when neededUser (browser)Tool call without token1401 + resource metadata (AS = IdP)2Fetch AS metadata3Register (DCR) or present metadata URL (CIMD)4Open authorization URL (PKCE, resource = MCP server)5Log in and consent6Authorization code (loopback redirect)7Code for token8Access token (aud = MCP server)9Tool call + Bearer10Validate token (JWKS, or introspection as client mcp-server)11Token exchange RFC 8693 (client mcp-server, audience = mail)12Short-lived mailbox token13AUTH XOAUTH2 with exchanged token14Introspection (client dovecot-introspect) or local JWT check15active: true16OK + mail data17Tool result18User (browser)
Scenario 3: MCP clients get their token directly from the IdP. The MCP server exchanges it for a short-lived mailbox token (RFC 8693) and stores nothing.

IdP clients: 1 for the MCP server + 1 pre-registered client for MCP clients, or n dynamic ones (plus introspection). If the MCP server validates incoming tokens by introspection rather than locally via JWKS, it can use its own confidential client for that. Same aud rule as in the last post: it must be in the token’s audience.

Pros: no token storage in the MCP server and no second user-facing flow. The exchanged token is short-lived and derived from the user’s current session. It is spec-compliant and the IdP keeps full control: policies, consent, and revocation all happen in one place.
Cons: the IdP must support token exchange, and MCP clients must be able to register there. With DCR, Keycloak collects a registration per client instance and needs client registration policies (trusted hosts, allowed scopes) so anonymous registration doesn’t become a free-for-all. Exchange permissions also need careful setup: which client may exchange which audience.

A variant without token exchange: IdP as authorization server for leg 1, plus a separate backend client for leg 2 with its own authorization code flow. The client count matches scenario 3 and the UX matches scenario 2, the extra click included. That makes it the worst combination of both, unless your IdP lacks token exchange.

Scenario 4: Hosted mail (Microsoft 365 / Google)

With hosted mail you don’t control the authorization server, which limits the options:

  • Google: one OAuth client in the Cloud Console with scope https://mail.google.com/. No DCR for third-party clients and no token exchange, so you are in scenario 1 with 1 client.
  • Microsoft Entra ID: one app registration with IMAP.AccessAsUser.All, SMTP.Send and offline_access gives you scenario 1. Entra has no DCR, but it does offer on-behalf-of. So with a second app registration for the MCP server’s API and a pre-registered client for MCP clients, scenario 3 is possible too (to be verified for the Outlook IMAP/SMTP scopes).

Both variants, step by step:

Gmail / Exchange OnlineGoogle / Entra IDMCP serverMCP clientGmail / Exchange OnlineGoogle / Entra IDMCP serverMCP clientLeg 1 + leg 2: one login yields both tokensStore provider tokens per userLeg 1: token for the MCP server’s APILeg 2: on-behalf-of, no user interactionalt[Broker (scenario 1): Google or Entra][On-behalf-of (scenario 3): Entra only]Provider validates its own tokens: no introspection clientUser (browser)DCR + authorization (PKCE) at the MCP server1Redirect to provider (one app registration)2Log in and consent (admin consent in Entra if required)3Code, then access + refresh token(mail.google.com or IMAP.AccessAsUser.All, SMTP.Send)4Own token (aud = MCP server)5Tool call + Bearer6AUTH XOAUTH2 with provider token7Authorization with pre-registered client (no DCR in Entra)8Log in and consent9Token for the MCP server’s API (app registration 2)10Tool call + Bearer11On-behalf-of request (MCP server’s app registration)12Token for IMAP/SMTP scopes13AUTH XOAUTH2 with OBO token14OK + mail data15Tool result16User (browser)
Scenario 4: with Google or Entra as a broker (top), or with Entra’s on-behalf-of flow (bottom). The provider validates its own tokens.

No introspection client is needed here. The provider’s mail servers validate their own tokens.

Pros: no mail server to run and no token validation, introspection client or audience mapper to configure. Consent and admin controls (Entra admin consent, Google Workspace app access control) come with the provider.
Cons: the provider decides which scenario you can use. With Google that is always the broker, so you store refresh tokens. The scopes are coarse: there is no read-only IMAP scope, so IMAP.AccessAsUser.All and https://mail.google.com/ both grant full mailbox access. On top of that, Gmail’s scope is a restricted scope that needs Google’s app verification for apps used outside your own organization, and Entra tenants often need an admin to consent.

The extra one: mail server introspection

On a self-hosted Dovecot/Postfix setup, add one more client in every scenario if Dovecot validates tokens via RFC 7662 introspection: introspection is an authenticated call, so Dovecot needs its own confidential client. As described last time, that client’s ID must also be in the token’s aud, or Keycloak answers {"active": false}.

The alternative is local validation: Dovecot checks the JWT signature against the IdP’s keys (local_validation_key_dict in the oauth2 config). No client is needed and there is no round-trip per login. The price is that a revoked token stays valid until it expires, so keep access token lifetimes short.

Both paths, and how IMAP and SMTP share them:

IdP(Keycloak)Dovecot(IMAP + auth)Postfix(SMTP)MCP serverIdP(Keycloak)Dovecot(IMAP + auth)Postfix(SMTP)MCP serverIMAP and SMTP end up in the same validation pathToken aud must contain dovecot-introspect+1 client, one round trip per login, revocation takes effect immediatelySigning keys configured locally (local_validation_key_dict)No client, no round trip, a revoked token stays valid until expalt[Introspection (RFC 7662)][Local validation (JWT)]IMAP AUTHENTICATE XOAUTH2 (bearer token)1SMTP AUTH XOAUTH2 (bearer token)2Hand SASL to Dovecot auth (smtpd_sasl_type = dovecot)3POST /introspect, Basic auth as client dovecot-introspect4active: true + claims (otherwise only active: false)5Check signature, exp and aud locally6IMAP login OK7SASL OK8SMTP login OK9
Mail server side: Postfix hands SMTP logins to Dovecot, which validates the token either by introspection (one more client) or locally against the IdP’s signing keys.

Why the last post needed four clients

With the scenarios laid out, the four Keycloak clients from the previous post are easy to place. Only one of them is strictly needed. The other three each come from a separate decision:

ClientWhy it was thereRoleCould it be dropped?
openwebuiOpen WebUI has its own SSO login for its users. Keycloak needs a client for that whether MCP is involved or not.Chat UI login, not part of the MCP/mail chainOnly by using an MCP client without a login of its own (Claude, an IDE, a CLI)
mcp-mailUpstream client of FastMCP’s OIDCProxy. The proxy acts as the authorization server for Open WebUI and logs the user in at Keycloak with this client. Its token carries the tool scopes.Leg 1, brokerNo. Leg 1 always needs a client.
mcp-mail-backendClaude’s design kept mailbox tokens apart from tool tokens, so a leaked token of one kind doesn’t grant the other, and mailbox access can be revoked on its own.Leg 2, which makes it scenario 2Yes: request the mail audience on mcp-mail‘s token (scenario 1), or use token exchange (scenario 3)
dovecot-introspectClaude’s design had Dovecot check tokens via introspection, and nobody weighed that against local validation. Introspection requires authentication, which was overlooked at first.Mail serverYes: local JWT validation in Dovecot

Open WebUI looks like it should count twice, but it doesn’t. It shows up once as a Keycloak client (openwebui, for user login) and once as an MCP client. The MCP client registration happens via DCR at the FastMCP proxy, not at Keycloak, so it doesn’t add a Keycloak client. That registration is the one that got lost on every container rebuild until I gave FASTMCP_HOME a volume.

So, using the same software, the demo could have run with a single Keycloak client: scenario 1, local token validation in Dovecot, and an MCP client without its own login. None of the extra clients was a conscious decision on my part. The whole design came from Claude, and the client count simply grew as pieces were added. Looking back, each extra client does buy something:

  • openwebui: real user accounts in the chat UI.
  • mcp-mail-backend: separation between tool permissions and mailbox access.
  • dovecot-introspect: revocation that takes effect immediately instead of at token expiry.

Four clients isn’t a requirement. It is the sum of three trade-offs that were made along the way without anyone weighing them. That is my main lesson from this: when an AI designs your auth setup, ask it to justify every client registration and every validation method, not just to make it work.

Summary

ScenarioWho registers MCP clientsClients at the IdP+ Dovecot introspectionNotesPros (+) / Cons (−)
0: Token passthroughIdP1+1Forbidden by the MCP spec+ one client, nothing stored
− a leaked token opens both tools and mailbox; confused deputy; breaks with audience-bound tokens
1: Single-client brokerMCP server1+1Simplest; works with any identity provider, including Google/M365+ one login, no DCR at the identity provider
− you run an authorization server, store refresh tokens, no privilege separation
2: Split brokerMCP server2+1The setup from the previous post+ least privilege, mailbox access revocable on its own
− second authorization step, stores refresh tokens
3: IdP as AS + token exchangeIdP (pre-registered, DCR or CIMD)1 + 1 (or 1 + n with DCR)+1Textbook; needs RFC 8693 support at the IdP+ no stored tokens, no extra click, central control at the IdP
− DCR clutter and registration policies, exchange permissions to configure
4: Hosted mailMCP server (or pre-registered)1 (Entra OBO: 2)–Provider validates tokens itself+ no mail server or token validation to run, provider handles consent
− provider dictates the scenario, coarse full-mailbox scopes, app verification or admin consent

Which one would I pick?

  • Hosted mail or a quick start: scenario 1. It is the only option that works everywhere.
  • Own Keycloak, few known MCP clients: scenario 3 with one pre-registered public client and token exchange. No stored refresh tokens, no extra click.
  • Own Keycloak, arbitrary MCP clients via DCR: a broker (scenario 1 or 2) keeps the DCR churn out of your IdP. Pick 2 if mailbox access should be separately consented to and revocable.
  • Introspection vs. local validation in Dovecot: introspection if you need immediate revocation, local validation if you’d rather save a client and a round-trip.

Leave a Reply

Your email address will not be published. Required fields are marked *

Are you human? Please solve:Captcha