go-auth
Routes

OAuth

Provider login, callback, link, unlink, and connected accounts.

OAuth

Mounted when AuthenticationConfig.OAuth is true with at least one configured provider. Enabling OAuth without a provider fails startup; registration OAuth independently permits account creation. Auth is either Public or Session as marked. Provider tokens are never exposed: the connected-accounts route returns profile fields only.

Initiate login

GET /auth/oauth/{provider}

Starts a login. Returns the provider authorization URL as JSON and sets a short-lived, host-only HttpOnly state cookie. The frontend must use the same browser and cookie jar for the callback. Auth: Public.

Path params:

ParamTypeRequiredNotes
providerstringRequiredA registered provider name (e.g. github)

Response 200 OK:

{ "url": "https://provider.example/authorize?..." }

Errors: provider_not_found (unknown provider name).

Callback

GET /auth/oauth/{provider}/callback, POST /auth/oauth/{provider}/callback

Provider redirect target (redirect URI ends here). GET reads code and state from the query without parsing a body. POST accepts application/x-www-form-urlencoded with a maximum 64 KiB body; body values take precedence over query values. Multipart uploads, other content types, malformed forms and oversized bodies redirect with invalid_request before consuming state. The callback requires the matching browser state cookie and clears it. A provider-link callback also requires the same live session that started the link. No JSON responses: every outcome is a redirect or an HTML page. Auth: Public, with the extra session requirement for links.

The state cookie uses SameSite=None; Secure when secure cookies are enabled so cross-site form POST callbacks can return it. When secure cookies are disabled for local HTTP development, it uses SameSite=Lax, which supports top-level GET callbacks; use HTTPS to test a cross-site POST callback.

Path params:

ParamTypeRequiredNotes
providerstringRequiredA registered provider name

Outcomes:

  • Missing code/state: 302 to {BaseURL}/auth/callback?error=invalid_request&provider={provider}.
  • Failure: 302 to {BaseURL}/auth/callback?error={code}&provider={provider}, where {code} is the error code (e.g. state_used, state_expired).
  • Link flow (caller already had a session): 302 to {BaseURL}/auth/callback?step=linked. No new session is issued.
  • Verification required: 302 to {BaseURL}/auth/callback?step=verify_email&provider={provider}.
  • 2FA required by global, per-account or admin policy: 302 to {BaseURL}/auth/callback with step=two_factor, provider, challengeId, and expiresAt query parameters, plus an HttpOnly challenge binding cookie. No session is issued. Submit the email code and challenge ID to POST /auth/2fa/verify using the same browser cookie jar. The binding credential never appears in the redirect URL.
  • Invitation email proof required: 302 to {BaseURL}/auth/callback?step=verify_invite_email&challengeId=..., plus a host-only HttpOnly invitation-binding cookie. Complete the code through POST /auth/oauth/invite/verify.
  • Normal login: 200 HTML page that sets both cookies via Set-Cookie and redirects client-side with window.location.replace("{BaseURL}/auth/callback?step=authenticated"). Cookies go in headers (not a bare 302) because Set-Cookie on cross-origin redirects is unreliable in some browsers.

POST /auth/oauth/{provider}/link

Starts linking a provider to the logged-in account. Returns the authorization URL as JSON and sets the browser state cookie. The callback must carry this cookie and the same live session. Auth: Session.

After provider exchange, finalization locks the user and initiating session and rechecks the exact session token, account eligibility and claimed state's current existence, bindings and expiry in the transaction that saves the link. A reset, email replacement, revocation or token rotation that wins before finalization prevents the link. The provider request runs outside those database locks.

Path params:

ParamTypeRequiredNotes
providerstringRequiredA registered provider name

Response 200 OK:

{ "url": "https://provider.example/authorize?..." }

Errors: provider_not_found.

POST /auth/oauth/{provider}/unlink

Removes the provider link. Blocked when it is the account's last usable login method. A disabled password method or an unavailable provider does not count as an alternative. Auth: Session. No body.

Path params:

ParamTypeRequiredNotes
providerstringRequiredA registered provider name

Response 200 OK:

{ "message": "Provider unlinked" }

Errors: provider_not_found, cannot_unlink_last_provider (last login method; set a password first).

List connected providers

GET /auth/oauth/providers

Lists the caller's connected providers. Profile fields only. Auth: Session.

Response 200 OK:

{
  "providers": [
    {
      "provider": "github",
      "email": "user@example.com",
      "name": "Ada",
      "avatarUrl": "https://...",
      "createdAt": "2025-01-01T00:00:00Z"
    }
  ]
}

Start invited signup

POST /auth/oauth/{provider}/invite

Starts provider signup from an account invitation while public signup can remain closed. Requires authentication OAuth, registration OAuth, and InviteSignup. Auth: Public, with mandatory CSRF and origin checks.

{ "inviteToken": "raw-token-from-the-invitation-link" }

Returns 200 {"url":"https://provider.example/authorize?..."} and the normal OAuth state cookie. Navigate the browser to url. The backend binds state to the selected provider and stores an encrypted copy of the current invitation code hash, so a resend that rotates the invitation invalidates an already-started flow. The returned provider address must match the invited email. A different address is rejected without consuming the invite. An expired, revoked or consumed invitation cannot create an account.

Confirm an invited provider email

POST /auth/oauth/invite/verify

Only needed when the provider did not confirm ownership of the invited address. The callback has already mailed a code and returned the invitation challenge ID. No account exists yet. The pending provider profile is encrypted in storage. Auth: Public, with the same browser's invitation-binding cookie and CSRF.

{ "challengeId": "...", "code": "123456" }

The one-time code uses VerificationCodeTTL, bounded by invitation expiry, and allows five guesses. Successful proof, account creation, provider connection, and invitation consumption commit together. An existing email is never silently connected to the provider; sign into that account and connect it explicitly.

Success sets the normal session/refresh cookies and returns 200 {"step":"authenticated","isNewUser":true}, or returns step: "two_factor", challengeId, codeSent and expiresAt with a challenge-binding cookie when local 2FA is required. Complete that challenge through /auth/2fa/verify. A failed account creation leaves the invitation and email proof available for retry.

Next

On this page