Skip to content

Commit

Permalink
fix: session refresh loop in all request interceptors (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
anku255 authored Jun 10, 2024
1 parent d16090c commit f10a931
Show file tree
Hide file tree
Showing 15 changed files with 617 additions and 6 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/java/com/supertokens/session/SuperTokens.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -63,6 +64,7 @@ private static void init(
apiDomain,
apiBasePath,
sessionExpiredStatusCode,
maxRetryAttemptsForSessionRefresh,
sessionTokenBackendDomain,
tokenTransferMethod,
customHeaderProvider,
Expand Down Expand Up @@ -260,6 +262,7 @@ public static class Builder {
Context applicationContext;
String apiBasePath;
Integer sessionExpiredStatusCode;
Integer maxRetryAttemptsForSessionRefresh;
String sessionTokenBackendDomain;
CustomHeaderProvider customHeaderProvider;
EventHandler eventHandler;
Expand All @@ -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;
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public static HttpURLConnection newRequest(URL url, PreConnectCallback preConnec
}

try {
int sessionRefreshAttempts = 0;
while (true) {
HttpURLConnection connection;
SuperTokensCustomHttpURLConnection customConnection;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand All @@ -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;
Expand Down
18 changes: 17 additions & 1 deletion app/src/main/java/com/supertokens/session/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -93,13 +101,15 @@ public NormalisedInputType(
String apiDomain,
String apiBasePath,
int sessionExpiredStatusCode,
int maxRetryAttemptsForSessionRefresh,
String sessionTokenBackendDomain,
String tokenTransferMethod,
CustomHeaderProvider customHeaderMapper,
EventHandler eventHandler) {
this.apiDomain = apiDomain;
this.apiBasePath = apiBasePath;
this.sessionExpiredStatusCode = sessionExpiredStatusCode;
this.maxRetryAttemptsForSessionRefresh = maxRetryAttemptsForSessionRefresh;
this.sessionTokenBackendDomain = sessionTokenBackendDomain;
this.customHeaderMapper = customHeaderMapper;
this.eventHandler = eventHandler;
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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);
}
}
Expand Down
4 changes: 2 additions & 2 deletions examples/with-thirdparty/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion examples/with-thirdparty/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 4 additions & 0 deletions testHelpers/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
Loading

0 comments on commit f10a931

Please sign in to comment.