UNPKG

supertokens-react-native

Version:
482 lines (481 loc) 25.3 kB
var __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); } return new (P || (P = Promise))(function(resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var _a; import { createAxiosErrorFromFetchResp } from "./axiosError"; import AuthHttpRequestFetch, { onUnauthorisedResponse } from "./fetch"; import FrontToken from "./frontToken"; import AntiCSRF from "./antiCsrf"; import { PROCESS_STATE, ProcessState } from "./processState"; import { fireSessionUpdateEventsIfNecessary, getLocalSessionState, getTokenForHeaderAuth, setToken } from "./utils"; import { logDebugMessage } from "./logger"; function incrementSessionRefreshAttemptCount(config) { if (config.__supertokensSessionRefreshAttempts === undefined) { config.__supertokensSessionRefreshAttempts = 0; } config.__supertokensSessionRefreshAttempts++; } function hasExceededMaxSessionRefreshAttempts(config) { if (config.__supertokensSessionRefreshAttempts === undefined) { config.__supertokensSessionRefreshAttempts = 0; } return config.__supertokensSessionRefreshAttempts >= AuthHttpRequestFetch.config.maxRetryAttemptsForSessionRefresh; } function getUrlFromConfig(config) { let url = config.url === undefined ? "" : config.url; let baseURL = config.baseURL; if (baseURL !== undefined) { if (url.charAt(0) === "/" && baseURL.charAt(baseURL.length - 1) === "/") { url = baseURL + url.substr(1); } else if (url.charAt(0) !== "/" && baseURL.charAt(baseURL.length - 1) !== "/") { url = baseURL + "/" + url; } else { url = baseURL + url; } } return url; } export function interceptorFunctionRequestFulfilled(config) { return __awaiter(this, void 0, void 0, function*() { logDebugMessage("interceptorFunctionRequestFulfilled: started axios interception"); let url = getUrlFromConfig(config); let doNotDoInterception = false; try { doNotDoInterception = typeof url === "string" && !AuthHttpRequestFetch.recipeImpl.shouldDoInterceptionBasedOnUrl( url, AuthHttpRequestFetch.config.apiDomain, AuthHttpRequestFetch.config.sessionTokenBackendDomain ); } catch (err) { // This is because when this function is called we always have a full URL (refer to getUrlFromConfig), // so we do not need to check for the case where axios is called with just a path (for example axios.post("/login")) throw err; } logDebugMessage("interceptorFunctionRequestFulfilled: Value of doNotDoInterception: " + doNotDoInterception); if (doNotDoInterception) { logDebugMessage("interceptorFunctionRequestFulfilled: Returning config unchanged"); // this check means that if you are using axios via inteceptor, then we only do the refresh steps if you are calling your APIs. return config; } logDebugMessage("interceptorFunctionRequestFulfilled: Modifying config"); ProcessState.getInstance().addState(PROCESS_STATE.CALLING_INTERCEPTION_REQUEST); const preRequestLocalSessionState = yield getLocalSessionState(); let configWithAntiCsrf = config; if (preRequestLocalSessionState.status === "EXISTS") { const antiCsrfToken = yield AntiCSRF.getToken(preRequestLocalSessionState.lastAccessTokenUpdate); if (antiCsrfToken !== undefined) { logDebugMessage("interceptorFunctionRequestFulfilled: Adding anti-csrf token to request"); configWithAntiCsrf = Object.assign(Object.assign({}, configWithAntiCsrf), { headers: configWithAntiCsrf === undefined ? { "anti-csrf": antiCsrfToken } : Object.assign(Object.assign({}, configWithAntiCsrf.headers), { "anti-csrf": antiCsrfToken }) }); } } if (AuthHttpRequestFetch.config.autoAddCredentials && configWithAntiCsrf.withCredentials === undefined) { logDebugMessage("interceptorFunctionRequestFulfilled: Adding credentials include"); configWithAntiCsrf = Object.assign(Object.assign({}, configWithAntiCsrf), { withCredentials: true }); } // adding rid for anti-csrf protection: Anti-csrf via custom header logDebugMessage( "interceptorFunctionRequestFulfilled: Adding rid header: anti-csrf (it may be overriden by the user's provided rid)" ); configWithAntiCsrf = Object.assign(Object.assign({}, configWithAntiCsrf), { headers: configWithAntiCsrf === undefined ? { rid: "anti-csrf" } : Object.assign({ rid: "anti-csrf" }, configWithAntiCsrf.headers) }); const transferMethod = AuthHttpRequestFetch.config.tokenTransferMethod; logDebugMessage("interceptorFunctionRequestFulfilled: Adding st-auth-mode header: " + transferMethod); configWithAntiCsrf.headers["st-auth-mode"] = transferMethod; configWithAntiCsrf = yield removeAuthHeaderIfMatchesLocalToken(configWithAntiCsrf); yield setAuthorizationHeaderIfRequired(configWithAntiCsrf); logDebugMessage("interceptorFunctionRequestFulfilled: returning modified config"); return configWithAntiCsrf; }); } export function responseInterceptor(axiosInstance) { return response => __awaiter(this, void 0, void 0, function*() { let doNotDoInterception = false; if (!AuthHttpRequestFetch.initCalled) { throw new Error("init function not called"); } logDebugMessage("responseInterceptor: started"); logDebugMessage( "responseInterceptor: already intercepted: " + response.headers["x-supertokens-xhr-intercepted"] ); let url = getUrlFromConfig(response.config); try { doNotDoInterception = typeof url === "string" && !AuthHttpRequestFetch.recipeImpl.shouldDoInterceptionBasedOnUrl( url, AuthHttpRequestFetch.config.apiDomain, AuthHttpRequestFetch.config.sessionTokenBackendDomain ); } catch (err) { // This is because when this function is called we always have a full URL (refer to getUrlFromConfig), // so we do not need to check for the case where axios is called with just a path (for example axios.post("/login")) throw err; } logDebugMessage("responseInterceptor: Value of doNotDoInterception: " + doNotDoInterception); if (doNotDoInterception) { logDebugMessage("responseInterceptor: Returning without interception"); // this check means that if you are using axios via inteceptor, then we only do the refresh steps if you are calling your APIs. return response; } logDebugMessage("responseInterceptor: Interception started"); ProcessState.getInstance().addState(PROCESS_STATE.CALLING_INTERCEPTION_RESPONSE); const preRequestLocalSessionState = yield getLocalSessionState(); yield saveTokensFromHeaders(response); fireSessionUpdateEventsIfNecessary( preRequestLocalSessionState.status === "EXISTS", response.status, response.headers["front-token"] ); if (response.status === AuthHttpRequestFetch.config.sessionExpiredStatusCode) { logDebugMessage("responseInterceptor: Status code is: " + response.status); let config = response.config; return AuthHttpRequest.doRequest( config => { // we create an instance since we don't want to intercept this. // const instance = axios.create(); // return instance(config); return axiosInstance(config); }, config, url, response, true ); } else { return response; } }); } export function responseErrorInterceptor(axiosInstance) { return error => { logDebugMessage("responseErrorInterceptor: called"); logDebugMessage( "responseErrorInterceptor: already intercepted: " + (error.response && error.response.headers["x-supertokens-xhr-intercepted"]) ); if ( error.response !== undefined && error.response.status === AuthHttpRequestFetch.config.sessionExpiredStatusCode ) { logDebugMessage("responseErrorInterceptor: Status code is: " + error.response.status); let config = error.config; return AuthHttpRequest.doRequest( config => { // we create an instance since we don't want to intercept this. // const instance = axios.create(); // return instance(config); return axiosInstance(config); }, config, getUrlFromConfig(config), undefined, error, true ); } else { throw error; } }; } /** * @class AuthHttpRequest * @description wrapper for common http methods. */ class AuthHttpRequest {} _a = AuthHttpRequest; /** * @description sends the actual http request and returns a response if successful/ * If not successful due to session expiry reasons, it * attempts to call the refresh token API and if that is successful, calls this API again. * @throws Error */ AuthHttpRequest.doRequest = (httpCall_1, config_1, url_1, prevResponse_1, prevError_1, ...args_1) => __awaiter(void 0, [httpCall_1, config_1, url_1, prevResponse_1, prevError_1, ...args_1], void 0, function*( httpCall, config, url, prevResponse, prevError, viaInterceptor = false ) { if (!AuthHttpRequestFetch.initCalled) { throw Error("init function not called"); } logDebugMessage("doRequest: called"); let doNotDoInterception = false; try { doNotDoInterception = typeof url === "string" && !AuthHttpRequestFetch.recipeImpl.shouldDoInterceptionBasedOnUrl( url, AuthHttpRequestFetch.config.apiDomain, AuthHttpRequestFetch.config.sessionTokenBackendDomain ) && viaInterceptor; } catch (err) { // This is because when this function is called we always have a full URL (refer to getUrlFromConfig), // so we do not need to check for the case where axios is called with just a path (for example axios.post("/login")) throw err; } logDebugMessage("doRequest: Value of doNotDoInterception: " + doNotDoInterception); if (doNotDoInterception) { logDebugMessage("doRequest: Returning without interception"); if (prevError !== undefined) { throw prevError; } else if (prevResponse !== undefined) { return prevResponse; } return yield httpCall(config); } logDebugMessage("doRequest: Interception started"); config = yield removeAuthHeaderIfMatchesLocalToken(config); let returnObj = undefined; while (true) { // we read this here so that if there is a session expiry error, then we can compare this value (that caused the error) with the value after the request is sent. // to avoid race conditions const preRequestLocalSessionState = yield getLocalSessionState(); let configWithAntiCsrf = config; if (preRequestLocalSessionState.status === "EXISTS") { const antiCsrfToken = yield AntiCSRF.getToken(preRequestLocalSessionState.lastAccessTokenUpdate); if (antiCsrfToken !== undefined) { logDebugMessage("doRequest: Adding anti-csrf token to request"); configWithAntiCsrf = Object.assign(Object.assign({}, configWithAntiCsrf), { headers: configWithAntiCsrf === undefined ? { "anti-csrf": antiCsrfToken } : Object.assign(Object.assign({}, configWithAntiCsrf.headers), { "anti-csrf": antiCsrfToken }) }); } } if (AuthHttpRequestFetch.config.autoAddCredentials && configWithAntiCsrf.withCredentials === undefined) { logDebugMessage("doRequest: Adding credentials include"); configWithAntiCsrf = Object.assign(Object.assign({}, configWithAntiCsrf), { withCredentials: true }); } // adding rid for anti-csrf protection: Anti-csrf via custom header logDebugMessage("doRequest: Adding rid header: anti-csrf (May get overriden by user's rid)"); configWithAntiCsrf = Object.assign(Object.assign({}, configWithAntiCsrf), { headers: configWithAntiCsrf === undefined ? { rid: "anti-csrf" } : Object.assign({ rid: "anti-csrf" }, configWithAntiCsrf.headers) }); const transferMethod = AuthHttpRequestFetch.config.tokenTransferMethod; logDebugMessage("doRequest: Adding st-auth-mode header: " + transferMethod); configWithAntiCsrf.headers["st-auth-mode"] = transferMethod; yield setAuthorizationHeaderIfRequired(configWithAntiCsrf); try { let localPrevError = prevError; let localPrevResponse = prevResponse; prevError = undefined; prevResponse = undefined; if (localPrevError !== undefined) { logDebugMessage("doRequest: Not making call because localPrevError is not undefined"); throw localPrevError; } if (localPrevResponse !== undefined) { logDebugMessage("doRequest: Not making call because localPrevResponse is not undefined"); } else { logDebugMessage("doRequest: Making user's http call"); } let response = localPrevResponse === undefined ? yield 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"); yield saveTokensFromHeaders(response); fireSessionUpdateEventsIfNecessary( preRequestLocalSessionState.status === "EXISTS", response.status, response.headers["front-token"] ); return response; } catch (err) { const response = err.response; if (response !== undefined) { yield saveTokensFromHeaders(response); fireSessionUpdateEventsIfNecessary( preRequestLocalSessionState.status === "EXISTS", response.status, response.headers["front-token"] ); 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 = yield 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 // Returning the original error if we did not attempt refreshing returnObj = refreshResult.error !== undefined ? yield createAxiosErrorFromFetchResp(refreshResult.error) : err; break; } logDebugMessage("doRequest: Retrying original request"); } else { throw err; } } else { throw err; } } } // if it comes here, means we called break. which happens only if we have logged out. // which means it's a 401, so we throw throw returnObj; }); export default AuthHttpRequest; function saveTokensFromHeaders(response) { return __awaiter(this, void 0, void 0, function*() { logDebugMessage("saveTokensFromHeaders: Saving updated tokens from the response"); const refreshToken = response.headers["st-refresh-token"]; if (refreshToken !== undefined && refreshToken !== null) { logDebugMessage("saveTokensFromHeaders: saving new refresh token"); yield setToken("refresh", refreshToken); } const accessToken = response.headers["st-access-token"]; if (accessToken !== undefined && accessToken !== null) { logDebugMessage("saveTokensFromHeaders: saving new access token"); yield setToken("access", accessToken); } const frontToken = response.headers["front-token"]; if (frontToken !== undefined && frontToken !== null) { logDebugMessage("doRequest: Setting sFrontToken: " + frontToken); yield FrontToken.setItem(frontToken); } const antiCsrfToken = response.headers["anti-csrf"]; if (antiCsrfToken !== undefined && antiCsrfToken !== null) { const tok = yield getLocalSessionState(); if (tok.status === "EXISTS") { logDebugMessage("doRequest: Setting anti-csrf token"); yield AntiCSRF.setItem(tok.lastAccessTokenUpdate, antiCsrfToken); } } }); } function setAuthorizationHeaderIfRequired(requestConfig) { return __awaiter(this, void 0, void 0, function*() { if (requestConfig.headers === undefined) { // This is makes TS happy requestConfig.headers = {}; } logDebugMessage("setAuthorizationHeaderIfRequired: adding existing tokens as header"); // We set the Authorization header even if the tokenTransferMethod preference set in the config is cookies // since the active session may be using cookies. By default, we want to allow users to continue these sessions. // The new session preference should be applied at the start of the next session, if the backend allows it. const accessToken = yield getTokenForHeaderAuth("access"); const refreshToken = yield getTokenForHeaderAuth("refresh"); // We don't add the refresh token because that's only required by the refresh call which is done with fetch // Still, we only add the Authorization header if both are present, because we are planning to add an option to expose the // access token to the frontend while using cookie based auth - so that users can get the access token to use if (accessToken !== undefined && refreshToken !== undefined) { if ( requestConfig.headers["Authorization"] !== undefined || requestConfig.headers["authorization"] !== undefined ) { logDebugMessage( "setAuthorizationHeaderIfRequired: Authorization header defined by the user, not adding" ); } else { logDebugMessage("setAuthorizationHeaderIfRequired: added authorization header"); requestConfig.headers = Object.assign(Object.assign({}, requestConfig.headers), { Authorization: `Bearer ${accessToken}` }); requestConfig.__supertokensAddedAuthHeader = true; } } else { logDebugMessage("setAuthorizationHeaderIfRequired: token for header based auth not found"); } }); } function removeAuthHeaderIfMatchesLocalToken(config) { return __awaiter(this, void 0, void 0, function*() { const accessToken = yield getTokenForHeaderAuth("access"); const refreshToken = yield getTokenForHeaderAuth("refresh"); const authHeader = config.headers.Authorization || config.headers.authorization; if (accessToken !== undefined && refreshToken !== undefined) { if (authHeader === `Bearer ${accessToken}` || "__supertokensAddedAuthHeader" in config) { // We are ignoring the Authorization header set by the user in this case, because it would cause issues // If we do not ignore this, then this header would be used even if the request is being retried after a refresh, even though it contains an outdated access token. // This causes an infinite refresh loop. logDebugMessage( "removeAuthHeaderIfMatchesLocalToken: Removing Authorization from user provided headers because it contains our access token" ); const res = Object.assign(Object.assign({}, config), { headers: Object.assign({}, config.headers) }); delete res.headers.authorization; delete res.headers.Authorization; return res; } } return config; }); }