Endurain oauth#1356
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a more robust authentication flow for the Endurain synchronizer, including a dedicated WebView-based OAuth/SSO login with PKCE and new token refresh handling, plus UX changes to support MFA and alternate login paths.
Changes:
- Added
EndurainLoginActivity(WebView) andPKCEUtilto support PKCE-based SSO/OAuth login and token capture. - Extended auth handling in
SyncManager/Synchronizerto support MFA and to better handle multi-step authentication outcomes. - Updated
EndurainSynchronizerto persist/access refresh + CSRF tokens, refresh tokens on demand, and trigger refresh on 401 during upload.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| app/src/main/org/runnerup/view/EndurainLoginActivity.java | New WebView-based Endurain login flow with PKCE and token exchange. |
| app/src/main/org/runnerup/export/util/PKCEUtil.java | New helper to generate PKCE verifier/challenge. |
| app/src/main/org/runnerup/export/SyncManager.java | Auth flow updates: MFA prompt, NEED_AUTH handling changes, Endurain “Web Login” option, logging. |
| app/src/main/org/runnerup/export/Synchronizer.java | Adds MFA to AuthMethod. |
| app/src/main/org/runnerup/export/EndurainSynchronizer.java | Adds refresh/CSRF token handling, refresh endpoint support, OAuth intent/result wiring, upload 401->refresh behavior. |
| app/build.gradle | Bumps Material Components dependency. |
| app/AndroidManifest.xml | Registers EndurainLoginActivity. |
Comments suppressed due to low confidence (1)
app/src/main/org/runnerup/export/EndurainSynchronizer.java:434
- When
refreshToken()fails and returnsNEED_AUTH,access_tokenis left unchanged. Sinceconnect()returnsOKwheneveraccess_token != null, the synchronizer can get stuck in a state where it never prompts for re-auth and keeps using an invalid token. Clearaccess_token(and likelycsrf_token) when refresh fails / before returningNEED_AUTH.
public Status refreshToken() {
if (TextUtils.isEmpty(refresh_token)) {
return getNeedAuthStatus();
}
String endpoint = getEndpoint(REFRESH_URL_PATH);
try {
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(MediaType.parse("application/json"), "{}");
Request request = new Request.Builder()
.url(endpoint)
.addHeader("X-Client-Type", "mobile")
.addHeader("Authorization", "Bearer " + refresh_token)
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
String responseBody = response.body() != null ? response.body().string() : "";
if (response.isSuccessful()) {
JSONObject obj = new JSONObject(responseBody);
return parseAuthData(obj);
}
}
} catch (Exception e) {
Log.e(getName(), "refreshToken: exception during request", e);
}
return getNeedAuthStatus();
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (mDB != null) { | ||
| DBHelper.closeDB(mDB); | ||
| } | ||
| executor.shutdown(); |
| authCallback = originalCallback; | ||
| authSynchronizer = synchronizer; | ||
|
|
||
| mSpinner.show(); | ||
|
|
||
| executor.execute(() -> { | ||
| Status newStatus = synchronizer.connect(); | ||
| mActivity.runOnUiThread(() -> handleAuth(originalCallback, synchronizer, newStatus.authMethod)); | ||
| }); | ||
|
|
| .setNeutralButton( | ||
| (authSynchronizer.getName().equals(EndurainSynchronizer.NAME)) | ||
| ? "Web Login" : "Skip", | ||
| (dialog, which) -> | ||
| { | ||
| if (authSynchronizer.getName().equals(EndurainSynchronizer.NAME)) { | ||
| try { | ||
| //noinspection ConstantConditions | ||
| authConfig.put("username", null); | ||
| authConfig.put("password", null); | ||
| if (authMethod == AuthMethod.USER_PASS_URL) { | ||
| authConfig.put(DB.ACCOUNT.URL, urlInput.getText()); | ||
| } | ||
| } catch (JSONException e) { | ||
| e.printStackTrace(); | ||
| } | ||
| testUserPass(sync, authConfig); |
| .setTitle("Error") | ||
| .setMessage("Authentication failed or rate limit exceeded. Please try again later.") |
| private boolean handleOverride(WebView view, String url, String challenge) { | ||
| if (url.contains("sso=success") && url.contains("session_id=")) { | ||
| Uri uri = Uri.parse(url); | ||
| String sessionId = uri.getQueryParameter("session_id"); |
| private boolean handleOverride(WebView view, String url, String challenge) { | ||
| if (url.contains("sso=success") && url.contains("session_id=")) { | ||
| Uri uri = Uri.parse(url); | ||
| String sessionId = uri.getQueryParameter("session_id"); | ||
| exchangeSessionForTokens(sessionId); | ||
| return true; | ||
| } | ||
|
|
||
| if (url.contains("/api/v1/public/idp/login/")) { | ||
| if (!url.contains("code_challenge=" + challenge)) { | ||
|
|
||
| Uri baseUri = Uri.parse(url); | ||
| Uri.Builder newUri = baseUri.buildUpon(); | ||
|
|
||
| newUri.clearQuery(); | ||
|
|
||
| newUri.appendQueryParameter("code_challenge", challenge); | ||
| newUri.appendQueryParameter("code_challenge_method", "S256"); | ||
|
|
||
| view.loadUrl(newUri.build().toString()); | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private void checkUrlAndExtractTokens(WebView view, String url, String challenge) { | ||
| if (isFinishing) return; | ||
|
|
||
| if (url.contains("sso=success") && url.contains("session_id=")) { | ||
| view.stopLoading(); | ||
| Uri uri = Uri.parse(url); | ||
| String sessionId = uri.getQueryParameter("session_id"); | ||
| exchangeSessionForTokens(sessionId); | ||
| return; | ||
| } |
| super.onDestroy(); | ||
| isFinishing = true; | ||
| executor.shutdown(); |
| try (Response response = client.newCall(request).execute()) { | ||
| if (response.isSuccessful()) { | ||
| s = Status.OK; | ||
| } else if (response.code() == 401) { | ||
| s = Status.NEED_REFRESH; | ||
| } else { | ||
| s = Status.ERROR; | ||
| } |
gerhardol
left a comment
There was a problem hiding this comment.
Some copilot comments that seem reasonable.
The changes seem reasonable, will look into more details later.
Someone else using Endurain that can test?
Squash at merge?
|
If you want you can test it with the demo instance of endurain (https://demo.endurain.com/ admin/admin) but be aware that is a public instance. I can have a look later again on your findings. Thanks in advance for reviewing. |
and review |
Implement Endurain Token Refresh and Robust OAuth2 Support
Related Issue
Closes #1340 (Fixes Endurain upload and session connection rotation failures).
Summary of Changes
This PR implements:
How to test these changes
If you like you can test the changes against the demo instance of endurain:
https://docs.endurain.com/#try-the-demo