Docs/Accounts/Sign in with Codegraff

Sign in with Codegraff

Codegraff is an OpenID Connect provider. Any app can authenticate users against their Codegraff account with the standard authorization-code flow — no SDK required.

Register your app

Client registration is manual for now: email hi@codegraff.com with your app's name, homepage, and exact redirect URIs (https, or http://localhost for development). You receive a client_id and, for server-side apps, a one-time client_secret. Browser-only and native apps are registered as public clients and use PKCE alone.

What your app gets

A stable account ID (sub) and, with the email scope, the user's verified email. Never credits, API keys, sandboxes, or repositories — those stay behind cg_sk_ API keys the user issues deliberately.

Endpoints

Everything is advertised by the discovery document, so most libraries only need the issuer or discovery URL:

bash
curl https://codegraff.com/.well-known/openid-configuration
  • Issuer https://codegraff.com
  • Authorization GET https://codegraff.com/oauth/authorize
  • Token POST https://codegraff.com/api/oauth/token
  • UserInfo GET https://codegraff.com/api/oauth/userinfo
  • JWKS https://codegraff.com/.well-known/jwks.json

Supported: response_type=code, scopes openid email profile offline_access, PKCE S256 (required for every client), client authentication via HTTP Basic or client_secret_post, refresh tokens. Not supported in this version: implicit or hybrid flows, dynamic client registration.

The flow

1. Send the user to the authorization endpoint with a PKCE challenge:

text
https://codegraff.com/oauth/authorize
  ?response_type=code
  &client_id=cg_client_...
  &redirect_uri=https://app.example.com/auth/callback/codegraff
  &scope=openid%20email
  &state=<random>
  &nonce=<random>
  &code_challenge=<base64url(sha256(code_verifier))>
  &code_challenge_method=S256

The user signs in to Codegraff if needed (email link or Google), sees a consent screen naming your app the first time, and is redirected back with code and state. Returning users skip the consent screen; pass prompt=consent to force it.

2. Exchange the code server-side:

bash
curl https://codegraff.com/api/oauth/token \
  -u "cg_client_...:cg_cs_..." \
  -d grant_type=authorization_code \
  -d code=<code> \
  -d redirect_uri=https://app.example.com/auth/callback/codegraff \
  -d code_verifier=<code_verifier>
json
{
  "access_token": "cg_at_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid email",
  "id_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9..."
}

3. Optionally read the profile with the access token:

bash
curl https://codegraff.com/api/oauth/userinfo -H "Authorization: Bearer cg_at_..."
# {"sub":"2","email":"you@example.com","email_verified":true}

Tokens and claims

The id_token is a JWT signed with ES256; verify it against the JWKS (kid in the header selects the key). Claims: iss, sub (the user's stable Codegraff account ID), aud (your client_id), exp, iat, auth_time, nonce (echoed from your request), and email plus email_verified when you request the email scope. Access tokens are opaque, last one hour, and are only good for the UserInfo endpoint. Authorization codes are single-use and expire after ten minutes. Errors use the RFC 6749 shape: {"error":"invalid_grant","error_description":"..."}.

Refresh tokens

Access tokens last an hour. If your app needs to keep calling Codegraff while the user is away, add the offline_access scope to the authorization request; the consent screen says so, and the code exchange then also returns a refresh_token. Redeem it at the token endpoint with the same client authentication:

bash
curl https://codegraff.com/api/oauth/token \
  -u "cg_client_...:cg_cs_..." \
  -d grant_type=refresh_token \
  -d refresh_token=cg_rt_...
json
{
  "access_token": "cg_at_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid email offline_access",
  "refresh_token": "cg_rt_..."
}

Refresh tokens last 30 days and are single-use: every refresh returns a new one and retires the old, so always store the newest. Presenting a retired token is treated as theft and revokes every token from that sign-in; the user then goes through the (silent) sign-in again. An optional scope parameter may narrow, never widen, the original grant.

Better Auth example

With Better Auth's generic OAuth plugin, discovery does the rest:

ts
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    genericOAuth({
      config: [
        {
          providerId: "codegraff",
          discoveryUrl: "https://codegraff.com/.well-known/openid-configuration",
          clientId: process.env.CODEGRAFF_OAUTH_CLIENT_ID!,
          clientSecret: process.env.CODEGRAFF_OAUTH_CLIENT_SECRET!,
          scopes: ["openid", "email", "offline_access"], // offline_access: refresh tokens
          pkce: true,
        },
      ],
    }),
  ],
});
// then: authClient.signIn.oauth2({ providerId: "codegraff", callbackURL: "/" })