From 19222ab36df4b93f0750706488a3d153c5a5b45f Mon Sep 17 00:00:00 2001
From: Vadym Samoilenko
Date: Fri, 20 Mar 2026 17:38:42 +0000
Subject: [PATCH] Fix auth for optical-dev.oliver.solutions/ppt-tool/
deployment
- AZURE_AD_REDIRECT_URI set to https://optical-dev.oliver.solutions/ppt-tool/
- Root page intercepts ?code= from Azure AD and forwards to backend callback
- Post-OAuth redirect uses NEXT_PUBLIC_BASE_PATH env var (/ppt-tool/dashboard)
- Cookie secure flag driven by COOKIE_SECURE env var (true in prod)
- Dev login now works alongside Azure AD when DEV_AUTH_PASSWORD is set
- Login page shows both Microsoft SSO and dev form when both modes enabled
- docker-compose.prod.yml: add COOKIE_SECURE=true and NEXT_PUBLIC_BASE_PATH
Co-Authored-By: Claude Sonnet 4.6
---
.env.example | 2 +-
backend/api/v1/auth/router.py | 22 +++++++++++++++-------
backend/services/auth_service.py | 9 +++++++--
docker-compose.prod.yml | 2 ++
frontend/app/login/page.tsx | 24 ++++++++++++++++++------
frontend/app/page.tsx | 17 ++++++++++++++++-
frontend/store/slices/authSlice.ts | 18 ++++++++++++++----
7 files changed, 73 insertions(+), 21 deletions(-)
diff --git a/.env.example b/.env.example
index 0cb83e9..e1ad6b5 100644
--- a/.env.example
+++ b/.env.example
@@ -8,7 +8,7 @@ REDIS_URL=redis://redis:6379/0
AZURE_AD_TENANT_ID=
AZURE_AD_CLIENT_ID=
AZURE_AD_CLIENT_SECRET=
-AZURE_AD_REDIRECT_URI=http://localhost/api/v1/auth/callback
+AZURE_AD_REDIRECT_URI=https://yourdomain.com/api/v1/auth/callback
# JWT
JWT_SECRET_KEY=change-me-to-a-random-256-bit-key
diff --git a/backend/api/v1/auth/router.py b/backend/api/v1/auth/router.py
index 20de54c..e1784ae 100644
--- a/backend/api/v1/auth/router.py
+++ b/backend/api/v1/auth/router.py
@@ -1,3 +1,4 @@
+import os
import uuid
from fastapi import APIRouter, Depends, HTTPException, Response, Request
from fastapi.responses import RedirectResponse, JSONResponse
@@ -20,8 +21,12 @@ class DevLoginRequest(BaseModel):
@AUTH_ROUTER.get("/dev-status")
async def dev_status():
- """Check if dev auth mode is enabled."""
- return {"dev_mode": auth_service.is_dev_mode}
+ """Check auth modes available."""
+ return {
+ "dev_mode": auth_service.is_dev_mode,
+ "dev_login_enabled": auth_service.dev_login_enabled,
+ "azure_enabled": not auth_service.is_dev_mode,
+ }
@AUTH_ROUTER.get("/login")
@@ -63,12 +68,14 @@ async def callback(
user = await auth_service.get_or_create_user(claims, session)
token = auth_service.create_session_jwt(user)
- response = RedirectResponse(url="/dashboard", status_code=302)
+ base_path = os.environ.get("NEXT_PUBLIC_BASE_PATH", "")
+ is_https = os.environ.get("COOKIE_SECURE", "false").lower() == "true"
+ response = RedirectResponse(url=f"{base_path}/dashboard", status_code=302)
response.set_cookie(
key="session_token",
value=token,
httponly=True,
- secure=False, # Set True in production with HTTPS
+ secure=is_https,
samesite="lax",
max_age=86400, # 24 hours
)
@@ -86,8 +93,8 @@ async def dev_login(
body: DevLoginRequest,
session: AsyncSession = Depends(get_async_session),
):
- """Dev-mode login with email and password. Only available when Azure AD is not configured."""
- if not auth_service.is_dev_mode:
+ """Dev login with email and password. Available when DEV_AUTH_PASSWORD is set."""
+ if not auth_service.dev_login_enabled:
raise HTTPException(status_code=404, detail="Dev login not available")
user = await auth_service.dev_login(body.email, body.password, session)
@@ -96,6 +103,7 @@ async def dev_login(
token = auth_service.create_session_jwt(user)
+ is_https = os.environ.get("COOKIE_SECURE", "false").lower() == "true"
response = JSONResponse(
content={
"id": str(user.id),
@@ -108,7 +116,7 @@ async def dev_login(
key="session_token",
value=token,
httponly=True,
- secure=False,
+ secure=is_https,
samesite="lax",
max_age=86400,
)
diff --git a/backend/services/auth_service.py b/backend/services/auth_service.py
index 20b505a..d3b7e5d 100644
--- a/backend/services/auth_service.py
+++ b/backend/services/auth_service.py
@@ -35,6 +35,11 @@ class AuthService:
def is_dev_mode(self) -> bool:
return not self.tenant_id
+ @property
+ def dev_login_enabled(self) -> bool:
+ """Dev login is available whenever DEV_AUTH_PASSWORD is set, regardless of Azure AD config."""
+ return bool(self.dev_password)
+
@property
def msal_app(self):
if self._msal_app is None and not self.is_dev_mode:
@@ -148,8 +153,8 @@ class AuthService:
return user
async def dev_login(self, email: str, password: str, session) -> Optional[UserModel]:
- """Dev-mode login: validate password, get or create user."""
- if not self.is_dev_mode:
+ """Dev login: validate password, get or create user. Works even when Azure AD is configured."""
+ if not self.dev_login_enabled:
return None
if password != self.dev_password:
return None
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index df8b647..a670e5c 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -22,6 +22,8 @@ services:
- "127.0.0.1:${API_PORT:-8000}:8000"
environment:
PYTHONUNBUFFERED: "1"
+ NEXT_PUBLIC_BASE_PATH: "/ppt-tool"
+ COOKIE_SECURE: "true"
worker:
environment:
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx
index 52d10d6..d16834d 100644
--- a/frontend/app/login/page.tsx
+++ b/frontend/app/login/page.tsx
@@ -4,17 +4,18 @@ import { useEffect, useState } from 'react';
import { apiFetch } from '../../lib/apiFetch';
import { useSelector } from 'react-redux';
import { RootState } from '@/store/store';
-import { useRouter } from 'next/navigation';
+import { useRouter, useSearchParams } from 'next/navigation';
export default function LoginPage() {
const router = useRouter();
- const { isAuthenticated, isDevMode } = useSelector(
+ const searchParams = useSearchParams();
+ const { isAuthenticated, isDevMode, devLoginEnabled, azureEnabled } = useSelector(
(state: RootState) => state.auth
);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
- const [error, setError] = useState('');
+ const [error, setError] = useState(searchParams.get('error') || '');
const [loading, setLoading] = useState(false);
useEffect(() => {
@@ -74,7 +75,7 @@ export default function LoginPage() {
- {!isDevMode && (
+ {azureEnabled && (