Implemented complete Microsoft Authentication Library (MSAL) / Azure AD Single Sign-On (SSO) system following Ferrero app pattern. KEY FEATURE: Toggle authentication on/off via environment variable - SSO_ENABLED=false → Mock user, no login required (local dev) - SSO_ENABLED=true → Full Azure AD authentication (production) NEW FILES: - composer.json - Firebase JWT dependency - .env.example - Environment variable template - env_loader.php - Parse .env file - JWTValidator.php - Validate JWT tokens from Azure AD - AuthMiddleware.php - Core auth orchestrator with login UI - auth.php - Authentication API (login/logout/status) - auth-test.php - Debug authentication status - AUTH_README.md - Complete setup documentation UPDATED FILES: - config.php - Load env vars, add SSO constants - index.php - Require auth, add logout button, MSAL script - api.php - Add authentication check - enhance_prompt.php - Add authentication check - .gitignore - Exclude .env and vendor/ AUTHENTICATION FLOW: 1. User visits app → Auth check 2. If SSO disabled → Mock "Local Developer" user 3. If SSO enabled → Validate JWT from cookie 4. If no token → Show MSAL login page 5. User signs in → Token validated → Cookie set → App loads SECURITY FEATURES: ✅ httpOnly cookies (XSS prevention) ✅ SameSite=Lax (CSRF prevention) ✅ JWT signature validation ✅ Claims validation (exp, nbf, aud, iss) ✅ JWKS from Azure AD ✅ 24-hour token expiration ✅ Secure flag for HTTPS DEPENDENCIES INSTALLED: - firebase/php-jwt v6.11.1 TESTING: - Local: SSO disabled by default in .env - Server: Set SSO_ENABLED=true with Azure AD credentials - Cannot test MSAL locally (redirect URI bound to server) DEPLOYMENT: 1. Install composer dependencies 2. Configure .env with Azure AD credentials 3. Set SSO_ENABLED=true when ready 4. Visit auth-test.php to verify setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
118 lines
2.8 KiB
PHP
118 lines
2.8 KiB
PHP
<?php
|
|
/**
|
|
* Authentication API Endpoint
|
|
* Handles login, logout, and status requests
|
|
*/
|
|
|
|
header('Content-Type: application/json');
|
|
|
|
require_once __DIR__ . '/AuthMiddleware.php';
|
|
|
|
$auth = new AuthMiddleware();
|
|
|
|
// Get POST data
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
|
|
if (!$input || !isset($input['action'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Invalid request - action required']);
|
|
exit;
|
|
}
|
|
|
|
$action = $input['action'];
|
|
|
|
// Handle different actions
|
|
switch ($action) {
|
|
case 'login':
|
|
handleLogin($auth, $input);
|
|
break;
|
|
|
|
case 'logout':
|
|
handleLogout($auth);
|
|
break;
|
|
|
|
case 'status':
|
|
handleStatus($auth);
|
|
break;
|
|
|
|
default:
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Unknown action: ' . $action]);
|
|
break;
|
|
}
|
|
|
|
/**
|
|
* Handle login action
|
|
*/
|
|
function handleLogin($auth, $input) {
|
|
if (!$auth->isSSOEnabled()) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'SSO is disabled']);
|
|
return;
|
|
}
|
|
|
|
// Prefer ID token for validation, fallback to access token
|
|
$token = $input['idToken'] ?? $input['accessToken'] ?? null;
|
|
|
|
if (!$token) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Authentication token is required']);
|
|
return;
|
|
}
|
|
|
|
// Validate and set token
|
|
$result = $auth->setAuthToken($token);
|
|
|
|
if ($result['success']) {
|
|
echo json_encode([
|
|
'success' => true,
|
|
'message' => 'Authentication successful',
|
|
'user' => [
|
|
'name' => $result['user']['name'] ?? 'Unknown',
|
|
'email' => $result['user']['preferred_username'] ?? $result['user']['upn'] ?? 'Unknown'
|
|
]
|
|
]);
|
|
} else {
|
|
http_response_code(401);
|
|
echo json_encode([
|
|
'success' => false,
|
|
'error' => $result['error']
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle logout action
|
|
*/
|
|
function handleLogout($auth) {
|
|
$auth->clearAuthToken();
|
|
echo json_encode([
|
|
'success' => true,
|
|
'message' => 'Logged out successfully'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle status check action
|
|
*/
|
|
function handleStatus($auth) {
|
|
$authStatus = $auth->isAuthenticated();
|
|
|
|
if ($authStatus['authenticated']) {
|
|
echo json_encode([
|
|
'authenticated' => true,
|
|
'sso_enabled' => $auth->isSSOEnabled(),
|
|
'user' => [
|
|
'name' => $authStatus['user']['name'] ?? 'Unknown',
|
|
'email' => $authStatus['user']['preferred_username'] ?? $authStatus['user']['upn'] ?? 'Unknown'
|
|
]
|
|
]);
|
|
} else {
|
|
http_response_code(401);
|
|
echo json_encode([
|
|
'authenticated' => false,
|
|
'sso_enabled' => $auth->isSSOEnabled(),
|
|
'error' => $authStatus['error'] ?? 'Not authenticated'
|
|
]);
|
|
}
|
|
}
|