neosourceDocs
Search docs

Begin an OAuth handshake to link a provider to the current account

POST/api/auth/oauth/{provider}/link

linkOauth

Session-authenticated caller only. The callback attaches the returned identity to the existing account; fails if the identity is already linked to a different one. **Attaching a login method is credential management (ADR 0069 §6), so every bearer access token — including unrestricted and legacy tokens — is refused** with `403 { error: "forbidden" }` before the handshake starts. The handshake ends in a session cookie, which carries no token grant and would therefore outrank the token that started it. **Requires sudo-mode** (plans/auth-flows.md slice #5) — *unless* the account holds no step-up credential yet (a passwordless / OAuth-claimed account with no password, TOTP, or passkey, which has no way to acquire sudo). Once any step-up credential exists this is sudo-gated again. Returns `403 { error: "sudo_required", challenge: "password"|"totp" }` if the sudo window has elapsed.

Requires authentication using a session cookie — see tokens and scopes.

curl

curl -X POST 'https://neosource.dev/api/auth/oauth/PROVIDER/link' \
  -b 'ns_session=$NEOSOURCE_SESSION' \
  -H 'Content-Type: application/json' \
  -d '{"intent":"string"}'

fetch

fetch("https://neosource.dev/api/auth/oauth/PROVIDER/link", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  credentials: "include",
  body: JSON.stringify({"intent":"string"}),
});

Path parameters

providerrequired

One of: `github`. Extend as more providers come online.

string

Request bodyrequired

application/json

OAuthStartRequest

object

claim_handle

string | null

`neosource` handle the user is trying to claim; required when `intent = "claim_handle"`. The callback enforces the provider login matches this value (case-insensitive).

intentrequired

OAuthIntent

string

"login""link""claim_handle"

requested_scope
one of
  • null

  • OAuthScope

    string

    How much provider access an OAuth handshake should request. Drives incremental authorization (see `plans/github-onboarding-tiers.md`): login asks for the minimum, and elevated tiers are requested only when the user opts into the feature that needs them. The provider client maps each tier onto the concrete provider scope string.

    "minimal""repo""repo_and_deploy_key"

return_to

string | null

Internal path the SPA wants to land on after the callback. Only values starting with `/` (and not `//`) are honoured.

additional propertiesnot allowed

Responses

200Redirect URL

application/json

OAuthStartResponse

object

redirect_urlrequired

string

403Forbidden — one of: forbidden, sudo_required

application/json

one of
  • ErrorForbidden
  • SudoRequiredError

    object

    Wire shape of the 403 a sudo-gated endpoint returns when sudo is missing or expired. `challenge` tells the SPA which sudo-prove endpoint to call.

    challengerequired

    string

    Which sudo-prove endpoint the account should use: `"passkey"` (`/api/auth/sudo/passkey/{start,finish}`), `"password"` (`/api/auth/sudo/password`), or `"totp"` (`/api/auth/sudo/totp`), chosen from the account's registered credentials — passkey first when present, since it is both the strongest credential and the least friction. A hint for which prompt to show first, not an authorization decision: each sudo route verifies its own credential regardless of what was advertised here, so a client may use a different one.

    errorrequired

    string

    Always `sudo_required` — this body exists to carry the extra fields that kind needs.

    "sudo_required"

    messagerequired

    string

Standard errors

Bodies documented once for the whole API — see standard errors.

  • 400Bad Request — one of: invalid_input
  • 401Authentication required
  • 429Rate limited — retry after the `Retry-After` header
  • 500Internal server error
  • 502Bad Gateway — one of: upstream
  • 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
  • 504Gateway timeout — the request exceeded the server's handling budget