Skip to main content

Overview

Studley AI uses a hybrid authentication system combining:
  • Supabase Auth - OAuth providers (Google, GitHub, etc.)
  • Custom JWT Sessions - Credential-based authentication
  • Clever SSO - Education platform integration
This guide covers setup for all authentication methods.

Architecture

Studley AI implements a dual authentication system:

Prerequisites

Supabase Project

Create a project at supabase.com

Database Setup

Supabase Authentication

Initial Configuration

1

Get Supabase Credentials

From your Supabase Dashboard → Settings → API:
NEXT_PUBLIC_SUPABASE_URL="https://xxxxx.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
2

Configure Auth Settings

In Supabase Dashboard → Authentication → Settings:Site URL:
http://localhost:3000  # Development
https://yourdomain.com # Production
Redirect URLs:
http://localhost:3000/**
https://yourdomain.com/**
3

Set Up User Profiles

Run the user profiles migration:
scripts/010_create_user_profiles.sql
CREATE TABLE IF NOT EXISTS public.user_profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  email TEXT UNIQUE NOT NULL,
  name TEXT,
  profile_picture_url TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Auto-create profile on signup
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.user_profiles (id, email, name)
  VALUES (NEW.id, NEW.email, COALESCE(NEW.raw_user_meta_data->>'full_name', ''));
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

OAuth Providers

1

Create Google OAuth App

  1. Go to Google Cloud Console
  2. Create new project or select existing
  3. Navigate to APIs & ServicesCredentials
  4. Click Create CredentialsOAuth 2.0 Client ID
  5. Application type: Web application
2

Configure Redirect URIs

Add authorized redirect URIs:
https://xxxxx.supabase.co/auth/v1/callback
Replace xxxxx with your Supabase project reference.
3

Add Credentials to Supabase

In Supabase Dashboard → Authentication → Providers → Google:
  • Enable Google provider
  • Client ID: <your-client-id>.apps.googleusercontent.com
  • Client Secret: <your-client-secret>
  • Click Save
4

Test Google Login

Implementation in your app:
import { createClient } from '@/lib/supabase/client'

const supabase = createClient()

const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
  },
})

Custom Credentials Authentication

Studley AI uses custom JWT-based sessions for email/password authentication.

Session Configuration

lib/auth/session.ts
import { SignJWT, jwtVerify } from "jose"

const SECRET_KEY = new TextEncoder().encode(
  process.env.SESSION_SECRET || "your-secret-key-change-in-production"
)

export interface SessionData {
  userId: string
  username: string
  isAdmin?: boolean
}

export async function createSession(data: SessionData): Promise<string> {
  const token = await new SignJWT(data)
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("7d")  // 7 day expiry
    .sign(SECRET_KEY)
  
  return token
}

Environment Setup

1

Generate Session Secret

Create a strong secret key:
openssl rand -base64 32
Add to environment variables:
SESSION_SECRET="<generated-secret>"
2

Configure Session Cookie

Session settings (already configured in lib/auth/session.ts):
cookieStore.set("session", token, {
  httpOnly: true,                          // Prevent XSS
  secure: process.env.NODE_ENV === "production", // HTTPS only in production
  sameSite: "lax",                        // CSRF protection
  maxAge: 60 * 60 * 24 * 7,               // 7 days
  path: "/",
})

Password Hashing

Passwords are hashed using bcrypt:
import bcryptjs from 'bcryptjs'

// Hash password on registration
const hashedPassword = await bcryptjs.hash(password, 10)

// Verify password on login
const isValid = await bcryptjs.compare(password, user.password)

Clever SSO Integration

For educational institutions using Clever.
1

Register Clever Application

  1. Go to Clever Developer Portal
  2. Create new application
  3. Select District SSO application type
  4. Note your Client ID and Client Secret
2

Configure Redirect URI

Set redirect URI in Clever dashboard:
https://yourdomain.com/api/auth/clever/callback
3

Add Environment Variables

CLEVER_CLIENT_ID="your-clever-client-id"
CLEVER_CLIENT_SECRET="your-clever-client-secret"
NEXT_PUBLIC_APP_URL="https://yourdomain.com"
4

Run Clever Tables Migration

Execute the Clever integration migration:
psql $DATABASE_URL -f scripts/create-clever-tables.sql
5

Test Clever Login

OAuth flow is implemented in:
  • /api/auth/clever/route.ts - Initiate OAuth
  • /api/auth/clever/callback/route.ts - Handle callback
The integration automatically:
  • Creates user accounts
  • Syncs user data
  • Manages token refresh

Row Level Security (RLS)

Protect user data with Supabase RLS policies.

Enable RLS

-- Enable RLS on user_profiles
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;

-- Users can read their own profile
CREATE POLICY "Users can read own profile" ON public.user_profiles
  FOR SELECT USING (auth.uid() = id);

-- Users can update their own profile
CREATE POLICY "Users can update own profile" ON public.user_profiles
  FOR UPDATE USING (auth.uid() = id);

Additional Policies

ALTER TABLE quiz_results ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view own results" ON quiz_results
  FOR SELECT USING (auth.uid()::text = user_id);

CREATE POLICY "Users can insert own results" ON quiz_results
  FOR INSERT WITH CHECK (auth.uid()::text = user_id);
ALTER TABLE study_materials ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can manage own materials" ON study_materials
  FOR ALL USING (auth.uid()::text = user_id);

Session Management

Server-Side Session Retrieval

import { getSession } from '@/lib/auth/session'

export default async function ProtectedPage() {
  const session = await getSession()
  
  if (!session) {
    redirect('/login')
  }
  
  return <div>Welcome, {session.username}!</div>
}

Client-Side Supabase Session

import { createClient } from '@/lib/supabase/client'

const supabase = createClient()

// Get current user
const { data: { user } } = await supabase.auth.getUser()

// Listen for auth changes
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    console.log('User signed in:', session.user)
  }
  if (event === 'SIGNED_OUT') {
    console.log('User signed out')
  }
})

Middleware Protection

Protect routes with Next.js middleware:
middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifySession } from '@/lib/auth/session'

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value
  
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  const session = await verifySession(token)
  
  if (!session) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/workspace/:path*',
    '/profile/:path*',
  ],
}

Testing Authentication

// POST /api/auth/login
const response = await fetch('/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: '[email protected]',
    password: 'password123',
  }),
})

const data = await response.json()
console.log('Session:', data.session)

Troubleshooting

Error: OAuth returns to wrong URLSolutions:
  • Verify redirect URL in provider settings matches exactly
  • Check NEXT_PUBLIC_APP_URL is set correctly
  • Ensure no trailing slashes in URLs
  • For Supabase: Use https://[project-ref].supabase.co/auth/v1/callback
Error: User logged out on page refreshSolutions:
  • Check cookie settings: httpOnly, secure, sameSite
  • Verify SESSION_SECRET is set
  • In development: ensure secure: false for HTTP
  • Check browser cookie settings
Error: JWTExpired or JWTInvalidSolutions:
  • Check SESSION_SECRET hasn’t changed
  • Verify token expiration (7 days default)
  • Clear cookies and re-login
  • Check system time is synchronized
Error: row-level security policy violationSolutions:
  • Verify user is authenticated: auth.uid() returns value
  • Check policy conditions match query
  • Use service role key for admin operations
  • Review policy with EXPLAIN in psql

Security Best Practices

Strong Session Secrets

Generate cryptographically secure secrets:
openssl rand -base64 32

HTTPS Only

Always use HTTPS in production:
secure: process.env.NODE_ENV === 'production'

httpOnly Cookies

Prevent XSS attacks:
httpOnly: true

CSRF Protection

Use SameSite cookies:
sameSite: 'lax'

Next Steps

AI Configuration

Set up Groq AI for study features

File Storage

Configure file uploads with Vercel Blob

User Management

Learn about user roles and permissions

API Reference

View authentication API endpoints

Build docs developers (and LLMs) love