From 64ca8f42426a58f4478593ad82e636662e06c92b Mon Sep 17 00:00:00 2001 From: Ankit Tiwari Date: Thu, 6 Jun 2024 18:59:20 +0530 Subject: [PATCH] fix: session refresh loop in all request interceptors --- CHANGELOG.md | 7 + app/build.gradle | 2 +- .../com/supertokens/session/SuperTokens.java | 10 +- .../session/SuperTokensHttpURLConnection.java | 15 ++ .../session/SuperTokensInterceptor.java | 15 ++ .../java/com/supertokens/session/Utils.java | 18 +- examples/with-thirdparty/README.md | 4 +- examples/with-thirdparty/app/build.gradle.kts | 2 +- testHelpers/server/index.js | 4 + .../SuperTokensHttpURLConnectionTest.java | 156 ++++++++++++++++++ .../example/SuperTokensOkHttpHeaderTests.java | 114 +++++++++++++ .../example/SuperTokensOkHttpTest.java | 117 +++++++++++++ .../SuperTokensRetrofitHeaderTests.java | 77 +++++++++ .../example/SuperTokensRetrofitTest.java | 80 +++++++++ .../android/RetrofitTestAPIService.java | 2 + 15 files changed, 617 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3aaf6..11251e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.0] - 2024-06-06 + +### Changes + +- Fixed the session refresh loop in all the request interceptors that occurred when an API returned a 401 response despite a valid session. Interceptors now attempt to refresh the session a maximum of ten times before throwing an error. The retry limit is configurable via the `maxRetryAttemptsForSessionRefresh` option. + + ## [0.4.2] - 2024-05-28 - re-Adds FDI 2.0 and 3.0 support diff --git a/app/build.gradle b/app/build.gradle index 91df9e5..d8716e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.library' apply plugin: 'maven-publish' -def publishVersionID = "0.4.2" +def publishVersionID = "0.5.0" android { compileSdkVersion 32 diff --git a/app/src/main/java/com/supertokens/session/SuperTokens.java b/app/src/main/java/com/supertokens/session/SuperTokens.java index 77c325b..c807614 100644 --- a/app/src/main/java/com/supertokens/session/SuperTokens.java +++ b/app/src/main/java/com/supertokens/session/SuperTokens.java @@ -50,6 +50,7 @@ private static void init( @NonNull String apiDomain, @Nullable String apiBasePath, @Nullable Integer sessionExpiredStatusCode, + @Nullable Integer maxRetryAttemptsForSessionRefresh, @Nullable String sessionTokenBackendDomain, @Nullable String tokenTransferMethod, @Nullable CustomHeaderProvider customHeaderProvider, @@ -63,6 +64,7 @@ private static void init( apiDomain, apiBasePath, sessionExpiredStatusCode, + maxRetryAttemptsForSessionRefresh, sessionTokenBackendDomain, tokenTransferMethod, customHeaderProvider, @@ -260,6 +262,7 @@ public static class Builder { Context applicationContext; String apiBasePath; Integer sessionExpiredStatusCode; + Integer maxRetryAttemptsForSessionRefresh; String sessionTokenBackendDomain; CustomHeaderProvider customHeaderProvider; EventHandler eventHandler; @@ -280,6 +283,11 @@ public Builder sessionExpiredStatusCode(Integer sessionExpiredStatusCode) { return this; } + public Builder maxRetryAttemptsForSessionRefresh(Integer maxRetryAttemptsForSessionRefresh) { + this.maxRetryAttemptsForSessionRefresh = maxRetryAttemptsForSessionRefresh; + return this; + } + public Builder sessionTokenBackendDomain(String cookieDomain) { this.sessionTokenBackendDomain = cookieDomain; return this; @@ -301,7 +309,7 @@ public Builder tokenTransferMethod(String tokenTransferMethod) { } public void build() throws MalformedURLException { - SuperTokens.init(applicationContext, apiDomain, apiBasePath, sessionExpiredStatusCode, sessionTokenBackendDomain, tokenTransferMethod, customHeaderProvider, eventHandler); + SuperTokens.init(applicationContext, apiDomain, apiBasePath, sessionExpiredStatusCode, maxRetryAttemptsForSessionRefresh, sessionTokenBackendDomain, tokenTransferMethod, customHeaderProvider, eventHandler); } } } diff --git a/app/src/main/java/com/supertokens/session/SuperTokensHttpURLConnection.java b/app/src/main/java/com/supertokens/session/SuperTokensHttpURLConnection.java index d67e4c1..83aa543 100644 --- a/app/src/main/java/com/supertokens/session/SuperTokensHttpURLConnection.java +++ b/app/src/main/java/com/supertokens/session/SuperTokensHttpURLConnection.java @@ -129,6 +129,7 @@ public static HttpURLConnection newRequest(URL url, PreConnectCallback preConnec } try { + int sessionRefreshAttempts = 0; while (true) { HttpURLConnection connection; SuperTokensCustomHttpURLConnection customConnection; @@ -184,8 +185,22 @@ public static HttpURLConnection newRequest(URL url, PreConnectCallback preConnec } if (responseCode == SuperTokens.config.sessionExpiredStatusCode) { + /** + * An API may return a 401 error response even with a valid session, causing a session refresh loop in the interceptor. + * To prevent this infinite loop, we break out of the loop after retrying the original request a specified number of times. + * The maximum number of retry attempts is defined by maxRetryAttemptsForSessionRefresh config variable. + */ + if (sessionRefreshAttempts >= SuperTokens.config.maxRetryAttemptsForSessionRefresh) { + String errorMsg = "Received a 401 response from " + url + ". Attempted to refresh the session and retry the request with the updated session tokens " + SuperTokens.config.maxRetryAttemptsForSessionRefresh + " times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."; + System.err.println(errorMsg); + throw new IllegalAccessException(errorMsg); + } + // Network call threw UnauthorisedAccess, try to call the refresh token endpoint and retry original call Utils.Unauthorised unauthorisedResponse = SuperTokensHttpURLConnection.onUnauthorisedResponse(preRequestLocalSessionState, applicationContext); + + sessionRefreshAttempts++; + if (unauthorisedResponse.status != Utils.Unauthorised.UnauthorisedStatus.RETRY) { if (unauthorisedResponse.error != null) { diff --git a/app/src/main/java/com/supertokens/session/SuperTokensInterceptor.java b/app/src/main/java/com/supertokens/session/SuperTokensInterceptor.java index 1bc4256..feef7cf 100644 --- a/app/src/main/java/com/supertokens/session/SuperTokensInterceptor.java +++ b/app/src/main/java/com/supertokens/session/SuperTokensInterceptor.java @@ -98,6 +98,7 @@ public Response intercept(@NotNull Chain chain) throws IOException { } try { + int sessionRefreshAttempts = 0; while (true) { Request.Builder requestBuilder = chain.request().newBuilder(); Utils.LocalSessionState preRequestLocalSessionState; @@ -134,6 +135,17 @@ public Response intercept(@NotNull Chain chain) throws IOException { } if (response.code() == SuperTokens.config.sessionExpiredStatusCode) { + /** + * An API may return a 401 error response even with a valid session, causing a session refresh loop in the interceptor. + * To prevent this infinite loop, we break out of the loop after retrying the original request a specified number of times. + * The maximum number of retry attempts is defined by maxRetryAttemptsForSessionRefresh config variable. + */ + if (sessionRefreshAttempts >= SuperTokens.config.maxRetryAttemptsForSessionRefresh) { + String errorMsg = "Received a 401 response from " + requestUrl + ". Attempted to refresh the session and retry the request with the updated session tokens " + SuperTokens.config.maxRetryAttemptsForSessionRefresh + " times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."; + System.err.println(errorMsg); + throw new IOException(errorMsg); + } + // Cloning the response object, if retry is false then we return this Response clonedResponse = new Response.Builder() .body(response.peekBody(Long.MAX_VALUE)) @@ -152,6 +164,9 @@ public Response intercept(@NotNull Chain chain) throws IOException { response.close(); Utils.Unauthorised unauthorisedResponse = onUnauthorisedResponse(preRequestLocalSessionState, applicationContext, chain); + + sessionRefreshAttempts++; + if (unauthorisedResponse.status != Utils.Unauthorised.UnauthorisedStatus.RETRY) { if (unauthorisedResponse.error != null) { throw unauthorisedResponse.error; diff --git a/app/src/main/java/com/supertokens/session/Utils.java b/app/src/main/java/com/supertokens/session/Utils.java index b253237..6d141b9 100644 --- a/app/src/main/java/com/supertokens/session/Utils.java +++ b/app/src/main/java/com/supertokens/session/Utils.java @@ -83,6 +83,14 @@ public static class NormalisedInputType { String apiDomain; String apiBasePath; int sessionExpiredStatusCode; + + /** + * This specifies the maximum number of times the interceptor will attempt to refresh + * the session when a 401 Unauthorized response is received. If the number of retries + * exceeds this limit, no further attempts will be made to refresh the session, and + * and an error will be thrown. + */ + int maxRetryAttemptsForSessionRefresh; String sessionTokenBackendDomain; CustomHeaderProvider customHeaderMapper; EventHandler eventHandler; @@ -93,6 +101,7 @@ public NormalisedInputType( String apiDomain, String apiBasePath, int sessionExpiredStatusCode, + int maxRetryAttemptsForSessionRefresh, String sessionTokenBackendDomain, String tokenTransferMethod, CustomHeaderProvider customHeaderMapper, @@ -100,6 +109,7 @@ public NormalisedInputType( this.apiDomain = apiDomain; this.apiBasePath = apiBasePath; this.sessionExpiredStatusCode = sessionExpiredStatusCode; + this.maxRetryAttemptsForSessionRefresh = maxRetryAttemptsForSessionRefresh; this.sessionTokenBackendDomain = sessionTokenBackendDomain; this.customHeaderMapper = customHeaderMapper; this.eventHandler = eventHandler; @@ -153,6 +163,7 @@ public static NormalisedInputType normaliseInputOrThrowError( String apiDomain, @Nullable String apiBasePath, @Nullable Integer sessionExpiredStatusCode, + @Nullable Integer maxRetryAttemptsForSessionRefresh, @Nullable String sessionTokenBackendDomain, @Nullable String tokenTransferMethod, @Nullable CustomHeaderProvider customHeaderProvider, @@ -169,6 +180,11 @@ public static NormalisedInputType normaliseInputOrThrowError( _sessionExpiredStatusCode = sessionExpiredStatusCode; } + int _maxRetryAttemptsForSessionRefresh = 10; + if (maxRetryAttemptsForSessionRefresh != null) { + _maxRetryAttemptsForSessionRefresh = maxRetryAttemptsForSessionRefresh; + } + String _sessionTokenBackendDomain = null; if (sessionTokenBackendDomain != null) { _sessionTokenBackendDomain = normaliseSessionScopeOrThrowError(sessionTokenBackendDomain); @@ -190,7 +206,7 @@ public static NormalisedInputType normaliseInputOrThrowError( _tokenTransferMethod = tokenTransferMethod; } - return new NormalisedInputType(_apiDomain, _apiBasePath, _sessionExpiredStatusCode, + return new NormalisedInputType(_apiDomain, _apiBasePath, _sessionExpiredStatusCode, _maxRetryAttemptsForSessionRefresh, _sessionTokenBackendDomain, _tokenTransferMethod, _customHeaderProvider, _eventHandler); } } diff --git a/examples/with-thirdparty/README.md b/examples/with-thirdparty/README.md index 96308ba..107f360 100644 --- a/examples/with-thirdparty/README.md +++ b/examples/with-thirdparty/README.md @@ -20,10 +20,10 @@ dependencyResolutionManagement { } ``` -Add the folliwing to your app level `build.gradle` +Add the following to your app level `build.gradle` ```gradle -implementation("com.github.supertokens:supertokens-android:0.4.2") +implementation("com.github.supertokens:supertokens-android:0.5.0") implementation ("com.google.android.gms:play-services-auth:20.7.0") implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("net.openid:appauth:0.11.1") diff --git a/examples/with-thirdparty/app/build.gradle.kts b/examples/with-thirdparty/app/build.gradle.kts index 5480a94..67b85f4 100644 --- a/examples/with-thirdparty/app/build.gradle.kts +++ b/examples/with-thirdparty/app/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.8.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("com.github.supertokens:supertokens-android:0.4.0") + implementation("com.github.supertokens:supertokens-android:0.5.0") implementation ("com.google.android.gms:play-services-auth:20.7.0") implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("net.openid:appauth:0.11.1") diff --git a/testHelpers/server/index.js b/testHelpers/server/index.js index c86d88d..ab2bf97 100644 --- a/testHelpers/server/index.js +++ b/testHelpers/server/index.js @@ -507,6 +507,10 @@ app.get("/testError", (req, res) => { res.status(500).send("test error message"); }); +app.get("/throw-401", (req, res) => { + res.status(401).send("Unauthorised"); +}) + app.get("/stop", async (req, res) => { process.exit(); }); diff --git a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensHttpURLConnectionTest.java b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensHttpURLConnectionTest.java index 6c81e28..442afc5 100644 --- a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensHttpURLConnectionTest.java +++ b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensHttpURLConnectionTest.java @@ -94,6 +94,7 @@ public class SuperTokensHttpURLConnectionTest { private final String testPingAPIURL = testBaseURL + "/ping"; private final String testErrorAPIURL = testBaseURL + "/testError"; private final String testCheckCustomRefresh = testBaseURL + "/refreshHeader"; + private final String throw401ErrorURL = testBaseURL + "/throw-401"; private final int sessionExpiryCode = 401; @@ -948,4 +949,159 @@ public void doAction(HttpURLConnection con) throws IOException { throw new Exception("Api request failed"); } } + + @Test + public void httpUrlConnection_testBreakOutOfSessionRefreshLoopAfterDefaultMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain).build(); + + //login request + HttpURLConnection loginRequestConnection = SuperTokensHttpURLConnection.newRequest(new URL(loginAPIURL), new SuperTokensHttpURLConnection.PreConnectCallback() { + @Override + public void doAction(HttpURLConnection con) throws IOException { + con.setDoOutput(true); + con.setRequestMethod("POST"); + con.setRequestProperty("Accept", "application/json"); + con.setRequestProperty("Content-Type", "application/json"); + + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("userId", Constants.userId); + + OutputStream outputStream = con.getOutputStream(); + outputStream.write(bodyJson.toString().getBytes(StandardCharsets.UTF_8)); + outputStream.close(); + } + }); + + if (loginRequestConnection.getResponseCode() != 200) { + throw new Exception("Login request failed"); + } + + loginRequestConnection.disconnect(); + + HttpURLConnection throw401Request = null; + try { + throw401Request = SuperTokensHttpURLConnection.newRequest(new URL(throw401ErrorURL), new SuperTokensHttpURLConnection.PreConnectCallback() { + @Override + public void doAction(HttpURLConnection con) throws IOException { + con.setRequestMethod("GET"); + } + }); + throw new Exception("Expected the request to throw an error"); + } catch (IllegalAccessException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 10 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } finally { + if (throw401Request != null) { + throw401Request.disconnect(); + } + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 10) { + throw new Exception("Expected session refresh endpoint to be called 10 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void httpUrlConnection_testBreakOutOfSessionRefreshLoopAfterConfiguredMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain).maxRetryAttemptsForSessionRefresh(5).build(); + + //login request + HttpURLConnection loginRequestConnection = SuperTokensHttpURLConnection.newRequest(new URL(loginAPIURL), new SuperTokensHttpURLConnection.PreConnectCallback() { + @Override + public void doAction(HttpURLConnection con) throws IOException { + con.setDoOutput(true); + con.setRequestMethod("POST"); + con.setRequestProperty("Accept", "application/json"); + con.setRequestProperty("Content-Type", "application/json"); + + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("userId", Constants.userId); + + OutputStream outputStream = con.getOutputStream(); + outputStream.write(bodyJson.toString().getBytes(StandardCharsets.UTF_8)); + outputStream.close(); + } + }); + + if (loginRequestConnection.getResponseCode() != 200) { + throw new Exception("Login request failed"); + } + + loginRequestConnection.disconnect(); + + HttpURLConnection throw401Request = null; + try { + throw401Request = SuperTokensHttpURLConnection.newRequest(new URL(throw401ErrorURL), new SuperTokensHttpURLConnection.PreConnectCallback() { + @Override + public void doAction(HttpURLConnection con) throws IOException { + con.setRequestMethod("GET"); + } + }); + throw new Exception("Expected the request to throw an error"); + } catch (IllegalAccessException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 5 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } finally { + if (throw401Request != null) { + throw401Request.disconnect(); + } + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 5) { + throw new Exception("Expected session refresh endpoint to be called 5 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void httpUrlConnection_testShouldNotDoSessionRefreshIfMaxRetryAttemptsForSessionRefreshIsZero() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain).maxRetryAttemptsForSessionRefresh(0).build(); + //login request + HttpURLConnection loginRequestConnection = SuperTokensHttpURLConnection.newRequest(new URL(loginAPIURL), new SuperTokensHttpURLConnection.PreConnectCallback() { + @Override + public void doAction(HttpURLConnection con) throws IOException { + con.setDoOutput(true); + con.setRequestMethod("POST"); + con.setRequestProperty("Accept", "application/json"); + con.setRequestProperty("Content-Type", "application/json"); + + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("userId", Constants.userId); + + OutputStream outputStream = con.getOutputStream(); + outputStream.write(bodyJson.toString().getBytes(StandardCharsets.UTF_8)); + outputStream.close(); + } + }); + + if (loginRequestConnection.getResponseCode() != 200) { + throw new Exception("Login request failed"); + } + + loginRequestConnection.disconnect(); + + HttpURLConnection throw401Request = null; + try { + throw401Request = SuperTokensHttpURLConnection.newRequest(new URL(throw401ErrorURL), new SuperTokensHttpURLConnection.PreConnectCallback() { + @Override + public void doAction(HttpURLConnection con) throws IOException { + con.setRequestMethod("GET"); + } + }); + throw new Exception("Expected the request to throw an error"); + } catch (IllegalAccessException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 0 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } finally { + if (throw401Request != null) { + throw401Request.disconnect(); + } + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 0) { + throw new Exception("Expected session refresh endpoint to be called 0 times but it was called " + sessionRefreshCalledCount + " times"); + } + } } diff --git a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensOkHttpHeaderTests.java b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensOkHttpHeaderTests.java index 6db32f4..8e7149d 100644 --- a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensOkHttpHeaderTests.java +++ b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensOkHttpHeaderTests.java @@ -87,6 +87,7 @@ public class SuperTokensOkHttpHeaderTests { private final String testCheckDeviceInfoAPIURL = testBaseURL + "/checkDeviceInfo"; private final String testErrorAPIURL = testBaseURL + "/testError"; private final String testPingAPIURL = testBaseURL + "/ping"; + private final String throw401APIURL = testBaseURL + "/throw-401"; private final int sessionExpiryCode = 401; private static OkHttpClient okHttpClient; @@ -1107,6 +1108,119 @@ public void okhttpHeaders_testThatAuthHeaderIsNotIgnoredEvenIfItMatchesTheStored response2.close(); } + @Test + public void okhttpHeaders_testBreakOutOfSessionRefreshLoopAfterDefaultMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .build(); + + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("userId", Constants.userId); + RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), bodyJson.toString()); + Request request = new Request.Builder() + .url(loginAPIURL) + .method("POST", body) + .addHeader("Accept", "application/json") + .addHeader("Content-Type", "application/json") + .build(); + Response loginResponse = okHttpClient.newCall(request).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + loginResponse.close(); + + try { + Request throw401Request = new Request.Builder() + .url(throw401APIURL) + .build(); + okHttpClient.newCall(throw401Request).execute(); + throw new Exception("Expected the request to throw an error"); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 10 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 10) { + throw new Exception("Expected session refresh endpoint to be called 10 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void okhttpHeaders_testBreakOutOfSessionRefreshLoopAfterConfiguredMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .maxRetryAttemptsForSessionRefresh(5) + .build(); + + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("userId", Constants.userId); + RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), bodyJson.toString()); + Request request = new Request.Builder() + .url(loginAPIURL) + .method("POST", body) + .addHeader("Accept", "application/json") + .addHeader("Content-Type", "application/json") + .build(); + Response loginResponse = okHttpClient.newCall(request).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + loginResponse.close(); + + try { + Request throw401Request = new Request.Builder() + .url(throw401APIURL) + .build(); + okHttpClient.newCall(throw401Request).execute(); + throw new Exception("Expected the request to throw an error"); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 5 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 5) { + throw new Exception("Expected session refresh endpoint to be called 5 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void okhttpHeaders_testShouldNotDoSessionRefreshIfMaxRetryAttemptsForSessionRefreshIsZero() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .maxRetryAttemptsForSessionRefresh(0) + .build(); + + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("userId", Constants.userId); + RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), bodyJson.toString()); + Request request = new Request.Builder() + .url(loginAPIURL) + .method("POST", body) + .addHeader("Accept", "application/json") + .addHeader("Content-Type", "application/json") + .build(); + Response loginResponse = okHttpClient.newCall(request).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + loginResponse.close(); + + try { + Request throw401Request = new Request.Builder() + .url(throw401APIURL) + .build(); + okHttpClient.newCall(throw401Request).execute(); + throw new Exception("Expected the request to throw an error"); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 0 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 0) { + throw new Exception("Expected session refresh endpoint to be called 0 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + //custom interceptors class customInterceptors implements Interceptor { @NotNull diff --git a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensOkHttpTest.java b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensOkHttpTest.java index 32aab5b..f195f13 100644 --- a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensOkHttpTest.java +++ b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensOkHttpTest.java @@ -82,6 +82,7 @@ public class SuperTokensOkHttpTest { private final String testCheckDeviceInfoAPIURL = testBaseURL + "/checkDeviceInfo"; private final String testErrorAPIURL = testBaseURL + "/testError"; private final String testPingAPIURL = testBaseURL + "/ping"; + private final String throw401APIURL = testBaseURL + "/throw-401"; private final int sessionExpiryCode = 401; private static OkHttpClient okHttpClient; @@ -1053,6 +1054,122 @@ public void okhttp_testThatOldSessionsStillWorkWhenUsingHeaders() throws Excepti assert accessToken != null; } + @Test + public void okHttp_testBreakOutOfSessionRefreshLoopAfterDefaultMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .tokenTransferMethod("cookie") + .build(); + + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("userId", Constants.userId); + RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), bodyJson.toString()); + Request request = new Request.Builder() + .url(loginAPIURL) + .method("POST", body) + .addHeader("Accept", "application/json") + .addHeader("Content-Type", "application/json") + .build(); + Response loginResponse = okHttpClient.newCall(request).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + loginResponse.close(); + + try { + Request throw401Request = new Request.Builder() + .url(throw401APIURL) + .build(); + okHttpClient.newCall(throw401Request).execute(); + throw new Exception("Expected the request to throw an error"); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 10 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 10) { + throw new Exception("Expected session refresh endpoint to be called 10 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void okHttp_testBreakOutOfSessionRefreshLoopAfterConfiguredMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .maxRetryAttemptsForSessionRefresh(5) + .tokenTransferMethod("cookie") + .build(); + + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("userId", Constants.userId); + RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), bodyJson.toString()); + Request request = new Request.Builder() + .url(loginAPIURL) + .method("POST", body) + .addHeader("Accept", "application/json") + .addHeader("Content-Type", "application/json") + .build(); + Response loginResponse = okHttpClient.newCall(request).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + loginResponse.close(); + + try { + Request throw401Request = new Request.Builder() + .url(throw401APIURL) + .build(); + okHttpClient.newCall(throw401Request).execute(); + throw new Exception("Expected the request to throw an error"); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 5 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 5) { + throw new Exception("Expected session refresh endpoint to be called 5 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void okHttp_testShouldNotDoSessionRefreshIfMaxRetryAttemptsForSessionRefreshIsZero() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .maxRetryAttemptsForSessionRefresh(0) + .tokenTransferMethod("cookie") + .build(); + + JsonObject bodyJson = new JsonObject(); + bodyJson.addProperty("userId", Constants.userId); + RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), bodyJson.toString()); + Request request = new Request.Builder() + .url(loginAPIURL) + .method("POST", body) + .addHeader("Accept", "application/json") + .addHeader("Content-Type", "application/json") + .build(); + Response loginResponse = okHttpClient.newCall(request).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + loginResponse.close(); + + try { + Request throw401Request = new Request.Builder() + .url(throw401APIURL) + .build(); + okHttpClient.newCall(throw401Request).execute(); + throw new Exception("Expected the request to throw an error"); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 0 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 0) { + throw new Exception("Expected session refresh endpoint to be called 0 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + //custom interceptors class customInterceptors implements Interceptor { @NotNull diff --git a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensRetrofitHeaderTests.java b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensRetrofitHeaderTests.java index df88c6a..b0f4de8 100644 --- a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensRetrofitHeaderTests.java +++ b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensRetrofitHeaderTests.java @@ -745,6 +745,83 @@ public void retrofitHeaders_testThatAuthHeaderIsNotIgnoredEvenIfItMatchesTheStor } } + @Test + public void retrofitHeaders_testBreakOutOfSessionRefreshLoopAfterDefaultMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .build(); + + JsonObject body = new JsonObject(); + body.addProperty("userId", Constants.userId); + Response loginResponse = retrofitTestAPIService.login(body).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + + try { + Response userInfoResponse = retrofitTestAPIService.throw401().execute(); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 10 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 10) { + throw new Exception("Expected session refresh endpoint to be called 10 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void retrofitHeaders_testBreakOutOfSessionRefreshLoopAfterConfiguredMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .maxRetryAttemptsForSessionRefresh(5) + .build(); + + JsonObject body = new JsonObject(); + body.addProperty("userId", Constants.userId); + Response loginResponse = retrofitTestAPIService.login(body).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + + try { + Response userInfoResponse = retrofitTestAPIService.throw401().execute(); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 5 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 5) { + throw new Exception("Expected session refresh endpoint to be called 5 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void retrofitHeaders_testShouldNotDoSessionRefreshIfMaxRetryAttemptsForSessionRefreshIsZero() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .maxRetryAttemptsForSessionRefresh(0) + .build(); + + JsonObject body = new JsonObject(); + body.addProperty("userId", Constants.userId); + Response loginResponse = retrofitTestAPIService.login(body).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + + try { + Response userInfoResponse = retrofitTestAPIService.throw401().execute(); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 0 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 0) { + throw new Exception("Expected session refresh endpoint to be called 0 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + class customInterceptors implements Interceptor { @NotNull @Override diff --git a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensRetrofitTest.java b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensRetrofitTest.java index b2e82ad..753b571 100644 --- a/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensRetrofitTest.java +++ b/testHelpers/testapp/app/src/test/java/com/example/example/SuperTokensRetrofitTest.java @@ -751,6 +751,86 @@ public void retrofit_testThatOldSessionsStillWorkWhenUsingHeaders() throws Excep assert accessToken != null; } + @Test + public void retrofit_testBreakOutOfSessionRefreshLoopAfterDefaultMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .tokenTransferMethod("cookie") + .build(); + + JsonObject body = new JsonObject(); + body.addProperty("userId", Constants.userId); + Response loginResponse = retrofitTestAPIService.login(body).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + + try { + Response userInfoResponse = retrofitTestAPIService.throw401().execute(); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 10 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 10) { + throw new Exception("Expected session refresh endpoint to be called 10 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void retrofit_testBreakOutOfSessionRefreshLoopAfterConfiguredMaxRetryAttempts() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .maxRetryAttemptsForSessionRefresh(5) + .tokenTransferMethod("cookie") + .build(); + + JsonObject body = new JsonObject(); + body.addProperty("userId", Constants.userId); + Response loginResponse = retrofitTestAPIService.login(body).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + + try { + Response userInfoResponse = retrofitTestAPIService.throw401().execute(); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 5 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 5) { + throw new Exception("Expected session refresh endpoint to be called 5 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + + @Test + public void retrofit_testShouldNotDoSessionRefreshIfMaxRetryAttemptsForSessionRefreshIsZero() throws Exception { + com.example.TestUtils.startST(); + new SuperTokens.Builder(context, Constants.apiDomain) + .maxRetryAttemptsForSessionRefresh(0) + .tokenTransferMethod("cookie") + .build(); + + JsonObject body = new JsonObject(); + body.addProperty("userId", Constants.userId); + Response loginResponse = retrofitTestAPIService.login(body).execute(); + if (loginResponse.code() != 200) { + throw new Exception("Error making login request"); + } + + try { + Response userInfoResponse = retrofitTestAPIService.throw401().execute(); + } catch (IOException e) { + assert e.getMessage().equals("Received a 401 response from http://127.0.0.1:8080/throw-401. Attempted to refresh the session and retry the request with the updated session tokens 0 times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config."); + } + + int sessionRefreshCalledCount = com.example.TestUtils.getRefreshTokenCounter(); + if (sessionRefreshCalledCount != 0) { + throw new Exception("Expected session refresh endpoint to be called 0 times but it was called " + sessionRefreshCalledCount + " times"); + } + } + class customInterceptors implements Interceptor { @NotNull @Override diff --git a/testHelpers/testapp/app/src/test/java/com/example/example/android/RetrofitTestAPIService.java b/testHelpers/testapp/app/src/test/java/com/example/example/android/RetrofitTestAPIService.java index 18299cd..3170ba2 100644 --- a/testHelpers/testapp/app/src/test/java/com/example/example/android/RetrofitTestAPIService.java +++ b/testHelpers/testapp/app/src/test/java/com/example/example/android/RetrofitTestAPIService.java @@ -88,4 +88,6 @@ public interface RetrofitTestAPIService { @POST("/multipleInterceptors") Call multipleInterceptors(); + @GET("/throw-401") + Call throw401(); }