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.

I have the same problem.
I updated to the latest webadmin and use stalwart 0.16.5.
pocket-id is set as the Authentication Directory in stalwart

The 401 error on /jmap/session after login is:

reason = "Missing required scope 'email', present scopes: []", causedBy = "crates/directory/src/core/dispatch.rs:17"

The discovery document of pocket-id is as follows, it includes the email scope as supported:

{
“authorization_endpoint”:“hxxps://idp.example.net/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”:“hxxps://idp.example.net/api/oidc/device/authorize”,
“end_session_endpoint”:“hxxps://idp.example.net/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”:“hxxps://idp.example.net/api/oidc/introspect”,
“issuer”:“hxxps://idp.example.net”,
“jwks_uri”:“hxxps://idp.example.net/.well-known/jwks.json”,
“prompt_values_supported”:[
“none”,
“login”,
“consent”,
“select_account”
],
“response_types_supported”:[
“code”,
“id_token”
],
“scopes_supported”:[
“openid”,
“profile”,
“email”,
“groups”
],
“subject_types_supported”:[
“public”
],
“token_endpoint”:“hxxps://idp.example.net/api/oidc/token”,
“token_endpoint_auth_methods_supported”:[
“client_secret_basic”,
“client_secret_post”,
“none”
],
“userinfo_endpoint”:“hxxps://idp.example.net/api/oidc/userinfo”
}

@stalwart Yes, it works now. The request has scope=“openid profile email”. Thanks.

@ruffy91 Try to set the “Required Scopes” field to empty.

Thanks, that worked. Now I can’t get groups to work with pocket-id. Any tips on this?

I think this should work.

Pocket-ID Configuration:
1 - Go to Administration > User Groups. Create Group. Add users to the group and click Save.
2 - Navigate to your OIDC Client. Select your group under Allowed User Groups and click Save.
Stalwart Configuration:
1 - Navigate to Directory > Groups Claim. Set the Claim field to “groups”

Unfortunately it doesn’t.
pocket-id only includes the groups scopes when it is requested, and stalwart says “The claim name used to retrieve the user’s group memberships from the token or user info response. Common values are groups or roles depending on your provider’s configuration. If not set, group information will not be populated. Note that some providers omit group claims from the token to keep its size small and only return them via the user info endpoint, if group information is missing, ensure your provider is configured to include it.”.

Which is why i set the required scopes in the first hand, because I wanted stalwart to request it.

Edit: yes that’s it. Stalwart doesn’t request the groups scope:
<response_type>code</response_type>
<client_id>stalwart-webui</client_id>
<redirect_uri> Portal </redirect_uri>
<code_challenge>deadbeef</code_challenge>
<code_challenge_method>S256</code_challenge_method>
<state>deadbeef</state>
<login_hint>[email protected]</login_hint>
<prompt>login</prompt>
<scope>openid+email+profile</scope>

Technically you are absolutely right (Stalwart’s OIDC implementation is still a bit raw).
However, as a temporary workaround, restricting access via Allowed User Groups in Pocket-ID works perfectly, if a user is removed from the group, Pocket-ID successfully blocks them with You’re not allowed to access this service.

The WebUI does not request the group claim because it does not needed. Also, you cannot rely on the group claim being present as mail clients won’t request it by default. You should manually pre-deploy the account if you want to make sure the group information is included.