Forge.Core Docs
Launch Dashboard

Sign in with Forge

Forge Core is a full OAuth2/OIDC provider — any app can let its users sign in with an existing Forge account, the same way a "Sign in with Google" button works. PKCE is mandatory for every client.

1. Register a client

Dynamic client registration (POST /oauth2/register) is disabled in this deployment and returns 403 access_denied. An admin registers your app instead, from Forge UI's OAuth Apps page or directly via POST /api/core/oauth-apps. The client_secret is shown once, at creation time.

applicationTyperedirect_uri rules
webMust be https — no loopback exception
nativehttp allowed only on a loopback address (127.0.0.1 / localhost), per RFC 8252

2. Required scopes

  • openid — required for OIDC; issues an ID token
  • profile — name and profile claims via userinfo
  • email — email and email-verified claims via userinfo
  • offline_access — issues a refresh token

Any other scope is rejected with an invalid_scope error.

3. PKCE is mandatory

Every client — confidential or public — must use PKCE with the S256 method. Generate acode_verifier and derive its code_challenge before redirecting to authorize.

4. The flow

  1. Redirect the user to /oauth2/authorize with your client_id, redirect_uri, scope, state, and PKCE challenge.
  2. Forge Core redirects back to your redirect_uri with a code and your state.
  3. Exchange the code (plus your code_verifier) at /oauth2/token for an access_token, id_token, and — if you requested offline_access — a refresh_token.
  4. Call /oauth2/userinfo with the access token to get profile/email claims.
GET /api/auth/oauth2/authorize
?response_type=code
&client_id=<your client_id>
&redirect_uri=<your redirect_uri>
&scope=openid profile email
&state=<random state>
&code_challenge=<derived from your verifier>
&code_challenge_method=S256
curl — token exchange
curl -X POST http://localhost:3030/api/auth/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=<code from the callback>" \
  -d "redirect_uri=<your redirect_uri>" \
  -d "client_id=<your client_id>" \
  -d "client_secret=<your client_secret>" \
  -d "code_verifier=<your original code_verifier>"

Verifying the ID token locally

Verify the ID token's signature against Forge Core's JWKS instead of trusting it blindly — no extra round trip needed to establish the user's sub.

JavaScript (jose)
import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(new URL("http://localhost:3030/api/auth/jwks"));
const { payload } = await jwtVerify(idToken, JWKS, {
  issuer: "http://localhost:3030",
  audience: "<your client_id>",
});

Prefer not to hand-roll this?

The @casteluke/forge-client package wraps this entire flow — including a React hook that owns the browser redirect for you. See /docs/sdk.

5. Error responses

Authorize-time errors are redirected — to your redirect_uri if it was already validated, otherwise to Forge's own generic error page. Token-exchange errors come back as a JSON body. One detail worth knowing: a PKCE mismatch returns 401 invalid_request, not the more typical 400 invalid_grant.

6. Revoking a token

curl
curl -X POST http://localhost:3030/api/auth/oauth2/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=<access_token or refresh_token>" \
  -d "client_id=<your client_id>" \
  -d "client_secret=<your client_secret>"

Per RFC 7009, this always returns 200 with an empty body — don't use it to probe whether a token is valid.

Hardening already in place

  • Secret rotation: an admin can rotate your client's secret via POST /core/oauth-apps/:id/rotate-secret.
  • Per-client rate limiting: OAuth2 endpoints are rate-limited per client_id, not just IP — see /docs/security.
  • JWKS rotation: signing keys rotate automatically on a 90-day schedule with a 30-day grace period.
  • Audit logging: token issuance, consent, and revocation events are all recorded — see /docs/security.

Known gap: multi-integrator validation

Only one independent sister app has exercised this flow end-to-end so far. If something here doesn't match real behavior for your integration, that's the most likely reason — flag it to the project owner.