Production-grade Google and Microsoft OAuth authentication with middleware access layer. Covers authentication architecture decisions, Supabase Auth integration, Microsoft Graph API, Google Workspace APIs, RBAC implementation, and comprehensive QA gates for security. Triggers on: OAuth, authentication, authorization, Google Auth, Microsoft Auth, Entra ID, Azure AD, MSAL, Graph API, PKCE, JWT, token, refresh token, RBAC, role, permission, middleware, session, SSO, single sign-on, MFA, multi-factor, service account, client credentials, scope, consent, tenant, identity provider, IdP
When to Use Supabase Auth
Supabase Auth Advantages
✓ OAuth setup in minutes (no client secret needed for SPAs)
✓ Automatic token refresh with httpOnly cookies
✓ Row-level security via auth.uid()
✓ Magic links, passwordless auth, MFA support
✓ Zero infrastructure to manage
✓ Built-in protection: PKCE flow enforced
✓ User metadata/custom claims for RBAC
When to Use Custom Auth
Custom Auth Considerations
⚠ Must implement PKCE (Proof Key Code Exchange)
⚠ Must enforce HTTPS only
⚠ Must implement token rotation/refresh
⚠ Must handle token revocation
⚠ Must prevent CSRF attacks
⚠ Requires ongoing security updates
START: Need user authentication?
│
├─ WEB APP (Server-side capable)?
│ ├─ YES → Authorization Code Flow with PKCE
│ │ (Most secure, uses code exchange)
│ │
│ └─ NO → Continue
│
├─ SPA (No backend / Backend can't store secrets)?
│ ├─ YES → Authorization Code Flow + PKCE
│ │ (Frontend gets code, backend exchanges)
│ │
│ └─ NO → Continue
│
├─ MOBILE APP?
│ ├─ YES → Authorization Code Flow + PKCE
│ │ (Can't store client secret safely)
│ │
│ └─ NO → Continue
│
├─ SERVICE ACCOUNT (No user interaction)?
│ └─ Client Credentials Flow
│ (Backend-to-backend, requires client secret)
│
├─ LEGACY APP (Must use Implicit)?
│ └─ ❌ DO NOT USE IMPLICIT FLOW
│ (Token exposed in URL fragments)
Flow Comparison Table
| Flow | User Interaction | Token in URL | When to Use |
|---|---|---|---|
| Auth Code + PKCE | Yes | No | SPAs, mobile, web apps |
| Client Credentials | No | N/A | Backend services, service accounts |
| Resource Owner Password | Yes | No | Legacy systems only |
| Implicit | Yes | Yes | ❌ NEVER (deprecated) |
Key Decision: MSAL vs Raw Azure SDK
Use MSAL (Microsoft Authentication Library) when:
Use Raw Azure SDK when:
Tenant Configuration
// Single-tenant (default)
// Only users in YOUR Azure AD can authenticate
const config = {
auth: {
clientId: 'app-id',
authority: 'https://login.microsoftonline.com/tenant-id'
}
};
// Multi-tenant
// Any Microsoft account or Azure AD user
const config = {
auth: {
clientId: 'app-id',
authority: 'https://login.microsoftonline.com/common'
}
};
// Tenant-aware (user selects tenant at login)
// Recommended for enterprise SaaS
const config = {
auth: {
clientId: 'app-id',
authority: 'https://login.microsoftonline.com/organizations'
}
};
Why PKCE is Essential
Traditional OAuth2 for SPAs was vulnerable:
1. Browser gets Authorization Code
2. Code exchanged for Access Token
3. ❌ If attacker steals code → can exchange for token (no challenge)
PKCE solves this:
1. Browser generates code_verifier (random 128-char string)
2. Browser generates code_challenge = SHA256(code_verifier)
3. Browser redirects to Google with code_challenge
4. Google returns authorization code
5. Browser sends code + code_verifier to backend
6. Backend validates: SHA256(verifier) == challenge stored by Google
7. ✓ Only legitimate app can exchange code
Implementation (with Supabase)
// Frontend automatically handles PKCE
import { useSupabaseClient } from '@supabase/auth-helpers-react';
export function LoginButton() {
const supabase = useSupabaseClient();
const handleLogin = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
};
return <button onClick={handleLogin}>Sign in with Google</button>;
}
Supabase handles:
Architecture Pattern
┌─────────────────────────────────────┐
│ User Login Page │
├─────────────────────────────────────┤
│ ┌─ Sign in with Google (PKCE) │
│ ├─ Sign in with Microsoft (PKCE) │
│ └─ Email + Password (Custom) │
└─────────────────────────────────────┘
↓
Supabase Auth
↓
┌─────────────────────────────────────┐
│ User Profile (unified identity) │
│ - email │
│ - user_metadata (name, avatar) │
│ - app_metadata (roles, org) │
│ - linked_identities │
└─────────────────────────────────────┘
Linking Provider Accounts
// User signs up with Google
// Later wants to enable email/password login too
async function linkPasswordToGoogleAccount(password) {
const { user, error } = await supabase.auth.updateUser({
password: password
});
// Now user can sign in with email+password OR Google OAuth
}
// User signed up with email
// Later wants to add Google OAuth
async function linkGoogleToPasswordAccount() {
const { data, error } = await supabase.auth.linkIdentity({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
// Now user can sign in with both methods
}
Implementation Steps
Enable Providers in Supabase Dashboard
Configure Redirect URLs (all OAuth providers)
http://localhost:3000/auth/callback (dev)
https://app.example.com/auth/callback (prod)
Create Unified Login Component
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const supabase = useSupabaseClient();
const handleEmailLogin = async (e) => {
e.preventDefault();
const { error } = await supabase.auth.signInWithPassword({
email,
password
});
if (error) alert(error.message);
};
const handleOAuth = async (provider) => {
await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
};
return (
<div>
<form onSubmit={handleEmailLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Sign In</button>
</form>
<div>
<button onClick={() => handleOAuth('google')}>
Google
</button>
<button onClick={() => handleOAuth('azure')}>
Microsoft
</button>
</div>
</div>
);
}
Service Account Pattern
Backend Application (Service Account)
↓ (with client credentials)
Microsoft Graph API / Google APIs
↓
Shared Tenant Resources
(Calendars, Drives, Email)
When to Use Service Accounts
User Delegation Pattern (Recommended)
User (authenticated via OAuth)
↓ (with access token)
Microsoft Graph API / Google APIs
↓
User's Own Resources
(Personal calendars, drives, emails)
When to Use User Delegation
On-Behalf-Of Flow (Best of Both)
// Backend exchanges user's refresh token
// for fresh access token to API
// User stays authenticated, backend can call APIs
async function callGraphApiOnBehalfOfUser(userId) {
// Get user's refresh token from secure storage
const refreshToken = await getStoredRefreshToken(userId);
// Request new access token
const newToken = await exchangeRefreshToken(refreshToken);
// Call API with fresh token
const response = await fetch(
'https://graph.microsoft.com/v1.0/me/events',
{
headers: { Authorization: `Bearer ${newToken}` }
}
);
return response.json();
}
Step 1: Create Google OAuth Credentials
https://xxxxxxxxxxxx.supabase.co/auth/v1/callback
Step 2: Configure Supabase Provider
In Supabase Dashboard → Authentication → Providers → Google:
Client ID: [from Google Cloud]
Client Secret: [from Google Cloud]
Step 3: Frontend Implementation
import { useSupabaseClient } from '@supabase/auth-helpers-react';
export function GoogleSignIn() {
const supabase = useSupabaseClient();
const handleSignIn = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
// PKCE is automatically enabled for SPAs
skipBrowserRedirect: false // Let Supabase handle redirect
}
});
if (error) {
console.error('Auth error:', error.message);
}
};
return <button onClick={handleSignIn}>Sign in with Google</button>;
}
Step 4: Handle OAuth Callback
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useSupabaseClient } from '@supabase/auth-helpers-react';
export default function AuthCallback() {
const router = useRouter();
const supabase = useSupabaseClient();
useEffect(() => {
const handleCallback = async () => {
// Supabase automatically exchanges code for session
const { data: { session }, error } = await supabase.auth.getSession();
if (error || !session) {
console.error('Auth failed:', error);
router.push('/login');
return;
}
// Session established, redirect to dashboard
router.push('/dashboard');
};
handleCallback();
}, [router, supabase]);
return <div>Signing you in...</div>;
}
Step 5: Token Storage & Security
Supabase automatically stores tokens in httpOnly cookies:
// Never access token directly in browser
// Supabase handles all token operations
// Get current session (token automatically included in requests)
const { data: { session } } = await supabase.auth.getSession();
// Session contains:
// - access_token (httpOnly cookie, not accessible to JS)
// - refresh_token (httpOnly cookie)
// - expires_in (seconds)
// - token_type ('Bearer')
Step 1: Register Application in Azure
- Single tenant (your org only)
- Multi-tenant (any Azure AD org)
- Microsoft accounts only
https://xxxxxxxxxxxx.supabase.co/auth/v1/callbackStep 2: Create Client Secret
Step 3: Configure Supabase Provider
In Supabase Dashboard → Authentication → Providers → Azure:
Client ID: [Application ID from Azure]
Client Secret: [Secret value]
Tenant: common (for multi-tenant) or your-tenant-id
Step 4: Frontend Implementation
export function MicrosoftSignIn() {
const supabase = useSupabaseClient();
const handleSignIn = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'azure',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
scopes: ['openid', 'profile', 'email']
}
});
if (error) console.error('Auth error:', error.message);
};
return <button onClick={handleSignIn}>Sign in with Microsoft</button>;
}
Store Roles in User Metadata
// After user logs in, fetch from database and store claims
async function updateUserRoles(userId, roles, org) {
const { error } = await supabase.auth.admin.updateUserById(
userId,
{
app_metadata: {
roles: roles, // ['admin', 'manager']
org_id: org,
permissions: ['read:contracts', 'write:leases']
}
}
);
if (error) console.error('Error updating roles:', error);
}
Access Claims in Session
const { data: { session } } = await supabase.auth.getSession();
// Roles available at:
const userRoles = session?.user?.app_metadata?.roles || [];
const orgId = session?.user?.app_metadata?.org_id;
const permissions = session?.user?.app_metadata?.permissions || [];
// Use in component
export function Dashboard() {
const { data: { session } } = useSessionContext();
const isAdmin = session?.user?.app_metadata?.roles?.includes('admin');
return (
<div>
{isAdmin && <AdminPanel />}
<UserContent />
</div>
);
}
Create RLS Policy on Contracts Table
-- Enable RLS
ALTER TABLE contracts ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see contracts for their organization
CREATE POLICY "Users see org contracts"
ON contracts
FOR SELECT
USING (
org_id IN (
SELECT org_id
FROM user_orgs
WHERE user_id = auth.uid()
)
);
-- Policy: Only managers can update contracts
CREATE POLICY "Managers can update contracts"
ON contracts
FOR UPDATE
USING (
org_id IN (
SELECT org_id
FROM user_orgs
WHERE user_id = auth.uid()
AND role = 'manager'
)
);
-- Policy: Only org admins can delete contracts
CREATE POLICY "Admins can delete contracts"
ON contracts
FOR DELETE
USING (
org_id IN (
SELECT org_id
FROM user_orgs
WHERE user_id = auth.uid()
AND role = 'admin'
)
);
Use RLS in Frontend
// RLS automatically applied by Supabase
const { data: contracts, error } = await supabase
.from('contracts')
.select('*')
.eq('status', 'active');
// User only sees contracts they have access to
// Thanks to RLS policies!
Automatic Token Refresh
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
auth: {
autoRefreshToken: true, // Auto-refresh when expiring
persistSession: true, // Store session in storage
detectSessionInUrl: true // Detect OAuth callback
}
}
);
// Token refresh happens automatically
// Supabase exchanges refresh token for new access token
// All transparent to your code!
Monitor Session Changes
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
console.log('Auth event:', event);
// event: 'INITIAL_SESSION' | 'SIGNED_IN' | 'SIGNED_OUT' |
// 'TOKEN_REFRESHED' | 'USER_UPDATED'
if (event === 'SIGNED_OUT') {
// Clear app state, redirect to login
router.push('/login');
}
if (event === 'TOKEN_REFRESHED') {
// New token obtained, continue normal operation
}
}
);
return () => subscription?.unsubscribe();
}, [supabase, router]);
With Supabase Auth Helpers
// lib/auth.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const res = NextResponse.next();
const supabase = createMiddlewareClient({ req: request, res });
const {
data: { session },
} = await supabase.auth.getSession();
// Protect routes
const protectedRoutes = ['/dashboard', '/contracts', '/admin'];
const adminRoutes = ['/admin'];
const isProtectedRoute = protectedRoutes.some(route =>
request.nextUrl.pathname.startsWith(route)
);
const isAdminRoute = adminRoutes.some(route =>
request.nextUrl.pathname.startsWith(route)
);
// No session → redirect to login
if (!session && isProtectedRoute) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Not admin → redirect to dashboard
if (isAdminRoute && !session?.user?.app_metadata?.roles?.includes('admin')) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return res;
}
export const config = {
matcher: ['/dashboard/:path*', '/contracts/:path*', '/admin/:path*']
};
Client-Side Protected Routes
import { useRouter } from 'next/router';
import { useSessionContext } from '@supabase/auth-helpers-react';
import { useEffect, ReactNode } from 'react';
interface ProtectedRouteProps {
children: ReactNode;
requiredRole?: string;
}
export function ProtectedRoute({
children,
requiredRole
}: ProtectedRouteProps) {
const router = useRouter();
const { session, isLoading } = useSessionContext();
useEffect(() => {
if (!isLoading && !session) {
router.push('/login');
}
if (
!isLoading &&
requiredRole &&
!session?.user?.app_metadata?.roles?.includes(requiredRole)
) {
router.push('/unauthorized');
}
}, [session, isLoading, requiredRole, router]);
if (isLoading) return <div>Loading...</div>;
if (!session) return null;
return <>{children}</>;
}
// Usage
export default function AdminDashboard() {
return (
<ProtectedRoute requiredRole="admin">
<AdminPanel />
</ProtectedRoute>
);
}
On-Behalf-Of Flow (Recommended for Web Apps)
// Backend receives user's authorization code
// Exchange code for user token + refresh token
// Use refresh token to get new access tokens
import msal from '@azure/msal-node';
const msalConfig = {
auth: {
clientId: 'your-app-id',
authority: 'https://login.microsoftonline.com/your-tenant',
clientSecret: 'your-secret'
}
};
const cca = new msal.ConfidentialClientApplication(msalConfig);
// In your backend API
app.post('/api/auth/token', async (req, res) => {
const { code } = req.body;
try {
// Exchange code for tokens
const tokenResponse = await cca.acquireTokenByCode({
code,
scopes: [
'https://graph.microsoft.com/.default'
],
redirectUri: 'https://app.example.com/auth/callback'
});
// Store refresh token securely (encrypted in database)
await storeUserToken(userId, {
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
expiresIn: tokenResponse.expiresOn
});
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Later, when you need to call Graph API
async function getGraphToken(userId) {
// Get stored refresh token
const storedToken = await getUserToken(userId);
// Exchange refresh for new access token
const tokenResponse = await cca.acquireTokenByRefreshToken({
refreshToken: storedToken.refreshToken,
scopes: ['https://graph.microsoft.com/.default']
});
return tokenResponse.accessToken;
}
Client Credentials Flow (Service Accounts)
// For background jobs with service account permissions
// No user interaction needed
const tokenResponse = await cca.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default']
});
const token = tokenResponse.accessToken;
// Use to call Graph API
const response = await fetch(
'https://graph.microsoft.com/v1.0/users',
{
headers: { Authorization: `Bearer ${token}` }
}
);
Create Calendar Event
async function createCalendarEvent(accessToken, event) {
const response = await fetch(
'https://graph.microsoft.com/v1.0/me/events',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
subject: event.title,
start: {
dateTime: event.startTime,
timeZone: 'UTC'
},
end: {
dateTime: event.endTime,
timeZone: 'UTC'
},
attendees: event.attendees.map(email => ({
emailAddress: { address: email },
type: 'required'
}))
})
}
);
return response.json();
}
Send Email via Mail API
async function sendEmail(accessToken, message) {
const response = await fetch(
'https://graph.microsoft.com/v1.0/me/sendMail',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: {
subject: message.subject,
body: {
contentType: 'HTML',
content: message.body
},
toRecipients: message.to.map(email => ({
emailAddress: { address: email }
}))
}
})
}
);
return response.json();
}
Access SharePoint Files
async function listSharePointFiles(accessToken, siteId, driveId) {
const response = await fetch(
`https://graph.microsoft.com/v1.0/sites/${siteId}/drives/${driveId}/root/children`,
{
headers: { Authorization: `Bearer ${accessToken}` }
}
);
return response.json();
}
List Teams & Channels
async function listTeams(accessToken) {
const response = await fetch(
'https://graph.microsoft.com/v1.0/me/joinedTeams',
{
headers: { Authorization: `Bearer ${accessToken}` }
}
);
return response.json();
}
async function listChannels(accessToken, teamId) {
const response = await fetch(
`https://graph.microsoft.com/v1.0/teams/${teamId}/channels`,
{
headers: { Authorization: `Bearer ${accessToken}` }
}
);
return response.json();
}
Batch Multiple Requests in One Call
async function batchGraphRequests(accessToken, requests) {
const response = await fetch(
'https://graph.microsoft.com/v1.0/$batch',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
requests: requests.map((req, id) => ({
id: String(id),
method: req.method,
url: req.url,
body: req.body
}))
})
}
);
const data = await response.json();
return data.responses;
}
// Usage: Get user profile, calendar, and drive in one call
const results = await batchGraphRequests(token, [
{ method: 'GET', url: '/me' },
{ method: 'GET', url: '/me/calendar' },
{ method: 'GET', url: '/me/drive' }
]);
// results[0] = user profile
// results[1] = calendar
// results[2] = drive
Track Changes Efficiently
// Initial call: Get all events + deltaLink
async function initialCalendarSync(accessToken) {
const response = await fetch(
'https://graph.microsoft.com/v1.0/me/events/delta',
{
headers: { Authorization: `Bearer ${accessToken}` }
}
);
const data = await response.json();
// Store deltaLink for next sync
const deltaLink = data['@odata.deltaLink'];
await store.set('calendar_deltaLink', deltaLink);
return {
events: data.value,
deltaLink
};
}
// Subsequent calls: Only get changes
async function syncCalendarChanges(accessToken) {
const deltaLink = await store.get('calendar_deltaLink');
if (!deltaLink) {
return initialCalendarSync(accessToken);
}
const response = await fetch(deltaLink, {
headers: { Authorization: `Bearer ${accessToken}` }
});
const data = await response.json();
// Update deltaLink for next time
const newDeltaLink = data['@odata.deltaLink'];
await store.set('calendar_deltaLink', newDeltaLink);
return {
// data.value contains ONLY added/modified/deleted items
changes: data.value,
deltaLink: newDeltaLink
};
}
Resilient API Calls
async function callGraphApiWithRetry(
accessToken,
endpoint,
options = {},
maxRetries = 3
) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(
`https://graph.microsoft.com/v1.0${endpoint}`,
{
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`
}
}
);
// Success
if (response.ok) {
return response.json();
}
// Retry-able errors (429, 503)
if (response.status === 429 || response.status === 503) {
const retryAfter = response.headers.get('Retry-After') ||
Math.pow(2, attempt);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
// Non-retry-able errors
const error = await response.json();
throw new Error(
`Graph API error [${response.status}]: ${error.error?.message}`
);
} catch (error) {
lastError = error;
// Exponential backoff for network errors
if (attempt < maxRetries - 1) {
const delayMs = Math.pow(2, attempt) * 1000;
console.log(`Retry in ${delayMs}ms...`);
await new Promise(r => setTimeout(r, delayMs));
}
}
}
throw lastError;
}
// Usage
const events = await callGraphApiWithRetry(
accessToken,
'/me/events?$orderby=start/dateTime'
);
Progressive Scope Requests
// Step 1: Initial login - minimal scopes
async function signIn() {
const result = await cca.acquireTokenByCode({
code: authCode,
scopes: ['User.Read'] // Just profile access
});
saveToken(result);
}
// Step 2: When user accesses calendar feature
async function ensureCalendarPermission() {
// Check if we have calendar permission
const token = await cca.acquireTokenSilent({
scopes: ['Calendars.Read'],
account: getAccount()
});
if (!token) {
// Permission not granted, request it
// Will show consent screen again
const result = await cca.acquireTokenByCode({
code: freshAuthCode,
scopes: ['User.Read', 'Calendars.Read']
});
}
return token;
}
// Scope hierarchy for Microsoft Graph
const SCOPES = {
// Read-only access
USER_READ: 'User.Read', // Profile
CALENDAR_READ: 'Calendars.Read', // Read events
MAIL_READ: 'Mail.Read', // Read emails
DRIVE_READ: 'Files.Read.All', // Read files
// Write access (more sensitive)
CALENDAR_WRITE: 'Calendars.ReadWrite',
MAIL_SEND: 'Mail.Send',
DRIVE_WRITE: 'Files.ReadWrite.All',
// Delegated admin access
DIRECTORY_READ: 'Directory.Read.All',
USER_READ_ALL: 'User.Read.All'
};
Create & Upload Documents
import { google } from 'googleapis';
const drive = google.drive({
version: 'v3',
auth: authClient // OAuth2 or service account auth
});
// Create folder
async function createFolder(folderName, parentId) {
const response = await drive.files.create({
requestBody: {
name: folderName,
mimeType: 'application/vnd.google-apps.folder',
parents: [parentId]
},
fields: 'id, name'
});
return response.data;
}
// Upload file
async function uploadFile(filePath, fileName, parentId) {
const response = await drive.files.create({
requestBody: {
name: fileName,
parents: [parentId]
},
media: {
mimeType: 'application/octet-stream',
body: fs.createReadStream(filePath)
}
});
return response.data;
}
// Create shared link
async function createShareLink(fileId) {
const permission = await drive.permissions.create({
fileId: fileId,
requestBody: {
role: 'reader',
type: 'anyone'
}
});
return {
fileId,
link: `https://drive.google.com/file/d/${fileId}/view`
};
}
Read & Write Spreadsheet Data
const sheets = google.sheets({
version: 'v4',
auth: authClient
});
// Read data from sheet
async function readSheetData(spreadsheetId, range) {
const response = await sheets.spreadsheets.values.get({
spreadsheetId,
range
});
return response.data.values; // [[header1, header2], [row1col1, row1col2]]
}
// Write data to sheet
async function writeSheetData(spreadsheetId, range, values) {
const response = await sheets.spreadsheets.values.update({
spreadsheetId,
range,
valueInputOption: 'RAW',
requestBody: {
values
}
});
return response.data;
}
// Append data (next empty row)
async function appendSheetData(spreadsheetId, range, values) {
const response = await sheets.spreadsheets.values.append({
spreadsheetId,
range,
valueInputOption: 'USER_ENTERED',
requestBody: {
values
}
});
return response.data;
}
// Create chart
async function createChart(spreadsheetId, chartConfig) {
const response = await sheets.spreadsheets.batchUpdate({
spreadsheetId,
requestBody: {
requests: [
{
addChart: {
chart: {
title: chartConfig.title,
chartType: 'COLUMN_CHART',
// ... more config
}
}
}
]
}
});
return response.data;
}
Send Email Notifications
const gmail = google.gmail({
version: 'v1',
auth: authClient
});
function encodeEmail(message) {
const email = [
`From: ${message.from}`,
`To: ${message.to}`,
`Subject: ${message.subject}`,
'',
message.body
].join('\n');
return Buffer.from(email)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
async function sendEmail(message) {
const response = await gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: encodeEmail(message)
}
});
return response.data;
}
// Watch for new emails
async function watchInbox() {
const response = await gmail.users.watch({
userId: 'me',
requestBody: {
topicName: 'projects/my-project/topics/gmail-notifications',
labelIds: ['INBOX']
}
});
return response.data;
}
Service Account with Delegated Authority
// Service account can impersonate any user in your Google Workspace
import { google } from 'googleapis';
import fs from 'fs';
const SERVICE_ACCOUNT_KEY = JSON.parse(
fs.readFileSync('service-account-key.json')
);
function createDelegatedAuth(userEmail) {
return new google.auth.JWT({
email: SERVICE_ACCOUNT_KEY.client_email,
key: SERVICE_ACCOUNT_KEY.private_key,
scopes: [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/gmail.send'
],
subject: userEmail // Impersonate this user
});
}
// Now call APIs as if you were that user
async function backupUserDrive(userEmail) {
const auth = createDelegatedAuth(userEmail);
const drive = google.drive({ version: 'v3', auth });
// List all files for this user
const response = await drive.files.list({
spaces: 'drive',
pageSize: 100,
fields: 'files(id, name, mimeType)'
});
return response.data.files;
}
// Setup (one-time):
// 1. Create service account in Google Cloud
// 2. Grant domain-wide delegation in Google Workspace Admin
// 3. Authorize scopes in Google Workspace Admin (OAuth scopes screen)
// 4. Now service account can impersonate any user
Setup Webhook for Real-time Updates
// Backend webhook endpoint
app.post('/webhook/gmail', async (req, res) => {
// Google sends notification when mail arrives
const { message } = req.body;
if (!message) {
res.sendStatus(200);
return;
}
// Decode notification
const data = JSON.parse(
Buffer.from(message.data, 'base64').toString()
);
// data.emailAddress, data.historyId
// Process new mail
const newEmails = await fetchNewEmails(data.emailAddress);
// Send to frontend via WebSocket or process in background
await notifyUser(data.emailAddress, newEmails);
res.sendStatus(200);
});
// Watch for changes
async function setupGmailWatch(userEmail) {
const auth = createDelegatedAuth(userEmail);
const gmail = google.gmail({ version: 'v1', auth });
const response = await gmail.users.watch({
userId: 'me',
requestBody: {
topicName: 'projects/my-project/topics/gmail-notifications',
labelIds: ['INBOX']
}
});
return response.data;
}
Recommended Role Pyramid
super_admin (0.1%)
↓
org_admin (1-2%)
↓
manager (5-10%)
↓
user (40-50%)
↓
viewer (read-only)
Role Definitions & Permissions
const ROLES = {
super_admin: {
description: 'System administrator, full access',
permissions: [
'read:*',
'write:*',
'delete:*',
'manage:users',
'manage:orgs',
'manage:settings'
]
},
org_admin: {
description: 'Organization administrator',
permissions: [
'read:org',
'write:org',
'read:contracts',
'write:contracts',
'read:leases',
'write:leases',
'manage:org_users',
'manage:org_settings'
]
},
manager: {
description: 'Project/portfolio manager',
permissions: [
'read:org',
'read:contracts',
'write:contracts',
'read:leases',
'write:leases',
'read:reports'
]
},
user: {
description: 'Regular user with standard access',
permissions: [
'read:contracts',
'read:leases',
'read:reports'
]
},
viewer: {
description: 'Read-only access',
permissions: [
'read:contracts',
'read:leases'
]
}
};
Resource-Based Access Control
const RESOURCE_PERMISSIONS = {
contract: {
create: ['org_admin', 'manager'],
read: ['org_admin', 'manager', 'user', 'viewer'],
update: ['org_admin', 'manager'],
delete: ['org_admin'],
share: ['org_admin', 'manager'],
export: ['org_admin', 'manager', 'user']
},
lease: {
create: ['org_admin', 'manager'],
read: ['org_admin', 'manager', 'user', 'viewer'],
update: ['org_admin', 'manager'],
delete: ['org_admin'],
approve: ['org_admin'],
renew: ['org_admin', 'manager']
},
report: {
create: ['org_admin'],
read: ['org_admin', 'manager', 'user'],
update: ['org_admin'],
delete: ['org_admin'],
schedule: ['org_admin', 'manager']
},
user: {
create: ['org_admin'],
read: ['org_admin'],
update: ['org_admin'],
delete: ['org_admin'],
deactivate: ['org_admin']
}
};
// Check permission
function canUserPerform(userRole, resource, action) {
return RESOURCE_PERMISSIONS[resource]?.[action]?.includes(userRole);
}
// Usage
if (canUserPerform(user.role, 'contract', 'delete')) {
// Show delete button
}
Dynamic RLS Based on User Role
-- Create role hierarchy
CREATE TYPE user_role AS ENUM ('super_admin', 'org_admin', 'manager', 'user', 'viewer');
-- Add roles to users table
ALTER TABLE auth.users ADD COLUMN role user_role DEFAULT 'viewer';
-- RLS policy: Check user role and organization
CREATE POLICY "Users can view org data by role"
ON leases
FOR SELECT
USING (
org_id IN (
SELECT org_id FROM user_orgs
WHERE user_id = auth.uid()
)
OR
(
SELECT role FROM auth.users WHERE id = auth.uid()
) = 'super_admin'
);
-- Manager can update leases in their org
CREATE POLICY "Managers can update leases"
ON leases
FOR UPDATE
USING (
org_id IN (
SELECT org_id FROM user_orgs
WHERE user_id = auth.uid()
AND role IN ('org_admin', 'manager')
)
);
-- Only org admins can delete
CREATE POLICY "Only admins can delete leases"
ON leases
FOR DELETE
USING (
(
SELECT role FROM user_orgs
WHERE user_id = auth.uid()
AND org_id = leases.org_id
) = 'org_admin'
);
Store Roles in JWT Claims
// Backend: Create JWT with custom claims
import jwt from 'jsonwebtoken';
function createAuthToken(user) {
const claims = {
sub: user.id,
email: user.email,
role: user.role,
org_id: user.org_id,
permissions: getUserPermissions(user.role),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
};
return jwt.sign(claims, process.env.JWT_SECRET);
}
// Frontend: Access claims from token
import jwtDecode from 'jwt-decode';
function useAuthClaims() {
const token = localStorage.getItem('authToken'); // ⚠️ Or better: httpOnly cookie
if (!token) return null;
const claims = jwtDecode(token);
return {
userId: claims.sub,
email: claims.email,
role: claims.role,
orgId: claims.org_id,
permissions: claims.permissions
};
}
// Use claims for authorization
export function useAuthorized(requiredPermission) {
const claims = useAuthClaims();
return claims?.permissions?.includes(requiredPermission) || false;
}
// Usage in component
export function AdminSettings() {
const canManageUsers = useAuthorized('manage:users');
if (!canManageUsers) {
return <Unauthorized />;
}
return <SettingsPanel />;
}
API Route Protection
// middleware/auth.ts
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
export function requireAuth(handler) {
return async (req: NextRequest) => {
const token = req.cookies.get('authToken')?.value;
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
try {
const claims = jwt.verify(
token,
process.env.JWT_SECRET!
);
// Attach claims to request
(req as any).user = claims;
return handler(req);
} catch (error) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
};
}
export function requireRole(...roles: string[]) {
return (handler) => {
return async (req: NextRequest) => {
const token = req.cookies.get('authToken')?.value;
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
try {
const claims = jwt.verify(token, process.env.JWT_SECRET!);
if (!roles.includes(claims.role)) {
return NextResponse.json(
{ error: 'Forbidden' },
{ status: 403 }
);
}
(req as any).user = claims;
return handler(req);
} catch (error) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
};
};
}
// Usage in API routes
// api/contracts/route.ts
export const POST = requireRole('org_admin', 'manager')(
async (req: NextRequest) => {
const user = (req as any).user;
// Create contract
const contract = await db.contracts.create({
...req.body,
org_id: user.org_id
});
return NextResponse.json(contract);
}
);
See /references/qa-gates.md for comprehensive checklist.
Quick Summary
DESIGN GATE (Architecture review)
IMPLEMENTATION GATE (Security checks)
PRODUCTION GATE (Penetration testing)
Run automated checks:
bash scripts/auth-qa-gate.sh
references/google-oauth-patterns.md - Google OAuth2 deep divereferences/microsoft-entra-patterns.md - Microsoft Entra ID configurationreferences/middleware-architecture.md - Middleware design patternsreferences/qa-gates.md - Complete security checklistFor Supabase + Google OAuth → See section 2.1 (Google OAuth2 setup)
For Microsoft Graph API → See section 3 (Microsoft Graph API Middleware)
For RBAC → See section 5 (RBAC Implementation)
For Production Security
→ Run bash scripts/auth-qa-gate.sh
→ Review references/qa-gates.md
Official Documentation
Security Guides
Common Patterns