Add Microsoft Azure AD SSO authentication

- Integrated MSAL authentication for web pages
- Added AuthMiddleware.php for SSO orchestration
- Added JWTValidator.php for token validation
- Protected report.php and webhook_caller.php
- Firebase PHP-JWT for token verification
- SSO can be disabled for local development
- Complete SSO setup documentation
- Environment-based configuration
This commit is contained in:
Dave Porter 2026-01-07 12:43:42 -05:00
parent d969cc384d
commit fe60d87dcb
11 changed files with 1178 additions and 4 deletions

View file

@ -12,3 +12,9 @@ SENDER_EMAIL=reports@your-domain.com
# Email Recipients (comma-separated)
REPORT_RECIPIENTS=user1@example.com,user2@example.com
# SSO Configuration (Microsoft Azure AD / MSAL)
# Set SSO_ENABLED=true for production, false for local development without SSO
SSO_ENABLED=true
SSO_TENANT_ID=your-azure-tenant-id
SSO_CLIENT_ID=your-azure-client-id

409
AuthMiddleware.php Normal file
View file

@ -0,0 +1,409 @@
<?php
/**
* Authentication Middleware for MSAL / Azure AD
* Central authentication orchestrator with login UI
*/
require_once __DIR__ . '/config.php';
class AuthMiddleware {
private $validator;
private $tenantId;
private $clientId;
private $ssoEnabled;
public function __construct() {
$this->ssoEnabled = SSO_ENABLED;
$this->tenantId = SSO_TENANT_ID;
$this->clientId = SSO_CLIENT_ID;
// Only initialize validator if SSO is enabled
if ($this->ssoEnabled) {
require_once __DIR__ . '/JWTValidator.php';
$this->validator = new JWTValidator($this->tenantId, $this->clientId);
}
}
/**
* Check if SSO is enabled
*
* @return bool
*/
public function isSSOEnabled() {
return $this->ssoEnabled;
}
/**
* Check if user is authenticated
*
* @return array ['authenticated' => bool, 'user' => array|null, 'error' => string|null]
*/
public function isAuthenticated() {
// If SSO is disabled, return authenticated with mock user
if (!$this->ssoEnabled) {
return [
'authenticated' => true,
'user' => [
'name' => 'Local Developer',
'preferred_username' => 'dev@localhost'
]
];
}
// Get token from cookie
$token = $this->getTokenFromCookie();
if (!$token) {
return ['authenticated' => false, 'error' => 'No authentication token found'];
}
// Validate token
$validation = $this->validator->validateToken($token);
if (!$validation['valid']) {
return ['authenticated' => false, 'error' => $validation['error']];
}
return ['authenticated' => true, 'user' => $validation['payload']];
}
/**
* Require authentication - blocks if not authenticated
*
* @return array User data
*/
public function requireAuth() {
// If SSO is disabled, return mock user
if (!$this->ssoEnabled) {
return [
'name' => 'Local Developer',
'preferred_username' => 'dev@localhost'
];
}
// Check authentication
$auth = $this->isAuthenticated();
if (!$auth['authenticated']) {
$this->handleUnauthorized($auth['error'] ?? 'Authentication required');
exit;
}
return $auth['user'];
}
/**
* Set authentication token (after login)
*
* @param string $token JWT token from MSAL
* @return array ['success' => bool, 'user' => array|null, 'error' => string|null]
*/
public function setAuthToken($token) {
if (!$this->ssoEnabled) {
return ['success' => false, 'error' => 'SSO is disabled'];
}
// Validate token
$validation = $this->validator->validateToken($token);
if (!$validation['valid']) {
return ['success' => false, 'error' => $validation['error']];
}
// Set httpOnly cookie with security options
$cookieOptions = [
'expires' => time() + (24 * 60 * 60), // 24 hours
'path' => '/',
'domain' => '',
'secure' => isset($_SERVER['HTTPS']), // Only over HTTPS in production
'httponly' => true,
'samesite' => 'Lax'
];
setcookie('auth_token', $token, $cookieOptions);
return ['success' => true, 'user' => $validation['payload']];
}
/**
* Clear authentication token (logout)
*/
public function clearAuthToken() {
setcookie('auth_token', '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true
]);
}
/**
* Get token from cookie
*
* @return string|null
*/
private function getTokenFromCookie() {
return isset($_COOKIE['auth_token']) ? $_COOKIE['auth_token'] : null;
}
/**
* Handle unauthorized access
*
* @param string $error Error message
*/
private function handleUnauthorized($error) {
if ($this->isAjaxRequest()) {
// For AJAX requests, return JSON
header('Content-Type: application/json');
http_response_code(401);
echo json_encode([
'error' => 'Authentication required',
'message' => $error,
'requiresAuth' => true
]);
} else {
// For page requests, show login interface
$this->showLoginPage($error);
}
}
/**
* Check if request is AJAX
*
* @return bool
*/
private function isAjaxRequest() {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
/**
* Show login page with MSAL integration
*
* @param string $error Optional error message
*/
public function showLoginPage($error = '') {
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign In - VEO3 Usage Report</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://alcdn.msauth.net/browser/2.15.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Montserrat', sans-serif;
background: #000;
color: #fff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
background: #1a1a1a;
border: 2px solid #FFC407;
border-radius: 12px;
padding: 50px 40px;
max-width: 450px;
width: 100%;
text-align: center;
box-shadow: 0 10px 50px rgba(255, 196, 7, 0.2);
}
.login-logo {
font-size: 4rem;
margin-bottom: 20px;
}
.login-title {
font-size: 2rem;
font-weight: 700;
color: #FFC407;
margin-bottom: 10px;
}
.login-subtitle {
font-size: 1rem;
color: #999;
margin-bottom: 40px;
}
.error-message {
background: #ff4444;
color: #fff;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 0.9rem;
}
.btn-login {
background: linear-gradient(135deg, #FFC407 0%, #ff9500 100%);
color: #000;
border: none;
padding: 18px 40px;
border-radius: 8px;
font-family: 'Montserrat', sans-serif;
font-weight: 700;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.3s;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.btn-login:hover {
transform: translateY(-3px);
box-shadow: 0 5px 20px rgba(255, 196, 7, 0.4);
}
.btn-login:active {
transform: translateY(-1px);
}
.loading {
display: none;
}
.loading.show {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #000;
border-top: 3px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.info-text {
margin-top: 30px;
font-size: 0.85rem;
color: #666;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-logo">📊</div>
<h1 class="login-title">VEO3 Usage Report</h1>
<p class="login-subtitle">Video Generation Analytics Dashboard</p>
<?php if ($error): ?>
<div class="error-message">
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<button class="btn-login" onclick="signIn()" id="loginBtn">
<span id="lockIcon">🔒</span>
<span id="loadingIcon" class="loading"></span>
<span id="btnText">Sign In with Microsoft</span>
</button>
<div class="info-text">
Sign in with your Microsoft account to access usage reports
</div>
</div>
<script>
const msalConfig = {
auth: {
clientId: "<?php echo $this->clientId; ?>",
authority: "https://login.microsoftonline.com/<?php echo $this->tenantId; ?>",
redirectUri: window.location.origin + window.location.pathname
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: true,
}
};
const loginRequest = {
scopes: ["openid", "profile", "email"],
prompt: "select_account"
};
const myMSALObj = new msal.PublicClientApplication(msalConfig);
function signIn() {
const loginBtn = document.getElementById('loginBtn');
const lockIcon = document.getElementById('lockIcon');
const loadingIcon = document.getElementById('loadingIcon');
const btnText = document.getElementById('btnText');
// Show loading state
loginBtn.disabled = true;
lockIcon.style.display = 'none';
loadingIcon.classList.add('show');
btnText.textContent = 'Signing in...';
myMSALObj.loginPopup(loginRequest)
.then(loginResponse => {
// Send token to server for validation and cookie setting
fetch('auth.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'login',
idToken: loginResponse.idToken,
accessToken: loginResponse.accessToken
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Authentication failed: ' + (data.error || 'Unknown error'));
resetButton();
}
})
.catch(error => {
console.error('Authentication error:', error);
alert('Authentication failed. Please try again.');
resetButton();
});
})
.catch(error => {
console.error("Login failed:", error);
alert('Login failed: ' + error.message);
resetButton();
});
}
function resetButton() {
const loginBtn = document.getElementById('loginBtn');
const lockIcon = document.getElementById('lockIcon');
const loadingIcon = document.getElementById('loadingIcon');
const btnText = document.getElementById('btnText');
loginBtn.disabled = false;
lockIcon.style.display = 'inline';
loadingIcon.classList.remove('show');
btnText.textContent = 'Sign In with Microsoft';
}
</script>
</body>
</html>
<?php
}
}

201
JWTValidator.php Normal file
View file

@ -0,0 +1,201 @@
<?php
/**
* JWT Token Validator for Azure AD / MSAL
* Validates JWT tokens from Microsoft Azure AD using JWKS
*/
// Load Composer autoload for Firebase JWT library
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
class JWTValidator {
private $tenantId;
private $clientId;
private $jwksCache = null;
private $jwksCacheTime = 0;
private $jwksCacheDuration = 3600; // Cache for 1 hour
public function __construct($tenantId, $clientId) {
$this->tenantId = $tenantId;
$this->clientId = $clientId;
}
/**
* Validate a JWT token
*
* @param string $token The JWT token to validate
* @return array ['valid' => bool, 'payload' => array|null, 'error' => string|null]
*/
public function validateToken($token) {
if (empty($token)) {
return ['valid' => false, 'error' => 'Token is empty'];
}
try {
// Get public keys from Azure AD
$jwks = $this->getJWKS();
if (!$jwks) {
return ['valid' => false, 'error' => 'Could not retrieve public keys from Azure AD'];
}
// Convert JWKS to Key objects
$keys = JWK::parseKeySet($jwks);
// Decode and validate the JWT
$decoded = JWT::decode($token, $keys);
$payload = (array) $decoded;
// Validate claims
$validation = $this->validateClaims($payload);
if (!$validation['valid']) {
return $validation;
}
return ['valid' => true, 'payload' => $payload];
} catch (\Firebase\JWT\ExpiredException $e) {
return ['valid' => false, 'error' => 'Token has expired'];
} catch (\Firebase\JWT\SignatureInvalidException $e) {
return ['valid' => false, 'error' => 'Token signature is invalid'];
} catch (\Firebase\JWT\BeforeValidException $e) {
return ['valid' => false, 'error' => 'Token is not yet valid'];
} catch (Exception $e) {
return ['valid' => false, 'error' => 'JWT validation failed: ' . $e->getMessage()];
}
}
/**
* Validate JWT claims
*
* @param array $payload The decoded JWT payload
* @return array ['valid' => bool, 'error' => string|null]
*/
private function validateClaims($payload) {
$now = time();
// Check expiration (exp claim)
if (isset($payload['exp']) && $payload['exp'] < $now) {
return ['valid' => false, 'error' => 'Token has expired'];
}
// Check not-before (nbf claim)
if (isset($payload['nbf']) && $payload['nbf'] > $now) {
return ['valid' => false, 'error' => 'Token is not yet valid'];
}
// Validate audience (aud claim) - must be our client ID or Microsoft Graph
if (isset($payload['aud'])) {
$validAudiences = [
$this->clientId,
'00000003-0000-0000-c000-000000000000', // Microsoft Graph
'https://graph.microsoft.com'
];
if (!in_array($payload['aud'], $validAudiences)) {
return ['valid' => false, 'error' => 'Invalid audience: ' . $payload['aud']];
}
}
// Validate issuer (iss claim) - must be from Azure AD tenant
if (isset($payload['iss'])) {
$validIssuers = [
"https://login.microsoftonline.com/{$this->tenantId}/v2.0",
"https://login.microsoftonline.com/{$this->tenantId}/",
"https://sts.windows.net/{$this->tenantId}/",
"https://login.microsoftonline.com/common/v2.0"
];
if (!in_array($payload['iss'], $validIssuers)) {
return ['valid' => false, 'error' => 'Invalid issuer: ' . $payload['iss']];
}
}
return ['valid' => true];
}
/**
* Get JWKS (JSON Web Key Set) from Azure AD
*
* @return array|null
*/
private function getJWKS() {
// Return cached JWKS if still valid
if ($this->jwksCache && (time() - $this->jwksCacheTime) < $this->jwksCacheDuration) {
return $this->jwksCache;
}
// Get OpenID configuration from Azure AD
$configUrl = "https://login.microsoftonline.com/{$this->tenantId}/v2.0/.well-known/openid-configuration";
$config = $this->fetchJson($configUrl);
if (!$config || !isset($config['jwks_uri'])) {
error_log("Failed to get OpenID configuration from: $configUrl");
return null;
}
// Fetch JWKS from jwks_uri
$jwks = $this->fetchJson($config['jwks_uri']);
if (!$jwks || !isset($jwks['keys'])) {
error_log("Failed to get JWKS from: " . $config['jwks_uri']);
return null;
}
// Ensure all keys have the 'alg' parameter
foreach ($jwks['keys'] as &$key) {
if (!isset($key['alg'])) {
// Default to RS256 for RSA keys
if (isset($key['kty']) && $key['kty'] === 'RSA') {
$key['alg'] = 'RS256';
}
}
}
// Cache the JWKS
$this->jwksCache = $jwks;
$this->jwksCacheTime = time();
return $jwks;
}
/**
* Fetch JSON from a URL
*
* @param string $url
* @return array|null
*/
private function fetchJson($url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
error_log("CURL Error fetching $url: $curlError");
return null;
}
if ($httpCode !== 200) {
error_log("HTTP Error $httpCode fetching $url");
return null;
}
$json = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("JSON decode error fetching $url: " . json_last_error_msg());
return null;
}
return $json;
}
}

View file

@ -1,10 +1,11 @@
# VEO3 Usage Report System
Automated reporting system for VEO3 video generation usage analytics.
Automated reporting system for VEO3 video generation usage analytics with Microsoft Azure AD Single Sign-On.
## Features
- **PHP Web Interface**: Interactive dashboard for viewing usage reports
- **SSO Authentication**: Microsoft Azure AD (MSAL) integration for secure access
- **Python Automation**: Automated daily/weekly/monthly email reports via SMTP (Mailgun)
- **Granular Analytics**:
- Last 24 hours activity
@ -15,14 +16,31 @@ Automated reporting system for VEO3 video generation usage analytics.
## Setup
### PHP Web Reports
### PHP Web Reports with SSO
1. Start PHP development server:
1. **Install PHP dependencies:**
```bash
composer install
```
2. **Configure SSO (see [SSO-SETUP.md](SSO-SETUP.md)):**
```bash
cp .env.example .env
nano .env # Add Azure AD tenant and client IDs
```
3. **For local development (disable SSO):**
```bash
# In .env
SSO_ENABLED=false
```
4. **Start PHP development server:**
```bash
php -S localhost:8000
```
2. Access the interfaces:
5. **Access the interfaces:**
- `http://localhost:8000/webhook_caller.php` - Fetch data from webhook
- `http://localhost:8000/report.php` - View full report
@ -134,6 +152,15 @@ The Python script:
### Configuration
- `.env.example` - Configuration template
- `.env` - Your configuration (not in git)
- `config.php` - SSO and session configuration
- `env_loader.php` - Environment variable loader
### SSO Files
- `AuthMiddleware.php` - Main authentication class
- `JWTValidator.php` - JWT token validator
- `auth.php` - Authentication API endpoint
- `composer.json` - PHP dependencies
- `SSO-SETUP.md` - Complete SSO setup guide
### Generated Files
- `webhook_response.json` - Cached webhook data (auto-generated)

318
SSO-SETUP.md Normal file
View file

@ -0,0 +1,318 @@
# SSO Setup Guide - Microsoft Azure AD (MSAL)
The VEO3 Report System uses Microsoft Azure AD for Single Sign-On authentication. This protects both the web dashboard and webhook caller pages.
## Prerequisites
- Microsoft Azure AD tenant
- Permission to register applications in Azure AD
- PHP 7.4 or higher
- Composer (PHP package manager)
## Setup Steps
### 1. Install PHP Dependencies
```bash
cd /path/to/veo3-report
composer install
```
This installs the `firebase/php-jwt` library required for token validation.
### 2. Register Application in Azure AD
1. **Login to Azure Portal**: https://portal.azure.com
2. **Navigate to**: Azure Active Directory → App registrations → New registration
3. **Register the application:**
- **Name**: VEO3 Usage Report
- **Supported account types**: Accounts in this organizational directory only
- **Redirect URI**: Web → `https://your-domain.com/report.php` (and add `/webhook_caller.php`)
- Click **Register**
4. **Note these values:**
- **Application (client) ID**: Copy this
- **Directory (tenant) ID**: Copy this
5. **Configure Authentication:**
- Go to **Authentication** in left menu
- Under **Implicit grant and hybrid flows**:
- ✅ Check **ID tokens**
- ✅ Check **Access tokens**
- Click **Save**
6. **Configure API Permissions:**
- Go to **API permissions** in left menu
- Should already have **Microsoft Graph****User.Read** (delegated)
- This is sufficient for SSO
### 3. Configure Environment Variables
Edit your `.env` file:
```bash
# SSO Configuration
SSO_ENABLED=true
SSO_TENANT_ID=your-tenant-id-here
SSO_CLIENT_ID=your-client-id-here
```
**For Local Development (disable SSO):**
```bash
SSO_ENABLED=false
```
When SSO is disabled, you'll be logged in as "Local Developer" automatically.
### 4. Deploy to Server
If deploying with the automated script:
```bash
# Ensure .env is configured
nano .env
# Run deployment
sudo ./deploy.sh
```
The deployment script will:
- Copy SSO files
- Install Composer dependencies
- Set up the systemd service
### 5. Configure Web Server
#### Apache (.htaccess)
```apache
# Enable PHP
AddHandler application/x-httpd-php .php
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
# Deny access to sensitive files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
<Files "composer.json">
Require all denied
</Files>
<Files "composer.lock">
Require all denied
</Files>
```
#### Nginx
```nginx
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to sensitive files
location ~ /\. {
deny all;
}
location ~ composer\.(json|lock)$ {
deny all;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
```
## Testing SSO
### 1. Test Locally (SSO Disabled)
```bash
# In .env
SSO_ENABLED=false
# Start PHP server
php -S localhost:8000
# Visit http://localhost:8000/report.php
# Should show reports without login
```
### 2. Test with SSO Enabled
```bash
# In .env
SSO_ENABLED=true
SSO_TENANT_ID=your-tenant-id
SSO_CLIENT_ID=your-client-id
# Visit https://your-domain.com/report.php
# Should redirect to Microsoft login
```
## How It Works
### Authentication Flow
1. **User visits protected page** (report.php or webhook_caller.php)
2. **AuthMiddleware checks** for valid authentication cookie
3. **If not authenticated:**
- Shows Microsoft login page
- User authenticates with Azure AD
- MSAL returns JWT token
- Token sent to `auth.php` for validation
- Valid token stored in secure cookie
- Page reloads with authenticated session
4. **If authenticated:**
- JWT token validated on each request
- User information available in `$user` variable
### Security Features
- ✅ **JWT validation** with Azure AD public keys
- ✅ **HttpOnly cookies** (not accessible to JavaScript)
- ✅ **Token expiration** checked (24-hour lifetime)
- ✅ **Audience validation** (ensures token is for this app)
- ✅ **Issuer validation** (ensures token from correct Azure tenant)
- ✅ **SameSite cookie** attribute (CSRF protection)
- ✅ **Secure cookies** over HTTPS in production
### File Structure
```
veo3-report/
├── AuthMiddleware.php # Main authentication class
├── JWTValidator.php # JWT token validator
├── auth.php # Authentication API endpoint
├── config.php # SSO configuration
├── env_loader.php # .env file loader
├── report.php # Protected: requires auth
├── webhook_caller.php # Protected: requires auth
└── composer.json # PHP dependencies
```
## Troubleshooting
### "Could not retrieve public keys from Azure AD"
**Cause**: Network issue or invalid tenant ID
**Fix**:
- Verify `SSO_TENANT_ID` in `.env`
- Check server can reach `login.microsoftonline.com`
- Check PHP error logs: `tail -f /var/log/apache2/error.log`
### "Token signature is invalid"
**Cause**: Token from wrong tenant or client
**Fix**:
- Verify `SSO_CLIENT_ID` matches Azure app registration
- Clear browser cookies and try again
- Ensure redirect URI matches exactly in Azure
### "Invalid audience"
**Cause**: Token not intended for this application
**Fix**:
- Verify `SSO_CLIENT_ID` in `.env`
- Check Azure app registration client ID
### Login popup blocked
**Cause**: Browser blocking popups
**Fix**:
- Allow popups for your domain
- Try different browser
### Token expired
**Cause**: Token older than 24 hours
**Fix**:
- Clear cookies and login again
- Automatic redirect to login should occur
## Disabling SSO
To temporarily disable SSO (for maintenance or testing):
```bash
# In .env
SSO_ENABLED=false
# Restart web server/PHP-FPM
sudo systemctl restart apache2
# or
sudo systemctl restart nginx
```
When disabled:
- No login required
- Mock user "Local Developer" used
- Full access to all features
## Adding More Protected Pages
To protect additional PHP pages:
```php
<?php
// At the top of your PHP file
require_once __DIR__ . '/AuthMiddleware.php';
$auth = new AuthMiddleware();
$user = $auth->requireAuth();
// User is authenticated, $user contains:
// - name: Display name
// - preferred_username: Email address
?>
```
## User Logout
Add a logout link:
```html
<a href="javascript:void(0)" onclick="logout()">Logout</a>
<script>
function logout() {
fetch('auth.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'logout'})
})
.then(() => {
window.location.reload();
});
}
</script>
```
## Production Checklist
- [ ] Azure app registered with correct redirect URIs
- [ ] `.env` configured with correct tenant/client IDs
- [ ] `SSO_ENABLED=true` in `.env`
- [ ] Composer dependencies installed
- [ ] HTTPS enabled (required for production)
- [ ] Security headers configured in web server
- [ ] Error logs monitored for auth issues
- [ ] Test login/logout flow
## Support
For SSO issues:
1. Check PHP error logs
2. Check browser console for JavaScript errors
3. Verify Azure app configuration
4. Test with `SSO_ENABLED=false` to isolate issue

118
auth.php Normal file
View file

@ -0,0 +1,118 @@
<?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'
]);
}
}

20
composer.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "veo3-report/usage-analytics",
"description": "VEO3 Usage Report System with MSAL Authentication",
"type": "project",
"require": {
"php": ">=7.4",
"firebase/php-jwt": "^6.0"
},
"require-dev": {},
"autoload": {
"classmap": [
"JWTValidator.php",
"AuthMiddleware.php"
]
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}

30
config.php Normal file
View file

@ -0,0 +1,30 @@
<?php
/**
* Configuration file for VEO3 Usage Report System
*/
// Load environment variables from .env file
if (file_exists(__DIR__ . '/env_loader.php')) {
require_once __DIR__ . '/env_loader.php';
}
// MSAL / Azure AD SSO Configuration
// Set SSO_ENABLED to false for local development, true for production with SSO
if (!defined('SSO_ENABLED')) {
define('SSO_ENABLED', getenv('SSO_ENABLED') === 'true');
}
if (!defined('SSO_TENANT_ID')) {
define('SSO_TENANT_ID', getenv('SSO_TENANT_ID') ?: '');
}
if (!defined('SSO_CLIENT_ID')) {
define('SSO_CLIENT_ID', getenv('SSO_CLIENT_ID') ?: '');
}
// Session configuration
ini_set('session.gc_maxlifetime', 3600); // 1 hour
ini_set('session.cookie_lifetime', 3600);
// Error reporting (set to 0 in production)
error_reporting(E_ALL);
ini_set('display_errors', 0); // Disabled for production
ini_set('log_errors', 1);

35
env_loader.php Normal file
View file

@ -0,0 +1,35 @@
<?php
/**
* Simple .env file loader
* Loads environment variables from .env file into $_ENV and getenv()
*/
$envFile = __DIR__ . '/.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// Skip comments
if (strpos(trim($line), '#') === 0) {
continue;
}
// Parse KEY=VALUE
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
// Remove quotes if present
if (preg_match('/^(["\'])(.*)\\1$/', $value, $matches)) {
$value = $matches[2];
}
// Set environment variable
putenv("$key=$value");
$_ENV[$key] = $value;
$_SERVER[$key] = $value;
}
}
}

View file

@ -3,6 +3,11 @@
$responseFile = 'webhook_response.json';
$costPerVideo = 3.20; // Veo 3.1 Standard: $3.20 per 8-second video
// SSO Authentication
require_once __DIR__ . '/AuthMiddleware.php';
$auth = new AuthMiddleware();
$user = $auth->requireAuth(); // This will redirect to login if not authenticated
// Load and parse JSON
if (!file_exists($responseFile)) {
die("Error: Response file not found. Please run webhook_caller.php first.");

View file

@ -1,4 +1,9 @@
<?php
// SSO Authentication
require_once __DIR__ . '/AuthMiddleware.php';
$auth = new AuthMiddleware();
$user = $auth->requireAuth(); // This will redirect to login if not authenticated
// Configuration
$webhookUrl = 'https://hook.us1.make.celonis.com/u8i4yq6rydu8u8g9bfhk0xbajsyckrmj';
$responseFile = 'webhook_response.json';