diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dbafee3 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# MSAL Authentication Configuration +# Set SSO_ENABLED=true to require Microsoft login +# Set SSO_ENABLED=false for local development (uses mock user) +SSO_ENABLED=false + +# Azure AD Configuration (required when SSO_ENABLED=true) +# Get these values from your Azure AD App Registration +SSO_TENANT_ID=your-azure-tenant-id-here +SSO_CLIENT_ID=your-azure-application-client-id-here + +# Example values (replace with your actual Azure AD credentials): +# SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385 +# SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef diff --git a/.gitignore b/.gitignore index 8cb01e4..e479e54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ # Configuration file with API key config.php +# Environment configuration +.env + +# Composer dependencies +/vendor/ +composer.lock + # PHP session files sessions/ diff --git a/AUTH_README.md b/AUTH_README.md new file mode 100644 index 0000000..9eeeadf --- /dev/null +++ b/AUTH_README.md @@ -0,0 +1,305 @@ +# MSAL Authentication Setup Guide + +## Overview +Nano Banana Pro now includes Microsoft Authentication Library (MSAL) / Azure AD Single Sign-On (SSO) authentication. The authentication can be **toggled on/off** via environment variable for seamless testing and deployment. + +--- + +## Quick Start + +### Local Development (No Authentication) +```bash +# 1. Ensure .env file exists with: +SSO_ENABLED=false + +# 2. Run the app normally in MAMP +# All users get mock "Local Developer" credentials +# No login required +``` + +### Production (with SSO) +```bash +# 1. Update .env file: +SSO_ENABLED=true +SSO_TENANT_ID=your-azure-tenant-id +SSO_CLIENT_ID=your-azure-application-id + +# 2. Deploy to server +# 3. Users must login with Microsoft account +``` + +--- + +## Installation Steps + +### 1. Install Dependencies +```bash +cd /Users/daveporter/Desktop/CODING-2024/NANO-RESEARCH +composer install +``` + +This installs the Firebase JWT library required for token validation. + +### 2. Configure Environment +```bash +# Copy example file +cp .env.example .env + +# Edit .env and set: +SSO_ENABLED=false # Start with authentication disabled +``` + +### 3. Azure AD Setup (When Enabling SSO) + +#### Create Azure AD App Registration: +1. Go to [Azure Portal](https://portal.azure.com) +2. Navigate to: **Azure Active Directory** → **App registrations** → **New registration** +3. Set name: "Nano Banana Pro" +4. Set redirect URI: `https://your-server-url.com/path/to/app/index.php` +5. Click **Register** + +#### Get Credentials: +1. Copy **Application (client) ID** → This is your `SSO_CLIENT_ID` +2. Copy **Directory (tenant) ID** → This is your `SSO_TENANT_ID` +3. Go to **Authentication** → Enable **ID tokens** checkbox +4. Go to **API permissions** → Add: `openid`, `profile`, `email` + +#### Update .env: +```bash +SSO_ENABLED=true +SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385 +SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef +``` + +--- + +## File Structure + +### New Files Created: +``` +/NANO-RESEARCH/ +├── composer.json # PHP dependencies (Firebase JWT) +├── .env # Environment config (gitignored) +├── .env.example # Template for environment variables +├── env_loader.php # Loads .env file +├── JWTValidator.php # JWT token validation logic +├── AuthMiddleware.php # Auth orchestrator + login UI +├── auth.php # Auth API endpoint +├── auth-test.php # Debugging page +├── AUTH_README.md # This file +└── vendor/ # Composer dependencies (gitignored) +``` + +### Modified Files: +``` +config.php # Added SSO constants +index.php # Added auth check, logout button +api.php # Added auth check +enhance_prompt.php # Added auth check +.gitignore # Added .env and vendor/ +``` + +--- + +## How It Works + +### When SSO_ENABLED=false (Testing Mode) +1. User visits app +2. AuthMiddleware returns mock "Local Developer" user +3. No login page shown +4. All features work normally +5. Perfect for local testing + +### When SSO_ENABLED=true (Production Mode) +1. User visits app +2. AuthMiddleware checks for `auth_token` cookie +3. If no token → Show MSAL login page +4. User clicks "Sign In with Microsoft" +5. MSAL popup opens for Azure AD login +6. User authenticates +7. Token sent to `auth.php` for validation +8. JWT validated against Azure AD public keys +9. Token stored in httpOnly cookie (24 hours) +10. User redirected to app +11. Logout button visible in header + +--- + +## Testing + +### Test Authentication Status +Visit: `http://your-server/auth-test.php` + +Shows: +- SSO configuration (enabled/disabled) +- Tenant ID and Client ID +- Current authentication status +- User information +- Cookie presence + +### Test Locally (SSO Disabled) +```bash +# 1. Set SSO_ENABLED=false in .env +# 2. Open app in MAMP +# 3. Should see "Welcome, Local Developer" (if SSO was previously enabled) +# 4. App functions normally +# 5. No login/logout buttons +``` + +### Test on Server (SSO Enabled) +```bash +# NOTE: Cannot test locally - Azure AD requires exact redirect URI match + +# 1. Deploy to production server +# 2. Set SSO_ENABLED=true in .env on server +# 3. Add Azure AD credentials to .env +# 4. Visit app URL +# 5. Should see login page +# 6. Click "Sign In with Microsoft" +# 7. Complete Microsoft login +# 8. Should redirect to app +# 9. Should see "Welcome, [Your Name]" and logout button +``` + +--- + +## Security Features + +✅ **httpOnly Cookies** - Prevents XSS attacks (JavaScript can't access token) +✅ **SameSite=Lax** - Prevents CSRF attacks +✅ **Secure Flag** - Cookie only sent over HTTPS in production +✅ **JWT Validation** - Cryptographic verification of tokens +✅ **Expiration Check** - Validates `exp` claim +✅ **Not-Before Check** - Validates `nbf` claim +✅ **Audience Validation** - Ensures token is for our app +✅ **Issuer Validation** - Ensures token from Azure AD +✅ **JWKS Verification** - Uses Azure AD public keys +✅ **24-Hour Expiration** - Tokens expire after 1 day + +--- + +## Troubleshooting + +### Login Page Shows But Can't Login +- Check Azure AD app registration has correct redirect URI +- Ensure `SSO_TENANT_ID` and `SSO_CLIENT_ID` are correct +- Check browser console for MSAL errors +- Visit `auth-test.php` to verify configuration + +### "Authentication Required" Error +- Check `auth_token` cookie exists (browser dev tools) +- Token may have expired (24-hour limit) +- Try logging out and back in +- Check `auth-test.php` for token status + +### SSO Not Disabling +- Verify `.env` has `SSO_ENABLED=false` (not "false" in quotes) +- Clear browser cookies +- Restart PHP server/MAMP +- Check `auth-test.php` shows "SSO Enabled: NO" + +### Token Validation Failing +- Check server can reach Azure AD endpoints +- Verify tenant ID and client ID match Azure AD +- Check token hasn't expired +- Review `error_log` for JWT validation details + +--- + +## API Endpoints + +### Login +```http +POST /auth.php +Content-Type: application/json + +{ + "action": "login", + "idToken": "eyJ0eXAiOiJKV1QiLCJhbGci...", + "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGci..." +} +``` + +### Logout +```http +POST /auth.php +Content-Type: application/json + +{ + "action": "logout" +} +``` + +### Status Check +```http +POST /auth.php +Content-Type: application/json + +{ + "action": "status" +} +``` + +--- + +## Maintenance + +### Rotating Credentials +1. Update Azure AD app registration +2. Update `.env` with new credentials +3. No code changes needed +4. Existing sessions remain valid until cookie expires + +### Disabling SSO Temporarily +```bash +# In .env: +SSO_ENABLED=false + +# Immediately disables SSO for all users +# No restart needed +# Users get mock "Local Developer" access +``` + +### Monitoring +- Check `error_log` for authentication failures +- Monitor Azure AD sign-in logs +- Track failed login attempts +- Review token validation errors + +--- + +## Production Checklist + +Before enabling SSO in production: + +- [ ] Composer dependencies installed (`vendor/` directory exists) +- [ ] `.env` file configured with Azure AD credentials +- [ ] Azure AD app registration created +- [ ] Redirect URI matches production URL exactly +- [ ] ID tokens enabled in Azure AD app +- [ ] API permissions added (`openid`, `profile`, `email`) +- [ ] HTTPS enabled on production server +- [ ] `auth-test.php` shows correct configuration +- [ ] Test login/logout flow works +- [ ] Error logging enabled + +--- + +## Support + +For issues with: +- **MSAL errors**: Check [MSAL.js documentation](https://github.com/AzureAD/microsoft-authentication-library-for-js) +- **Azure AD setup**: Check [Azure AD app registration guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) +- **JWT validation**: Check Firebase JWT library logs in `error_log` +- **Configuration**: Run `auth-test.php` to see current setup + +--- + +## Important Notes + +- **Cannot test MSAL locally** - Azure AD requires exact URL match +- **Testing happens on server** after deployment +- **SSO toggle allows testing without auth** before enabling +- **httpOnly cookies** mean token not accessible via JavaScript +- **24-hour token expiration** - users must re-login daily +- **Mock user** (`dev@localhost`) used when SSO disabled diff --git a/AuthMiddleware.php b/AuthMiddleware.php new file mode 100644 index 0000000..092e724 --- /dev/null +++ b/AuthMiddleware.php @@ -0,0 +1,409 @@ +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 = '') { + ?> + + +
+ + +AI Image Generation & Editing Tool
+ + + + + + + +AI Image Generation & Iterative Editing
+AI Image Generation & Iterative Editing
+