fix: OIDC Authentication with PKCE for dev bypass

- Fixed operational auth session bug returning False despite valid Mock Session
- Removed dev user ID hardcoding logic ensuring auth flow uses the defined config.
This commit is contained in:
br committed 2026-08-04 13:49:37 +02:00
1 parent 03b0e33723
commit 8498781c85
2 files changed
+165 -17

No files matched your search

+84 -17
View File
@@ -46,10 +46,11 @@ auth_bp = Blueprint('auth', __name__, template_folder='../templates')
@auth_bp.before_request
def check_dev_mode_auth():
"""
Middleware to handle development mode authentication.
Middleware to handle development mode and mock authentication.
If DEV_MODE is enabled and user is not authenticated,
automatically create a dev user session.
If DEV_MODE is enabled and user is not authenticated:
- If MOCK_AUTH is enabled: create mock user session
- Otherwise: create dev user session
"""
# Skip for auth routes themselves
if request.path in ['/auth/login', '/auth/callback', '/auth/logout', '/auth/status']:
@@ -61,6 +62,19 @@ def check_dev_mode_auth():
# In dev mode, auto-authenticate
if AuthService.is_dev_mode():
if AuthService.is_mock_auth_enabled():
# Use mock authentication
mock_user = AuthService.create_mock_user_session()
session['user_id'] = mock_user['id']
session['user'] = mock_user
session['authenticated'] = True
session.permanent = True
# Ensure mock user and tenant exist in database
AuthService.ensure_mock_user_exists()
AuthService.ensure_mock_tenant_exists()
else:
# Use dev mode authentication
dev_user = AuthService.create_dev_user_session()
session['user_id'] = dev_user['id']
session['user'] = dev_user
@@ -75,17 +89,43 @@ def check_dev_mode_auth():
@auth_bp.route('/login')
def login():
"""
Login route - initiates OIDC flow or handles dev mode.
Login route - initiates OIDC flow or handles dev/mock mode.
In production: Redirects to Zitadel for OIDC authentication
In development: Creates fake session and redirects to admin
In development with MOCK_AUTH: Creates mock session and redirects to admin
In development without MOCK_AUTH: Creates fake session and redirects to admin
Query parameters:
next: URL to redirect to after successful login
"""
next_url = request.args.get('next', url_for('admin.dashboard'))
# Development mode: create fake session and redirect
# Mock authentication mode: create mock session and redirect
if AuthService.is_mock_auth_enabled():
mock_user = AuthService.create_mock_user_session()
session.clear()
session['user_id'] = mock_user['id']
session['user'] = mock_user
session['authenticated'] = True
session.permanent = True
# Ensure mock user and tenant exist in database
AuthService.ensure_mock_user_exists()
AuthService.ensure_mock_tenant_exists()
logger.info(f'Mock auth login: {mock_user["email"]}')
AuditLog.log_action(
action='login',
resource_type='auth',
user=User.query.get(mock_user['id']),
tenant=Tenant.query.get(mock_user['tenant_id']),
details={'mode': 'mock', 'mock_auth': True, 'dev_mode': True}
)
return redirect(next_url)
# Development mode (without MOCK_AUTH): create fake session and redirect
if AuthService.is_dev_mode():
dev_user = AuthService.create_dev_user_session()
session.clear()
@@ -122,16 +162,31 @@ def login():
# Configure Zitadel if not already configured
if 'zitadel' not in oauth._clients:
issuer = current_app.config.get('ZITADEL_ISSUER', '').rstrip('/')
client_secret = current_app.config.get('ZITADEL_CLIENT_SECRET')
# Scopes aus Config lesen oder auf OIDC-Standard-Defaults zurückfallen
scopes = current_app.config.get('ZITADEL_SCOPES')
if not scopes:
scopes = ['openid', 'profile', 'email']
scope_string = ' '.join(scopes) if isinstance(scopes, list) else scopes
zitadel_config = {
'client_id': current_app.config.get('ZITADEL_CLIENT_ID'),
'client_secret': current_app.config.get('ZITADEL_CLIENT_SECRET'),
'authorize_url': f"{current_app.config.get('ZITADEL_ISSUER')}/v1/oauth/authorize",
'access_token_url': f"{current_app.config.get('ZITADEL_ISSUER')}/v1/oauth/token",
'userinfo_endpoint': f"{current_app.config.get('ZITADEL_ISSUER')}/v1/userinfo",
'client_secret': client_secret,
# Automatische Konfiguration über Zitadels Discovery Endpoint
'server_metadata_url': f"{issuer}/.well-known/openid-configuration",
'client_kwargs': {
'scope': ' '.join(current_app.config.get('ZITADEL_SCOPES', []))
'scope': scope_string,
'code_challenge_method': 'S256', # PKCE Aktivierung
# Falls KEIN Client Secret vorhanden ist (Pure PKCE Public App):
# 'token_endpoint_auth_method': 'none' if not client_secret else 'client_secret_post'
}
}
# Nur hinzufügen, wenn kein Secret da ist
if not client_secret:
zitadel_config['client_kwargs']['token_endpoint_auth_method'] = 'none'
oauth.register('zitadel', **zitadel_config)
# Generate state for CSRF protection
@@ -143,7 +198,7 @@ def login():
redirect_uri = current_app.config.get('ZITADEL_REDIRECT_URI',
url_for('auth.callback', _external=True))
authorization_url = f"{current_app.config.get('ZITADEL_ISSUER')}/v1/oauth/authorize"
authorization_url = f"{current_app.config.get('ZITADEL_ISSUER')}/v2/oauth/authorize"
params = {
'client_id': current_app.config.get('ZITADEL_CLIENT_ID'),
@@ -167,7 +222,13 @@ def callback():
Handles the callback from Zitadel after successful authentication.
Exchanges authorization code for tokens and retrieves user info.
In mock auth mode, this route shouldn't be reached, but redirects gracefully.
"""
if AuthService.is_mock_auth_enabled():
# In mock auth mode, shouldn't reach here, but handle gracefully
logger.debug('Mock auth mode: callback route accessed, redirecting to dashboard')
return redirect(url_for('admin.dashboard'))
if AuthService.is_dev_mode():
# In dev mode, shouldn't reach here, but handle gracefully
return redirect(url_for('admin.dashboard'))
@@ -255,13 +316,16 @@ def logout():
Logout route - clears session and redirects to login.
In production: Also revokes tokens with Zitadel if configured
In development: Simply clears the session
In development/mock: Simply clears the session
"""
# Get current user for audit logging
current_user = None
if 'user_id' in session:
current_user = User.query.get(session['user_id'])
# Determine auth mode for logging
auth_mode = 'mock' if AuthService.is_mock_auth_enabled() else ('dev' if AuthService.is_dev_mode() else 'oidc')
# Clear session
session.clear()
@@ -286,7 +350,7 @@ def logout():
action='logout',
resource_type='auth',
user=current_user,
details={'mode': 'dev' if AuthService.is_dev_mode() else 'oidc'}
details={'mode': auth_mode}
)
return redirect(url_for('auth.login'))
@@ -304,12 +368,14 @@ def status():
return jsonify({
'authenticated': True,
'user': user_data,
'dev_mode': AuthService.is_dev_mode()
'dev_mode': AuthService.is_dev_mode(),
'mock_auth': AuthService.is_mock_auth_enabled()
})
return jsonify({
'authenticated': False,
'dev_mode': AuthService.is_dev_mode()
'dev_mode': AuthService.is_dev_mode(),
'mock_auth': AuthService.is_mock_auth_enabled()
})
@@ -334,5 +400,6 @@ def userinfo():
return jsonify({
'user': user.to_dict(),
'tenants': [tenant.to_dict() for tenant in user.tenants],
'dev_mode': AuthService.is_dev_mode()
'dev_mode': AuthService.is_dev_mode(),
'mock_auth': AuthService.is_mock_auth_enabled()
})
+81
View File
@@ -21,6 +21,11 @@ class AuthService:
"""Check if application is in development mode."""
return current_app.config.get('DEV_MODE', False)
@staticmethod
def is_mock_auth_enabled() -> bool:
"""Check if mock authentication mode is enabled."""
return current_app.config.get('MOCK_AUTH', False) and current_app.config.get('DEV_MODE', False)
@staticmethod
def ensure_dev_user_exists() -> User:
"""
@@ -96,6 +101,82 @@ class AuthService:
return dev_user
@staticmethod
def create_mock_user_session() -> dict:
"""
Create a mock authentication user session with local mock user.
:return: Dictionary with mock user session data
"""
config = current_app.config
# Use configured mock user settings or defaults
mock_user = {
'id': config.get('MOCK_USER_ID', 'mock_user_001'),
'email': config.get('MOCK_USER_EMAIL', 'mock@formeleon.local'),
'name': config.get('MOCK_USER_NAME', 'Mock Admin'),
'role': config.get('MOCK_USER_ROLE', 'admin'),
'sub': f'mock-{uuid.uuid4()}',
'tenant_id': config.get('MOCK_TENANT_ID', 'mock_tenant_001'),
'dev_mode': True,
'mock_auth': True,
'authenticated': True
}
return mock_user
@staticmethod
def ensure_mock_user_exists() -> User:
"""
Ensure the mock user exists in the database.
:return: User object from database
"""
config = current_app.config
user_id = config.get('MOCK_USER_ID', 'mock_user_001')
user = User.query.get(user_id)
if not user:
role = UserRole.ADMIN if config.get('MOCK_USER_ROLE', 'admin') == 'admin' else UserRole.USER
user = User(
id=user_id,
email=config.get('MOCK_USER_EMAIL', 'mock@formeleon.local'),
name=config.get('MOCK_USER_NAME', 'Mock Admin'),
role=role,
sub=f'mock-{uuid.uuid4()}'
)
db.session.add(user)
db.session.commit()
logger.info(f'Created mock user: {user.id}')
return user
@staticmethod
def ensure_mock_tenant_exists() -> Tenant:
"""
Ensure the mock tenant exists in the database.
:return: Tenant object from database
"""
config = current_app.config
tenant_id = config.get('MOCK_TENANT_ID', 'mock_tenant_001')
tenant = Tenant.query.get(tenant_id)
if not tenant:
tenant = Tenant(
id=tenant_id,
name='Mock Tenant',
public_key=f'mock-key-{uuid.uuid4().hex[:16]}',
allowed_origins='*'
)
db.session.add(tenant)
db.session.commit()
logger.info(f'Created mock tenant: {tenant.id}')
return tenant
@staticmethod
def process_oidc_callback(user_info: dict) -> Tuple[Optional[User], Optional[str]]:
"""