Pocket-ID OIDC, redirect loop/no login after successful auth

Your question

Hi,
I’m trying to set up Pocket-ID as an OIDC provider, but I’ve hit a wall with the final redirection.
The Issue:
1 - I initiate the login on Stalwart.
2 - I am correctly redirected to Pocket-ID.
3 - I enter my credentials and authorize the request.
4 - Pocket-ID redirects me back to the Stalwart redirect URL (…/admin/login).
The result: I end up on the login page again, and I’m not logged in.

There are no errors in the logs in trace. All HTTP requests return a 200 status.

My conf (Pocket-ID)
Url: https://mail.domain.com/admin/oauth/callback
Client ID: stalwart-webui

My conf (Stalwart)
Issuer URL*: https://pocket.domain.com
Required Audience: empty
Required Scopes: openid profile
Username Claim*: preferred_username

Any help or pointers would be greatly appreciated! Thanks in advance.

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

Let me look up the OIDC configuration details to help diagnose this.

This isn’t covered in the current docs. A community member or staff member will follow up.

That said, there are a few configuration details worth double-checking while you wait for a human to reply:

  1. Client Secret — Most OIDC providers (including Pocket-ID) require a client secret to be configured on the Stalwart side as well. Confirm that the client secret set in Pocket-ID matches what’s entered in Stalwart’s OIDC settings.

  2. Required Audience — Try setting the Required Audience to your Client ID (stalwart-webui). Many OIDC providers include the client ID in the aud claim of the ID token, and if Stalwart’s audience check doesn’t match it will silently reject the token.

  3. Scopes — Ensure Pocket-ID is actually returning the preferred_username claim under the openid profile scopes. You can decode the ID token Pocket-ID issues (e.g. via jwt.io) to verify the claim is present with the right name.

  4. Redirect URI mismatch — Confirm the redirect URI registered in Pocket-ID is exactly https://mail.domain.com/admin/oauth/callback (not .../admin/login). A subtle mismatch here could cause a silent failure even with a 200 response.

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.

@helpbot

/.well-known/jwks.json

{"keys":[{"alg":"RS256","e":"AQAB","kid":"dp3FP4tmMsg","kty":"RSA","n":"wRe0-fgdgdgdgdgdgd","use":"sig"}]}%  

/.well-known/openid-configuration

{"authorization_endpoint":"https://pocket.domain.com/authorize","authorization_response_iss_parameter_supported":true,"claims_supported":["sub","given_name","family_name","name","email","email_verified","preferred_username","picture","groups"],"code_challenge_methods_supported":["plain","S256"],"device_authorization_endpoint":"https://pocket.domain.com/api/oidc/device/authorize","end_session_endpoint":"https://pocket.domain.com/api/oidc/end-session","grant_types_supported":["authorization_code","refresh_token","urn:ietf:params:oauth:grant-type:device_code","client_credentials"],"id_token_signing_alg_values_supported":["RS256"],"introspection_endpoint":"https://pocket.domain.com/api/oidc/introspect","issuer":"https://pocket.domain.com","jwks_uri":"https://pocket.domain.com/.well-known/jwks.json","prompt_values_supported":["none","login","consent"],"response_types_supported":["code","id_token"],"scopes_supported":["openid","profile","email","groups"],"subject_types_supported":["public"],"token_endpoint":"https://pocket.domain.com/api/oidc/token","token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","none"],"userinfo_endpoint":"https://pocket.domain.com/api/oidc/userinfo"}%  

The token looks perfect: iss, sub, aud, and email are all there.

Pocket log:

May 11 05:12:38 INF Request app=pocket-id version=2.6.2 status=200 method=GET path=/authorize query="response_type=code&client_id=stalwart-webui&redirect_uri=https%3A%2F%2Fmail.domain.com%2Fadmin%2Foauth%2Fcallback&code_challenge=B5vwqcG14TJPIrLtxTOGwtC0qaa0mQFPYVo7uboB4_Q&code_challenge_method=S256&state=84c5cea2bc846a23e6378bbb57781fdd&login_hint=user%40domain.com&prompt=login&scope=openid" route="" ip=10.10.10.10 latency=290µs referer="" user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36" body_size=2166

Stalwart is only requesting the openid scope. Although I’m not an OIDC expert, I think the authorization request must have scope=“openid profile email” to work correctly.

Let me look into the OIDC configuration details for Stalwart to give you accurate guidance.

This is very helpful. The source code reveals the root cause clearly.

The correct callback URL for the WebUI is /oauth/callback, not /admin/login and not /admin/oauth/callback. The WebUI handles the OIDC authorization code exchange at the path <origin>/oauth/callback (or <basePath>/oauth/callback).

Looking at the WebUI source (src/services/auth/oauth.ts), the redirect URI is constructed as:

${window.location.origin}${basePath}/oauth/callback

So if your WebUI is at https://mail.domain.com/admin, the correct redirect URI to register in Pocket-ID is:

https://mail.domain.com/admin/oauth/callback

Your Pocket-ID registration actually looks correct on that front. The issue is that Pocket-ID is redirecting back to ...admin/login instead of ...admin/oauth/callback. This is a Pocket-ID side mismatch – the registered redirect URI and the redirect_uri sent in the authorization request need to match exactly.

A few things to verify:

  1. Check what Stalwart is actually sending as redirect_uri in the authorization request. From the Pocket-ID log you shared, the redirect_uri parameter in the /authorize call is https://mail.domain.com/admin/oauth/callback – that part looks correct.

  2. The scope issue you noticed is real but likely not the cause of the loop. The WebUI code builds the scope dynamically from the provider’s scopes_supported list (openid email profile), so in practice it should be requesting all three if they appear in Pocket-ID’s discovery document – and they do. If only openid is going out, this could be a browser sessionStorage issue or a stale session.

  3. The most likely culprit: the token exchange is silently failing. After Pocket-ID redirects to /admin/oauth/callback, the WebUI’s OAuthCallback component exchanges the code for a token by POSTing to Pocket-ID’s token_endpoint. Check your browser’s Network tab during the login flow – look for the POST to https://pocket.domain.com/api/oidc/token. If that request fails (or returns an error JSON), the WebUI catches the error and redirects back to /login, which is exactly what you’re seeing. Common reasons: wrong or missing client secret, PKCE misconfiguration, or the client_id in Stalwart’s WebUI not matching what’s in Pocket-ID.

  4. Pocket-ID client settings – make sure “Token Endpoint Auth Method” in Pocket-ID is set to none (since the WebUI uses PKCE without a client secret) or that the client secret is configured on both sides.

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.

@helpbot

It’s a very strange behavior :smiley: . I’m trying to reproduce a setup that was 100% working before, but this time it returns a 401 Unauthorized error at https://mail.domain.com/jmap/session. The logs show ‘Missing required scope email’.

URL https://pocket.domain.com/api/oidc/token

Status 200 OK

2026-05-11T10:09:14Z DEBUG HTTP request URL (http.request-url) listenerId = "http", localPort = 8080, remoteIp = 127.0.0.1, remotePort = 50696, remoteIp = 10.10.10.10, url = "/jmap/session"
2026-05-11T10:09:14Z TRACE Expression evaluation result (eval.result) listenerId = "http", localPort = 8080, remoteIp = 127.0.0.1, remotePort = 50696, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-05-11T10:09:14Z DEBUG Authentication failed (auth.failed) listenerId = "http", localPort = 8080, remoteIp = 127.0.0.1, remotePort = 50696, reason = "Missing required scope 'email', present scopes: []", causedBy = "crates/directory/src/core/dispatch.rs:17", remoteIp = 10.10.10.10
2026-05-11T10:09:14Z TRACE HTTP response body (http.response-body) listenerId = "http", localPort = 8080, remoteIp = 127.0.0.1, remotePort = 50696, contents = "{"type":"about:blank","status":401,"title":"Unauthorized","detail":"You have to authenticate first."}", code = 401, size = 101

I think the authorization request must have scope=“openid profile email” to work correctly

Try upgrading to the latest WebUI version.
If the problem persists, make sure PocketId is advertising profile and email in the supported_scopes field of the OIDC discovery document.