Skip to main content

Overview

The platform uses Google OAuth 2.0 for authentication with extended permissions to access Google Calendar Events. This enables integration with Google Calendar for scheduling virtual classes.

OAuth Configuration

The Google provider is configured in src/auth.ts:16 with custom authorization parameters:
Google({
  clientId: process.env.AUTH_GOOGLE_ID,
  clientSecret: process.env.AUTH_GOOGLE_SECRET,
  authorization: {
    params: {
      scope: "openid email profile https://www.googleapis.com/auth/calendar.events",
      access_type: "offline",
    }
  }
})

OAuth Scopes

The following OAuth 2.0 scopes are requested during authentication:
openid
string
required
Required for OpenID Connect authentication
email
string
required
Access to user’s email address
profile
string
required
Access to user’s basic profile information (name, picture)
https://www.googleapis.com/auth/calendar.events
string
required
Full access to Google Calendar events - allows creating, reading, updating, and deleting calendar events

Authorization Parameters

access_type
string
default:"offline"
Request offline access to receive a refresh token. This allows the application to access Google APIs when the user is not present.
prompt
string
default:"consent"
Force consent screen to appear (commented out in production). When enabled, users see the consent screen every time.

Environment Setup

Required Variables

AUTH_GOOGLE_ID
string
required
Your Google OAuth 2.0 Client ID obtained from Google Cloud Console
AUTH_GOOGLE_SECRET
string
required
Your Google OAuth 2.0 Client Secret obtained from Google Cloud Console

Obtaining Credentials

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Enable the Google Calendar API
  4. Navigate to APIs & Services > Credentials
  5. Click Create Credentials > OAuth 2.0 Client ID
  6. Configure the OAuth consent screen
  7. Add authorized redirect URIs:
    • http://localhost:3000/api/auth/callback/google (development)
    • https://yourdomain.com/api/auth/callback/google (production)
  8. Copy the Client ID and Client Secret to your .env file

Sign In Flow

Initiate Sign In

import { signIn } from '@/auth'

export default async function SignInPage() {
  return (
    <form
      action={async () => {
        'use server'
        await signIn('google')
      }}
    >
      <button type="submit">Sign in with Google</button>
    </form>
  )
}

Callback Handling

When Google redirects back to the application, the signIn callback is triggered (src/auth.ts:35):
async signIn({ user, account, profile }) {
  try {
    const { name, email, image } = user as User

    console.log('Trying to sign in user: ', email);

    // Check if user is admin
    const userFound = await loggedAsAdmin(email)

    // Create new user if doesn't exist
    if (!userFound) {
      await createUser(name, email, image)
      return true
    }

    return true
  } catch (error) {
    console.error("We found the following error: ", error)
    return false
  }
}

Callback Parameters

user
object
User information from Google OAuth
account
object
OAuth account information
profile
object
Full OAuth profile from Google

Access Token Handling

The jwt callback processes the OAuth tokens and stores them in the JWT (src/auth.ts:55):
async jwt({ token, account, profile }) {
  if (account && profile) {
    const user = await db.user.findUnique({
      where: { email: profile.email! },
      select: { id: true, totalClasses: true }
    })
    
    if (user) {
      token.id = user?.id
      token.iat = Math.floor(Date.now() / 1000)
      token.exp = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 // 30 days
      token.accessToken = account.access_token
      
      // Store refresh token for admin users
      if (profile.email === process.env.ADMIN_EMAIL! && account.refresh_token) {
        token.refreshToken = account.refresh_token
        await updateRefreshTokenInDb(user.id, token.refreshToken as string)
      }
    }
  }

  return token;
}

Token Structure

id
string
User’s database ID
iat
number
Issued at timestamp (Unix timestamp)
exp
number
Expiration timestamp (Unix timestamp) - 30 days from issuance
accessToken
string
Google OAuth access token for API calls
refreshToken
string
Google OAuth refresh token (admin users only)

Refresh Token Management

Refresh tokens are only stored for admin users and persisted to the database:
1

Check Admin Status

Verify if the user’s email matches ADMIN_EMAIL environment variable
2

Extract Refresh Token

Get the refresh token from the account object (only available with access_type: "offline")
3

Store in JWT

Add refresh token to the JWT token object
4

Persist to Database

Call updateRefreshTokenInDb() to store the refresh token in the database

Using the Access Token

Accessing Google Calendar API

import { auth } from '@/auth'

export async function createCalendarEvent() {
  const session = await auth()
  
  if (!session?.user?.accessToken) {
    throw new Error('Not authenticated')
  }

  const response = await fetch(
    'https://www.googleapis.com/calendar/v3/calendars/primary/events',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${session.user.accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        summary: 'Virtual Class',
        start: {
          dateTime: '2024-03-15T10:00:00-07:00',
          timeZone: 'America/Los_Angeles',
        },
        end: {
          dateTime: '2024-03-15T12:00:00-07:00',
          timeZone: 'America/Los_Angeles',
        },
      }),
    }
  )

  return response.json()
}

Error Handling

The sign-in callback includes error handling (src/auth.ts:50):
try {
  // Sign in logic
  return true
} catch (error) {
  console.error("We found the following error: ", error)
  return false
}
Returning false from the callback will:
  • Prevent the user from signing in
  • Redirect to the error page (configured as /)
  • Not create a session

Security Considerations

Access tokens are stored in the encrypted JWT and never exposed to the client-side code. They’re only available in server components and API routes.
Refresh tokens are only stored for admin users to minimize security exposure. Regular users must re-authenticate after token expiration.
The access_type: "offline" parameter ensures the application can access Google APIs even when the user is not actively using the application.
Only request the minimum scopes necessary. The current implementation requests Calendar Events access for class scheduling functionality.

Testing

Check OAuth Configuration

# Get available providers
curl http://localhost:3000/api/auth/providers | jq

# Expected response
{
  "google": {
    "id": "google",
    "name": "Google",
    "type": "oauth",
    "signinUrl": "http://localhost:3000/api/auth/signin/google",
    "callbackUrl": "http://localhost:3000/api/auth/callback/google"
  }
}

Verify Session After Sign In

# Get current session
curl http://localhost:3000/api/auth/session \
  -H "Cookie: next-auth.session-token=YOUR_SESSION_TOKEN"

Common Issues

Solution: Ensure access_type: "offline" is set and the user consents to offline access. Google only provides refresh tokens on the first authorization or when forcing consent with prompt: "consent".
Solution: Verify the redirect URI in Google Cloud Console matches exactly with your callback URL, including protocol (http/https) and port.
Solution: Ensure the Google Calendar API is enabled in your Google Cloud Console project and the correct scope is requested.

Next Steps

Session Management

Learn about JWT tokens and session handling

Authentication Overview

Back to authentication overview

Build docs developers (and LLMs) love