[Bug]: X shared-key OAuth cannot complete after #2051, since x.provider.ts still uses the pre-hardening init query
What
The shared-OAuth hardening in #2051 updated seven of the eight providers that use /api/auth/oauth/shared/callback/{state}. x.provider.ts was not updated, so X shared-key login now cannot complete.
This fails closed, so it is an availability regression rather than a hole. Flagging it that way deliberately: nothing here weakens the fix.
Proof
Eight providers post to the hardened callback, seven call the new init helper:
$ git grep -l "shared/callback" -- backend/src/providers/oauth/ | wc -l
8
$ git grep -l "buildSharedOAuthInitQuery" -- backend/src/providers/oauth/ | wc -l
7The one in the first list and not the second is x.provider.ts, which still sends the pre-#2051 query:
// backend/src/providers/oauth/x.provider.ts
const redirectUri = `${selfBaseUrl}/api/auth/oauth/shared/callback/${state}`;
const response = await axios.get(
`${cloudBasedUrl}/oauth/twitter?redirect_uri=${encodeURIComponent(redirectUri)}`,against what the other seven now send:
// shared-oauth.service.ts
return new URLSearchParams({
redirect_uri: redirectUri,
project_id: appConfig.cloud.projectId as string,
sign,
flow_id: sharedOAuthFlowId(state),
}).toString();Why the callback then rejects it
oauth.routes.ts runs verifyIdentityToken for every provider whose config has useSharedKey; there is no per-provider bypass. Two of its checks cannot be satisfied for X:
payload.projectId !== appConfig.cloud.projectIdrejects, andproject_idis never sent. The init request is an unauthenticated GET with no headers, so the cloud has no other way to learn which project is asking.payload.sid !== sharedOAuthFlowId(context.state)rejects, andflow_idis never sent.
The sid check is arguably recoverable, since state is embedded in redirect_uri and the cloud could hash it itself. The projectId check is not: nothing in the X init request identifies the project.
X also goes to a different cloud endpoint (/oauth/twitter) than the seven (/auth/v1/shared/<provider>), so it may be on an older cloud path that was never in scope for #2051.
Root cause
The provider list was updated per file rather than swept. apple, discord, facebook, github, google, linkedin and microsoft were changed in the same commit; x uses the same callback and the same config?.useSharedKey branch but was missed.
backend/tests/unit/shared-oauth-identity.test.ts has no X case, so nothing failed.
What I could not verify
Whether the cloud's /oauth/twitter endpoint already emits a signed assertion. It does not change the conclusion, because the projectId binding cannot be satisfied without project_id in the init request, but I want to be clear about the boundary: I can read this repo and the published CLI, not the cloud backend.
If X shared keys are not actually offered to projects, this is moot and I would rather be told that than guess. oauth-config.service.ts does carry a case 'x': with scopes, and x.provider.ts has a full useSharedKey branch, which is why I read it as reachable.
Proposed fix
Route X through the same helper as the rest:
const response = await axios.get(
`${cloudBasedUrl}/oauth/twitter?${buildSharedOAuthInitQuery(redirectUri, state)}`,That needs the cloud's /oauth/twitter to accept the extra params and mint the bound assertion, which is a cloud-side change I cannot make or test. If the intent was instead to drop X from shared keys, the fix is to remove its useSharedKey branch so it fails at config time with a clear message rather than at callback time.
Acceptance criteria
- Every provider that posts to
/api/auth/oauth/shared/callback/{state}either usesbuildSharedOAuthInitQueryor no longer offersuseSharedKey. - A test covers the X path, so a future provider addition cannot silently repeat this.
git grep -l "shared/callback" -- backend/src/providers/oauth/andgit grep -l "buildSharedOAuthInitQuery" -- backend/src/providers/oauth/return the same set.
Note on process
I am at the 3-issue assignment limit (#1918, #2020, #2029), so I have not claimed this. Filing it now anyway because it is a regression in a security fix that landed hours ago and seemed worth surfacing promptly rather than sitting on until a slot frees.
Source: InsForge/InsForge