Setting up SSO with an OIDC provider (tested with Pocket ID) fails at the token exchange step. Three bugs compound:
1. URLSearchParams body serialized as '{}'packages/http_client/src/HttpClientRequestInternals.tsx — resolveRequestBody() JSON-stringifies non-string bodies. JSON.stringify(new URLSearchParams(...)) produces '{}', so the token exchange POST body is empty.
+ if (body instanceof URLSearchParams) {+ return body.toString();+ }
if (typeof body === 'string') {
2. client_secret not included in token exchangepackages/api/src/auth/services/SsoService.tsx calls getSsoConfig() without { includeSecret: true }, so clientSecret is always undefined.
3. Basic auth header ignored by some IdPs
Upstream sends client_id in the POST body and client_secret via Authorization: Basic header. Some IdPs (e.g. Pocket ID) only parse Basic auth when client_id is absent from the body. Since it's present, the Basic auth is ignored entirely.
Fix: send client_secret in the POST body instead:
SSO users treated as "unclaimed" — User.tsx considers users with passwordHash === null && !isBot as unclaimed. SSO-provisioned users have no password but are legitimate. Fix: add && !this._traits.has('sso') to the condition.
SSO callback route blocked by auth guard — RootComponent.tsx redirects unauthenticated users to /login before the callback page at /auth/sso/callback can process the authorization code. Fix: add the path to the allowlist alongside the login route.
Thread
Comment by @mgabor3141
5: SSO/OIDC token exchange broken (3 compounding bugs)
Setting up SSO with an OIDC provider (tested with Pocket ID) fails at the token exchange step. Three bugs compound: 1.URLSearchParamsbody serialized as'{}'packages/http_client/src/HttpClientRequestInternals.tsx—resolveRequestBody()JSON-stringifies non-string bodies.JSON.stringify(new URLSearchParams(...))produces'{}', so the token exchange POST body is empty.client_secretnot included in token exchangepackages/api/src/auth/services/SsoService.tsxcallsgetSsoConfig()without{ includeSecret: true }, soclientSecretis alwaysundefined.client_idin the POST body andclient_secretviaAuthorization: Basicheader. Some IdPs (e.g. Pocket ID) only parse Basic auth whenclient_idis absent from the body. Since it's present, the Basic auth is ignored entirely. Fix: sendclient_secretin the POST body instead:User.tsxconsiders users withpasswordHash === null && !isBotas unclaimed. SSO-provisioned users have no password but are legitimate. Fix: add&& !this._traits.has('sso')to the condition.RootComponent.tsxredirects unauthenticated users to/loginbefore the callback page at/auth/sso/callbackcan process the authorization code. Fix: add the path to the allowlist alongside the login route.