Fix auth flow: switch from popup to redirect-based MSAL login

The popup login flow was broken because the Flask 302 redirect from
/ to /reporting/index caused MSAL in the popup to consume the auth
code hash before the parent window could detect it, leaving the
parent stuck on "Authenticating..." while the popup rendered the
full app.

- Switch signIn() from loginPopup() to loginRedirect()
- Add handleRedirectPromise() at start of initAuth() to process
  the auth code on page load after returning from Microsoft
- Change root route from 302 redirect to direct template render
  so the #code=... hash fragment is preserved for MSAL
- Switch signOut() from logoutPopup() to clearCache()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
nickviljoen 2026-02-25 12:22:33 +02:00
parent 677736943a
commit 1dff8fece5
2 changed files with 56 additions and 43 deletions

9
app.py
View file

@ -109,12 +109,13 @@ def create_app(config_class=app_config.Config):
app.register_blueprint(video_master_bp)
logger.info("Video Master blueprint (BETA) registered at /video-master")
# Register root route (redirect to reporting)
# Register root route - render reporting page directly (no 302 redirect).
# A redirect would strip the URL hash fragment (#code=...) that MSAL
# needs to process after returning from Microsoft login.
@app.route('/')
def root():
"""Redirect root to reporting module."""
from flask import redirect, url_for
return redirect(url_for('reporting.index'))
"""Render reporting index directly at root."""
return render_template('reporting/index.html', active_tab='reporting')
# Register error handlers
register_error_handlers(app)

View file

@ -1,6 +1,7 @@
/**
* Authentication Module for HM QC Report Dashboard
* Uses Microsoft MSAL Browser library for Azure AD authentication
* Uses redirect flow (not popup) to avoid cross-window timing issues.
*/
// Azure AD Configuration
@ -101,7 +102,9 @@ async function checkAuthStatus() {
}
/**
* Sign in with Microsoft
* Sign in with Microsoft using redirect flow.
* The page navigates to Microsoft login, then redirects back.
* On return, handleRedirectPromise() in initAuth() processes the response.
*/
async function signIn() {
if (!msalInstance) {
@ -111,35 +114,13 @@ async function signIn() {
try {
showLoading();
// Perform popup login
const loginResponse = await msalInstance.loginPopup(loginRequest);
if (loginResponse && loginResponse.idToken) {
// Send token to backend for validation and cookie storage
const success = await submitTokenToBackend(loginResponse.idToken);
if (success) {
currentUser = loginResponse.account;
isAuthenticated = true;
showAuthenticatedState();
updateUserInfo();
} else {
throw new Error('Failed to validate token with backend');
}
}
// Redirect the whole page to Microsoft login.
// After auth, Microsoft redirects back to redirectUri.
// initAuth() -> handleRedirectPromise() will pick up the response.
await msalInstance.loginRedirect(loginRequest);
} catch (error) {
console.error('Login failed:', error);
if (error.errorCode === 'popup_window_error') {
showError('Popup was blocked. Please allow popups for this site.');
} else if (error.errorCode === 'user_cancelled') {
console.log('User cancelled login');
showUnauthenticatedState();
} else {
showError('Login failed: ' + (error.errorMessage || error.message || 'Unknown error'));
}
console.error('Login redirect failed:', error);
showError('Login failed: ' + (error.errorMessage || error.message || 'Unknown error'));
showUnauthenticatedState();
}
}
@ -181,7 +162,7 @@ async function signOut() {
try {
showLoading();
// Clear backend cookie
// Clear backend session
await fetch('/auth/logout', {
method: 'POST',
credentials: 'include'
@ -189,12 +170,10 @@ async function signOut() {
// Clear MSAL cache
if (msalInstance) {
const currentAccount = msalInstance.getActiveAccount();
if (currentAccount) {
await msalInstance.logoutPopup({
account: currentAccount,
postLogoutRedirectUri: window.location.origin
});
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
// Clear local MSAL state without redirecting to Microsoft logout
msalInstance.clearCache();
}
}
@ -273,7 +252,10 @@ function showError(message) {
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'alert alert-danger mt-3';
authRequired.querySelector('.text-center').appendChild(errorDiv);
const textCenter = authRequired.querySelector('.text-center');
if (textCenter) {
textCenter.appendChild(errorDiv);
}
}
errorDiv.textContent = message;
} else {
@ -329,14 +311,44 @@ async function initAuth() {
return;
}
// Handle redirect response from Microsoft login.
// After loginRedirect(), the page reloads with auth code in the URL hash.
// handleRedirectPromise() extracts the code, exchanges it for tokens,
// and cleans up the URL hash.
try {
const redirectResponse = await msalInstance.handleRedirectPromise();
if (redirectResponse && redirectResponse.idToken) {
console.log('Redirect login successful, sending token to backend...');
// Send token to backend for session creation
const success = await submitTokenToBackend(redirectResponse.idToken);
if (success) {
currentUser = redirectResponse.account;
isAuthenticated = true;
// Setup event listeners and show authenticated state
setupEventListeners();
showAuthenticatedState();
updateUserInfo();
return;
} else {
console.error('Backend token validation failed after redirect');
}
}
} catch (error) {
console.error('Error handling redirect response:', error);
}
// Setup event listeners
setupEventListeners();
// Check authentication status
// No redirect response - check if we have an existing backend session
const authenticated = await checkAuthStatus();
if (authenticated) {
console.log('User is authenticated');
console.log('User is authenticated (existing session)');
} else {
console.log('User is not authenticated');
}