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#
| Credential | Used by | Requirements |
|---|---|---|
| Session cookie | Browser apps via the SDK | httpOnly cookies set by /auth/token; every state-changing call must include X-Osgarden-Csrf: 1 and use credentials: include. |
| Bearer token | Server-to-server | Authorization: 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#
auth.signIn()→ redirect to the gateway'sGET /auth/authorizewith 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 }.- User authenticates and consents (see Consent).
- Ory redirects back with
?code=…. auth.handleRedirectCallback()→POST /auth/tokenexchanges the code; the gateway sets session cookies and returns{ authenticated: true }.- On a later
401, the SDK callsPOST /auth/refreshonce 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 }
Consent endpoints#
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.