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 #125

Merged
merged 4 commits 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [5.1.0] - 2024-06-04

### 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.

## [5.0.2] - 2024-05-28

- Adds FDI 2.0 and 3.0 to the list of supported FDI versions
Expand Down
7 changes: 6 additions & 1 deletion lib/build/axios.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 34 additions & 15 deletions lib/build/axios.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions lib/build/axiosError.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 0 additions & 11 deletions lib/build/axiosError.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions lib/build/fetch.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions lib/build/types.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions lib/build/utils.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 47 additions & 17 deletions lib/ts/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from "axios";
import { createAxiosErrorFromAxiosResp, createAxiosErrorFromFetchResp } from "./axiosError";
import { AxiosPromise, AxiosRequestConfig as OriginalAxiosRequestConfig, AxiosResponse } from "axios";
import { createAxiosErrorFromFetchResp } from "./axiosError";

import AuthHttpRequestFetch, { onUnauthorisedResponse } from "./fetch";

Expand All @@ -23,6 +23,26 @@ import { PROCESS_STATE, ProcessState } from "./processState";
import { fireSessionUpdateEventsIfNecessary, getLocalSessionState, getTokenForHeaderAuth, setToken } from "./utils";
import { logDebugMessage } from "./logger";

type AxiosRequestConfig = OriginalAxiosRequestConfig & {
__supertokensSessionRefreshAttempts?: number;
__supertokensAddedAuthHeader?: boolean;
};

function incrementSessionRefreshAttemptCount(config: AxiosRequestConfig) {
if (config.__supertokensSessionRefreshAttempts === undefined) {
config.__supertokensSessionRefreshAttempts = 0;
}
config.__supertokensSessionRefreshAttempts++;
}

function hasExceededMaxSessionRefreshAttempts(config: AxiosRequestConfig): boolean {
if (config.__supertokensSessionRefreshAttempts === undefined) {
config.__supertokensSessionRefreshAttempts = 0;
}

return config.__supertokensSessionRefreshAttempts >= AuthHttpRequestFetch.config.maxRetryAttemptsForSessionRefresh;
}

function getUrlFromConfig(config: AxiosRequestConfig) {
let url: string = config.url === undefined ? "" : config.url;
let baseURL: string | undefined = config.baseURL;
Expand Down Expand Up @@ -363,6 +383,11 @@ export default class AuthHttpRequest {
}
let response =
localPrevResponse === undefined ? await httpCall(configWithAntiCsrf) : localPrevResponse;

// NOTE: No need to check for unauthorized response status here for session refresh,
// as we only reach this point on a successful response. Axios handles error responses
// by throwing an error, which is handled in the catch block.

logDebugMessage("doRequest: User's http call ended");

await saveTokensFromHeaders(response);
Expand All @@ -373,21 +398,7 @@ export default class AuthHttpRequest {
response.headers["front-token"]
);

if (response.status === AuthHttpRequestFetch.config.sessionExpiredStatusCode) {
logDebugMessage("doRequest: Status code is: " + response.status);
const refreshResult = await onUnauthorisedResponse(preRequestLocalSessionState);

if (refreshResult.result !== "RETRY") {
logDebugMessage("doRequest: Not retrying original request");
returnObj = refreshResult.error
? await createAxiosErrorFromFetchResp(refreshResult.error)
: await createAxiosErrorFromAxiosResp(response);
break;
}
logDebugMessage("doRequest: Retrying original request");
} else {
return response;
}
return response;
} catch (err) {
const response = (err as any).response;
if (response !== undefined) {
Expand All @@ -401,7 +412,26 @@ export default class AuthHttpRequest {

if (err.response.status === AuthHttpRequestFetch.config.sessionExpiredStatusCode) {
logDebugMessage("doRequest: Status code is: " + response.status);

/**
* 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 (hasExceededMaxSessionRefreshAttempts(config)) {
logDebugMessage(
`doRequest: Maximum session refresh attempts reached. sessionRefreshAttempts: ${config.__supertokensSessionRefreshAttempts}, maxRetryAttemptsForSessionRefresh: ${AuthHttpRequestFetch.config.maxRetryAttemptsForSessionRefresh}`
);
throw new Error(
`Received a 401 response from ${url}. Attempted to refresh the session and retry the request with the updated session tokens ${AuthHttpRequestFetch.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.`
);
}

const refreshResult = await onUnauthorisedResponse(preRequestLocalSessionState);
incrementSessionRefreshAttemptCount(config);
logDebugMessage(
"doRequest: sessionRefreshAttempts: " + config.__supertokensSessionRefreshAttempts
);
if (refreshResult.result !== "RETRY") {
logDebugMessage("doRequest: Not retrying original request");
// Returning refreshResult.error as an Axios Error if we attempted a refresh
Expand Down
10 changes: 0 additions & 10 deletions lib/ts/axiosError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,3 @@ export async function createAxiosErrorFromFetchResp(response: Response): Promise
axiosResponse
);
}

export async function createAxiosErrorFromAxiosResp(response: AxiosResponse): Promise<AxiosError> {
return enhanceAxiosError(
new Error("Request failed with status code " + response.status),
response.config,
undefined,
response.request,
response
);
}
Loading
Loading