Skip to content

Keycloak - offline_access & auto login#5400

Open
brettedw wants to merge 20 commits into
mainfrom
task/keycloak-asago-tweaks
Open

Keycloak - offline_access & auto login#5400
brettedw wants to merge 20 commits into
mainfrom
task/keycloak-asago-tweaks

Conversation

@brettedw

@brettedw brettedw commented May 11, 2026

Copy link
Copy Markdown
Collaborator
  • Before this PR, ASA Go was not reliably using Keycloak’s 60-day offline sessions on mobile.

-Android was not requesting offline_access, so its refresh token payload had "typ": "Refresh" instead of the expected "typ": "Offline". iOS was receiving offline tokens, but its AppAuth state was not persisted somewhere the app could reuse after restart. Android also persisted AppAuth state but did not check the stored state before starting a new browser login flow.

  • Login was relying mostly on browser session cookies stored on the device. On iOS, clearing recent Safari cache/cookies could cause ASA Go to prompt for login again

  • This PR

    • Adds the offline_access scope to Android authentication requests.
    • Stores Android AppAuth state in encrypted DataStore using Tink, with migration from the legacy SharedPreferences state.
    • Stores iOS OIDAuthState in Keychain.
    • Checks stored native auth state before opening the login browser.
    • Refreshes stored auth state when possible and returns a current access token to the app.
    • Avoids opening the login browser when a valid stored offline token can be reused.
    • Adds native refreshAuthState() and clearAuthState() APIs.
    • Updates ASA Go’s 401 handling to try a native token refresh and retry the failed request once before clearing auth.
    • Falls back to browser login when the stored offline token is expired, revoked, missing, or otherwise unusable.
    • Pauses automatic native token refresh while the app is backgrounded and resumes/refreshes as needed when the app returns to the foreground.
    • Added a 401 recovery path for authenticated requests. If an API request fails because the Redux access token is stale, ASA Go asks the native AppAuth state to refresh the token and retries the failed request once. This covers timing gaps where the app resumes or sends a request before automatic token refresh has updated Redux. Guest/login sessions do not trigger this recovery path.

Test Links:

Landing Page
MoreCast
Percentile Calculator
C-Haines
FireCalc
FireCalc bookmark
Auto Spatial Advisory (ASA)
HFI Calculator
SFMS Insights
Fire Watch
Weather Toolkit

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 53.21888% with 109 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.52%. Comparing base (315f191) to head (9dde066).

Files with missing lines Patch % Lines
.../main/java/ca/bcgov/plugins/keycloak/Keycloak.java 12.64% 73 Missing and 3 partials ⚠️
...java/ca/bcgov/plugins/keycloak/KeycloakPlugin.java 7.69% 23 Missing and 1 partial ⚠️
...java/ca/bcgov/plugins/keycloak/AuthStateStore.java 86.56% 8 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #5400      +/-   ##
============================================
+ Coverage     67.04%   67.52%   +0.47%     
- Complexity        0       46      +46     
============================================
  Files           381      481     +100     
  Lines         22619    25053    +2434     
  Branches       3095     3481     +386     
============================================
+ Hits          15166    16918    +1752     
- Misses         6291     6881     +590     
- Partials       1162     1254      +92     
Flag Coverage Δ
android 29.97% <39.44%> (?)
asa_go 82.21% <100.00%> (?)

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@brettedw brettedw marked this pull request as ready for review May 11, 2026 18:51
@sonarqubecloud

Copy link
Copy Markdown

@brettedw brettedw requested review from conbrad and dgboss May 13, 2026 18:24
Comment thread mobile/keycloak/android/src/main/java/ca/bcgov/plugins/keycloak/Keycloak.java Outdated
Comment thread mobile/keycloak/android/src/main/java/ca/bcgov/plugins/keycloak/Keycloak.java Outdated
Comment thread mobile/keycloak/android/src/main/java/ca/bcgov/plugins/keycloak/Keycloak.java Outdated
@brettedw brettedw closed this May 14, 2026
@brettedw brettedw reopened this Jun 25, 2026
@brettedw brettedw requested a review from conbrad June 25, 2026 22:43
@sonarqubecloud

Copy link
Copy Markdown

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe we can rename this to ContinueAsGuestButton for consistency.

}
}

private func refreshExistingAuthState(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Some duplication with refreshAuthState above, do we need both or can we deduplicate some code?

setupAutomaticRefresh();
}

private AuthState restoreAuthState() {

@conbrad conbrad Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All of this runs on the Android main thread (Capacitor calls KeycloakPlugin.load()new Keycloak(context)restoreAuthState()authStateStore.read()DataStore.data().firstOrError().blockingGet(). ). On cold start with slow flash I/O, this could exceed Android's 5-second ANR threshold and force-closes the app. write() (called from persistAuthState() inside performActionWithFreshTokens callbacks) and clear() carry the same risk if AppAuth delivers those callbacks on the main thread.

We can use a shared IO executor thread pool to keep this off the main thread like: task/keycloak-asago-tweaks...task/executor-non-blocking-fix

expiresIn?: number
scope?: string
}) => {
const handleTokenRefresh = (tokenResponse: KeycloakTokenResponse) => {

@conbrad conbrad Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Less likely to happen now but cheap to fix:

Every time authenticate() is dispatched a second handleTokenRefresh closure is registered. After N login sessions, N closures fire per native tokenRefresh event: N dispatches of authenticateFinished, N Sentry.setUser calls. The PluginListenerHandle returned by addListener is discarded.

We can have a module level handle to register the tokenRefresh handle once to avoid, like: task/keycloak-asago-tweaks...task/single-login-listener

self.notifyListeners("tokenRefresh", data: tokenResponse)
}
} else {
self.notifyListeners(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should clearStoredAuthState() here to avoid looping retries, see: task/keycloak-asago-tweaks...task/failed-foreground-fix-swift

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