Web app

A browser application signs users in with the OAuth 2.1 authorization code flow and PKCE. Gate is a standard OpenID Connect provider, so any conformant library works — you do not need a Gate SDK.

Register the client

Create a web client on the project's OIDC tab, or through the API. The client secret is returned once.

const response = await fetch(
  `https://evonia-gate.annatarhe.com/api/v1/projects/${projectId}/oidc-clients`,
  {
    method: "POST",
    headers: { "content-type": "application/json", cookie: request.headers.get("cookie")! },
    body: JSON.stringify({
      name: "Rawback Web",
      type: "web",
      redirectUris: ["https://rawback.app/auth/callback"],
      postLogoutRedirectUris: ["https://rawback.app/"],
      scopes: ["openid", "profile", "email", "offline_access"],
    }),
  },
);

const { data } = await response.json();
// data.clientSecret is null on subsequent reads — store it now.

Rules Gate enforces on registration:

  • Interactive clients (web and native) require at least one redirect URI. Up to 20 are allowed, and each must be an absolute URL.
  • If environmentId is given, it must belong to the project.
  • offline_access in scopes is what adds refresh_token to the client's grant types. Without it, you get authorization codes only and no refresh.
  • Registration requires a user session with credential:manage. A service API key is rejected.

Discovery

Never hard-code endpoints. Read them from the discovery document and cache it.

curl
curl -sS https://evonia-gate.annatarhe.com/.well-known/openid-configuration

The issuer is https://evonia-gate.annatarhe.com/api/auth, and the endpoints hang off it:

PurposeEndpoint
Authorization/api/auth/oauth2/authorize
Token/api/auth/oauth2/token
User info/api/auth/oauth2/userinfo
Introspection/api/auth/oauth2/introspect
Revocation/api/auth/oauth2/revoke
End session/api/auth/oauth2/end-session
JSON Web Keys/.well-known/jwks.json

The flow

  1. Generate a PKCE verifier and state. Keep both server-side, in a short-lived HTTP-only cookie or session store.

  2. Redirect to the authorization endpoint. Gate shows /sign-in if the user has no session, then /consent unless the client is marked to skip consent.

  3. Receive the code at your redirect URI. Verify state matches before doing anything else.

  4. Exchange the code at the token endpoint, authenticating with HTTP Basic and sending the verifier. Codes expire after 5 minutes and are single-use.

  5. Verify the tokens before trusting them. See Verifying tokens.

Build the authorization URL

import { createHash, randomBytes } from "node:crypto";

const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
const state = randomBytes(16).toString("base64url");

const url = new URL("https://evonia-gate.annatarhe.com/api/auth/oauth2/authorize");
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", clientId);
url.searchParams.set("redirect_uri", "https://rawback.app/auth/callback");
url.searchParams.set("scope", "openid profile email offline_access");
url.searchParams.set("state", state);
url.searchParams.set("code_challenge", challenge);
url.searchParams.set("code_challenge_method", "S256");
// Optional: ask for a token your own API will accept as its audience.
url.searchParams.set("resource", "https://api.rawback.app");

Exchange the code

const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");

const response = await fetch("https://evonia-gate.annatarhe.com/api/auth/oauth2/token", {
  method: "POST",
  headers: {
    authorization: `Basic ${basic}`,
    "content-type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code,
    redirect_uri: "https://rawback.app/auth/callback",
    code_verifier: verifier,
  }),
});

const tokens = await response.json();
// { access_token, token_type, expires_in: 600, refresh_token?, id_token, scope }

Lifetimes

TokenLifetime
Authorization code300 s, single use
Access token600 s
ID token600 s
Refresh token2,592,000 s (30 days)

Refresh with grant_type=refresh_token and the same client authentication. Opaque access tokens are prefixed gate_at_ and refresh tokens gate_rt_.

Signing out

Send the user to /api/auth/oauth2/end-session. Provide id_token_hint and, if you want them returned to your app, a post_logout_redirect_uri that was registered on the client.

Clearing your own cookie does not end the Gate session — the next authorization request would sign them straight back in.

Cross-origin

/api/v1 allows credentialed requests only from the origins in src/server/origins.ts — GATE_BASE_URL, https://shelltime.xyz, https://rawback.app, https://athena.annatarhe.com, and https://athena.annatarhe.cn — accepting the headers content-type, authorization, x-api-key, x-request-id, and idempotency-key. The same list is better-auth's trusted origins.