fix(auth): drop redundant second Outlook token request on sign-in #317

Merged
JMR-dev merged 22 commits from fix-306-outlook-redundant-token into main 2026-07-05 18:10:20 +00:00
JMR-dev commented 2026-07-04 07:22:27 +00:00 (Migrated from github.com)

Closes #306

What & why

OutlookAuthManager.exchangeToken's authorization-code exchange already requests openid email offline_access $OUTLOOK_SCOPE, so tokenResponse.accessToken is already a valid outlook.office.com (Exchange Online) token usable for IMAP verification, and the resulting AuthState already carries the refresh token and access-token expiry.

It nonetheless immediately called refreshForScope(authState, OUTLOOK_SCOPE) — a second round-trip for the same resource that only rotated the just-issued refresh token and added a needless onboarding failure point (a transient network error there failed the whole sign-in after consent + code-exchange had already succeeded).

Change

Build OAuthResult directly from the code-exchange tokenResponse (accessToken + authState.jsonSerializeString()), dropping the extra refresh call.

  • Durable auth state still persisted for refresh: the AuthState is updated with the code-exchange tokenResponse and serialized via jsonSerializeString() into OAuthResult.authStateJson, which addOutlookAccount stores through credentialStore.saveSecret. It carries the refresh token and the access token's expiry for later refreshes.
  • Graph token unaffected: it is a distinct resource, still minted on demand via freshGraphToken.
  • Downstream (AccountRepositoryImpl.addOutlookAccount) uses accessToken once for IMAP verification and authStateJson as the stored credential — both preserved.

Tests

Updated OutlookAuthManagerTest's happy-path test to assert the result is built from the code-exchange token (accessToken == "code-access"), that the durable AuthState (refresh token) is serialized into authStateJson, and — via verify(exactly = 1) — that performTokenRequest is called exactly once (no second refresh). Simplified the preferred_username fallback test to a single token response.

Gate run locally (no emulator, per scope): :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt — all green.

🤖 Generated with Claude Code

Closes #306 ## What & why `OutlookAuthManager.exchangeToken`'s authorization-code exchange already requests `openid email offline_access $OUTLOOK_SCOPE`, so `tokenResponse.accessToken` is already a valid `outlook.office.com` (Exchange Online) token usable for IMAP verification, and the resulting `AuthState` already carries the refresh token and access-token expiry. It nonetheless immediately called `refreshForScope(authState, OUTLOOK_SCOPE)` — a second round-trip for the **same** resource that only rotated the just-issued refresh token and added a needless onboarding failure point (a transient network error there failed the whole sign-in after consent + code-exchange had already succeeded). ## Change Build `OAuthResult` directly from the code-exchange `tokenResponse` (`accessToken` + `authState.jsonSerializeString()`), dropping the extra refresh call. - **Durable auth state still persisted for refresh:** the `AuthState` is updated with the code-exchange `tokenResponse` and serialized via `jsonSerializeString()` into `OAuthResult.authStateJson`, which `addOutlookAccount` stores through `credentialStore.saveSecret`. It carries the refresh token and the access token's expiry for later refreshes. - **Graph token unaffected:** it is a distinct resource, still minted on demand via `freshGraphToken`. - Downstream (`AccountRepositoryImpl.addOutlookAccount`) uses `accessToken` once for IMAP verification and `authStateJson` as the stored credential — both preserved. ## Tests Updated `OutlookAuthManagerTest`'s happy-path test to assert the result is built from the code-exchange token (`accessToken == "code-access"`), that the durable `AuthState` (refresh token) is serialized into `authStateJson`, and — via `verify(exactly = 1)` — that `performTokenRequest` is called exactly once (no second refresh). Simplified the `preferred_username` fallback test to a single token response. Gate run locally (no emulator, per scope): `:app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt` — all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
JMR-dev commented 2026-07-04 07:24:08 +00:00 (Migrated from github.com)

Coordinator review: the analysis is sound (code-exchange token is already outlook.office.com-scoped; Graph token minted separately; AuthState persisted) and unit-tested. Leaving auto-merge OFF pending a real-Outlook-account sign-in test — this touches the OAuth token flow (3 prior bugs in this area) and CI can't validate against a live MS account. It's a LOW/efficiency optimization, so no urgency.

Coordinator review: the analysis is sound (code-exchange token is already outlook.office.com-scoped; Graph token minted separately; AuthState persisted) and unit-tested. Leaving auto-merge OFF pending a real-Outlook-account sign-in test — this touches the OAuth token flow (3 prior bugs in this area) and CI can't validate against a live MS account. It's a LOW/efficiency optimization, so no urgency.
Sign in to join this conversation.