Overview
Arraf Auth implements OAuth 2.0 with PKCE (Proof Key for Code Exchange) for enhanced security. The OAuth flow:- User clicks “Sign in with Google”
- Redirect to provider’s authorization page
- User grants permission
- Provider redirects back with authorization code
- Exchange code for access token
- Fetch user profile
- Create/login user
Supported Providers
Arraf Auth supports any OAuth 2.0 provider. Built-in examples include:- GitHub
- Microsoft
- Apple
- Custom OAuth providers
Setup
Google
- Go to Google Cloud Console
- Create a new project or select existing
- Navigate to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Select Web application
- Add authorized redirect URI:
- Save the Client ID and Client Secret
GitHub
- Go to GitHub Developer Settings
- Click New OAuth App
- Fill in application details:
- Application name: Your App Name
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
- Click Register application
- Save the Client ID and generate a Client Secret
# .env.local
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
import { createAuth, OAuthProvider } from '@arraf-auth/core'
import { buildAuthorizationUrl, exchangeCodeForTokens } from '@arraf-auth/core'
// Google OAuth Provider
const googleProvider: OAuthProvider = {
id: 'google',
name: 'Google',
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scopes: ['openid', 'email', 'profile'],
getAuthorizationUrl(state, codeChallenge) {
return buildAuthorizationUrl('https://accounts.google.com/o/oauth2/v2/auth', {
client_id: this.clientId,
redirect_uri: `${process.env.NEXT_PUBLIC_URL}/api/auth/callback/google`,
response_type: 'code',
scope: this.scopes.join(' '),
state,
code_challenge: codeChallenge!,
code_challenge_method: 'S256',
access_type: 'offline',
prompt: 'consent'
})
},
async exchangeCode(code, codeVerifier) {
const response = await exchangeCodeForTokens(
'https://oauth2.googleapis.com/token',
{
client_id: this.clientId,
client_secret: this.clientSecret,
code,
code_verifier: codeVerifier!,
grant_type: 'authorization_code',
redirect_uri: `${process.env.NEXT_PUBLIC_URL}/api/auth/callback/google`
}
)
const data = await response.json()
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
tokenType: data.token_type
}
},
async getUserProfile(accessToken) {
const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${accessToken}` }
})
const data = await response.json()
return {
id: data.id,
email: data.email,
name: data.name,
image: data.picture,
emailVerified: data.verified_email
}
}
}
// GitHub OAuth Provider
const githubProvider: OAuthProvider = {
id: 'github',
name: 'GitHub',
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scopes: ['user:email'],
getAuthorizationUrl(state) {
return buildAuthorizationUrl('https://github.com/login/oauth/authorize', {
client_id: this.clientId,
redirect_uri: `${process.env.NEXT_PUBLIC_URL}/api/auth/callback/github`,
scope: this.scopes.join(' '),
state,
allow_signup: 'true'
})
},
async exchangeCode(code) {
const response = await exchangeCodeForTokens(
'https://github.com/login/oauth/access_token',
{
client_id: this.clientId,
client_secret: this.clientSecret,
code
}
)
const data = await response.json()
return {
accessToken: data.access_token,
tokenType: data.token_type,
refreshToken: data.refresh_token,
expiresIn: data.expires_in
}
},
async getUserProfile(accessToken) {
const [userRes, emailsRes] = await Promise.all([
fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${accessToken}` }
}),
fetch('https://api.github.com/user/emails', {
headers: { Authorization: `Bearer ${accessToken}` }
})
])
const user = await userRes.json()
const emails = await emailsRes.json()
const primaryEmail = emails.find((e: any) => e.primary)
return {
id: String(user.id),
email: primaryEmail?.email || user.email,
name: user.name || user.login,
image: user.avatar_url,
emailVerified: primaryEmail?.verified || false
}
}
}
import { createAuth } from '@arraf-auth/core'
import { prismaAdapter } from '@arraf-auth/adapter-prisma'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const auth = createAuth({
secret: process.env.AUTH_SECRET!,
database: prismaAdapter(prisma),
providers: [googleProvider, githubProvider],
trustedOrigins: [process.env.NEXT_PUBLIC_URL!]
})
export { auth }
// app/api/auth/[provider]/route.ts
import { auth } from '@/lib/auth'
export async function GET(
req: Request,
{ params }: { params: { provider: string } }
) {
return auth.handler(`/auth/${params.provider}`, req)
}
// app/api/auth/callback/[provider]/route.ts
export async function GET(
req: Request,
{ params }: { params: { provider: string } }
) {
return auth.handler(`/auth/callback/${params.provider}`, req)
}
OAuth Flow
Starting OAuth Flow
Redirect users to the provider’s authorization page:OAuth Callback
After the user grants permission, the provider redirects to your callback URL with:code: Authorization codestate: State token (for CSRF protection)
oauth-callback.ts:15-131):
- Validates the state token
- Exchanges code for access token using PKCE verifier
- Fetches user profile
- Creates or finds user by email
- Creates account record
- Creates session
- Redirects to application
The OAuth flow uses PKCE (Proof Key for Code Exchange) to prevent authorization code interception attacks. The code verifier and challenge are generated in
oauth.ts:1-25.Account Linking
When a user signs in with OAuth, the system:- Looks up user by email (from OAuth profile)
- If user exists, links the OAuth account
- If user doesn’t exist, creates new user
Client Implementation
Example sign-in buttons:Custom Redirect
Redirect users to a specific page after OAuth:oauth-callback.ts:121).
Token Refresh
If your provider supports refresh tokens, you can implement token refresh:Security Considerations
oauth-start.ts:22-27)// Code verifier: Random string
const codeVerifier = await generateCodeVerifier()
// Code challenge: SHA-256 hash of verifier
const codeChallenge = await generateCodeChallenge(codeVerifier)
// Send challenge in authorization URL
// Send verifier when exchanging code
// Google returns verified_email
emailVerified: profile.verified_email
// GitHub email verification status
emailVerified: primaryEmail?.verified || false