Fix login authentication flow and improve proxy configuration
Build And Push Image / docker (push) Successful in 2m50s
Details
Build And Push Image / docker (push) Successful in 2m50s
Details
- Refactor login page to use auth composable for better state management - Update nginx proxy settings with proper timeouts and buffering - Improve PWA service worker caching strategy for API calls - Add debug files and documentation for login troubleshooting
This commit is contained in:
parent
57428f437c
commit
2c545dcaaa
|
|
@ -0,0 +1,243 @@
|
||||||
|
# MonacoUSA Portal - Login Issue Fix Summary
|
||||||
|
|
||||||
|
## Issue Description
|
||||||
|
|
||||||
|
The login functionality was experiencing redirect failures after successful authentication. Users could successfully authenticate with Keycloak, but the redirect to the dashboard was failing with 502 Bad Gateway errors and CORS issues.
|
||||||
|
|
||||||
|
## Root Causes Identified
|
||||||
|
|
||||||
|
1. **Service Worker Interference**: The PWA service worker was caching authentication-related routes, causing conflicts during login redirects.
|
||||||
|
2. **Cookie Domain Issues**: The session cookie was being set with an explicit domain (`.monacousa.org`) which could cause issues in certain deployment scenarios.
|
||||||
|
3. **Redirect Method**: Using `window.location.href` for redirects was causing hard page reloads that conflicted with the SPA architecture.
|
||||||
|
4. **Nginx Proxy Timeouts**: Missing proxy timeout configurations could cause 502 errors during authentication flows.
|
||||||
|
|
||||||
|
## Fixes Applied
|
||||||
|
|
||||||
|
### 1. PWA Service Worker Configuration (`nuxt.config.ts`)
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- Added authentication-related paths to `navigateFallbackDenylist`
|
||||||
|
- Reduced API cache duration from 24 hours to 5 minutes
|
||||||
|
- Added `cleanupOutdatedCaches: true` for better cache management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
navigateFallbackDenylist: [
|
||||||
|
/^\/api\//,
|
||||||
|
/^\/auth\//,
|
||||||
|
/^\/login/,
|
||||||
|
/^\/dashboard/
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why This Fixes It:**
|
||||||
|
- Prevents service worker from intercepting authentication flows
|
||||||
|
- Ensures fresh API responses during login
|
||||||
|
- Eliminates cache-related redirect conflicts
|
||||||
|
|
||||||
|
### 2. Cookie Domain Fix (`server/api/auth/direct-login.post.ts`)
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- Removed explicit cookie domain setting
|
||||||
|
- Let cookies default to the current domain
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: domain: cookieDomain,
|
||||||
|
// After: No domain specified (defaults to current domain)
|
||||||
|
setCookie(event, 'monacousa-session', cookieValue, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why This Fixes It:**
|
||||||
|
- Eliminates cross-domain cookie issues
|
||||||
|
- Ensures cookies work correctly in all deployment scenarios
|
||||||
|
- Follows browser security best practices
|
||||||
|
|
||||||
|
### 3. Login Page Refactor (`pages/login.vue`)
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- Replaced direct API calls with `useAuth()` composable
|
||||||
|
- Removed `window.location.href` redirect
|
||||||
|
- Improved state management integration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: window.location.href = response.redirectTo || '/dashboard';
|
||||||
|
// After: Uses useAuth().login() which handles navigation properly
|
||||||
|
const result = await login({
|
||||||
|
username: credentials.value.username,
|
||||||
|
password: credentials.value.password,
|
||||||
|
rememberMe: credentials.value.rememberMe
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why This Fixes It:**
|
||||||
|
- Proper SPA navigation without hard page reloads
|
||||||
|
- Better integration with Nuxt's authentication state
|
||||||
|
- Eliminates service worker conflicts during navigation
|
||||||
|
|
||||||
|
### 4. Nginx Proxy Configuration (`nginx-portal.conf`)
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- Added proxy timeout configurations
|
||||||
|
- Improved buffer settings for better performance
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_buffer_size 4k;
|
||||||
|
proxy_buffers 4 32k;
|
||||||
|
proxy_busy_buffers_size 64k;
|
||||||
|
proxy_temp_file_write_size 64k;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why This Fixes It:**
|
||||||
|
- Prevents 502 errors during authentication flows
|
||||||
|
- Handles larger response payloads better
|
||||||
|
- Improves overall proxy stability
|
||||||
|
|
||||||
|
## Testing the Fix
|
||||||
|
|
||||||
|
### 1. Clear Browser Cache
|
||||||
|
```bash
|
||||||
|
# Clear all browser data for portal.monacousa.org
|
||||||
|
# Or use incognito/private browsing mode
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Restart Services
|
||||||
|
```bash
|
||||||
|
# Restart Nginx
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
|
||||||
|
# Restart the Nuxt application
|
||||||
|
# (depends on your deployment method)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Login Flow
|
||||||
|
1. Navigate to `https://portal.monacousa.org/login`
|
||||||
|
2. Enter valid credentials
|
||||||
|
3. Verify successful redirect to dashboard
|
||||||
|
4. Check browser console for errors
|
||||||
|
|
||||||
|
### 4. Use Debug Script
|
||||||
|
```bash
|
||||||
|
# Run the debug script to test API endpoints
|
||||||
|
node debug-login.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
### Browser Console Checks
|
||||||
|
- No CORS errors should appear
|
||||||
|
- No 502 Bad Gateway errors
|
||||||
|
- Service worker should not intercept auth routes
|
||||||
|
|
||||||
|
### Network Tab Verification
|
||||||
|
- Login POST request should return 200
|
||||||
|
- Session cookie should be set correctly
|
||||||
|
- Dashboard redirect should work without errors
|
||||||
|
|
||||||
|
### Server Logs
|
||||||
|
- Should show successful login messages
|
||||||
|
- No cookie domain warnings
|
||||||
|
- Proper session creation logs
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### If Login Still Fails
|
||||||
|
|
||||||
|
1. **Check Service Worker**
|
||||||
|
```javascript
|
||||||
|
// In browser console
|
||||||
|
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||||
|
registrations.forEach(registration => registration.unregister());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify Cookie Settings**
|
||||||
|
- Check browser dev tools > Application > Cookies
|
||||||
|
- Ensure `monacousa-session` cookie is set
|
||||||
|
- Verify cookie domain matches current domain
|
||||||
|
|
||||||
|
3. **Check Nginx Logs**
|
||||||
|
```bash
|
||||||
|
tail -f /var/log/nginx/portal.monacousa.org.error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify Keycloak Configuration**
|
||||||
|
- Ensure all environment variables are set
|
||||||
|
- Test Keycloak connectivity
|
||||||
|
- Verify client configuration
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Still Getting 502 Errors**
|
||||||
|
- Check if Nuxt application is running on port 6060
|
||||||
|
- Verify nginx can connect to backend
|
||||||
|
- Check firewall settings
|
||||||
|
|
||||||
|
2. **Cookies Not Being Set**
|
||||||
|
- Verify HTTPS is working correctly
|
||||||
|
- Check if secure flag is appropriate for environment
|
||||||
|
- Ensure no conflicting cookie policies
|
||||||
|
|
||||||
|
3. **Service Worker Issues**
|
||||||
|
- Clear browser cache completely
|
||||||
|
- Unregister service worker manually
|
||||||
|
- Check PWA configuration
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Ensure these are properly configured:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Keycloak Configuration
|
||||||
|
NUXT_KEYCLOAK_ISSUER=https://auth.monacousa.org/realms/monacousa
|
||||||
|
NUXT_KEYCLOAK_CLIENT_ID=monacousa-portal
|
||||||
|
NUXT_KEYCLOAK_CLIENT_SECRET=your-secret
|
||||||
|
|
||||||
|
# Session Security
|
||||||
|
NUXT_SESSION_SECRET=your-48-character-secret
|
||||||
|
NUXT_ENCRYPTION_KEY=your-32-character-key
|
||||||
|
|
||||||
|
# Public Configuration
|
||||||
|
NUXT_PUBLIC_DOMAIN=monacousa.org
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Notes
|
||||||
|
|
||||||
|
1. **Nginx Configuration**
|
||||||
|
- Reload nginx after configuration changes
|
||||||
|
- Test configuration with `nginx -t`
|
||||||
|
|
||||||
|
2. **Application Restart**
|
||||||
|
- Restart Nuxt application to pick up changes
|
||||||
|
- Clear any application-level caches
|
||||||
|
|
||||||
|
3. **SSL Certificates**
|
||||||
|
- Ensure SSL certificates are valid
|
||||||
|
- Verify HTTPS is working correctly
|
||||||
|
|
||||||
|
## Success Indicators
|
||||||
|
|
||||||
|
✅ Login form submits without errors
|
||||||
|
✅ User is redirected to dashboard
|
||||||
|
✅ Session is maintained across page refreshes
|
||||||
|
✅ No console errors related to authentication
|
||||||
|
✅ Health check endpoint returns "healthy"
|
||||||
|
✅ Service worker doesn't interfere with auth flows
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. `nuxt.config.ts` - PWA service worker configuration
|
||||||
|
2. `pages/login.vue` - Login page refactor
|
||||||
|
3. `server/api/auth/direct-login.post.ts` - Cookie domain fix
|
||||||
|
4. `nginx-portal.conf` - Proxy timeout configuration
|
||||||
|
5. `debug-login.js` - Debug script (new file)
|
||||||
|
6. `LOGIN_FIX_SUMMARY.md` - This documentation (new file)
|
||||||
|
|
||||||
|
The fixes address the core issues causing login redirect failures and should provide a stable authentication experience.
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Simple debug script to test the login flow
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const BASE_URL = 'https://portal.monacousa.org';
|
||||||
|
|
||||||
|
async function makeRequest(url, options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const protocol = url.startsWith('https') ? https : http;
|
||||||
|
|
||||||
|
const req = protocol.request(url, {
|
||||||
|
method: options.method || 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'Debug-Script/1.0',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', chunk => data += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
resolve({
|
||||||
|
status: res.statusCode,
|
||||||
|
headers: res.headers,
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
|
||||||
|
if (options.body) {
|
||||||
|
req.write(JSON.stringify(options.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testLoginFlow() {
|
||||||
|
console.log('🔍 Testing MonacoUSA Portal Login Flow');
|
||||||
|
console.log('=====================================\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test 1: Health check
|
||||||
|
console.log('1. Testing health endpoint...');
|
||||||
|
const health = await makeRequest(`${BASE_URL}/api/health`);
|
||||||
|
console.log(` Status: ${health.status}`);
|
||||||
|
console.log(` Response: ${health.body.substring(0, 100)}...\n`);
|
||||||
|
|
||||||
|
// Test 2: Session check (should be unauthenticated)
|
||||||
|
console.log('2. Testing session endpoint...');
|
||||||
|
const session = await makeRequest(`${BASE_URL}/api/auth/session`);
|
||||||
|
console.log(` Status: ${session.status}`);
|
||||||
|
console.log(` Response: ${session.body.substring(0, 100)}...\n`);
|
||||||
|
|
||||||
|
// Test 3: Login attempt
|
||||||
|
console.log('3. Testing direct login...');
|
||||||
|
const login = await makeRequest(`${BASE_URL}/api/auth/direct-login`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
username: 'present',
|
||||||
|
password: 'your-password-here',
|
||||||
|
rememberMe: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(` Status: ${login.status}`);
|
||||||
|
console.log(` Headers: ${JSON.stringify(login.headers, null, 2)}`);
|
||||||
|
console.log(` Response: ${login.body.substring(0, 200)}...\n`);
|
||||||
|
|
||||||
|
// Test 4: Check if we can access dashboard
|
||||||
|
console.log('4. Testing dashboard access...');
|
||||||
|
const dashboard = await makeRequest(`${BASE_URL}/dashboard`, {
|
||||||
|
headers: {
|
||||||
|
'Cookie': login.headers['set-cookie'] ? login.headers['set-cookie'].join('; ') : ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(` Status: ${dashboard.status}`);
|
||||||
|
console.log(` Response: ${dashboard.body.substring(0, 100)}...\n`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during testing:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testLoginFlow();
|
||||||
|
|
@ -31,6 +31,13 @@ server {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_buffer_size 4k;
|
||||||
|
proxy_buffers 4 32k;
|
||||||
|
proxy_busy_buffers_size 64k;
|
||||||
|
proxy_temp_file_write_size 64k;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
|
|
||||||
|
|
@ -91,16 +91,21 @@ export default defineNuxtConfig({
|
||||||
workbox: {
|
workbox: {
|
||||||
navigateFallback: '/',
|
navigateFallback: '/',
|
||||||
globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico}'],
|
globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico}'],
|
||||||
navigateFallbackDenylist: [/^\/api\//],
|
navigateFallbackDenylist: [
|
||||||
|
/^\/api\//,
|
||||||
|
/^\/auth\//,
|
||||||
|
/^\/login/,
|
||||||
|
/^\/dashboard/
|
||||||
|
],
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: /^https:\/\/.*\.monacousa\.org\/.*/i,
|
urlPattern: /^https:\/\/.*\.monacousa\.org\/api\/.*/i,
|
||||||
handler: 'NetworkFirst',
|
handler: 'NetworkFirst',
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'api-cache',
|
cacheName: 'api-cache',
|
||||||
expiration: {
|
expiration: {
|
||||||
maxEntries: 10,
|
maxEntries: 10,
|
||||||
maxAgeSeconds: 60 * 60 * 24 // 24 hours
|
maxAgeSeconds: 60 * 5 // 5 minutes for API calls
|
||||||
},
|
},
|
||||||
cacheableResponse: {
|
cacheableResponse: {
|
||||||
statuses: [0, 200]
|
statuses: [0, 200]
|
||||||
|
|
@ -109,7 +114,8 @@ export default defineNuxtConfig({
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
skipWaiting: true,
|
skipWaiting: true,
|
||||||
clientsClaim: true
|
clientsClaim: true,
|
||||||
|
cleanupOutdatedCaches: true
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
installPrompt: true,
|
installPrompt: true,
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,10 @@ definePageMeta({
|
||||||
layout: false
|
layout: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use the auth composable
|
||||||
|
const { user, login, loading: authLoading, error: authError, checkAuth } = useAuth();
|
||||||
|
|
||||||
// Check if user is already authenticated
|
// Check if user is already authenticated
|
||||||
const { user } = useAuth();
|
|
||||||
if (user.value) {
|
if (user.value) {
|
||||||
await navigateTo('/dashboard');
|
await navigateTo('/dashboard');
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +139,6 @@ const credentials = ref({
|
||||||
|
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
const showForgotPassword = ref(false);
|
const showForgotPassword = ref(false);
|
||||||
const loading = ref(false);
|
|
||||||
const loginError = ref('');
|
const loginError = ref('');
|
||||||
const errors = ref({
|
const errors = ref({
|
||||||
username: '',
|
username: '',
|
||||||
|
|
@ -147,6 +148,7 @@ const errors = ref({
|
||||||
const loginForm = ref();
|
const loginForm = ref();
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
|
const loading = computed(() => authLoading.value);
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return credentials.value.username.length > 0 &&
|
return credentials.value.username.length > 0 &&
|
||||||
credentials.value.password.length > 0 &&
|
credentials.value.password.length > 0 &&
|
||||||
|
|
@ -174,33 +176,22 @@ const validateForm = () => {
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return;
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
loginError.value = '';
|
loginError.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await $fetch<{
|
const result = await login({
|
||||||
success: boolean;
|
username: credentials.value.username,
|
||||||
redirectTo?: string;
|
password: credentials.value.password,
|
||||||
user?: any;
|
rememberMe: credentials.value.rememberMe
|
||||||
}>('/api/auth/direct-login', {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
username: credentials.value.username,
|
|
||||||
password: credentials.value.password,
|
|
||||||
rememberMe: credentials.value.rememberMe
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (!result.success) {
|
||||||
// Force a page reload to ensure the session cookie is properly read
|
loginError.value = result.error || 'Login failed. Please check your credentials and try again.';
|
||||||
// This is more reliable than trying to update the auth state manually
|
|
||||||
window.location.href = response.redirectTo || '/dashboard';
|
|
||||||
}
|
}
|
||||||
|
// If successful, the login method will handle the redirect
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
loginError.value = error.data?.message || 'Login failed. Please check your credentials and try again.';
|
loginError.value = authError.value || 'Login failed. Please check your credentials and try again.';
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,10 +234,10 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
// Create session with appropriate expiration
|
// Create session with appropriate expiration
|
||||||
const sessionManager = createSessionManager();
|
const sessionManager = createSessionManager();
|
||||||
const cookieDomain = process.env.COOKIE_DOMAIN || undefined;
|
|
||||||
const maxAge = !!rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24 * 7; // 30 days vs 7 days
|
const maxAge = !!rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24 * 7; // 30 days vs 7 days
|
||||||
|
|
||||||
console.log(`🍪 Setting session cookie (Remember Me: ${!!rememberMe}) with domain:`, cookieDomain);
|
// Don't set a domain for the cookie - let it default to the current domain
|
||||||
|
console.log(`🍪 Setting session cookie (Remember Me: ${!!rememberMe}) without explicit domain`);
|
||||||
|
|
||||||
// Create the session cookie string using the session manager
|
// Create the session cookie string using the session manager
|
||||||
const sessionCookieString = sessionManager.createSession(sessionData, !!rememberMe);
|
const sessionCookieString = sessionManager.createSession(sessionData, !!rememberMe);
|
||||||
|
|
@ -250,7 +250,6 @@ export default defineEventHandler(async (event) => {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
domain: cookieDomain,
|
|
||||||
maxAge,
|
maxAge,
|
||||||
path: '/',
|
path: '/',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue