Google OAuth authenticator for Kenmon
Google OAuth authenticator for Kenmon authentication system.
``bash`
npm install @kenmon/google-oauth-authenticator kenmon
1. Go to Google Cloud Console
2. Create a new project or select an existing one
3. Enable the Google+ API
4. Go to Credentials → Create Credentials → OAuth 2.0 Client ID
5. Set up OAuth consent screen
6. Add authorized redirect URIs (e.g., http://localhost:3000/auth/callback/google)
7. Copy your Client ID and Client Secret
`env`
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback/google
SESSION_SECRET=your-session-secret-for-jwt-signing
`typescript
import { KenmonGoogleOAuthAuthenticator } from '@kenmon/google-oauth-authenticator'
export const googleAuth = new KenmonGoogleOAuthAuthenticator({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI!,
secret: process.env.SESSION_SECRET!, // For signing state tokens
scopes: ['openid', 'email', 'profile'], // Optional, these are defaults
})
`
Choose the intent based on the user's action:
`typescript
// For sign-in
const authUrl = googleAuth.getAuthUrl('sign-in')
redirect(authUrl)
// For sign-up
const authUrl = googleAuth.getAuthUrl('sign-up')
redirect(authUrl)
`
The callback route handles both sign-in and sign-up based on the intent:
`typescript
// In your OAuth callback route (e.g., /auth/callback/google)
export async function GET(request: Request) {
const url = new URL(request.url)
const code = url.searchParams.get('code')
const state = url.searchParams.get('state')
if (!code || !state) {
return redirect('/signin?error=Missing parameters')
}
// Verify callback and get intent and identifier
const result = await googleAuth.verifyCallback(code, state)
if (!result.success) {
console.error(result.error)
return redirect(/signin?error=${encodeURIComponent(result.error.message)})
}
const { intent, identifier } = result.data
// Get request metadata
const ipAddress = request.headers.get('x-forwarded-for') || undefined
const userAgent = request.headers.get('user-agent') || undefined
// Sign in or sign up based on intent
if (intent === 'sign-in') {
const signInResult = await auth.signIn(identifier, { ipAddress, userAgent })
if (!signInResult.success) {
return redirect(/signin?error=${encodeURIComponent(signInResult.error.message)})/signup?error=${encodeURIComponent(signUpResult.error.message)}
}
} else {
const signUpResult = await auth.signUp(identifier, {}, { ipAddress, userAgent })
if (!signUpResult.success) {
return redirect()
}
}
return redirect('/')
}
`
The authenticator exports typed interfaces for better IDE support:
`typescript
import type { KenmonGoogleOAuthIdentifier } from '@kenmon/google-oauth-authenticator'
// The identifier returned from verifyCallback has this shape:
interface KenmonGoogleOAuthIdentifier {
type: 'google-oauth'
value: string // Google user ID
data: {
googleId: string // Unique Google user ID
email: string // User's email
emailVerified: boolean // Is email verified by Google?
name: string // Full name
givenName: string // First name
familyName: string // Last name
picture: string // Profile picture URL
locale: string // User's locale (e.g., "en")
}
}
`
- State Token: Uses JWT-signed state tokens for CSRF protection
- Expiration: State tokens expire in 10 minutes
- Nonce: Each state token includes a random nonce
- Stateless: No database required for OAuth flow
The authenticator can return these error reasons:
- invalid-state - JWT state verification failedexpired-state
- - State token expiredinvalid-code
- - OAuth authorization code is invalid or expiredtoken-exchange-failed
- - Failed to exchange code for tokensprofile-fetch-failed
- - Failed to fetch user profile
`typescript
// app/auth/google/route.ts
import { googleAuth } from '@/lib/auth/authenticators/google'
export async function GET() {
const authUrl = googleAuth.getAuthUrl('sign-in')
return Response.redirect(authUrl)
}
`
`typescript
// app/auth/callback/google/route.ts
import { googleAuth } from '@/lib/auth/authenticators/google'
import { auth } from '@/lib/auth/auth'
export async function GET(request: Request) {
const url = new URL(request.url)
const code = url.searchParams.get('code')
const state = url.searchParams.get('state')
if (!code || !state) {
return Response.redirect('/signin?error=Missing parameters')
}
const result = await googleAuth.verifyCallback(code, state)
if (!result.success) {
return Response.redirect(/signin?error=${encodeURIComponent(result.error.message)})
}
const { intent, identifier } = result.data
const ipAddress = request.headers.get('x-forwarded-for') || undefined
const userAgent = request.headers.get('user-agent') || undefined
if (intent === 'sign-in') {
await auth.signIn(identifier, { ipAddress, userAgent })
} else {
await auth.signUp(identifier, {}, { ipAddress, userAgent })
}
return Response.redirect('/')
}
``
MIT