From 88ea6a75a111019731d4d7d4f0e8158a80543528 Mon Sep 17 00:00:00 2001 From: Chad Palmer Date: Thu, 20 Aug 2026 01:03:54 +0000 Subject: [PATCH 1/3] fix-logout-route - fixed logout 404 error in dev environment. --- application/single_app/config.py | 2 +- .../route_frontend_authentication.py | 4 + ...VELOPMENT_LOGOUT_EASY_AUTH_REDIRECT_FIX.md | 24 +++ .../test_app_service_easy_auth_logout.py | 157 ++++++++++++++++-- 4 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 docs/explanation/fixes/DEVELOPMENT_LOGOUT_EASY_AUTH_REDIRECT_FIX.md diff --git a/application/single_app/config.py b/application/single_app/config.py index 5393dc20a..6abbbcb76 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -96,7 +96,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.250.224" +VERSION = "0.250.225" IS_DEVELOPMENT = is_development_env_enabled() SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') diff --git a/application/single_app/route_frontend_authentication.py b/application/single_app/route_frontend_authentication.py index c07b51519..51cba1e72 100644 --- a/application/single_app/route_frontend_authentication.py +++ b/application/single_app/route_frontend_authentication.py @@ -6,6 +6,7 @@ import requests from config import * +from config import IS_DEVELOPMENT from functions_activity_logging import log_user_login, record_user_login_session_activity from functions_terms_of_use import ( apply_pending_pre_auth_terms_of_use, @@ -43,6 +44,9 @@ def build_front_door_urls(front_door_url): def _use_app_service_easy_auth_logout(): """Return True when the current request is running behind App Service Easy Auth.""" + if IS_DEVELOPMENT: + return False + if not os.getenv('WEBSITE_HOSTNAME'): return False diff --git a/docs/explanation/fixes/DEVELOPMENT_LOGOUT_EASY_AUTH_REDIRECT_FIX.md b/docs/explanation/fixes/DEVELOPMENT_LOGOUT_EASY_AUTH_REDIRECT_FIX.md new file mode 100644 index 000000000..a92933ee3 --- /dev/null +++ b/docs/explanation/fixes/DEVELOPMENT_LOGOUT_EASY_AUTH_REDIRECT_FIX.md @@ -0,0 +1,24 @@ +# Development Logout Easy Auth Redirect Fix + +Fixed/Implemented in version: **0.250.225** + +## Issue Description + +In the development environment, user-initiated logout and idle-timeout logout could redirect to `/.auth/logout?post_logout_redirect_uri=%2Flogin`. That platform Easy Auth URL returned a 404 when the development deployment was not actually serving App Service Easy Auth logout endpoints. + +## Root Cause Analysis + +The logout route detected Azure hosting variables and Easy Auth-related signals, then routed local logout through `/.auth/logout`. Development deployments can still expose those hosting signals even when the Easy Auth endpoint is unavailable for the current custom-domain path. + +## Technical Details + +- Modified `application/single_app/route_frontend_authentication.py` so Easy Auth logout routing is skipped when `IS_DEVELOPMENT` is enabled. +- Preserved the existing Easy Auth logout path for non-development Azure App Service deployments with Easy Auth signals. +- Updated `application/single_app/config.py` to version `0.250.225`. +- Added regression coverage in `functional_tests/test_app_service_easy_auth_logout.py` for the development-mode fallback. + +## Validation + +- Ran `functional_tests/test_app_service_easy_auth_logout.py`. +- Confirmed production-style Easy Auth local and full logout still redirect through `/.auth/logout`. +- Confirmed development-mode local logout avoids `/.auth/logout` and redirects to the local app index after clearing the Flask session. \ No newline at end of file diff --git a/functional_tests/test_app_service_easy_auth_logout.py b/functional_tests/test_app_service_easy_auth_logout.py index 569202a87..c3acc0452 100644 --- a/functional_tests/test_app_service_easy_auth_logout.py +++ b/functional_tests/test_app_service_easy_auth_logout.py @@ -1,20 +1,23 @@ # test_app_service_easy_auth_logout.py """ Functional test for Azure App Service Easy Auth logout recovery. -Version: 0.241.095 +Version: 0.250.225 Implemented in: 0.241.095 This test ensures Azure-hosted logout routes clear the upstream App Service authentication session by redirecting through /.auth/logout before re-entering -the Flask login flow. +the Flask login flow, while development-mode deployments avoid a missing +/.auth/logout platform endpoint. """ from pathlib import Path +import importlib import os import sys -from unittest.mock import patch +import types +from unittest.mock import patch, Mock -from flask import Flask, session +from flask import Blueprint, Flask, session ROOT = Path(__file__).resolve().parents[1] @@ -24,7 +27,103 @@ sys.path.insert(0, str(APP_DIR)) -import route_frontend_authentication as route_module # noqa: E402 +class FakeConfigCosmosContainer: + """Minimal Cosmos container stand-in for config.py import-time setup.""" + + def read(self): + return {} + + +class FakeConfigCosmosDatabase: + """Minimal Cosmos database stand-in for importing config.py without live I/O.""" + + def __init__(self): + self.containers = {} + + def create_container_if_not_exists(self, id, **kwargs): + if id not in self.containers: + self.containers[id] = FakeConfigCosmosContainer() + return self.containers[id] + + def get_container_client(self, id): + return self.containers.setdefault(id, FakeConfigCosmosContainer()) + + +class FakeConfigCosmosClient: + """Minimal Cosmos client stand-in for config.py import-time container setup.""" + + def __init__(self, *args, **kwargs): + self.database = FakeConfigCosmosDatabase() + + def create_database_if_not_exists(self, *args, **kwargs): + return self.database + + +def import_module_without_live_cosmos(module_name): + """Import app modules without letting config.py connect to live Cosmos.""" + if module_name in sys.modules: + return sys.modules[module_name] + + import azure.cosmos as azure_cosmos + + original_cosmos_client = azure_cosmos.CosmosClient + azure_cosmos.CosmosClient = FakeConfigCosmosClient + stub_modules = _install_route_dependency_stubs() + try: + return importlib.import_module(module_name) + finally: + azure_cosmos.CosmosClient = original_cosmos_client + for stub_name in stub_modules: + sys.modules.pop(stub_name, None) + + +def _install_route_dependency_stubs(): + """Install lightweight stubs for dependencies unrelated to logout routing.""" + stub_modules = {} + + functions_activity_logging = types.ModuleType("functions_activity_logging") + functions_activity_logging.log_user_login = Mock() + functions_activity_logging.record_user_login_session_activity = Mock() + stub_modules["functions_activity_logging"] = functions_activity_logging + + functions_terms_of_use = types.ModuleType("functions_terms_of_use") + functions_terms_of_use.apply_pending_pre_auth_terms_of_use = Mock() + functions_terms_of_use.get_terms_of_use_config = Mock(return_value={"enabled": False}) + functions_terms_of_use.has_terms_of_use_acceptance = Mock(return_value=True) + stub_modules["functions_terms_of_use"] = functions_terms_of_use + + functions_authentication = types.ModuleType("functions_authentication") + functions_authentication._build_msal_app = Mock() + functions_authentication._load_cache = Mock(return_value=None) + functions_authentication._save_cache = Mock() + functions_authentication.clear_requested_oauth_scopes = Mock() + functions_authentication.create_ci_bearer_session = Mock(return_value=("", 204)) + functions_authentication.get_graph_authority = Mock(return_value="https://graph.microsoft.com") + functions_authentication.get_graph_endpoint = Mock(side_effect=lambda path: f"https://graph.microsoft.com/v1.0{path}") + functions_authentication.get_requested_oauth_scopes = Mock(return_value=[]) + stub_modules["functions_authentication"] = functions_authentication + + functions_debug = types.ModuleType("functions_debug") + functions_debug.debug_print = Mock() + stub_modules["functions_debug"] = functions_debug + + functions_settings = types.ModuleType("functions_settings") + functions_settings.get_settings = Mock(return_value={}) + functions_settings.sanitize_settings_for_user = Mock(side_effect=lambda settings: settings) + stub_modules["functions_settings"] = functions_settings + + swagger_wrapper = types.ModuleType("swagger_wrapper") + swagger_wrapper.swagger_route = Mock(side_effect=lambda *args, **kwargs: (lambda function: function)) + swagger_wrapper.get_auth_security = Mock(return_value=[]) + stub_modules["swagger_wrapper"] = swagger_wrapper + + for stub_name, stub_module in stub_modules.items(): + sys.modules[stub_name] = stub_module + + return stub_modules + + +route_module = import_module_without_live_cosmos("route_frontend_authentication") EXPECTED_EASY_AUTH_LOGOUT = "/.auth/logout?post_logout_redirect_uri=%2Flogin" @@ -34,11 +133,14 @@ def _build_test_app(): app = Flask(__name__) app.secret_key = "test-secret" - @app.route("/") def index(): return "ok" - route_module.register_route_frontend_authentication(app) + app.add_url_rule("/", endpoint="public_app.index", view_func=index) + + auth_blueprint = Blueprint("frontend_authentication", __name__) + route_module.register_route_frontend_authentication(auth_blueprint) + app.register_blueprint(auth_blueprint) return app @@ -55,7 +157,7 @@ def test_local_logout_uses_app_service_easy_auth_logout(): "WEBSITE_AUTH_AAD_ALLOWED_TENANTS": "tenant-id", }, clear=False, - ): + ), patch.object(route_module, "IS_DEVELOPMENT", False): with app.test_request_context( "/logout/local", base_url="https://example.azurewebsites.net", @@ -63,7 +165,7 @@ def test_local_logout_uses_app_service_easy_auth_logout(): ): session["user"] = {"name": "Test User"} - response = app.view_functions["local_logout"]() + response = app.view_functions["frontend_authentication.local_logout"]() assert response.status_code == 302, f"Expected redirect response, got {response.status_code}" assert response.headers.get("Location") == EXPECTED_EASY_AUTH_LOGOUT, ( @@ -87,7 +189,7 @@ def test_full_logout_uses_app_service_easy_auth_logout(): "WEBSITE_AUTH_AAD_ALLOWED_TENANTS": "tenant-id", }, clear=False, - ): + ), patch.object(route_module, "IS_DEVELOPMENT", False): with app.test_request_context( "/logout", base_url="https://example.azurewebsites.net", @@ -98,7 +200,7 @@ def test_full_logout_uses_app_service_easy_auth_logout(): "preferred_username": "user@example.com", } - response = app.view_functions["logout"]() + response = app.view_functions["frontend_authentication.logout"]() assert response.status_code == 302, f"Expected redirect response, got {response.status_code}" assert response.headers.get("Location") == EXPECTED_EASY_AUTH_LOGOUT, ( @@ -109,10 +211,43 @@ def test_full_logout_uses_app_service_easy_auth_logout(): print("App Service Easy Auth full logout redirects through /.auth/logout") +def test_development_mode_does_not_use_app_service_easy_auth_logout(): + """Verify development mode skips Easy Auth logout even when Azure hosting variables exist.""" + print("Testing development-mode logout avoids App Service Easy Auth redirect...") + + app = _build_test_app() + + with patch.dict( + os.environ, + { + "WEBSITE_HOSTNAME": "oigchat-dev.dhs-oig.gov", + "WEBSITE_AUTH_AAD_ALLOWED_TENANTS": "tenant-id", + }, + clear=False, + ), patch.object(route_module, "IS_DEVELOPMENT", True), patch.object(route_module, "get_settings", Mock(return_value={})): + with app.test_request_context( + "/logout/local", + base_url="https://oigchat-dev.dhs-oig.gov", + headers={"X-MS-CLIENT-PRINCIPAL-ID": "user-oid"}, + ): + session["user"] = {"name": "Test User"} + + response = app.view_functions["frontend_authentication.local_logout"]() + + assert response.status_code == 302, f"Expected redirect response, got {response.status_code}" + assert response.headers.get("Location") == "/", ( + f"Unexpected development local logout redirect: {response.headers.get('Location')}" + ) + assert "user" not in session, f"Expected Flask session to be cleared, got {dict(session)}" + + print("Development-mode local logout avoids /.auth/logout") + + if __name__ == "__main__": tests = [ test_local_logout_uses_app_service_easy_auth_logout, test_full_logout_uses_app_service_easy_auth_logout, + test_development_mode_does_not_use_app_service_easy_auth_logout, ] results = [] From 173fe9b878f69afcf7dc3d498ab3eb46eb4f56fd Mon Sep 17 00:00:00 2001 From: Chad Palmer Date: Tue, 25 Aug 2026 21:02:35 +0000 Subject: [PATCH 2/3] fix-logout-route - Bumped version and updated files/tests to hopefully prevent confusion. --- application/single_app/config.py | 2 +- .../fixes/EASY_AUTH_LOGOUT_DETECTION_FIX.md | 4 ++-- docs/explanation/release_notes.md | 22 +++++++++---------- .../test_app_service_easy_auth_logout.py | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/application/single_app/config.py b/application/single_app/config.py index d57f7c871..865c4aa98 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -96,7 +96,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.260.028" +VERSION = "0.260.029" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/docs/explanation/fixes/EASY_AUTH_LOGOUT_DETECTION_FIX.md b/docs/explanation/fixes/EASY_AUTH_LOGOUT_DETECTION_FIX.md index c1383fc0a..e7ba6f275 100644 --- a/docs/explanation/fixes/EASY_AUTH_LOGOUT_DETECTION_FIX.md +++ b/docs/explanation/fixes/EASY_AUTH_LOGOUT_DETECTION_FIX.md @@ -1,6 +1,6 @@ # Easy Auth Logout Detection Fix -Fixed/Implemented in version: **0.260.019** +Fixed/Implemented in version: **0.260.029** ## Issue Description @@ -49,7 +49,7 @@ so it needs an explicit opt-out. "Logout Behavior Across Environments" section to `docs/explanation/running_simplechat_locally.md` covering the behavior per environment and the troubleshooting steps for a logout 404. -- Updated `application/single_app/config.py` to version `0.260.019`. +- Updated `application/single_app/config.py` to version `0.260.029`. - Reworked regression coverage in `functional_tests/test_app_service_easy_auth_logout.py`. ### Behavior by environment diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index 0fe2c88bd..51df0f359 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,17 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes). +### **(v0.260.029)** + +#### Bug Fixes + +* **Logout No Longer Redirects To A Missing Easy Auth Endpoint** + * Logout could redirect to `/.auth/logout?post_logout_redirect_uri=%2Flogin` and return a 404 on Azure App Service deployments that were not actually serving App Service Easy Auth. This affected production deployments as well as development ones. + * The root cause was Easy Auth detection treating the manually configured `WEBSITE_AUTH_AAD_ALLOWED_TENANTS` application setting as proof that Easy Auth was intercepting requests. SimpleChat's own advanced environment variable guidance instructs operators to set that value by hand, so it was never a reliable signal. + * Detection now relies only on the `X-MS-CLIENT-PRINCIPAL` request headers that App Service Easy Auth injects on requests it actually intercepts, so deployments genuinely behind Easy Auth still clear the upstream platform session, and everyone else gets a clean local logout. + * Idle-timeout logout uses the same local logout path, so automatic session expiration follows the corrected behavior as well. + * (Ref: `route_frontend_authentication.py`, `_use_app_service_easy_auth_logout`, `test_app_service_easy_auth_logout.py`, [Easy Auth Logout Detection Fix](fixes/EASY_AUTH_LOGOUT_DETECTION_FIX.md)) + ### **(v0.260.025)** #### Bug Fixes @@ -118,17 +129,6 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Ver * The Cosmos DB tab checked the wrong thing for the debug setting, so the backfill controls, shadow validation metrics and reset option stayed hidden even after an admin turned the setting on. * (Ref: `admin/_panes/cosmos.html`, `enable_dai_debug`) -### **(v0.260.019)** - -#### Bug Fixes - -* **Logout No Longer Redirects To A Missing Easy Auth Endpoint** - * Logout could redirect to `/.auth/logout?post_logout_redirect_uri=%2Flogin` and return a 404 on Azure App Service deployments that were not actually serving App Service Easy Auth. This affected production deployments as well as development ones. - * The root cause was Easy Auth detection treating the manually configured `WEBSITE_AUTH_AAD_ALLOWED_TENANTS` application setting as proof that Easy Auth was intercepting requests. SimpleChat's own advanced environment variable guidance instructs operators to set that value by hand, so it was never a reliable signal. - * Detection now relies only on the `X-MS-CLIENT-PRINCIPAL` request headers that App Service Easy Auth injects on requests it actually intercepts, so deployments genuinely behind Easy Auth still clear the upstream platform session, and everyone else gets a clean local logout. - * Idle-timeout logout uses the same local logout path, so automatic session expiration follows the corrected behavior as well. - * (Ref: `route_frontend_authentication.py`, `_use_app_service_easy_auth_logout`, `test_app_service_easy_auth_logout.py`, [Easy Auth Logout Detection Fix](fixes/EASY_AUTH_LOGOUT_DETECTION_FIX.md)) - #### New Features * **Opt-Out For App Service Easy Auth Logout** diff --git a/functional_tests/test_app_service_easy_auth_logout.py b/functional_tests/test_app_service_easy_auth_logout.py index 89e1701a1..9188381a9 100644 --- a/functional_tests/test_app_service_easy_auth_logout.py +++ b/functional_tests/test_app_service_easy_auth_logout.py @@ -1,8 +1,8 @@ # test_app_service_easy_auth_logout.py """ Functional test for Azure App Service Easy Auth logout detection. -Version: 0.260.019 -Implemented in: 0.260.019 +Version: 0.260.029 +Implemented in: 0.260.029 This test ensures logout routes through /.auth/logout only when App Service Easy Auth is actually serving the request, so deployments that are not behind Easy Auth no longer hit a From 47599380cc1feb94df1bebbdf8714afbb5c1905c Mon Sep 17 00:00:00 2001 From: Chad Palmer Date: Wed, 26 Aug 2026 16:14:50 -0400 Subject: [PATCH 3/3] Update version to 0.261.002 Fixed goofy github conflict --- application/single_app/config.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/application/single_app/config.py b/application/single_app/config.py index c259b7eb1..d043daca7 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -97,11 +97,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -<<<<<<< HEAD -VERSION = "0.260.029" -======= -VERSION = "0.261.001" ->>>>>>> upstream/Development +VERSION = "0.261.002" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform