Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: session refresh loop in all request interceptors #71

Merged
merged 1 commit into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading