Skip to content

OAuth link callback targets /v1/users/{id}/oauth-links, which CIRISServer does not route #3

Description

@emooreatx

The GUI's OAuth-link callback calls cirisClient.users.linkOAuthAccount(user_id, …), which resolves to:

POST /v1/users/{user_id}/oauth-links

CIRISServer does not route that path, and never has. A web user linking a Google account gets a 404.

Where the path came from

client/generated-api/…/UsersApi.kt is generated from the agent's OpenAPI spec, so /v1/users/{id}/oauth-links is the agent server's surface. The node's equivalent has always been a different route, and the two halves each pointed at a counterpart that did not exist — tracked as CIRISServer#432.

What to call instead

POST /v1/self/oauth-link on the node, owner-session gated.

{ "provider": "google", "email": "person@example.org" }

Omit wa_id to target the node's owner — the case someone linking their own account is in. Pass it to pre-provision a different user.

It takes an email, not an external_id. That is the substantive change in CIRISServer 0.5.182 and the reason the surface is usable at all: the route previously required a provider subject id, and nobody can look up their own Google sub. An address is a handle a person actually has. The node records it and binds the verified pair on that person's next sign-in — so the response is linked: false with a note saying so, and the link completes on the following OAuth round trip rather than immediately.

Typed refusals worth surfacing distinctly, because they need different things from the reader:

reason_id meaning
auth.oauth_link.already_has_identity that account already has a linked identity; remove it first
auth.link.ambiguous_handle both an account id and an email were sent; they mean different things
auth.not_the_owner signed in, but not as the owner
auth.not_signed_in no owner session

Both new ids are localized into all 29 languages as of 0.5.182.

Reference implementation

CIRISApiClient.preprovisionOAuthEmail in the vendored KMP client (client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/api/CIRISApiClient.kt) does exactly this call and is the shape to mirror.

Verification

tests/oauth_state_matrix.rs in CIRISServer covers the flow end to end against a synthetic provider, including the assertion that matters: the identity lands on the owner's existing cert rather than minting a second account for the same human.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions