Authentication

osgarden uses OAuth 2.0 Authorization Code + PKCE against an Ory (Hydra/Kratos) issuer. The gateway brokers the flow so that tokens never reach page scripts — they are held in httpOnly cookies, and the browser only ever learns "signed in or not."

Most apps never call these endpoints directly; the SDK auth.* methods drive the whole flow. They are documented here for completeness and for non-browser clients.

Session model#

CredentialUsed byRequirements
Session cookieBrowser apps via the SDKhttpOnly cookies set by /auth/token; every state-changing call must include X-Osgarden-Csrf: 1 and use credentials: include.
Bearer tokenServer-to-serverAuthorization: Bearer <access_token>; no CSRF header.

Token validation is cached briefly (60s) so a burst of requests doesn't hit Ory per call. When ORY_API_KEY is configured the gateway introspects the token (learning the app's OAuth client_id, which per-app budgets key on); otherwise it falls back to userinfo, which proves the user but not the app (budgets are then unenforced).

The browser flow#

  1. auth.signIn() → redirect to the gateway's GET /auth/authorize with a PKCE challenge; the gateway 302s to Ory's /oauth2/auth. Apps never configure the issuer URL directly — only the gateway knows it (PROXY_BASE_URL/ORY_HYDRA_URL), so the SDK config is just { gatewayUrl, clientId }.
  2. User authenticates and consents (see Consent).
  3. Ory redirects back with ?code=….
  4. auth.handleRedirectCallback()POST /auth/token exchanges the code; the gateway sets session cookies and returns { authenticated: true }.
  5. On a later 401, the SDK calls POST /auth/refresh once and retries.

Endpoints#

All are under /auth.

GET /auth/authorize#

Browser-facing OAuth entry point. Redirects (302) to the configured issuer's /oauth2/auth, forwarding the query string untouched (client_id, redirect_uri, code_challenge, state, …). Exists so apps depend only on the gateway, never the issuer's URL. 503 if no issuer is configured.

POST /auth/token#

Exchanges an authorization code for a session. The gateway performs the exchange server-to-server and sets httpOnly cookies; the token is never in the response body.

Body

{
  "code": "",
  "code_verifier": "",
  "redirect_uri": "https://app.example.com",
  "client_id": ""
}

200 { "authenticated": true, "expires_in": 3600 } · 4xx/502 { "error": "…" }

POST /auth/refresh#

Rotates the session using the refresh cookie. Requires X-Osgarden-Csrf.

Body { "client_id": "…" }200 { "authenticated": true, "expires_in": 3600 } · 401 if the refresh token is missing/invalid (cookies are cleared).

POST /auth/logout#

Best-effort revokes the refresh token at Ory, then clears cookies. Requires X-Osgarden-Csrf. Always 200 { "ok": true } from the client's perspective.

Body { "client_id"?: "…" }

GET /auth/session#

Reports whether the session cookie is valid. Read-only.

200 { "authenticated": true, "user_id": "…" } or { "authenticated": false }

Rendered by the osgarden dashboard's consent screen. Accepting consent makes the user a member of the app's _app resource and credits a welcome balance ($5 default, WELCOME_CREDIT_USD) if the user has never received one.

GET /auth/consent-request?challenge=…#

Returns the consent request details (subject, requested scope, is_new_user). If Ory reports skip: true (already granted with remember), it auto-accepts and returns { "skip": true, "redirect_to": "…" } — and still grants the welcome credit to a never-welcomed user, so a remembered consent can't skip past it.

POST /auth/consent/accept#

Body { "challenge": "…", "grant_scope": ["openid", …], "subject": "…" }200 { "redirect_to": "…", "welcomed": true|false }

Best-effort side effect: resolves the consenting client → its active deployment → joins the user to the _app resource. Never blocks consent (the app may not have deployed a schema yet).

POST /auth/consent/reject#

Body { "challenge": "…" }200 { "redirect_to": "…" }

GET /auth/kratos/flow?type=…&id=…#

Server-to-server proxy for Kratos self-service flow data (the browser can't call Ory directly because of CORS + SameSite cookies).

SDK#

const osgarden = createClient({ gatewayUrl, clientId });

await osgarden.auth.signIn(); // → redirect to gateway /auth/authorize → Ory
await osgarden.auth.handleRedirectCallback(); // on return, exchanges ?code
const { authenticated, userId } = await osgarden.auth.getSession();
await osgarden.auth.signOut();

See sdk.md.