Sign in with Codegraff
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
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:
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:
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:
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>
{
"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:
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:
curl https://codegraff.com/api/oauth/token \ -u "cg_client_...:cg_cs_..." \ -d grant_type=refresh_token \ -d refresh_token=cg_rt_...
{
"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:
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: "/" })