feat: Enable UGA SSO with Microsoft Entra
This commit is contained in:
139
docs/OAUTH_USER_SYNC_FIX.md
Normal file
139
docs/OAUTH_USER_SYNC_FIX.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# OAuth User Synchronization Fix
|
||||
|
||||
## Problem
|
||||
When a user signs on with an OAuth provider (Microsoft Entra/Azure AD) for the first time, the user is added to `auth.users` in Supabase but NOT to the application's `user_profiles` table. This causes the application to fail when trying to load user data, as there's no user profile available.
|
||||
|
||||
## Solution
|
||||
Implemented automatic user profile creation for OAuth users through a multi-layered approach:
|
||||
|
||||
### 1. Database Trigger (00003_oauth_user_sync.sql)
|
||||
- **Location**: `supabase/migrations/00003_oauth_user_sync.sql` and `management-dashboard-web-app/supabase/migrations/00003_oauth_user_sync.sql`
|
||||
- **Function**: `handle_new_oauth_user()`
|
||||
- Automatically creates a user profile in `public.user_profiles` when a new user is created in `auth.users`
|
||||
- Handles the synchronization at the database level, ensuring it works regardless of where users are created
|
||||
- Includes race condition handling with `ON CONFLICT DO NOTHING`
|
||||
|
||||
- **Trigger**: `on_auth_user_created`
|
||||
- Fires after INSERT on `auth.users`
|
||||
- Ensures every new OAuth user gets a profile entry
|
||||
|
||||
### 2. Client-Side Utility Function (src/lib/supabase.ts)
|
||||
- **Function**: `userManagement.syncOAuthUser()`
|
||||
- Provides a fallback synchronization mechanism for any OAuth users that slip through
|
||||
- Checks if user profile exists before creating
|
||||
- Handles race conditions gracefully (duplicate key errors)
|
||||
- Includes comprehensive error logging for debugging
|
||||
|
||||
**Logic**:
|
||||
```typescript
|
||||
1. Get current authenticated user from Supabase Auth
|
||||
2. Check if user profile already exists in user_profiles table
|
||||
3. If exists: return (no action needed)
|
||||
4. If not exists: create user profile with:
|
||||
- id: user's UUID from auth.users
|
||||
- email: user's email from auth.users
|
||||
- status: 'active' (default status)
|
||||
5. Handle errors gracefully:
|
||||
- "No rows returned" (PGRST116): Expected, user doesn't exist yet
|
||||
- "Duplicate key" (23505): Race condition, another process created it first
|
||||
- Other errors: Log and continue
|
||||
```
|
||||
|
||||
### 3. App Integration (src/App.tsx)
|
||||
- **Updated**: Auth state change listener
|
||||
- **Triggers**: When `SIGNED_IN` or `INITIAL_SESSION` event occurs
|
||||
- **Action**: Calls `userManagement.syncOAuthUser()` asynchronously
|
||||
- **Benefit**: Ensures user profile exists before the rest of the app tries to access it
|
||||
|
||||
## How It Works
|
||||
|
||||
### OAuth Sign-In Flow (New)
|
||||
```
|
||||
1. User clicks "Sign in with Microsoft"
|
||||
↓
|
||||
2. Redirected to Microsoft login
|
||||
↓
|
||||
3. Microsoft authenticates and redirects back
|
||||
↓
|
||||
4. Supabase creates entry in auth.users
|
||||
↓
|
||||
5. Database trigger fires → user_profiles entry created
|
||||
↓
|
||||
6. App receives SIGNED_IN event
|
||||
↓
|
||||
7. App calls syncOAuthUser() as extra safety measure
|
||||
↓
|
||||
8. User profile is guaranteed to exist
|
||||
↓
|
||||
9. getUserProfile() and loadData() succeed
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
- **Non-invasive**: The solution uses triggers and utility functions, doesn't modify existing tables
|
||||
- **Graceful degradation**: If either layer fails, the other provides a fallback
|
||||
- **No breaking changes**: Existing APIs and components remain unchanged
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Test 1: First-Time OAuth Sign-In
|
||||
1. Clear browser cookies/session
|
||||
2. Click "Sign in with Microsoft"
|
||||
3. Complete OAuth flow
|
||||
4. Verify:
|
||||
- User is in Supabase auth
|
||||
- User is in `user_profiles` table
|
||||
- App loads user data without errors
|
||||
- Dashboard displays correctly
|
||||
|
||||
### Test 2: Verify Database Trigger
|
||||
1. Directly create a user in `auth.users` via SQL
|
||||
2. Verify that `user_profiles` entry is automatically created
|
||||
3. Check timestamp to confirm trigger fired
|
||||
|
||||
### Test 3: Verify Client-Side Fallback
|
||||
1. Manually delete a user's `user_profiles` entry
|
||||
2. Reload the app
|
||||
3. Verify that `syncOAuthUser()` recreates the profile
|
||||
4. Check browser console for success logs
|
||||
|
||||
## Files Modified
|
||||
1. **Database Migrations**:
|
||||
- `/usda-vision/supabase/migrations/00003_oauth_user_sync.sql` (NEW)
|
||||
- `/usda-vision/management-dashboard-web-app/supabase/migrations/00003_oauth_user_sync.sql` (NEW)
|
||||
|
||||
2. **TypeScript/React**:
|
||||
- `/usda-vision/management-dashboard-web-app/src/lib/supabase.ts` (MODIFIED)
|
||||
- Added `syncOAuthUser()` method to `userManagement` object
|
||||
|
||||
- `/usda-vision/management-dashboard-web-app/src/App.tsx` (MODIFIED)
|
||||
- Import `userManagement`
|
||||
- Call `syncOAuthUser()` on auth state change
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
1. **Apply Database Migrations**:
|
||||
```bash
|
||||
# Run the new migration
|
||||
supabase migration up
|
||||
```
|
||||
|
||||
2. **Deploy Application Code**:
|
||||
- Push the changes to `src/lib/supabase.ts` and `src/App.tsx`
|
||||
- No environment variable changes needed
|
||||
- No configuration changes needed
|
||||
|
||||
3. **Test in Staging**:
|
||||
- Test OAuth sign-in with a fresh account
|
||||
- Verify user profile is created
|
||||
- Check app functionality
|
||||
|
||||
4. **Monitor in Production**:
|
||||
- Watch browser console for any errors from `syncOAuthUser()`
|
||||
- Check database logs to confirm trigger is firing
|
||||
- Monitor user creation metrics
|
||||
|
||||
## Future Enhancements
|
||||
- Assign default roles to new OAuth users (currently requires manual assignment)
|
||||
- Pre-populate `first_name` and `last_name` from OAuth provider data
|
||||
- Add user profile completion workflow for new OAuth users
|
||||
- Auto-disable account creation for users outside organization
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from './lib/supabase'
|
||||
import { supabase, userManagement } from './lib/supabase'
|
||||
import { Login } from './components/Login'
|
||||
import { Dashboard } from './components/Dashboard'
|
||||
import { CameraRoute } from './components/CameraRoute'
|
||||
@@ -19,6 +19,13 @@ function App() {
|
||||
setIsAuthenticated(!!session)
|
||||
setLoading(false)
|
||||
|
||||
// Sync OAuth user on successful sign in (creates user profile if needed)
|
||||
if ((event === 'SIGNED_IN' || event === 'INITIAL_SESSION') && session) {
|
||||
userManagement.syncOAuthUser().catch((err) => {
|
||||
console.error('Failed to sync OAuth user:', err)
|
||||
})
|
||||
}
|
||||
|
||||
// Handle signout route
|
||||
if (event === 'SIGNED_OUT') {
|
||||
setCurrentRoute('/')
|
||||
|
||||
@@ -557,6 +557,60 @@ export const userManagement = {
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
},
|
||||
|
||||
// Sync OAuth user - ensures user profile exists for OAuth-authenticated users
|
||||
async syncOAuthUser(): Promise<void> {
|
||||
try {
|
||||
const { data: { user: authUser }, error: authError } = await supabase.auth.getUser()
|
||||
|
||||
if (authError || !authUser) {
|
||||
console.warn('No authenticated user found for OAuth sync')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user profile already exists
|
||||
const { data: existingProfile, error: checkError } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('id')
|
||||
.eq('id', authUser.id)
|
||||
.single()
|
||||
|
||||
// If profile already exists, no need to create it
|
||||
if (existingProfile && !checkError) {
|
||||
console.log('User profile already exists for user:', authUser.id)
|
||||
return
|
||||
}
|
||||
|
||||
// If error is not "no rows returned", it's a real error
|
||||
if (checkError && checkError.code !== 'PGRST116') {
|
||||
console.error('Error checking for existing profile:', checkError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create user profile for new OAuth user
|
||||
const { error: insertError } = await supabase
|
||||
.from('user_profiles')
|
||||
.insert({
|
||||
id: authUser.id,
|
||||
email: authUser.email || '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
if (insertError) {
|
||||
// Ignore "duplicate key value" errors in case of race condition
|
||||
if (insertError.code === '23505') {
|
||||
console.log('User profile was already created (race condition handled)')
|
||||
return
|
||||
}
|
||||
console.error('Error creating user profile for OAuth user:', insertError)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Successfully created user profile for OAuth user:', authUser.id)
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in syncOAuthUser:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
-- OAuth User Synchronization
|
||||
-- This migration adds functionality to automatically create user profiles when users sign up via OAuth
|
||||
|
||||
-- =============================================
|
||||
-- 1. CREATE FUNCTION FOR OAUTH USER AUTO-PROFILE CREATION
|
||||
-- =============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_oauth_user()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Check if user profile already exists
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.user_profiles WHERE id = NEW.id
|
||||
) THEN
|
||||
-- Create user profile with default active status
|
||||
INSERT INTO public.user_profiles (id, email, status)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
NEW.email,
|
||||
'active'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =============================================
|
||||
-- 2. CREATE TRIGGER FOR NEW AUTH USERS
|
||||
-- =============================================
|
||||
|
||||
-- Drop the trigger if it exists to avoid conflicts
|
||||
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
||||
|
||||
-- Create trigger that fires after a new user is created in auth.users
|
||||
CREATE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_new_oauth_user();
|
||||
|
||||
-- =============================================
|
||||
-- 3. COMMENT FOR DOCUMENTATION
|
||||
-- =============================================
|
||||
|
||||
COMMENT ON FUNCTION public.handle_new_oauth_user() IS
|
||||
'Automatically creates a user profile in public.user_profiles when a new user is created via OAuth in auth.users. This ensures OAuth users are immediately accessible in the application without manual provisioning.';
|
||||
@@ -284,9 +284,9 @@ client_id = "env(AZURE_CLIENT_ID)"
|
||||
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||
secret = "env(AZURE_CLIENT_SECRET)"
|
||||
# Overrides the default auth redirectUrl.
|
||||
redirect_uri = ""
|
||||
redirect_uri = "env(AZURE_REDIRECT_URI)"
|
||||
# Azure tenant ID or 'common' for multi-tenant. Use 'common', 'organizations', 'consumers', or your specific tenant ID.
|
||||
url = "https://login.microsoftonline.com/env(AZURE_TENANT_ID)/v2.0"
|
||||
url = "env(AZURE_TENANT_URL)"
|
||||
# If enabled, the nonce check will be skipped.
|
||||
skip_nonce_check = false
|
||||
|
||||
|
||||
46
supabase/migrations/00003_oauth_user_sync.sql
Normal file
46
supabase/migrations/00003_oauth_user_sync.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- OAuth User Synchronization
|
||||
-- This migration adds functionality to automatically create user profiles when users sign up via OAuth
|
||||
|
||||
-- =============================================
|
||||
-- 1. CREATE FUNCTION FOR OAUTH USER AUTO-PROFILE CREATION
|
||||
-- =============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_oauth_user()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Check if user profile already exists
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.user_profiles WHERE id = NEW.id
|
||||
) THEN
|
||||
-- Create user profile with default active status
|
||||
INSERT INTO public.user_profiles (id, email, status)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
NEW.email,
|
||||
'active'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =============================================
|
||||
-- 2. CREATE TRIGGER FOR NEW AUTH USERS
|
||||
-- =============================================
|
||||
|
||||
-- Drop the trigger if it exists to avoid conflicts
|
||||
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
||||
|
||||
-- Create trigger that fires after a new user is created in auth.users
|
||||
CREATE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_new_oauth_user();
|
||||
|
||||
-- =============================================
|
||||
-- 3. COMMENT FOR DOCUMENTATION
|
||||
-- =============================================
|
||||
|
||||
COMMENT ON FUNCTION public.handle_new_oauth_user() IS
|
||||
'Automatically creates a user profile in public.user_profiles when a new user is created via OAuth in auth.users. This ensures OAuth users are immediately accessible in the application without manual provisioning.';
|
||||
Reference in New Issue
Block a user