Your question
I’m playing with stalwart for the first time and I’m hitting a wall trying to set up OIDC. I have a keycloak instance that I had previously successfully used with mailcow. I have configured the directory and OIDC provider, and when I click “login” I am redirected to Keycloak for my credentials. However, after I enter valid credentials, I hit a “Token exchange failed: 401” page. I’d be willing to bet it’s a silly misconfiguration on my part, but I can’t figure it out. I’m running v0.16. Cheers!
That 401 is Keycloak refusing the client, not Stalwart refusing the user. The WebUI gets the provider’s endpoints from /api/discover and then runs the authorization-code exchange directly against Keycloak, sending a client id and a PKCE verifier and no client secret.
So the Keycloak client has to be a Public client (Client authentication OFF) with Standard flow enabled and PKCE method S256. If it is currently confidential, that alone produces exactly this. The other half is the client id: the WebUI defaults to stalwart-webui, so either name your Keycloak client that, or set oauthClientId on the Application object to match (Application | Stalwart). The valid redirect URI in Keycloak needs to be https://<your-webui-host>/oauth/callback.
Which 0.16 build are you on? oauthClientId only exists from 0.16.19, so on anything earlier the Keycloak client has to be named stalwart-webui exactly.
Once login works you will hit the next thing: requireAudience defaults to "stalwart" and Keycloak’s default aud is account, so you will need an audience mapper or a matching requireAudience.