Skip to main content

Overview

The platform uses Google OAuth via Supabase Auth for admin dashboard access. Authentication is domain-restricted to @nj.sgadi.us email addresses for security.

Features

  • Google OAuth Sign-In - One-click authentication via Google accounts
  • Domain Restriction - Only @nj.sgadi.us emails can access admin features
  • Session Management - Cookie-based sessions with automatic refresh
  • Redirect Preservation - Returns users to intended page after auth
  • Row Level Security - Database-level access control tied to user email

Setup

Prerequisites

1

Supabase Project

Create a project at supabase.com and note your project URL.
2

Google Cloud Project

Create a project at Google Cloud Console.
3

Environment Variables

Add to .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...

Google OAuth Configuration

1

Create OAuth Credentials

  1. Go to Google Cloud Console → APIs & ServicesCredentials
  2. Click Create CredentialsOAuth client ID
  3. Choose Web application
2

Configure Authorized Origins

Add these JavaScript origins:
http://localhost:3000
https://njrajatmahotsav.com
3

Configure Redirect URIs

Add these redirect URIs:
https://your-project.supabase.co/auth/v1/callback
http://localhost:3000/auth/callback
https://njrajatmahotsav.com/auth/callback
4

Copy Client ID and Secret

Save the Client ID and Client Secret for the next step.

Supabase Configuration

1

Enable Google Provider

  1. Go to Supabase Dashboard → AuthenticationProviders
  2. Find Google and toggle it on
  3. Paste your Client ID and Client Secret
  4. Save changes
2

Configure Redirect URLs

  1. Go to AuthenticationURL Configuration
  2. Add these to Redirect URLs:
    http://localhost:3000/auth/callback
    https://njrajatmahotsav.com/auth/callback
    
  3. Set Site URL to https://njrajatmahotsav.com

Authentication Flow

Sign-In Component

The admin sign-in button triggers the Google OAuth flow:
"use client"

import { useState } from "react"
import { supabase } from "@/utils/supabase/client"
import { Button } from "@/components/atoms/button"
import { Loader2 } from "lucide-react"

export function AdminSignIn() {
  const [loading, setLoading] = useState(false)

  const handleGoogleSignIn = async () => {
    setLoading(true)
    try {
      const nextPath = "/admin/registrations"
      const cookieParts = [
        `rm-auth-next=${encodeURIComponent(nextPath)}`,
        "Path=/",
        "Max-Age=600",
        "SameSite=Lax",
      ]
      // Ensure the cookie survives redirects between www and apex domain
      const host = window.location.hostname
      if (host.endsWith("njrajatmahotsav.com")) {
        cookieParts.push("Domain=.njrajatmahotsav.com")
      }
      if (window.location.protocol === "https:") cookieParts.push("Secure")
      document.cookie = cookieParts.join("; ")

      const { error } = await supabase.auth.signInWithOAuth({
        provider: "google",
        options: {
          redirectTo: `${window.location.origin}/auth/callback`,
        },
      })
      if (error) throw error
    } catch (err) {
      console.error("Sign-in error:", err)
      setLoading(false)
    }
  }

  return (
    <div className="flex flex-col items-center gap-6">
      <Button
        onClick={handleGoogleSignIn}
        disabled={loading}
        className="inline-flex items-center gap-2 rounded-full px-8 py-4"
      >
        {loading ? (
          <Loader2 className="size-5 animate-spin" />
        ) : (
          <svg className="size-5" viewBox="0 0 24 24">
            {/* Google icon SVG paths */}
          </svg>
        )}
        {loading ? "Signing in…" : "Sign in with Google"}
      </Button>
      <p className="text-sm text-gray-600 max-w-xs text-center">
        Access restricted to @nj.sgadi.us accounts
      </p>
    </div>
  )
}
The component stores the intended redirect path (/admin/registrations) in a cookie to preserve it through the OAuth flow, which may drop query parameters.

Callback Handler

The callback route exchanges the OAuth code for a session:
import { NextResponse } from "next/server"
import { cookies } from "next/headers"
import { createClient } from "@/utils/supabase/server"

/**
 * OAuth callback route. Exchanges auth code for session and redirects.
 */
export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get("code")
  const queryNext = searchParams.get("next")
  let next = queryNext ?? "/"
  
  // Check cookie for redirect path
  if (!queryNext) {
    const cookieStore = await cookies()
    const cookieNext = cookieStore.get("rm-auth-next")?.value
    if (cookieNext) next = decodeURIComponent(cookieNext)
  }
  
  // Validate redirect path (prevent open redirects)
  if (!next.startsWith("/") || next.includes("//") || next.includes(":")) {
    next = "/"
  }

  if (!code) {
    return NextResponse.redirect(`${origin}/auth/auth-code-error`)
  }

  const supabase = await createClient()
  const { error } = await supabase.auth.exchangeCodeForSession(code)

  if (error) {
    console.error("[auth/callback] exchangeCodeForSession error:", error.message)
    return NextResponse.redirect(`${origin}/auth/auth-code-error`)
  }

  const forwardedHost = request.headers.get("x-forwarded-host")
  const requestHost = request.headers.get("host")
  const effectiveHost = forwardedHost ?? requestHost ?? new URL(request.url).host
  const isLocalEnv = process.env.NODE_ENV === "development"

  const redirectUrl = isLocalEnv
    ? `${origin}${next}`
    : forwardedHost
      ? `https://${forwardedHost}${next}`
      : `${origin}${next}`

  const response = NextResponse.redirect(redirectUrl)
  
  // Clear the redirect cookie
  const clearCookieOptions: Parameters<typeof response.cookies.set>[2] = {
    path: "/",
    maxAge: 0,
  }
  if (effectiveHost.endsWith("njrajatmahotsav.com")) {
    clearCookieOptions.domain = ".njrajatmahotsav.com"
  }
  response.cookies.set("rm-auth-next", "", clearCookieOptions)
  
  return response
}
The callback route validates redirect paths to prevent open redirect attacks. Only relative paths starting with / are allowed.

Domain Restriction

Admin Auth Helper

Domain checking is centralized in lib/admin-auth.ts:
const ALLOWED_DOMAIN = "@nj.sgadi.us"

/**
 * Checks if the given email is allowed for admin access.
 * @param email - User email (e.g. from session.user.email)
 * @returns true if email ends with @nj.sgadi.us (case-insensitive)
 */
export function isAllowedAdminDomain(email: string | null | undefined): boolean {
  if (!email || typeof email !== "string") return false
  return email.toLowerCase().endsWith(ALLOWED_DOMAIN.toLowerCase())
}

/**
 * Checks if the given user has an allowed admin domain email.
 * @param user - User object with optional email (e.g. from session.user)
 * @returns true if user exists and email ends with @nj.sgadi.us
 */
export function isAdminDomainUser(user: { email?: string | null } | null | undefined): boolean {
  return isAllowedAdminDomain(user?.email)
}

Server Component Protection

import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
import { isAdminDomainUser } from '@/lib/admin-auth'

export default async function AdminPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    redirect('/admin/login')
  }

  if (!isAdminDomainUser(user)) {
    redirect('/admin/unauthorized')
  }

  return (
    <div>
      <h1>Welcome, {user.email}</h1>
      {/* Admin dashboard content */}
    </div>
  )
}

Session Management

Middleware Auto-Refresh

The middleware automatically refreshes sessions on every request:
middleware.ts
import { updateSession } from "@/utils/supabase/middleware"

export async function middleware(request: NextRequest) {
  return await updateSession(request)
}
See the Supabase Integration page for the full implementation in utils/supabase/middleware.ts:10.

Manual Sign Out

import { supabase } from '@/utils/supabase/client'

async function handleSignOut() {
  const { error } = await supabase.auth.signOut()
  if (error) {
    console.error('Sign out error:', error)
  } else {
    window.location.href = '/'
  }
}

Security Best Practices

1

Always Verify Domain Server-Side

Never trust client-side checks alone. Always verify isAdminDomainUser() in API routes and Server Components.
2

Use Row Level Security

Enforce database-level access control with RLS policies that check auth.email().
CREATE POLICY "admin_domain_select"
  ON public.registrations
  FOR SELECT
  TO authenticated
  USING ((select auth.email())::text ilike '%@nj.sgadi.us');
3

Validate Redirect URLs

Prevent open redirect attacks by validating all redirect paths:
if (!path.startsWith('/') || path.includes('//') || path.includes(':')) {
  path = '/'
}
4

Set Secure Cookies

Use Secure, HttpOnly, and SameSite=Lax flags:
document.cookie = [
  `name=value`,
  `Path=/`,
  `SameSite=Lax`,
  `Secure`, // HTTPS only
].join('; ')

Troubleshooting

Common Issues

“redirect_uri_mismatch” error:
  • Verify the callback URL in Google Console exactly matches your Supabase redirect URL
  • Check both local (http://localhost:3000/auth/callback) and production URLs
  • Ensure no trailing slashes or query parameters
Session not persisting:
  • Check that middleware is running (see middleware.ts:4)
  • Verify cookies are not blocked (check DevTools → Application → Cookies)
  • Look for sb-*-auth-token cookies
“Invalid OAuth client” error:
  • Confirm Client ID and Secret in Supabase match Google Console values
  • Check that the Google OAuth consent screen is configured
User redirected to / instead of intended page:
  • Check that the rm-auth-next cookie is being set before auth
  • Verify cookie domain settings for production (.njrajatmahotsav.com)
  • Ensure cookie is not expired (10-minute TTL)

Debug Checklist

// Check if user is authenticated
const { data: { user } } = await supabase.auth.getUser()
console.log('Current user:', user?.email)

// Check domain restriction
import { isAdminDomainUser } from '@/lib/admin-auth'
console.log('Is admin?', isAdminDomainUser(user))

// Check session in cookies
// DevTools → Application → Cookies → look for:
// - sb-{project-ref}-auth-token
// - sb-{project-ref}-auth-token-code-verifier

Resources

Supabase OAuth Setup Guide

Complete Supabase configuration for Google OAuth

Google OAuth Documentation

Official Google OAuth 2.0 reference

Supabase Auth Documentation

Supabase authentication guides and API reference

Build docs developers (and LLMs) love