$npx -y skills add mjunaidca/mjs-agent-skills --skill better-auth-setupGuide implementation of OAuth 2.1 / OIDC authentication using Better Auth with the OIDC Provider plugin. Use this skill when setting up centralized authentication for multiple apps, implementing SSO across a platform, creating an OAuth authorization server, or integrating Better
| 1 | # Better Auth OAuth/OIDC Setup Skill |
| 2 | |
| 3 | ## Purpose |
| 4 | Guide implementation of OAuth 2.1 / OIDC authentication using Better Auth with the OIDC Provider plugin. |
| 5 | |
| 6 | ## When to Use |
| 7 | - Setting up centralized authentication for multiple apps |
| 8 | - Implementing SSO (Single Sign-On) across a platform |
| 9 | - Creating an OAuth authorization server |
| 10 | - Integrating Better Auth as an identity provider |
| 11 | |
| 12 | ## Key Questions to Ask |
| 13 | |
| 14 | 1. **Architecture** |
| 15 | - How many apps will use this auth server? |
| 16 | - Is this for first-party apps only or third-party OAuth clients too? |
| 17 | - Do you need dynamic client registration? |
| 18 | |
| 19 | 2. **Database** |
| 20 | - Which database? (Postgres recommended with Neon for serverless) |
| 21 | - Need user profiles beyond core auth fields? |
| 22 | |
| 23 | 3. **Features** |
| 24 | - Role-based access control needed? |
| 25 | - Admin dashboard for user management? |
| 26 | - Consent screen for third-party apps? |
| 27 | |
| 28 | ## Implementation Checklist |
| 29 | |
| 30 | ### 1. Auth Server Setup (Public Client with PKCE) |
| 31 | |
| 32 | ```typescript |
| 33 | // src/lib/auth.ts |
| 34 | import { betterAuth } from "better-auth"; |
| 35 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; |
| 36 | import { oidcProvider } from "better-auth/plugins/oidc-provider"; |
| 37 | import { admin } from "better-auth/plugins/admin"; |
| 38 | |
| 39 | export const auth = betterAuth({ |
| 40 | database: drizzleAdapter(db, { provider: "pg", schema }), |
| 41 | |
| 42 | emailAndPassword: { enabled: true }, |
| 43 | |
| 44 | session: { |
| 45 | expiresIn: 60 * 60 * 24 * 7, // 7 days |
| 46 | updateAge: 60 * 60 * 24, // Refresh daily |
| 47 | }, |
| 48 | |
| 49 | trustedOrigins: process.env.ALLOWED_ORIGINS?.split(","), |
| 50 | |
| 51 | plugins: [ |
| 52 | oidcProvider({ |
| 53 | loginPage: "/auth/sign-in", |
| 54 | consentPage: "/auth/consent", |
| 55 | trustedClients: [{ |
| 56 | clientId: "your-app", |
| 57 | // No clientSecret for public clients - use PKCE instead |
| 58 | type: "public", // Public client for SPAs |
| 59 | redirectUrls: ["http://localhost:3000/auth/callback"], // Note: lowercase 'urls' |
| 60 | skipConsent: true, // First-party apps |
| 61 | }], |
| 62 | // Add custom claims to userinfo |
| 63 | async getAdditionalUserInfoClaim(user) { |
| 64 | return { role: user.role }; |
| 65 | }, |
| 66 | }), |
| 67 | admin({ |
| 68 | defaultRole: "user", |
| 69 | adminRoles: ["admin"], |
| 70 | }), |
| 71 | ], |
| 72 | }); |
| 73 | ``` |
| 74 | |
| 75 | ### 2. OAuth Client with PKCE (Recommended for SPAs) |
| 76 | |
| 77 | ```typescript |
| 78 | // Client app: src/lib/auth-client.ts |
| 79 | |
| 80 | // PKCE helpers |
| 81 | function generateCodeVerifier(): string { |
| 82 | const array = new Uint8Array(32); |
| 83 | crypto.getRandomValues(array); |
| 84 | return base64UrlEncode(array); |
| 85 | } |
| 86 | |
| 87 | async function generateCodeChallenge(verifier: string): Promise<string> { |
| 88 | const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)); |
| 89 | return base64UrlEncode(new Uint8Array(hash)); |
| 90 | } |
| 91 | |
| 92 | // Authorization URL with PKCE |
| 93 | export async function getOAuthAuthorizationUrl(state: string) { |
| 94 | const codeVerifier = generateCodeVerifier(); |
| 95 | const codeChallenge = await generateCodeChallenge(codeVerifier); |
| 96 | |
| 97 | // Store verifier for token exchange |
| 98 | sessionStorage.setItem('pkce_code_verifier', codeVerifier); |
| 99 | |
| 100 | const params = new URLSearchParams({ |
| 101 | client_id: 'your-app', |
| 102 | redirect_uri: 'http://localhost:3000/auth/callback', |
| 103 | response_type: 'code', |
| 104 | scope: 'openid profile email', |
| 105 | state, |
| 106 | code_challenge: codeChallenge, |
| 107 | code_challenge_method: 'S256', |
| 108 | }); |
| 109 | return `${AUTH_SERVER_URL}/api/auth/oauth2/authorize?${params}`; |
| 110 | } |
| 111 | |
| 112 | // Callback: exchange code for tokens with PKCE (no client_secret!) |
| 113 | const codeVerifier = sessionStorage.getItem('pkce_code_verifier'); |
| 114 | const tokenResponse = await fetch(`${AUTH_SERVER_URL}/api/auth/oauth2/token`, { |
| 115 | method: 'POST', |
| 116 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, |
| 117 | body: new URLSearchParams({ |
| 118 | grant_type: 'authorization_code', |
| 119 | code, |
| 120 | redirect_uri: 'http://localhost:3000/auth/callback', |
| 121 | client_id: 'your-app', |
| 122 | code_verifier: codeVerifier, // PKCE: verifier instead of secret |
| 123 | }), |
| 124 | }); |
| 125 | sessionStorage.removeItem('pkce_code_verifier'); |
| 126 | ``` |
| 127 | |
| 128 | ### 3. Session Management (Client) |
| 129 | |
| 130 | ```typescript |
| 131 | // AuthContext.tsx pattern |
| 132 | const checkSession = async () => { |
| 133 | const accessToken = localStorage.getItem('access_token'); |
| 134 | if (accessToken) { |
| 135 | const response = await fetch(`${AUTH_URL}/api/auth/oauth2/userinfo`, { |
| 136 | headers: { Authorization: `Bearer ${accessToken}` }, |
| 137 | }); |
| 138 | if (response.ok) { |
| 139 | setSession({ user: await response.json() }); |
| 140 | } else { |
| 141 | localStorage.removeItem('access_token'); |
| 142 | } |
| 143 | } |
| 144 | }; |
| 145 | |
| 146 | const signOut = () => { |
| 147 | localStorage.removeItem('access_token'); |
| 148 | localStorage.removeItem('refresh_to |