CrydenSync
HTTP API

OAuth

Login/signup with Google, GitHub, Microsoft, Discord, GitLab, or Apple. The flow involves a browser redirect, so most of it doesn't fit a single curl command.

Six providers: google, github, microsoft, discord, gitlab, apple. The router is provider-agnostic via a {provider} path parameter — every provider but Apple is a switch-statement case plus its own client ID/secret env vars, not a routing difference.

Routes

MethodPathAuthResult
GET/oauth/{provider}public302 to the provider's consent screen
GET/oauth/{provider}/callbackpublic200 tokens, or the paused second-factor shape
GET/oauth/{provider}/linkrequires auth302 to the provider
GET/oauth/{provider}/link/callbackpublic200 {"data":{"status":"linked","provider":"..."}}

A provider with no credentials configured on the server answers 404 oauth_provider_not_configured — this is a normal runtime state, not a startup failure, so a deployment can enable providers incrementally.

# not something you curl directly — a browser redirect:
open https://api.example.com/v1/oauth/google

An OAuth login is still a login: if the account also has a second factor enrolled, the callback returns the same paused shape /login would — see Authentication.

When a provider returns an email that already belongs to an existing password-based account, the answer is 409 oauth_email_conflict, with a message naming the provider — this is deliberately not resolved silently and is not folded into a generic login failure, since auto-linking on email match alone is treated as an account-takeover vector.

Resolution:

  1. The user logs in with their existing password via /v1/login.
  2. With a valid access token, GET /v1/oauth/{provider}/link starts linking, completed at /v1/oauth/{provider}/link/callback.

Why link/callback is public despite linking requiring auth

A browser redirect coming back from a provider carries no Authorization header at all — there's no way for it to. Instead, the linking user's identity travels in a short-lived, HMAC-signed cookie that link sets before redirecting, signed with JWT_SECRET and verified on return rather than trusted blindly. A tampered cookie cannot be used to link a different account. The cookie lives for 600 seconds — long enough for a real provider round trip, short enough that a stale one is useless.

Errors

oauth_provider_not_configured (404), oauth_state_mismatch (400), oauth_email_not_available (400), oauth_identity_verification_failed (400), oauth_link_session_missing (400), oauth_link_not_configured (500), oauth_email_conflict (409), oauth_identity_already_linked (409).

Apple is the one provider that isn't another switch case

The other five are the same OAuth2 authorization-code flow with different URLs. Apple genuinely isn't:

  • Apple's client "secret" isn't a static string — it's an ES256 JWT this server signs itself, per exchange, from a private key. That means Apple needs all four of APPLE_CLIENT_ID (the Services ID, e.g. com.example.web — not the app's bundle ID), APPLE_TEAM_ID, APPLE_KEY_ID, and APPLE_PRIVATE_KEY (the contents of the .p8 key file), where every other provider needs only a client ID and a client secret.
  • A PEM-format private key can't sit on one line of a .env file, so literal \n sequences in APPLE_PRIVATE_KEY are converted to real newlines — but only when the value actually looks like it needs that, so a key supplied through a real secret manager (which already has real newlines) is left untouched.
  • The user's email comes from the token response's id_token, a signed JWT, verified against Apple's own published JWKS before anything inside it is trusted — not from a separate userinfo call the way the other five work.
  • Apple sends the user's name only on the first authorization ever, never again after that; this deployment doesn't attempt to capture it at all, only the email from the verified id_token.

None of this is visible from a client's perspective — routes and response shapes are identical to every other provider. It only means an Apple login takes a different path once it reaches the server.