Skip to content

Fix Microsoft 365 sign-in session race and auto-create the workflow key - #1716

Merged
Paul Lizer (paullizer) merged 2 commits into
microsoft:paullizer-react-v2-uifrom
paullizer:paullizer-workflow-encryption-key-autogen
Oct 8, 2026
Merged

Paul Lizer (paullizer) merged 2 commits into
microsoft:paullizer-react-v2-uifrom
paullizer:paullizer-workflow-encryption-key-autogen

Conversation

@paullizer

Copy link
Copy Markdown
Contributor

Summary

Microsoft 365 connections broke on a 3-instance deployment even though chat could read mail and SharePoint. Reconnecting for chat failed with m365_auth_state_invalid, and connecting for workflows asked for a Key Vault secret nobody had created. The settings card also reported no sources, and V2 links opened classic pages. Application Insights showed why sign-in failed: Flask-Session rewrites the whole session at the end of every request. A V2 bootstrap refresh fires on window focus and takes about 5 s. When it overlapped the connect call, it wrote back a stale copy that erased the pending sign-in before /getAToken ran.

  • Concurrent requests no longer overwrite each other's session writes. New merging session interfaces (Redis and filesystem) write only the keys a request changed onto the latest stored session, using Redis WATCH/MULTI/EXEC. A request that changed nothing only refreshes the TTL, and a session deleted mid-request is never recreated. This fixes the sign-in failure and protects every other session write. V2 also skips a focus refresh while one is running or one started in the last 15 s.
  • Workflow connections set themselves up. The deployment-wide workflow encryption key is created in Key Vault on the first connect or the next admin settings save. M365_WORKFLOW_TOKEN_KEY_SECRET_NAME still overrides the name. Workflow sign-in now returns through /getAToken, which is already registered. Connect is disabled with an explanation until Key Vault secret storage and a vault name are configured.
  • Sign-in results are readable. Callbacks render a result page instead of raw JSON. In a popup it reports back to the opener and closes; as a full page it continues on success or explains the error.
  • Status and links are accurate. The chat connection card lists the sources the session's token cache can actually use, and Microsoft 365 links in V2 open V2 Settings and Approvals. Connecting for workflows uses a popup.

Review notes:

  • The session interface handles every request in the app. The first request on each worker is still opened by the stock interface, because configure_sessions runs in before_request. That session has no snapshot and is written whole, as before.
  • Creating the key needs the app identity to hold Key Vault Secrets Officer. Without it, connecting fails with m365_key_provision_forbidden, and an admin save that changes the Key Vault settings shows a warning. Decryption pins the stored key version and never creates a key, so two racing first connects only leave an unused extra version.
  • The chat card's derived sources assume Entra returns every consented Graph scope in each token's scope, which MSAL stores as the access token target. This hasn't been checked against a live tenant yet.
  • Not in this PR: the ~5 s GET /api/v2/bootstrap latency, and the delegated Graph permissions in Initialize-EntraApplication.ps1 (Deployers don't configure Microsoft 365 workflow connections (Key Vault key, callback redirect URI, delegated Graph permissions) #1604).

Linked issue

Fixes #1714
Refs #1604 (this covers the workflow key and callback; the deployer's delegated Graph permissions remain open)

Release Notes & Latest Features

  • New Feature
  • Bug Fix
  • UI Enhancement
  • Breaking Change
  • Internal only

Is this visible to end users?

  • Yes
  • No

Is this admin-facing (Admin Settings, governance, deployment, config)?

  • Yes
  • No

Should this become a Latest Feature card?

  • Yes
  • No
  • Already added

Screenshot needed for the card?

  • Yes
  • No
  • Attached

Version bump

  • application/single_app/config.py VERSION third segment bumped, or not needed because this is docs-only
  • deployers/version.txt bumped, or not needed because deployers/ was not changed

0.261.301 to 0.261.302. Nothing under deployers/ changed.

Testing / validation

Run on the pushed commit (a4d5bcdbc) on Windows with Python 3.12 and PYTHONIOENCODING=utf-8. Every failure listed also happens on the base commit fa3422dee, checked in a clean worktree.

  • python -m pytest functional_tests\test_session_store_merge.py functional_tests\test_m365_connections.py functional_tests\test_m365_routes.py functional_tests\test_v2_user_settings_memory_m365.py functional_tests\test_v2_workflow_run_tracking_xss_guardrail.py functional_tests\test_ai_connection_embedding_defaults_api.py functional_tests\test_content_screening_admin_settings_fix.py -q -p no:cacheprovider -W ignore: 149 passed, 153 subtests passed, 10 failed. Pre-existing: 9 in test_ai_connection_embedding_defaults_api.py (NameError: KeyVaultSecretStorageError in its test harness) and test_m365_routes.py::M365RouteTests::test_real_notification_helper_only_targets_subject_and_deduplicates (local ImportError: cannot import name 'assert_group_role').
  • python -m pytest functional_tests\route_tests\test_route_blueprint_policy_inventory.py functional_tests\route_tests\test_route_unauthenticated_policy_contract.py functional_tests\route_tests\test_route_policy_test_coverage.py -q -p no:cacheprovider -W ignore: 26 passed.
  • python .\functional_tests\test_docs_app_surface_coverage.py: 7/7 checks passed.
  • python .\functional_tests\test_docs_site_quality.py: 6/6 checks passed.
  • npm --prefix application\v2_ui run typecheck: passed.
  • node functional_tests\<name>.mjs for all 84 .mjs suites: 80 passed, 4 failed. Pre-existing: test_v2_admin_nested_setting_reads, test_v2_workspace_action_authoring_logic, test_v2_workspace_action_connectors_logic, test_v2_workspace_agent_picker_rendering.
  • npm --prefix application\v2_ui run build -- --outDir ..\..\ui_tests\artifacts\orchestration-plan-editor, then python -m pytest ui_tests\test_m365_connection_result_page.py ui_tests\test_v2_settings_m365_connections.py ui_tests\test_v2_orchestration_m365_recovery.py ui_tests\test_v2_orchestration_workflow_proposal_card.py ui_tests\test_m365_lifecycle_and_approvals.py ui_tests\test_m365_pending_action_cards.py -q -p no:cacheprovider -W ignore: 220 passed, 1 error. Pre-existing: setup of test_real_typed_tools_and_flask_stream_produce_usable_browser_cards fails on a local pyOpenSSL GEN_EMAIL mismatch.
  • python scripts\check_xss_sinks.py --base-sha fa3422dee --head-sha HEAD $(git diff --name-only fa3422dee..HEAD -- '*.js' '*.html' '*.ts' '*.tsx' '*.py'): passed for 38 files.
  • python -m pyflakes $(git diff --name-only fa3422dee..HEAD -- '*.py'): no new findings compared with fa3422dee, apart from more uses of names these files already take from star imports (flash, request) and the shared Playwright fixture import already marked # noqa: F401.
  • Earlier in development, a sweep of 131 functional test files found no failures beyond those that also happen on fa3422dee.
  • Not yet verified on a deployment. The race was diagnosed from Application Insights on the affected App Service; the timings are in the fix doc.

Documentation

  • Release notes updated, or not needed
  • Feature documentation updated, or not needed
  • Fix documentation updated, or not needed

Release notes are under v0.261.302. New fix doc: docs/explanation/fixes/M365_CONNECTION_SESSION_RACE_FIX.md. Also updated MICROSOFT_365_ACTIONS.md, CHAT_ORCHESTRATION_WORKFLOWS_ROADMAP.md, docs/admin/security.md, docs/reference/logging-tags.md ([SESSION_STORE]), and the regenerated docs/_data/app_surface.yml.

Security checklist

  • New Flask routes include @swagger_route(security=get_auth_security())
  • Settings sent to non-admin frontends use sanitize_settings_for_user()
  • Browser JavaScript is served from local SimpleChat static assets only; no CDN-hosted JS
  • No secrets, keys, connection strings, or local-only artifacts are included

No new application routes; the only new @app.routes are in the session test's throwaway Flask app. GET /api/m365/connections adds a derived readiness object (available, reason, message), not settings. The result page loads the local static/js/profile/profile-m365-connection-result.js and posts only to its same-origin opener. The key value is never logged or sent to the browser.

…ate the workflow key automatically

Flask-Session rewrote the whole session at the end of every request, so a
slow V2 bootstrap refresh that overlapped a Microsoft 365 connect wrote back
a stale copy, erased the pending sign-in, and /getAToken answered
m365_auth_state_invalid.

- Install merging Redis and filesystem session interfaces that save only the
  keys a request changed onto the latest stored session, refresh only the TTL
  when nothing changed, and never recreate a session deleted mid-request.
- Create the deployment-wide workflow encryption key in Key Vault on first
  connect and on admin settings saves, report readiness, and route workflow
  sign-in through /getAToken.
- Show sign-in results on a page that reports back to the popup's opener
  instead of raw JSON.
- Derive the chat connection's sources from the session token cache, keep
  V2 links in V2, and skip V2 focus refreshes while a recent one exists.
- Bump to 0.261.302 with tests, fix documentation and release notes.

Fixes microsoft#1714
Refs microsoft#1604

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread ui_tests/test_m365_connection_result_page.py Fixed
Comment thread ui_tests/test_m365_connection_result_page.py Fixed
Comment thread ui_tests/test_m365_connection_result_page.py Fixed
… assert result page statuses

Code scanning reported the workflow key's Key Vault URL as full server-side
request forgery: the vault name, which an admin settings save can supply,
became the request's hostname. The readiness check already fullmatched the
name, but in another function, so the request site did not show it.
_load_workflow_key now fullmatches the exact stripped name it puts in the URL,
so every request stays on a vault in the configured Key Vault domain. A new
test bypasses readiness and confirms that invalid names never reach
SecretClient.

The result page tests now assert the HTTP status they were discarding.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) merged commit 81e78a3 into microsoft:paullizer-react-v2-ui Oct 8, 2026
11 checks passed
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 8, 2026
Resolve the version and release-notes conflicts with microsoft#1716: keep VERSION
0.261.303 and keep both release-note sections, v0.261.303 above v0.261.302.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 8, 2026
… flow branch

Brings in microsoft#1716 (Microsoft 365 sign-in session race fix, automatic workflow
encryption key, Microsoft 365 links that stay in V2).

Conflict and semantic resolutions:
- release_notes.md: both branches added entries under v0.261.302; kept both
  sets. The next commit moves this branch's entries to their own version.
- microsoft#1716 moved M365_APPROVALS_HREF from m365Connect.ts to m365Links.ts as an
  in-app router path (/approvals/m365) rendered with Link. The inline
  approval card now imports it from there and renders a Link, and its
  Settings link uses the new section deep link to the Microsoft 365 sharing
  card, where Extended analysis is set (M365_SHARING_PREFERENCES_HREF).
- The m365Links guardrail test pins every exported link the XSS checker
  trusts by name; it now pins the new sharing-preferences path.
- The inline approval UI test expects the new in-app link paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 8, 2026
microsoft#1716 shipped as 0.261.302, the version this branch used, and the open
Microsoft 365 citations PR (microsoft#1718) holds 0.261.303. Move this branch to
0.261.304: config.py, its own release-notes section above v0.261.302, and
the version headers and "Since" notes in its code, tests and docs.

The fix doc also lists the sharing-preferences link and guardrail pin added
when the base was merged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants