-
-
Notifications
You must be signed in to change notification settings - Fork 553
Open
Description
Better Auth OAuth Architecture Reference
This document provides a comprehensive reference for the centralized authentication system using Better Auth with OAuth 2.1.
Architecture Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Auth Server (apps/auth, port 3001) β
β PostgreSQL + Better Auth β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β OAuth 2.1 Provider (issues tokens to clients) β β
β β Google/LinkedIn OAuth (upstream verification) β β
β β Credentials + Token plugins (DLAI API validation) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββ
βββββββββββββββββββββΌββββββββββββββββββββ
β β β
βββββββββββ ββββββββββββββββ βββββββββββ
β Next.js β βTanStack Startβ β Expo β
β (3000) β β (3002) β β Mobile β
βStatelessβ β Stateless β β β
βββββββββββ ββββββββββββββββ βββββββββββ
1. Auth Server Setup (apps/auth/)
Required Environment Variables
# Core
AUTH_URL=http://localhost:3001
AUTH_SECRET=<min 32 chars> # Encrypts tokens + signs cookies
POSTGRES_URL=postgresql://...
# Google OAuth
GOOGLE_CLIENT_ID=<from Google Console>
GOOGLE_CLIENT_SECRET=<from Google Console>
# LinkedIn OAuth (optional)
LINKEDIN_CLIENT_ID=<from LinkedIn>
LINKEDIN_CLIENT_SECRET=<from LinkedIn>
# Upstream API
DLAI_PYTHON_LEARN_API_URL=https://api.deeplearning.aiDatabase Tables
Run pnpm auth:generate to generate schema in packages/db/src/auth-schema.ts:
| Table | Purpose |
|---|---|
user |
User accounts + dlaiUserId, dlaiJwtToken (staging) |
session |
Sessions + dlaiJwtToken (authoritative, encrypted) |
account |
OAuth provider links (Google, LinkedIn tokens) |
verification |
Email verification tokens |
jwks |
JWT signing keys (auto-rotated) |
oauth_client |
Registered OAuth clients |
oauth_access_token |
Issued access tokens |
oauth_refresh_token |
Issued refresh tokens |
oauth_consent |
User consent records |
Better Auth Configuration
File: apps/auth/src/lib/auth.ts
betterAuth({
database: drizzleAdapter(db, { provider: "pg" }),
baseURL: env.AUTH_URL,
secret: env.AUTH_SECRET,
plugins: [
expo(), // React Native support
credentialsPlugin({...}), // Email/password β DLAI API
tokenPlugin({...}), // Existing token β DLAI API
linkedInProvider({...}), // LinkedIn legacy OAuth
customSession({...}), // Decrypt token in responses
jwt(), // JWT signing (required for OAuth Provider)
oauthProvider({...}), // OAuth 2.1 server
],
socialProviders: {
google: { clientId, clientSecret, prompt: "select_account" }
},
session: {
storeSessionInDatabase: true, // Required for OAuth Provider
additionalFields: { dlaiJwtToken: { type: "string" } }
},
databaseHooks: {
account: accountHooks, // OAuth token β DLAI API validation
session: sessionHooks, // Single session + token staging
}
})Plugin Reference
| Plugin | Endpoint | Purpose |
|---|---|---|
credentialsPlugin |
POST /api/auth/sign-in/credentials |
Email/password auth |
tokenPlugin |
POST /api/auth/sign-in/token |
Existing DLAI token auth |
oauthProvider |
Various OAuth 2.1 endpoints | Issues tokens to clients |
customSession |
Transforms GET /api/auth/get-session |
Decrypts dlaiJwtToken |
jwt |
Internal | Generates/rotates signing keys |
Token Storage Strategy
OAuth Flow (Google/LinkedIn):
1. account.create.after β Call DLAI API, get token
2. Store encrypted token on user.dlaiJwtToken (staging)
3. session.create.after β Copy to session.dlaiJwtToken
4. customSession β Decrypt for API responses
Credentials/Token Flow:
1. Plugin validates with DLAI API
2. Store encrypted token directly on session.dlaiJwtToken
Why two dlaiJwtToken fields?
- Better Auth creates sessions AFTER account hooks complete
user.dlaiJwtToken= temporary staging during OAuthsession.dlaiJwtToken= authoritative source (encrypted)
2. Client Integration (apps/nextjs/)
Required Environment Variables
# Server-side
AUTH_SECRET=<must match auth server>
DLAI_OAUTH_CLIENT_ID=<registered client ID>
DLAI_OAUTH_CLIENT_SECRET=<registered client secret>
# Client-side
NEXT_PUBLIC_AUTH_URL=http://localhost:3001
NEXT_PUBLIC_APP_URL=http://localhost:3000Register Client with Auth Server
Insert into oauth_client table:
INSERT INTO oauth_client (
id, client_id, client_secret, name,
redirect_uris, scopes, skip_consent, public
) VALUES (
gen_random_uuid(),
'nextjs-app',
'<generated-secret>',
'Next.js App',
ARRAY['http://localhost:3000/api/auth/callback/dlai'],
ARRAY['openid', 'profile', 'email', 'dlai'],
true, -- First-party app, no consent screen
false -- Confidential client (has secret)
);Better Auth Config (Stateless Mode)
File: apps/nextjs/src/lib/auth.ts
betterAuth({
baseURL: env.NEXT_PUBLIC_APP_URL,
secret: env.AUTH_SECRET,
trustedOrigins: [env.NEXT_PUBLIC_AUTH_URL],
plugins: [
genericOAuth({
config: [{
providerId: "dlai",
clientId: env.DLAI_OAUTH_CLIENT_ID,
clientSecret: env.DLAI_OAUTH_CLIENT_SECRET,
discoveryUrl: `${AUTH_URL}/.well-known/openid-configuration`,
scopes: ["openid", "profile", "email", "dlai"],
pkce: true,
authorizationUrlParams: { prompt: "login" },
async getUserInfo(tokens) {
const claims = decodeJwt(tokens.idToken);
pendingClaims.set(claims.sub, {
dlaiJwtToken: claims.dlaiJwtToken,
dlaiUserId: claims.dlaiUserId
});
return { id: claims.sub, email: claims.email, ... };
}
}]
})
],
hooks: {
after: createAuthMiddleware(async (ctx) => {
// On callback: store DLAI claims in cookies
const claims = pendingClaims.get(userId);
ctx.setCookie("dlai_jwt_token", claims.dlaiJwtToken);
ctx.setCookie("dlai_user_id", String(claims.dlaiUserId));
})
}
})Session Retrieval
File: apps/nextjs/src/auth/server.ts
export const getSession = cache(async () => {
const session = await betterAuth.api.getSession({ headers });
// Enrich with DLAI cookies (stateless mode)
const cookieStore = await cookies();
return {
user: {
...session.user,
dlaiJwtToken: cookieStore.get("dlai_jwt_token")?.value,
dlaiUserId: parseInt(cookieStore.get("dlai_user_id")?.value)
}
};
});3. Shared Packages
@dlai/auth (packages/auth/)
| Export | Purpose |
|---|---|
createUpstreamClient() |
DLAI API client wrapper |
credentialsPlugin, tokenPlugin |
Auth plugins for Better Auth |
createAccountHooks() |
OAuth token β DLAI validation hooks |
encrypt(), decrypt() |
AES-256-GCM token encryption |
findOrCreateUser() |
User management utility |
@dlai/db (packages/db/)
| Export | Purpose |
|---|---|
db |
Drizzle ORM client |
user, session, account, ... |
Auth schema tables |
4. Setup Checklist
New Auth Server
- Set
POSTGRES_URLand runpnpm db:push - Run
pnpm auth:generateto create schema - Configure
AUTH_SECRET(min 32 chars) - Configure
AUTH_URL - Set up Google OAuth in Google Console
- Redirect URI:
{AUTH_URL}/api/auth/callback/google
- Redirect URI:
- (Optional) Set up LinkedIn OAuth
- Redirect URI:
{AUTH_URL}/api/auth/oauth2/callback/linkedin
- Redirect URI:
New Client App
- Register OAuth client in
oauth_clienttable:client_id,client_secretredirect_uris:["{APP_URL}/api/auth/callback/dlai"]scopes:["openid", "profile", "email", "dlai"]skip_consent:true(first-party apps)
- Configure same
AUTH_SECRETas auth server - Set
DLAI_OAUTH_CLIENT_IDandDLAI_OAUTH_CLIENT_SECRET - Configure
discoveryUrlpointing to auth server - Implement DLAI claims extraction from
id_token - Store claims in cookies for server component access
5. Auth Flow Diagrams
OAuth Flow (Google/LinkedIn)
User clicks "Sign in"
β
Auth Server redirects to Google/LinkedIn
β
User authenticates with provider
β
Provider redirects back with code
β
Auth Server exchanges code for tokens
β
Auth Server calls DLAI API to validate
β
Auth Server creates user + session
β
Auth Server issues JWT with dlaiJwtToken claim
β
Client receives JWT, decodes claims
β
Client stores DLAI claims in cookies
β
β User authenticated
Credentials Flow
User submits email/password
β
POST /api/auth/sign-in/credentials
β
Auth Server calls DLAI API (loginByPassword)
β
Auth Server creates user + session
β
Auth Server returns { user, session, dlaiJwtToken }
β
β User authenticated
Token Flow
User has existing DLAI JWT
β
POST /api/auth/sign-in/token
β
Auth Server calls DLAI API (getProfile)
β
Auth Server creates user + session
β
Auth Server returns { user, session, dlaiJwtToken }
β
β User authenticated
6. Key Files Reference
| File | Purpose |
|---|---|
apps/auth/src/lib/auth.ts |
Auth server Better Auth config |
apps/auth/src/lib/linkedin-provider.ts |
LinkedIn legacy OAuth |
apps/auth/src/lib/trusted-origins.ts |
CORS allowed origins |
apps/nextjs/src/lib/auth.ts |
Client Better Auth config |
apps/nextjs/src/auth/server.ts |
Session retrieval helpers |
apps/nextjs/src/lib/cookies.ts |
Cookie configuration |
packages/auth/src/plugins/dlai-auth.ts |
Credentials + Token plugins |
packages/auth/src/auth-service.ts |
User management + OAuth hooks |
packages/auth/src/crypto.ts |
AES-256-GCM encryption |
packages/db/src/auth-schema.ts |
Generated database schema |
7. Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| "Invalid credentials" | DLAI API call fails | Check DLAI_PYTHON_LEARN_API_URL |
| CORS error | Origin not trusted | Add to trustedOrigins or TRUSTED_ORIGINS env |
Missing dlaiJwtToken |
Not in id_token claims | Verify customIdTokenClaims in oauthProvider |
| Multiple sessions | Hook not working | Check session.create.before deletes old sessions |
| "No id_token" | OIDC misconfiguration | Verify discoveryUrl returns valid metadata |
| Cookie not set | SameSite/Secure mismatch | Check cookieOptions match environment |
Related Documentation
- apps/auth/README.md - Detailed auth server docs with Mermaid diagrams
- docs/oauth-client-management.md - OAuth client registration
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels