Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def create_app() -> Flask:
app.config["SLACK_BOT_TOKEN"] = os.environ.get("SLACK_BOT_TOKEN")
app.config["SLACK_CHANNEL_ID"] = os.environ.get("SLACK_CHANNEL_ID")

# --- Proxy Fix for AWS AppRunner ---
# --- Proxy Fix for reverse proxies (Heroku, AWS, etc.) ---
if os.environ.get("BEHIND_PROXY", "false").lower() == "true":
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(
Expand Down
14 changes: 14 additions & 0 deletions app/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from flask import Blueprint, redirect, url_for, session, flash, current_app, request, render_template
from authlib.integrations.flask_client import OAuth
from authlib.integrations.base_client import MismatchingStateError

from app import db

Expand Down Expand Up @@ -171,6 +172,11 @@ def _handle_google_callback():

try:
token = oauth.google.authorize_access_token()
except MismatchingStateError:
current_app.logger.info('Google state mismatch — stale session, retrying OAuth')
log_login_failure("stale_session", provider="google", severity="INFO")
db.session.commit()
return redirect(url_for('auth.login'))
except Exception as e:
current_app.logger.error(f'Google OAuth error: {e}')
log_login_failure("oauth_error", provider="google")
Expand Down Expand Up @@ -217,6 +223,14 @@ def _handle_keycloak_callback():

try:
token = oauth.keycloak.authorize_access_token()
except MismatchingStateError:
# Stale session: user's Flask session expired but Keycloak SSO session
# was still active, so the callback state doesn't match. This is benign
# — just re-initiate the OAuth flow and it will succeed on the retry.
current_app.logger.info('Keycloak state mismatch — stale session, retrying OAuth')
log_login_failure("stale_session", provider="keycloak", severity="INFO")
db.session.commit()
return redirect(url_for('auth.login'))
except Exception as e:
current_app.logger.error(f'Keycloak OAuth error: {e}')
log_login_failure("oauth_error", provider="keycloak")
Expand Down
8 changes: 5 additions & 3 deletions app/security_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,20 @@ def log_login_success(user_id: str, provider: str, email: str) -> SecurityAuditL
)


def log_login_failure(reason: str, email: str = None, provider: str = None) -> SecurityAuditLog:
def log_login_failure(reason: str, email: str = None, provider: str = None,
severity: str = SEVERITY_WARNING) -> SecurityAuditLog:
"""Log failed authentication attempt.

Args:
reason: Why the login failed (e.g., 'domain_restricted', 'oauth_error')
reason: Why the login failed (e.g., 'domain_restricted', 'oauth_error', 'stale_session')
email: Email that attempted to log in (if known)
provider: OAuth provider that was used (if known)
severity: Severity level (default WARNING, use INFO for benign failures like stale_session)
"""
return log_security_event(
EVENT_LOGIN_FAILURE,
CATEGORY_AUTH,
SEVERITY_WARNING,
severity,
user_id=None,
details={"reason": reason, "email": email, "provider": provider},
)
Expand Down
Loading
Loading