moved to SPA auth architecture with MSAL.js in the browser
This commit is contained in:
parent
b778276bd3
commit
b8a34d8b48
3 changed files with 275 additions and 147 deletions
245
index.php
245
index.php
|
|
@ -1,139 +1,21 @@
|
|||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
use League\OAuth2\Client\Provider\GenericProvider;
|
||||
// Check if this might be an MSAL redirect callback
|
||||
// MSAL uses hash fragments for tokens, so we need to let the page load
|
||||
// and let JavaScript handle the callback before PHP redirects
|
||||
$isPotentialMSALCallback = !empty($_SERVER['HTTP_REFERER']) &&
|
||||
strpos($_SERVER['HTTP_REFERER'], 'login.microsoftonline.com') !== false;
|
||||
|
||||
// HANDLE OAUTH CALLBACK FROM AZURE AD
|
||||
// Azure redirects to root URL with ?code= and ?state= parameters
|
||||
if (isset($_GET['code']) && isset($_GET['state'])) {
|
||||
// Start session to access PKCE verifier and state
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Verify state to prevent CSRF attacks
|
||||
if (empty($_GET['state']) || (isset($_SESSION['oauth2state']) && $_GET['state'] !== $_SESSION['oauth2state'])) {
|
||||
unset($_SESSION['oauth2state']);
|
||||
unset($_SESSION['oauth2_code_verifier']);
|
||||
die('Invalid state. Possible CSRF attack.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve code verifier from session
|
||||
if (!isset($_SESSION['oauth2_code_verifier'])) {
|
||||
die('Code verifier not found in session.');
|
||||
}
|
||||
|
||||
$codeVerifier = $_SESSION['oauth2_code_verifier'];
|
||||
|
||||
// Configure Azure AD OAuth2 Provider
|
||||
$provider = new GenericProvider([
|
||||
'clientId' => AZURE_CLIENT_ID,
|
||||
'redirectUri' => AZURE_REDIRECT_URI,
|
||||
'urlAuthorize' => AZURE_AUTHORITY . '/oauth2/v2.0/authorize',
|
||||
'urlAccessToken' => AZURE_AUTHORITY . '/oauth2/v2.0/token',
|
||||
'urlResourceOwnerDetails' => 'https://graph.microsoft.com/v1.0/me',
|
||||
'scopes' => 'openid profile email User.Read'
|
||||
]);
|
||||
|
||||
// Exchange authorization code for access token with PKCE
|
||||
$accessToken = $provider->getAccessToken('authorization_code', [
|
||||
'code' => $_GET['code'],
|
||||
'code_verifier' => $codeVerifier
|
||||
]);
|
||||
|
||||
// Get user information from Microsoft Graph API
|
||||
$request = $provider->getAuthenticatedRequest(
|
||||
'GET',
|
||||
'https://graph.microsoft.com/v1.0/me',
|
||||
$accessToken->getToken()
|
||||
);
|
||||
|
||||
$client = new \GuzzleHttp\Client();
|
||||
$response = $client->send($request);
|
||||
$userData = json_decode($response->getBody(), true);
|
||||
|
||||
// Store user information in session
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['user_id'] = $userData['id'];
|
||||
$_SESSION['user_name'] = $userData['displayName'] ?? $userData['userPrincipalName'];
|
||||
$_SESSION['user_email'] = $userData['userPrincipalName'] ?? $userData['mail'];
|
||||
$_SESSION['access_token'] = $accessToken->getToken();
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
// Initialize user files array for tracking uploads
|
||||
$_SESSION['user_files'] = [];
|
||||
|
||||
// Clean up temporary session variables
|
||||
unset($_SESSION['oauth2state']);
|
||||
unset($_SESSION['oauth2_code_verifier']);
|
||||
|
||||
// Regenerate session ID for security
|
||||
session_regenerate_id(true);
|
||||
|
||||
// Redirect to main application (clean URL, no query parameters)
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
|
||||
} catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
|
||||
// Handle authentication errors - DETAILED DEBUG OUTPUT
|
||||
echo '<html><body style="font-family: monospace; padding: 20px;">';
|
||||
echo '<h2 style="color: #c00;">Authentication Failed</h2>';
|
||||
echo '<p><strong>Error Message:</strong> ' . htmlspecialchars($e->getMessage()) . '</p>';
|
||||
|
||||
echo '<h3>Azure AD Response Body:</h3>';
|
||||
echo '<pre style="background: #f5f5f5; padding: 10px; border: 1px solid #ccc;">';
|
||||
$responseBody = $e->getResponseBody();
|
||||
if (is_array($responseBody)) {
|
||||
echo htmlspecialchars(print_r($responseBody, true));
|
||||
} else {
|
||||
echo htmlspecialchars($responseBody);
|
||||
}
|
||||
echo '</pre>';
|
||||
|
||||
echo '<h3>Session Variables:</h3>';
|
||||
echo '<pre style="background: #f5f5f5; padding: 10px; border: 1px solid #ccc;">';
|
||||
echo 'oauth2state: ' . (isset($_SESSION['oauth2state']) ? $_SESSION['oauth2state'] : '<span style="color:red;">NOT SET</span>') . "\n";
|
||||
echo 'oauth2_code_verifier: ' . (isset($_SESSION['oauth2_code_verifier']) ? 'SET (length: ' . strlen($_SESSION['oauth2_code_verifier']) . ')' : '<span style="color:red;">NOT SET</span>') . "\n";
|
||||
echo 'authenticated: ' . (isset($_SESSION['authenticated']) ? ($_SESSION['authenticated'] ? 'true' : 'false') : '<span style="color:red;">NOT SET</span>') . "\n";
|
||||
echo '</pre>';
|
||||
|
||||
echo '<h3>Request Parameters:</h3>';
|
||||
echo '<pre style="background: #f5f5f5; padding: 10px; border: 1px solid #ccc;">';
|
||||
echo 'GET code: ' . (isset($_GET['code']) ? 'present (length: ' . strlen($_GET['code']) . ')' : '<span style="color:red;">missing</span>') . "\n";
|
||||
echo 'GET state: ' . (isset($_GET['state']) ? $_GET['state'] : '<span style="color:red;">missing</span>') . "\n";
|
||||
echo '</pre>';
|
||||
|
||||
echo '<h3>Configuration:</h3>';
|
||||
echo '<pre style="background: #f5f5f5; padding: 10px; border: 1px solid #ccc;">';
|
||||
echo 'AZURE_CLIENT_ID: ' . substr(AZURE_CLIENT_ID, 0, 8) . '...' . "\n";
|
||||
echo 'AZURE_AUTHORITY: ' . AZURE_AUTHORITY . "\n";
|
||||
echo 'AZURE_REDIRECT_URI: ' . AZURE_REDIRECT_URI . "\n";
|
||||
echo 'DEV_MODE: ' . (DEV_MODE ? 'true' : 'false') . "\n";
|
||||
echo '</pre>';
|
||||
|
||||
echo '<p><a href="login.php">← Back to Login</a></p>';
|
||||
echo '</body></html>';
|
||||
die();
|
||||
} catch (\Exception $e) {
|
||||
// Handle other errors
|
||||
echo '<html><body style="font-family: monospace; padding: 20px;">';
|
||||
echo '<h2 style="color: #c00;">An Error Occurred</h2>';
|
||||
echo '<p><strong>Error:</strong> ' . htmlspecialchars($e->getMessage()) . '</p>';
|
||||
echo '<pre style="background: #f5f5f5; padding: 10px; border: 1px solid #ccc;">';
|
||||
echo htmlspecialchars($e->getTraceAsString());
|
||||
echo '</pre>';
|
||||
echo '<p><a href="login.php">← Back to Login</a></p>';
|
||||
echo '</body></html>';
|
||||
die();
|
||||
}
|
||||
// If not authenticated and not a potential MSAL callback, redirect to login
|
||||
if (!$isPotentialMSALCallback && !isAuthenticated()) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Normal flow - require authentication
|
||||
requireAuth();
|
||||
|
||||
// Get current user info
|
||||
// Get current user info (may be null if MSAL callback is being processed)
|
||||
$user = getCurrentUser();
|
||||
$isAuthenticated = isAuthenticated();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
|
@ -146,22 +28,23 @@ $user = getCurrentUser();
|
|||
<script src="https://cdn.jsdelivr.net/npm/dompurify@2.3.3/dist/purify.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<?php if (DEV_MODE): ?>
|
||||
<div class="dev-mode-banner">
|
||||
🔧 DEV MODE ACTIVE - Authentication Bypassed
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="user-header">
|
||||
<div class="user-info">
|
||||
<span class="user-name"><?php echo htmlspecialchars($user['name']); ?></span>
|
||||
<span class="user-email"><?php echo htmlspecialchars($user['email']); ?></span>
|
||||
<div class="app-container" id="appContainer">
|
||||
<?php if ($isAuthenticated): ?>
|
||||
<?php if (DEV_MODE): ?>
|
||||
<div class="dev-mode-banner">
|
||||
🔧 DEV MODE ACTIVE - Authentication Bypassed
|
||||
</div>
|
||||
<a href="logout.php" class="logout-btn">Logout</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<img src="V2T.svg" alt="Voice to Text" class="logo">
|
||||
<div class="user-header">
|
||||
<div class="user-info">
|
||||
<span class="user-name"><?php echo htmlspecialchars($user['name']); ?></span>
|
||||
<span class="user-email"><?php echo htmlspecialchars($user['email']); ?></span>
|
||||
</div>
|
||||
<a href="logout.php" class="logout-btn">Logout</a>
|
||||
</div>
|
||||
|
||||
<img src="V2T.svg" alt="Voice to Text" class="logo">
|
||||
|
||||
<div id="initialInstruction" class="initial-instruction">
|
||||
Before we start, select output format and upload the Voice File (Max 350 Megabytes in size)
|
||||
|
|
@ -227,13 +110,84 @@ $user = getCurrentUser();
|
|||
<input type="file" id="fileUpload" name="voiceFile" hidden>
|
||||
</div>
|
||||
|
||||
<div id="chatArea" class="chat-area"></div>
|
||||
<div id="chatArea" class="chat-area"></div>
|
||||
|
||||
<button id="downloadButton" style="display: none;">Download Response</button>
|
||||
<button id="downloadButton" style="display: none;">Download Response</button>
|
||||
<?php else: ?>
|
||||
<!-- Loading/Authentication in progress -->
|
||||
<div style="text-align: center; padding: 40px;">
|
||||
<div style="color: #FFC407; font-size: 18px; margin-bottom: 20px;">
|
||||
Processing authentication...
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://alcdn.msauth.net/browser/2.38.1/js/msal-browser.min.js"></script>
|
||||
<script>
|
||||
// MSAL Configuration for handling OAuth redirect callback
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: '<?php echo AZURE_CLIENT_ID; ?>',
|
||||
authority: '<?php echo AZURE_AUTHORITY; ?>',
|
||||
redirectUri: '<?php echo AZURE_REDIRECT_URI; ?>'
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: 'sessionStorage',
|
||||
storeAuthStateInCookie: false
|
||||
}
|
||||
};
|
||||
|
||||
const msalInstance = new msal.PublicClientApplication(msalConfig);
|
||||
|
||||
// Handle redirect callback from Azure AD
|
||||
// This runs when Azure redirects back after authentication
|
||||
msalInstance.handleRedirectPromise()
|
||||
.then(async (response) => {
|
||||
if (response) {
|
||||
// Just authenticated - have access token
|
||||
console.log('Authentication successful, creating session...');
|
||||
|
||||
try {
|
||||
// Send token to PHP backend to create session
|
||||
const validateResponse = await fetch('validate_token.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accessToken: response.accessToken
|
||||
})
|
||||
});
|
||||
|
||||
const result = await validateResponse.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log('Session created, reloading page...');
|
||||
// Session created - reload page to show app
|
||||
window.location.href = 'index.php';
|
||||
} else {
|
||||
alert('Session creation failed: ' + result.error);
|
||||
window.location.href = 'login.php';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session creation error:', error);
|
||||
alert('Failed to create session: ' + error.message);
|
||||
window.location.href = 'login.php';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('MSAL redirect error:', error);
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
<?php if ($isAuthenticated): ?>
|
||||
// Application functionality - only load if authenticated
|
||||
$(document).ready(function() {
|
||||
// Toggle language selector when translation is enabled/disabled
|
||||
$('#enableTranslation').on('change', function() {
|
||||
|
|
@ -348,6 +302,7 @@ $user = getCurrentUser();
|
|||
URL.revokeObjectURL(url);
|
||||
});
|
||||
});
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
91
login.php
91
login.php
|
|
@ -162,7 +162,7 @@ if (isAuthenticated()) {
|
|||
Translate with DeepL into 30+ languages
|
||||
</p>
|
||||
|
||||
<a href="auth.php" class="microsoft-login-btn">
|
||||
<button id="loginButton" class="microsoft-login-btn">
|
||||
<svg class="microsoft-icon" viewBox="0 0 23 23" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="11" height="11" fill="#f25022"/>
|
||||
<rect x="12" width="11" height="11" fill="#7fba00"/>
|
||||
|
|
@ -170,7 +170,10 @@ if (isAuthenticated()) {
|
|||
<rect x="12" y="12" width="11" height="11" fill="#ffb900"/>
|
||||
</svg>
|
||||
Sign in with Microsoft
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<div id="loginError" style="display: none; margin-top: 20px; padding: 15px; background: #ff3333; color: white; border-radius: 8px; font-size: 14px;"></div>
|
||||
<div id="loginProgress" style="display: none; margin-top: 20px; padding: 15px; background: #0a0a0a; color: #FFC407; border-radius: 8px; font-size: 14px; border: 1px solid #333;"></div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
|
|
@ -179,5 +182,89 @@ if (isAuthenticated()) {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MSAL.js Library -->
|
||||
<script src="https://alcdn.msauth.net/browser/2.38.1/js/msal-browser.min.js"></script>
|
||||
<script>
|
||||
// MSAL Configuration
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: '<?php echo AZURE_CLIENT_ID; ?>',
|
||||
authority: '<?php echo AZURE_AUTHORITY; ?>',
|
||||
redirectUri: '<?php echo AZURE_REDIRECT_URI; ?>'
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: 'sessionStorage',
|
||||
storeAuthStateInCookie: false
|
||||
}
|
||||
};
|
||||
|
||||
// Create MSAL instance
|
||||
const msalInstance = new msal.PublicClientApplication(msalConfig);
|
||||
|
||||
// Login request configuration
|
||||
const loginRequest = {
|
||||
scopes: ['openid', 'profile', 'email', 'User.Read']
|
||||
};
|
||||
|
||||
// Handle login button click
|
||||
document.getElementById('loginButton').addEventListener('click', async function() {
|
||||
try {
|
||||
document.getElementById('loginProgress').style.display = 'block';
|
||||
document.getElementById('loginProgress').textContent = 'Redirecting to Microsoft...';
|
||||
document.getElementById('loginError').style.display = 'none';
|
||||
|
||||
// Sign in with redirect
|
||||
await msalInstance.loginRedirect(loginRequest);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
document.getElementById('loginError').style.display = 'block';
|
||||
document.getElementById('loginError').textContent = 'Login failed: ' + error.message;
|
||||
document.getElementById('loginProgress').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Handle redirect callback
|
||||
msalInstance.handleRedirectPromise()
|
||||
.then(async (response) => {
|
||||
if (response) {
|
||||
// User just logged in - we have a response
|
||||
document.getElementById('loginProgress').style.display = 'block';
|
||||
document.getElementById('loginProgress').textContent = 'Authentication successful! Creating session...';
|
||||
|
||||
const account = response.account;
|
||||
const accessToken = response.accessToken;
|
||||
|
||||
// Send token to PHP backend to create session
|
||||
const validateResponse = await fetch('validate_token.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accessToken: accessToken
|
||||
})
|
||||
});
|
||||
|
||||
const result = await validateResponse.json();
|
||||
|
||||
if (result.success) {
|
||||
// Session created successfully - redirect to app
|
||||
document.getElementById('loginProgress').textContent = 'Session created! Redirecting...';
|
||||
window.location.href = 'index.php';
|
||||
} else {
|
||||
document.getElementById('loginError').style.display = 'block';
|
||||
document.getElementById('loginError').textContent = 'Session creation failed: ' + result.error;
|
||||
document.getElementById('loginProgress').style.display = 'none';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Redirect callback error:', error);
|
||||
document.getElementById('loginError').style.display = 'block';
|
||||
document.getElementById('loginError').textContent = 'Authentication error: ' + error.message;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
86
validate_token.php
Normal file
86
validate_token.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
/**
|
||||
* Token Validation Endpoint
|
||||
* Receives access token from client-side MSAL.js and creates PHP session
|
||||
*/
|
||||
require_once 'config.php';
|
||||
|
||||
// Only accept POST requests
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get JSON payload
|
||||
$json = file_get_contents('php://input');
|
||||
$data = json_decode($json, true);
|
||||
|
||||
if (!isset($data['accessToken'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Access token required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$accessToken = $data['accessToken'];
|
||||
|
||||
try {
|
||||
// Validate token by calling Microsoft Graph API
|
||||
// If the token is valid, Graph API will return user info
|
||||
// If invalid, it will fail
|
||||
$ch = curl_init('https://graph.microsoft.com/v1.0/me');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid token']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Parse user data
|
||||
$userData = json_decode($response, true);
|
||||
|
||||
if (!$userData || !isset($userData['id'])) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to retrieve user information']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Start session and store user information
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['user_id'] = $userData['id'];
|
||||
$_SESSION['user_name'] = $userData['displayName'] ?? $userData['userPrincipalName'];
|
||||
$_SESSION['user_email'] = $userData['userPrincipalName'] ?? $userData['mail'];
|
||||
$_SESSION['access_token'] = $accessToken;
|
||||
$_SESSION['last_activity'] = time();
|
||||
$_SESSION['user_files'] = [];
|
||||
|
||||
// Regenerate session ID for security
|
||||
session_regenerate_id(true);
|
||||
|
||||
// Return success
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'user' => [
|
||||
'name' => $_SESSION['user_name'],
|
||||
'email' => $_SESSION['user_email']
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue