MSAL and Security Update

This commit is contained in:
Manish Tanwar 2025-12-09 20:11:35 +05:30
parent fc3e43bdc5
commit eebe7c41aa
12 changed files with 580 additions and 4 deletions

View file

@ -1,3 +1,15 @@
# Google Gemini API Key
# Get your API key from: https://makersuite.google.com/app/apikey
VITE_GEMINI_API_KEY=your_key_here
VITE_GEMINI_API_KEY=AIzaSyCMKLSJJYEx4c6-TtBACnjdULrLzsr_fts
# Microsoft SSO Configuration Local Development Credentials
# Client ID and Tenant ID from Azure AD App Registration
# VITE_MSAL_CLIENT_ID=15c0c4e2-bac0-4564-a3a6-c2717f00a6d9
# VITE_MSAL_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
# VITE_MSAL_REDIRECT_URI=http://localhost:3000
# Redirect URI - CONFIGURE FOR PRODUCTION DEPLOYMENT
VITE_MSAL_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
VITE_MSAL_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
VITE_MSAL_REDIRECT_URI=https://ai-sandbox.oliver.solutions/prompt/index.html

24
.gitignore vendored
View file

@ -11,10 +11,15 @@ node_modules
dist
dist-ssr
*.local
.claude/
# Environment variables
# Environment variables (NEVER commit these - contain API keys and secrets)
.env
.env.local
.env.development
.env.production
.env*.local
# Editor directories and files
.vscode/*
@ -30,3 +35,20 @@ dist-ssr
# Test files and folders
TESTS/
TESTS/*
# Backup files
*.bak
*.backup
*.old
*.orig
*.save
*.swp
*~
# Security and sensitive files
*.key
*.pem
*.p12
*.pfx
credentials.json
secrets.json

122
.htaccess Normal file
View file

@ -0,0 +1,122 @@
# ============================================
# CinePrompt Studio - Apache Security Configuration
# Safe version that works on most servers
# ============================================
# ------------------------------
# 1. PREVENT DIRECTORY LISTING
# ------------------------------
<IfModule mod_autoindex.c>
Options -Indexes
</IfModule>
# ------------------------------
# 2. BLOCK ACCESS TO SENSITIVE FILES
# ------------------------------
# Block .env files (contains API keys)
<Files ".env">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
</Files>
<Files ".env.example">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
</Files>
# Block .git directory
<Files ".git">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
</Files>
<Files ".gitignore">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
</Files>
# Block configuration files
<Files "package.json">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
</Files>
<Files "package-lock.json">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
</Files>
# ------------------------------
# 3. SINGLE PAGE APPLICATION ROUTING (OPTIONAL)
# Only enable if deploying built app (npm run build)
# Comment out if causing issues
# ------------------------------
<IfModule mod_rewrite.c>
RewriteEngine On
# Don't rewrite for actual files and directories
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Redirect everything else to index.html
RewriteRule . /index.html [L]
</IfModule>
# ------------------------------
# 4. BASIC SECURITY HEADERS (OPTIONAL)
# Only if mod_headers is enabled
# ------------------------------
<IfModule mod_headers.c>
# Prevent clickjacking
Header set X-Frame-Options "SAMEORIGIN"
# Prevent MIME sniffing
Header set X-Content-Type-Options "nosniff"
# XSS Protection
Header set X-XSS-Protection "1; mode=block"
</IfModule>
# ------------------------------
# 5. GZIP COMPRESSION (OPTIONAL)
# Only if mod_deflate is enabled
# ------------------------------
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css
AddOutputFilterByType DEFLATE application/javascript text/javascript
AddOutputFilterByType DEFLATE application/json
</IfModule>

36
package-lock.json generated
View file

@ -8,6 +8,8 @@
"name": "cineprompt-studio",
"version": "0.0.0",
"dependencies": {
"@azure/msal-browser": "^4.27.0",
"@azure/msal-react": "^3.0.23",
"@google/generative-ai": "^0.24.1",
"lucide-react": "^0.555.0",
"react": "^19.2.0",
@ -41,6 +43,40 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@azure/msal-browser": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.27.0.tgz",
"integrity": "sha512-bZ8Pta6YAbdd0o0PEaL1/geBsPrLEnyY/RDWqvF1PP9RUH8EMLvUMGoZFYS6jSlUan6KZ9IMTLCnwpWWpQRK/w==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "15.13.3"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "15.13.3",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.3.tgz",
"integrity": "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-react": {
"version": "3.0.23",
"resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-3.0.23.tgz",
"integrity": "sha512-tHvq441nwlJD9QfQP4ZStiw6xb2hQoujNHZhZb+wpUbImb3wyr2FF6/umhX/p+yzc/aq0Lee7mbdDDpzRZzxcA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@azure/msal-browser": "^4.27.0",
"react": "^16.8.0 || ^17 || ^18 || ^19.2.1"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",

View file

@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"@azure/msal-browser": "^4.27.0",
"@azure/msal-react": "^3.0.23",
"@google/generative-ai": "^0.24.1",
"lucide-react": "^0.555.0",
"react": "^19.2.0",

View file

@ -1,7 +1,12 @@
import CinePromptStudio from './components/CinePromptStudio';
import AuthGate from './components/AuthGate';
function App() {
return <CinePromptStudio />;
return (
<AuthGate>
<CinePromptStudio />
</AuthGate>
);
}
export default App;

View file

@ -0,0 +1,58 @@
import { AlertCircle, RefreshCw, LogOut } from 'lucide-react';
function AuthError({ error, onRetry, onLogout }) {
return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-slate-900 rounded-lg shadow-xl p-8 border border-red-500/20">
<div className="flex items-center justify-center mb-6">
<div className="bg-red-500/10 p-4 rounded-full">
<AlertCircle className="w-12 h-12 text-red-500" />
</div>
</div>
<h2 className="text-2xl font-bold text-white text-center mb-4">
Authentication Failed
</h2>
<p className="text-slate-400 text-center mb-6">
{error?.message || "An error occurred during authentication. Please try again."}
</p>
{error?.errorCode && (
<div className="bg-slate-800 rounded p-3 mb-6">
<p className="text-xs text-slate-500 mb-1">Error Code:</p>
<p className="text-sm text-slate-300 font-mono">{error.errorCode}</p>
</div>
)}
<div className="flex flex-col gap-3">
<button
onClick={onRetry}
className="flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors"
>
<RefreshCw className="w-5 h-5" />
Retry Authentication
</button>
{onLogout && (
<button
onClick={onLogout}
className="flex items-center justify-center gap-2 w-full bg-slate-800 hover:bg-slate-700 text-slate-300 font-medium py-3 px-4 rounded-lg transition-colors"
>
<LogOut className="w-5 h-5" />
Clear Session
</button>
)}
</div>
<div className="mt-6 pt-6 border-t border-slate-800">
<p className="text-xs text-slate-500 text-center">
If the problem persists, please contact your administrator or check your network connection.
</p>
</div>
</div>
</div>
);
}
export default AuthError;

191
src/components/AuthGate.jsx Normal file
View file

@ -0,0 +1,191 @@
import { useEffect, useState, useRef } from 'react';
import { useMsal, useIsAuthenticated } from '@azure/msal-react';
import { InteractionStatus } from '@azure/msal-browser';
import { loginRequest } from '../config/msalConfig';
import LoginPage from './LoginPage';
import AuthError from './AuthError';
import { Loader2 } from 'lucide-react';
function AuthGate({ children }) {
const { instance, inProgress } = useMsal();
const isAuthenticated = useIsAuthenticated();
const [error, setError] = useState(null);
const [isInitializing, setIsInitializing] = useState(true);
const initializationAttempted = useRef(false);
useEffect(() => {
const initializeAuth = async () => {
// Prevent multiple initialization attempts
if (initializationAttempted.current) {
return;
}
// Wait for any ongoing interaction to complete
if (inProgress !== InteractionStatus.None) {
return;
}
initializationAttempted.current = true;
try {
// Clear any stale interaction state on mount
const keys = Object.keys(sessionStorage);
keys.forEach(key => {
if (key.includes('interaction.status')) {
sessionStorage.removeItem(key);
}
});
// Wait for MSAL to finish initialization
await instance.initialize();
// Handle redirect promise (for returning from Microsoft login)
const redirectResponse = await instance.handleRedirectPromise();
if (redirectResponse) {
console.log('Login successful via redirect');
setIsInitializing(false);
return;
}
// If not authenticated and no interaction in progress, attempt silent SSO
if (!isAuthenticated) {
const accounts = instance.getAllAccounts();
if (accounts.length > 0) {
try {
// Attempt silent SSO with existing account
console.log('Attempting silent SSO...');
await instance.ssoSilent({
...loginRequest,
account: accounts[0],
});
console.log('Silent SSO successful');
} catch (silentError) {
// Silent SSO failed - user will need to login interactively
console.log('Silent SSO failed:', silentError.errorCode || silentError.message);
// Not setting error here as this is expected for new users
}
}
}
setIsInitializing(false);
} catch (err) {
console.error('Auth initialization error:', err);
setError(err);
setIsInitializing(false);
}
};
initializeAuth();
}, [instance, isAuthenticated, inProgress]);
const clearInteractionState = () => {
try {
// Clear any stale interaction state from session storage
const keys = Object.keys(sessionStorage);
keys.forEach(key => {
if (key.includes('interaction.status')) {
sessionStorage.removeItem(key);
console.log('Cleared stale interaction state:', key);
}
});
} catch (err) {
console.warn('Failed to clear interaction state:', err);
}
};
const handleLogin = async () => {
try {
// Clear any stale interaction state first
clearInteractionState();
// Give MSAL a moment to update its internal state
await new Promise(resolve => setTimeout(resolve, 100));
// Check if interaction is still in progress after clearing
if (inProgress !== InteractionStatus.None) {
console.warn('Authentication still in progress after clearing state. Forcing clear...');
// Force clear and try again
clearInteractionState();
await new Promise(resolve => setTimeout(resolve, 200));
}
setError(null);
console.log('Starting login redirect...');
// Use redirect flow for authentication
await instance.loginRedirect(loginRequest);
} catch (err) {
console.error('Login error:', err);
// If it's an interaction_in_progress error, clear state and set a helpful error
if (err.errorCode === 'interaction_in_progress') {
clearInteractionState();
setError({
...err,
message: 'Authentication was interrupted. Please try again.',
errorCode: 'interaction_in_progress'
});
} else {
setError(err);
}
}
};
const handleRetry = () => {
setError(null);
// Clear interaction state before retrying
clearInteractionState();
// Wait a bit before retrying
setTimeout(() => {
handleLogin();
}, 300);
};
const handleLogout = async () => {
try {
setError(null);
// Clear interaction state before logout
clearInteractionState();
await instance.logoutRedirect();
} catch (err) {
console.error('Logout error:', err);
// Clear all session storage and force page reload
sessionStorage.clear();
window.location.reload();
}
};
// Show loading state during initialization or authentication
if (isInitializing || inProgress !== InteractionStatus.None) {
return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 text-blue-500 animate-spin mx-auto mb-4" />
<p className="text-slate-400">Authenticating...</p>
</div>
</div>
);
}
// Show error if authentication failed
if (error) {
return (
<AuthError
error={error}
onRetry={handleRetry}
onLogout={handleLogout}
/>
);
}
// Show login page if not authenticated
if (!isAuthenticated) {
return <LoginPage onLogin={handleLogin} />;
}
// User is authenticated - render the protected content
return children;
}
export default AuthGate;

View file

@ -0,0 +1,53 @@
import { LogIn, Shield } from 'lucide-react';
function LoginPage({ onLogin }) {
return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center p-4">
<div className="max-w-md w-full">
<div className="text-center mb-8">
<div className="flex items-center justify-center mb-4">
<div className="bg-gradient-to-br from-blue-500 to-purple-600 p-4 rounded-2xl shadow-lg">
<Shield className="w-12 h-12 text-white" />
</div>
</div>
<h1 className="text-4xl font-bold text-white mb-2">
Prompt Studio
</h1>
<p className="text-slate-400">
AI-Powered Cinematography Prompts
</p>
</div>
<div className="bg-slate-900 rounded-lg shadow-xl p-8 border border-slate-800">
<h2 className="text-xl font-semibold text-white mb-2 text-center">
Welcome
</h2>
<p className="text-slate-400 text-center mb-6 text-sm">
Sign in with your Microsoft account to continue
</p>
<button
onClick={onLogin}
className="flex items-center justify-center gap-3 w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-medium py-4 px-6 rounded-lg transition-all transform hover:scale-[1.02] shadow-lg"
>
<LogIn className="w-5 h-5" />
Sign in with Microsoft
</button>
<div className="mt-6 pt-6 border-t border-slate-800">
<div className="flex items-center gap-2 text-xs text-slate-500">
<Shield className="w-4 h-4" />
<span>Secured by Microsoft Azure Active Directory</span>
</div>
</div>
</div>
<p className="text-xs text-slate-600 text-center mt-6">
By signing in, you agree to use this application in accordance with your organization's policies.
</p>
</div>
</div>
);
}
export default LoginPage;

63
src/config/msalConfig.js Normal file
View file

@ -0,0 +1,63 @@
// MSAL Configuration for Microsoft SSO Authentication
// Environment variables are loaded from .env file (must be prefixed with VITE_)
// Validate required environment variables
const clientId = import.meta.env.VITE_MSAL_CLIENT_ID;
const tenantId = import.meta.env.VITE_MSAL_TENANT_ID;
const redirectUri = import.meta.env.VITE_MSAL_REDIRECT_URI;
if (!clientId || !tenantId || !redirectUri) {
console.error('MSAL Configuration Error: Missing required environment variables');
console.error('Required variables:');
console.error(`- VITE_MSAL_CLIENT_ID: ${clientId ? '✓' : '✗ MISSING'}`);
console.error(`- VITE_MSAL_TENANT_ID: ${tenantId ? '✓' : '✗ MISSING'}`);
console.error(`- VITE_MSAL_REDIRECT_URI: ${redirectUri ? '✓' : '✗ MISSING'}`);
console.error('Please check your .env file and ensure all MSAL variables are set.');
}
export const msalConfig = {
auth: {
clientId: clientId || "",
authority: `https://login.microsoftonline.com/${tenantId || ""}`,
redirectUri: redirectUri || window.location.origin,
},
cache: {
cacheLocation: "sessionStorage", // Store tokens in session storage
storeAuthStateInCookie: true, // Set to true for IE11 or Edge compatibility
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case 0: // Error
console.error('[MSAL Error]', message);
break;
case 1: // Warning
console.warn('[MSAL Warning]', message);
break;
case 2: // Info
console.info('[MSAL Info]', message);
break;
case 3: // Verbose
console.debug('[MSAL Debug]', message);
break;
default:
break;
}
},
},
},
};
// Scopes for login request
export const loginRequest = {
scopes: ["User.Read"], // Basic authentication - read user profile
};
// Optional: Token request for API calls
export const tokenRequest = {
scopes: ["User.Read"],
};

View file

@ -1,10 +1,18 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { PublicClientApplication } from '@azure/msal-browser'
import { MsalProvider } from '@azure/msal-react'
import './index.css'
import App from './App.jsx'
import { msalConfig } from './config/msalConfig'
// Initialize MSAL instance
const msalInstance = new PublicClientApplication(msalConfig);
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
<MsalProvider instance={msalInstance}>
<App />
</MsalProvider>
</StrictMode>,
)

View file

@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
strictPort: true,
},
})