OAuth Integration Guide
This document explains how PAM integrates GitHub, Google and other third-party sign-in via Supabase Auth, and how third-party apps connect to its standard OAuth 2.0 authorization endpoints.
Architecture: authentication client and OAuth authorization
PAM connects to Supabase Auth via SupabaseOAuthProvider, supporting GitHub, Google sign-in and email/password, phone OTP. It also provides standard OAuth 2.0 endpoints (authorize, consent, token, PKCE, userinfo) via @qlover/oauth-wrapper for third-party app integration.
Current deployment: Supabase Auth
Users can sign in via GitHub, Google, or email/password (POST /api/user/login), phone OTP, email Magic Link. OAuth authorization endpoints are independent of the sign-in method. See .env.template for SUPABASE_URL, SESSION_SECRET, ENCRYPTION_KEY, etc.
Protocol overview
Exposes the OAuth 2.0 authorization code grant (RFC 6749) for confidential and public clients (PKCE for public). The access_token returned to clients comes from Supabase Auth (JWT); the platform also issues its own refresh_token for third-party refresh grants.
Authorization code flow
- Create an app in the developer console and register redirect_uri values (HTTPS required except localhost).
- Redirect the user to GET /oauth/authorize with client_id, redirect_uri, response_type=code; public clients must include PKCE parameters.
- The end user signs in on this site (GitHub, Google, or email/password), grants consent, then the browser returns to redirect_uri with ?code=...&state=....
- Your backend calls POST /oauth/token with the code to obtain access_token (and optional refresh_token).
- Call GET /userinfo with Bearer access_token to load the user profile.
Endpoints
| Method | Path | Role | Notes |
|---|---|---|---|
| POST | /api/oauth/verify | End-user sign-in (server) | Email/password sign-in for the consent UI session; orchestrated by Supabase Auth—not for OAuth clients. |
| GET | /oauth/authorize | Authorize (browser) | Consent UI; unauthenticated users are sent to login then returned here. |
| POST | /oauth/token | Token (server) | grant_type=authorization_code or refresh_token; client auth via Basic or form body. |
| GET | /oauth/userinfo | Userinfo (server) | Requires Authorization: Bearer; returns sub, name (prefers display_name), business email (no @phone.pam.local), optional phone_number. |
Token exchange
Exchange authorization code
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https%3A%2F%2Fapp.example%2Fcallback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&code_verifier=VERIFIERRefresh token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET// 200 OK { "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "...", "scope": "openid profile" }
PKCE (public clients)
Generate code_verifier before authorize, pass its S256 digest as code_challenge, then send code_verifier when exchanging the code. Confidential clients may omit PKCE.
Userinfo
Success returns JSON with sub (user id), name (prefers pam_users.display_name), email (real business email only; may be empty for phone login), email_verified, and phone_number when present. Invalid or expired tokens yield 401 with error=invalid_token. Internal @phone.pam.local placeholders are never returned.
GET /oauth/userinfo
Authorization: Bearer ACCESS_TOKENError responses
Authorization errors redirect with error and error_description query params; token and userinfo errors are JSON (e.g. invalid_request, invalid_grant, invalid_client, invalid_token).