Skip to content

[MOB-4187] Implement proactive token refresh to prevent 401 errors#1047

Merged
d4r1091 merged 13 commits intomainfrom
dc-mob-4187-proactively-refresh-credentials
Feb 23, 2026
Merged

[MOB-4187] Implement proactive token refresh to prevent 401 errors#1047
d4r1091 merged 13 commits intomainfrom
dc-mob-4187-proactively-refresh-credentials

Conversation

@d4r1091
Copy link
Copy Markdown
Member

@d4r1091 d4r1091 commented Feb 19, 2026

[MOB-4187

Context

Previously, the app would wait for 401 responses before refreshing expired tokens, causing excessive authentication errors and poor user experience.

This change implements a proactive approach where tokens are automatically refreshed before API calls, leveraging Auth0's CredentialsManager which handles token expiry detection and refresh seamlessly.

Approach

  • Add getFreshAccessToken() method to EcosiaAuthenticationService that retrieves fresh tokens from Auth0
  • Update EcosiaAuthUIStateProvider to use fresh tokens for API calls
  • Simplify AccountsService by removing reactive 401 retry logic
  • Add new error cases to AuthError enum for better error handling
  • Add comprehensive tests for the new token refresh behavior
  • Update AccountsServiceTests to reflect that 401s now indicate genuine auth failures

This approach eliminates unnecessary 401 errors and provides a more reliable authentication experience.

Other

Before merging

Checklist

  • I performed some relevant testing on a real device and/or simulator for both iPhone and iPad
  • I wrote Unit Tests that confirm the expected behaviour

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements proactive access-token refresh via Auth0’s CredentialsManager so API calls use fresh tokens and avoid “refresh-on-401” behavior, updating the UI integration and tests accordingly.

Changes:

  • Add getFreshAccessToken() to EcosiaAuthenticationService and extend AuthError with retrieval/not-logged-in errors.
  • Update EcosiaAuthUIStateProvider to fetch a fresh token before calling registerVisit.
  • Remove reactive 401 retry/renew logic from AccountsService and adjust/add unit tests.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
firefox-ios/EcosiaTests/Core/Accounts/AccountsServiceTests.swift Updates 401/unauthorized expectations in tests.
firefox-ios/EcosiaTests/Account/Auth/AuthTests.swift Adds unit tests for getFreshAccessToken().
firefox-ios/Ecosia/UI/Account/EcosiaAuthUIStateProvider.swift Injects auth service + uses fresh token for registerVisit.
firefox-ios/Ecosia/Core/Accounts/Service/AccountsService.swift Simplifies registerVisit by removing 401 retry/renew flow.
firefox-ios/Ecosia/Account/Auth/EcosiaAuthenticationService.swift Introduces getFreshAccessToken() using Auth0 credential retrieval.
firefox-ios/Ecosia/Account/Auth/AuthError.swift Adds credentialsRetrievalFailed and notLoggedIn error cases.

Comment thread firefox-ios/Ecosia/Account/Auth/EcosiaAuthenticationService.swift Outdated
Comment on lines +71 to +77
public init(accountsProvider: AccountsProviderProtocol, authenticationService: EcosiaAuthenticationService = .shared) {
self.accountsProvider = accountsProvider
self.authenticationService = authenticationService

// Initialize state synchronously to prevent flickering
self.isLoggedIn = EcosiaAuthenticationService.shared.isLoggedIn
self.userProfile = EcosiaAuthenticationService.shared.userProfile
self.isLoggedIn = authenticationService.isLoggedIn
self.userProfile = authenticationService.userProfile
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that EcosiaAuthUIStateProvider supports injecting an authenticationService, the class should use that dependency consistently. There are still code paths (e.g. user profile updates) that read from EcosiaAuthenticationService.shared, which makes the new initializer injection ineffective in tests and can desync state if a non-shared service is passed. Please replace remaining ...shared reads in this type with the authenticationService property.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +506 to +516
func testGetFreshAccessToken_withExpiredToken_refreshesAutomatically() async throws {
// Arrange - Auth0's CredentialsManager automatically refreshes expired tokens
let freshToken = "refreshed-access-token"
let credentials = Credentials(
accessToken: freshToken,
tokenType: "Bearer",
idToken: "test-id-token",
refreshToken: "test-refresh-token",
expiresIn: Date().addingTimeInterval(3600),
scope: "openid profile email"
)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testGetFreshAccessToken_withExpiredToken_refreshesAutomatically doesn’t currently set up an expired token scenario or assert that a refresh path was taken (it just verifies whatever retrieveCredentials() returns). Consider renaming the test to reflect what’s actually being validated, or adjusting the mock/setup to simulate an expired credential + refresh to make the test match its intent.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot that's a fair point. I would adjust the mock/setup to match the test's real purpose

Comment thread firefox-ios/EcosiaTests/Core/Accounts/AccountsServiceTests.swift Outdated
Comment on lines +272 to +279
let credentials = try await auth0Provider.retrieveCredentials()
// Update cached tokens with fresh values
setupTokensWithCredentials(credentials, settingLoggedInStateTo: true)

let accessToken = credentials.accessToken
guard !accessToken.isEmpty else {
EcosiaLogger.auth.error("Retrieved credentials do not contain a valid access token")
throw AuthError.notLoggedIn
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getFreshAccessToken() calls setupTokensWithCredentials(..., settingLoggedInStateTo: true), which (a) dispatches an auth state change as .userLoggedIn every time and can trigger observers (e.g. EcosiaAuthUIStateProvider) to re-run login-only side effects like registerVisitIfNeeded(), potentially causing repeated calls/loops; and (b) sets isLoggedIn = true before validating that credentials.accessToken is non-empty. Consider updating cached tokens without broadcasting a login event (or only dispatching when isLoggedIn actually changes), and only marking logged-in after the access token is validated.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Copy link
Copy Markdown
Collaborator

@lucaschifino lucaschifino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good from my side, a couple minor comments

I see there are some linting errors to be fixed too

Comment thread firefox-ios/Ecosia/Core/Accounts/Service/AccountsService.swift Outdated
Comment thread firefox-ios/Ecosia/Account/Auth/EcosiaAuthenticationService.swift Outdated
Copy link
Copy Markdown

Copilot AI commented Feb 20, 2026

@d4r1091 I've opened a new pull request, #1050, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Copy Markdown

Copilot AI commented Feb 20, 2026

@d4r1091 I've opened a new pull request, #1051, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Copy Markdown

Copilot AI commented Feb 20, 2026

@d4r1091 I've opened a new pull request, #1052, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Copy Markdown

Copilot AI commented Feb 20, 2026

@d4r1091 I've opened a new pull request, #1053, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Copy Markdown

Copilot AI commented Feb 20, 2026

@d4r1091 I've opened a new pull request, #1054, to work on those changes. Once the pull request is ready, I'll request review from you.

d4r1091 and others added 11 commits February 22, 2026 20:55
Previously, the app would wait for 401 responses before refreshing expired tokens, causing excessive authentication errors and poor user experience.

This change implements a proactive approach where tokens are automatically refreshed before API calls, leveraging Auth0's CredentialsManager which handles token expiry detection and refresh seamlessly.

Changes:
- Add getFreshAccessToken() method to EcosiaAuthenticationService that retrieves fresh tokens from Auth0
- Update EcosiaAuthUIStateProvider to use fresh tokens for API calls
- Simplify AccountsService by removing reactive 401 retry logic
- Add new error cases to AuthError enum for better error handling
- Add comprehensive tests for the new token refresh behavior
- Update AccountsServiceTests to reflect that 401s now indicate genuine auth failures

This approach eliminates unnecessary 401 errors and provides a more reliable authentication experience.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…teProvider` (#1050)

* Initial plan

* Replace EcosiaAuthenticationService.shared with injected authenticationService in handleUserProfileUpdate

Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>
#1053)

* Initial plan

* Remove performVisitRequest and inline body into registerVisit

Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>
…ials` protocol (#1054)

* Initial plan

* Clarify retrieveCredentials protocol wraps CredentialsManager.credentials() in doc comment

Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>
… dispatch auth events on actual transitions (#1052)

* Initial plan

* Fix getFreshAccessToken: validate token before updating state; only dispatch on state transitions

Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>
…o actually simulate expiry and refresh (#1051)

* Initial plan

* Adjust mock/setup for expired token refresh test

Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: d4r1091 <3584008+d4r1091@users.noreply.github.com>
@d4r1091 d4r1091 force-pushed the dc-mob-4187-proactively-refresh-credentials branch from 4726ac9 to c2ab192 Compare February 22, 2026 20:16
@d4r1091
Copy link
Copy Markdown
Member Author

d4r1091 commented Feb 22, 2026

I had to apply some changes to the following implementation when realising that the SSO exchanges were failing with "Session Transfer Token not found or expired". This was caused by a race condition where getFreshAccessToken() was rotating the refresh token between SSO token generation and usage.
I modified getSSOCredentials() to use credentials(withScope: nil, minTTL: 0) instead of retrieveCredentials(). Setting minTTL: 0 prevents Auth0's CredentialsManager from automatically refreshing tokens during SSO token generation, as we make the Impact API call when the SSO injection via "silent tabs" is still happening.

@d4r1091 d4r1091 requested a review from lucaschifino February 23, 2026 08:28

## How It Works

1. Calls `auth0Provider.retrieveCredentials()` (protocol implementation backed by Auth0's `CredentialsManager.credentials()`) which checks token expiry
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot can we refernce the protocoll here, is there something written in a public RFC or on a website about this Auth0 standard

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the addition @falkorichter!
I double-checked the documentation @copilot suggested. I guess it adds something generic and somewhat confusing. What can be relevant here is this doc. @copilot update your existing PR mentionig solely this doc

Copy link
Copy Markdown

Copilot AI commented Feb 23, 2026

@falkorichter I've opened a new pull request, #1055, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Copy Markdown

Copilot AI commented Feb 23, 2026

@d4r1091 I've opened a new pull request, #1056, to work on those changes. Once the pull request is ready, I'll request review from you.

@d4r1091 d4r1091 force-pushed the dc-mob-4187-proactively-refresh-credentials branch from 97067fc to 3ac8d7f Compare February 23, 2026 10:38
@d4r1091 d4r1091 merged commit e3be459 into main Feb 23, 2026
2 of 3 checks passed
@d4r1091 d4r1091 deleted the dc-mob-4187-proactively-refresh-credentials branch February 23, 2026 11:43
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.

5 participants