Skip to content
Open
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
8 changes: 8 additions & 0 deletions application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@
VERSION = "0.260.028"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
# /.auth/logout endpoint is not reachable on the public host (for example, when a
# custom domain or gateway does not route /.auth/* to the App Service origin).
DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT = os.getenv(
'DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT',
''
).strip().lower() == 'true'

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
SESSION_COOKIE_HTTPONLY = os.getenv('SESSION_COOKIE_HTTPONLY', 'true').lower() != 'false'
SESSION_COOKIE_SECURE = os.getenv('SESSION_COOKIE_SECURE', 'false').lower() == 'true'
Expand Down
10 changes: 9 additions & 1 deletion application/single_app/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ AZURE_ENVIRONMENT="public"
# Optional Graph overrides (for cross-cloud identity/Graph scenarios)
# Example values:
# CUSTOM_GRAPH_URL_VALUE="https://graph.microsoft.com"
# CUSTOM_GRAPH_AUTHORITY_URL_VALUE="https://login.microsoftonline.com"
# CUSTOM_GRAPH_AUTHORITY_URL_VALUE="https://login.microsoftonline.com"

# Logout behavior on Azure App Service
# SimpleChat detects App Service Easy Auth from the X-MS-CLIENT-PRINCIPAL request headers
# the platform injects, and routes logout through /.auth/logout so the platform session is
# cleared. Set this to "true" only if Easy Auth is active but /.auth/logout is not reachable
# on your public host, for example when a custom domain or gateway does not route /.auth/*
# to the App Service origin. Has no effect when running locally.
# DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT="true"
25 changes: 23 additions & 2 deletions application/single_app/route_frontend_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import requests

from config import *
from config import DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT
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,
Expand Down Expand Up @@ -42,16 +43,36 @@ 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."""
"""
Determine whether logout should route through the App Service Easy Auth endpoint.

Args:
None.

Returns:
bool: True when the current request is being served behind App Service Easy Auth.
Raises:
None.
"""
if DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT:
debug_print("Easy Auth logout disabled by DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT; using local logout.")
return False

if not os.getenv('WEBSITE_HOSTNAME'):
return False

# Easy Auth injects these headers only on requests it actually intercepts, so they are
# the reliable per-request signal that /.auth/logout is being served for this host.
easy_auth_headers = (
request.headers.get('X-MS-CLIENT-PRINCIPAL'),
request.headers.get('X-MS-CLIENT-PRINCIPAL-ID'),
request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME'),
)
return any(easy_auth_headers) or bool(os.getenv('WEBSITE_AUTH_AAD_ALLOWED_TENANTS'))
if not any(easy_auth_headers):
debug_print("No App Service Easy Auth principal headers on this request; using local logout.")
return False

return True


def _build_app_service_easy_auth_logout_url():
Expand Down
85 changes: 85 additions & 0 deletions docs/explanation/fixes/EASY_AUTH_LOGOUT_DETECTION_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Easy Auth Logout Detection Fix

Fixed/Implemented in version: **0.260.019**

## Issue Description

User-initiated logout and idle-timeout 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. The problem was first
reported on a development custom domain, but it was never limited to development
environments; any deployment matching the same conditions was affected, including
production.

## Root Cause Analysis

`_use_app_service_easy_auth_logout()` in
`application/single_app/route_frontend_authentication.py` decided that Easy Auth was active
when either the `X-MS-CLIENT-PRINCIPAL` request headers were present **or** the
`WEBSITE_AUTH_AAD_ALLOWED_TENANTS` environment variable was set:

```python
return any(easy_auth_headers) or bool(os.getenv('WEBSITE_AUTH_AAD_ALLOWED_TENANTS'))
```

That environment variable is not evidence that Easy Auth is running. SimpleChat's own
advanced configuration guidance in
`application/single_app/example_advance_edit_environment_variables.json` instructs
operators to set it by hand, so any deployment that followed those instructions without
enabling Easy Auth was misdetected. Logout then redirected to a platform endpoint that the
App Service was not serving, producing the 404.

A secondary case exists where Easy Auth genuinely is enabled but `/.auth/*` is not routed
through to the App Service origin, for example behind a custom domain, gateway or front
door with restrictive path routing. Request-based detection cannot distinguish that case,
so it needs an explicit opt-out.

## Technical Details

- Modified `application/single_app/route_frontend_authentication.py` so Easy Auth detection
relies only on the `X-MS-CLIENT-PRINCIPAL`, `X-MS-CLIENT-PRINCIPAL-ID` and
`X-MS-CLIENT-PRINCIPAL-NAME` headers that App Service injects into requests it actually
intercepts. The `WEBSITE_AUTH_AAD_ALLOWED_TENANTS` fallback was removed.
- Added `debug_print` output on both non-Easy-Auth paths so the logout routing decision and
its reason are visible with `FLASK_DEBUG=1`.
- Added the `DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT` environment flag in
`application/single_app/config.py` for deployments where Easy Auth is active but
`/.auth/logout` is unreachable on the public host.
- Documented the flag in `application/single_app/example.env` and added a
"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`.
- Reworked regression coverage in `functional_tests/test_app_service_easy_auth_logout.py`.

### Behavior by environment

| Environment | Easy Auth headers | Logout path |
| --- | --- | --- |
| Local machine (`python app.py`) | No | Local logout |
| App Service with Easy Auth enabled | Yes | Easy Auth logout via `/.auth/logout` |
| App Service without Easy Auth enabled | No | Local logout |
| Easy Auth enabled, `/.auth/*` not routed | Yes | Local logout after setting the opt-out flag |

Local development was never affected by the original defect, because `WEBSITE_HOSTNAME` is
not set outside App Service and the function returned early.

## Validation

- `functional_tests/test_app_service_easy_auth_logout.py` — 5/5 passing, covering Easy Auth
local logout, Easy Auth full logout, the reported no-headers case, preservation of Easy
Auth logout on a non-production host, and the opt-out flag.
- `functional_tests/test_idle_logout_timeout.py` — 4/4 passing. The idle-timeout path routes
through `local_logout`, so it inherits the corrected behavior.
- Confirmed deployments genuinely behind Easy Auth still redirect through `/.auth/logout`,
so the upstream platform session continues to be cleared.
- Confirmed deployments with Azure hosting variables but no Easy Auth headers now perform a
local logout instead of redirecting to a missing platform endpoint.

## Notes

The previous iteration of this fix skipped Easy Auth logout whenever the `is_development`
environment flag was set. That approach was replaced because it left the underlying
detection defect in place for production, it reused a flag documented for Latest Features
navigation to control session termination, and it disabled platform logout even in
development environments where Easy Auth was genuinely active and working.
1 change: 1 addition & 0 deletions docs/explanation/fixes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ category: Version History
- [New Chat Conversation Documents Drawer Reset Fix](NEW_CHAT_CONVERSATION_DOCUMENTS_DRAWER_RESET_FIX.md)
- [Collaboration Mention Tab Autocomplete Fix](COLLABORATION_MENTION_TAB_AUTOCOMPLETE_FIX.md)
- [Generated Artifact Paging, Truncation, and Guidance Carry-Forward Fix](GENERATED_ARTIFACT_PAGING_AND_GUIDANCE_FIX.md)
- [Easy Auth Logout Detection Fix](EASY_AUTH_LOGOUT_DETECTION_FIX.md)
- [Admin Settings Pane Variable Scope Fix](ADMIN_SETTINGS_PANE_VARIABLE_SCOPE_FIX.md)
- [Inline Media Cited-Only Gating Fix](INLINE_MEDIA_CITED_ONLY_GATING_FIX.md)
- [Agent Actions With Workspace Evidence Fix](AGENT_ACTIONS_WITH_WORKSPACE_EVIDENCE_FIX.md)
18 changes: 18 additions & 0 deletions docs/explanation/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ 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**
* Added the `DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT` environment variable for deployments where Easy Auth is genuinely active but the platform `/.auth/logout` endpoint is not reachable on the public host, such as when a custom domain or gateway does not route `/.auth/*` to the App Service origin.
* Setting it to `true` keeps logout on the local path instead of redirecting to the platform endpoint. Logout routing decisions are now also traced through debug logging, so `FLASK_DEBUG=1` shows which path was taken and why.
* (Ref: `DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT`, `config.py`, `example.env`, [Running SimpleChat Locally](running_simplechat_locally.md))

### **(v0.260.018)**

#### Bug Fixes
Expand Down
47 changes: 47 additions & 0 deletions docs/explanation/running_simplechat_locally.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,53 @@ If you want to test the scheduler separately, run:
python simplechat_scheduler.py
```

## Logout Behavior Across Environments

Logout takes one of two paths, chosen per request. Knowing which one you are on explains
most unexpected logout behavior.

- **Local logout** clears the Flask session and redirects to the app home page.
- **Easy Auth logout** first redirects through the App Service platform endpoint
`/.auth/logout` so the platform sign-in session is cleared too, then returns to the
SimpleChat login page.

SimpleChat picks Easy Auth logout only when the request carries the
`X-MS-CLIENT-PRINCIPAL` headers that App Service Easy Auth injects into requests it
intercepts. That gives the following behavior:

| Where you are running | Easy Auth headers present | Logout path |
| --- | --- | --- |
| Local machine (`python app.py`) | No | Local logout |
| App Service with Easy Auth enabled | Yes | Easy Auth logout |
| App Service without Easy Auth enabled | No | Local logout |

Running locally, you always get local logout, because `WEBSITE_HOSTNAME` is not set and no
platform headers exist. There is nothing to configure for local development.

### Troubleshooting a 404 on logout

If logout lands on a 404 at `/.auth/logout`, the app believed Easy Auth was serving that
host but the endpoint was not reachable. Work through the following:

1. Run with `FLASK_DEBUG=1` and sign out again. The logout path decision is written to the
debug log, including the reason when local logout is chosen.
2. Confirm whether Easy Auth is actually enabled for the App Service. Setting only the
`WEBSITE_AUTH_AAD_ALLOWED_TENANTS` application setting does not enable it.
3. If Easy Auth is enabled, confirm that `/.auth/*` is routed through to the App Service
origin. A custom domain, gateway or front door that does not forward those paths will
return a 404 even though Easy Auth is running.

If Easy Auth must stay enabled and `/.auth/*` cannot be routed, keep logout on the local
path with:

```bash
DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT=true
```

Use that only when the routing issue cannot be fixed. While it is set, signing out no
longer clears the App Service platform session, so the next sign-in can complete without
prompting for credentials.

## Practical Guidance

- Create `.venv` with Python 3.12 and select it in VS Code before installing dependencies.
Expand Down
Loading