Implementing Refresh Token Rotation for Enhanced Security in Your application
- Performance and Security
Implementing Refresh Token Rotation for Enhanced Security in Your application
In today’s digital landscape, with the rise of online services, the need to protect sensitive user data has never been more critical. One essential aspect of securing your application’s authentication system is implementing refresh tokens. In this blog, we’ll explore what refresh tokens are, why they are important, and how to implement them in your application.
What are Refresh Tokens?
Refresh tokens are long-lived tokens used to obtain a new access token after the original access token has expired. Access tokens typically have a short lifespan to mitigate security risks, but refresh tokens are a secure mechanism to keep users authenticated without requiring frequent password re-entry.
How does Refresh Token Rotation work?
• User Authentication: When a user logs in, they receive both an access token and a refresh token.
• Access Token Usage: The access token is used for accessing protected resources, such as API endpoints or user data. However, access tokens are short-lived tokens.
• Refresh Token Usage: When the access token expires, the refresh token is used to request a new access token without requiring the user to log in again. This process is transparent to the user and helps maintain a seamless user experience.
Why Implement Refresh Token Rotation?
Implementing refresh tokens offers several advantages for your application’s security and user experience:
• Enhanced Security: Since refresh tokens have a longer lifespan and are not used for every request, they reduce the risk of access token theft or exposure. If an access token is compromised, the attacker has limited time to exploit it.
• User Convenience: Refresh tokens keep users logged in, reducing the friction of having to re-enter their credentials frequently. This is especially important for applications that require persistent user sessions.
• Reduced Load: Refresh tokens reduce the load on authentication servers by extending the validity of access tokens.
Implementing Refresh Tokens
To implement refresh tokens in your application, follow these steps:
- Create a global Axios instance: creating a global Axios instance enables us to define API headers in one place for all API calls. In the code snippet below the callApi function is a global function for calling APIs and the setRequestHeader function sets the API headers.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556class ApiService {constructor() {this.service = axios.create({validateStatus: this.validateStatus.bind(this),});this.isRefreshing = false;this.service.interceptors.request.use(this.setRequestHeaders, (error) =>Promise.reject(error));}setRequestHeaders(config) {const updatedConfig = { ...config };const accessToken = getCookie("accessToken");if (tokenHolder) {updatedConfig.headers.Authorization = `Bearer ${accessToken}`;} else {delete updatedConfig.headers.Authorization;}if (!("Content-Type" in updatedConfig.headers)) {updatedConfig.headers["Content-Type"] = "application/json";}updatedConfig.headers["Access-Control-Allow-Headers"] ="Content-Type, Authorization";return updatedConfig;}async apiCall(config,params) {return new Promise((resolve, reject) => {this.service.request(config).then(({ data }) => {const responseMessage = data?.message;resolve(data);}).catch((err) => {if (err?.response) {const { status, data } = err.response;if (status === UNAUTHORIZED_STATUS_CODE) {this.logout();}reject({ status, data });}})});}}const service = new ApiService();export default service;
- Store and update the token on user login: Store the refreshToken and accessToken on user login in cookie storage and update the Axios instance as well.
12345678910111213141516171819202122232425262728293031323334353637383940const handleLogin = (e) => {e.preventDefault();Service.callApi({url: LOGIN_API_ENDPOINT,data,method: "POST",}).then((result) => {const { data, status } = result;if (status === SUCCESS) {setCookie("isTokenValid",true,{expires: data?.access_token_expiry,},"hrs");setCookie("accessToken",data?.access_token,{expires: data?.refresh_token_expiry,},"hrs");setCookie("refreshToken",data?.refresh_token,{expires: data?.refresh_token_expiry,},"hrs");Service.updateToken(data?.access_token);router.push({ pathname: "/dashboard" });}});};123456789101112131415// this function will update the cookies during the time of the loginasync updateToken(accessToken) {this.service.defaults.headers.Authorization = `Bearer ${accessToken}`;}// remove all the cookies during the time of logoutremoveToken() {delete this.service.defaults.headers.Authorization;}logout(){this.removeToken();clearCookieStorage();Router.push("/login");};
- Call refreshToken API: create a handleRefreshAccessToken function that calls API if the cookie storage doesn’t have isValidToken. This function checks if the refreshToken exists in cookie storage as well since there may be some routes that do not have refreshToken like before logged-in routes or allowed all-time routes.
1234567891011121314151617181920212223242526272829303132333435363738394041async handleRefreshAccessToken() {const isTokenValid = getCookie("isTokenValid");const refreshToken = getCookie("refreshToken");if (!isTokenValid && refreshToken && !this.isRefreshing) {this.isRefreshing = true;try {const response = await axios.post(REFRESH_TOKEN_API_ENDPOINT, {refresh: refreshToken,});if (response?.data?.status === SUCCESS) {setCookie("accessToken",response?.data?.data?.access_token,{expires: response?.data?.data?.refresh_token_expiry,},"hrs");setCookie("isTokenValid",true,{expires: response?.data?.data?.access_token_expiry,},"hrs");this.updateToken(response?.data?.data?.access_token);return true;}} catch (error) {this.logout();return false;} finally {this.isRefreshing = false;}}return true;}
- Wait for the token to refresh: the handleRefreshAccessToken function executes for every API call. Hence, the refreshAccessTokenApi is triggered by the first API called after the token expires. Therefore, we need to hold off on other API calls while the token is refreshing. For this purpose, we create a function waitForCondition which goes into recursion while this.isRefreshing variable is true.
123456async waitForCondition() {while (this.isRefreshing) {// Wait for 100 milliseconds before checking againawait new Promise((resolve) => setTimeout(resolve, 100));}}
- Finally, check if the cookies exist.: In our implementation of refresh tokens, the cookie storage always has refreshToken and accessToken for all the routes after login. So, if for any reason these 2 tokens are absent the user is logged out.
1234567891011121314151617181920212223242526272829303132333435async callApi(config ,params) {await this.handleRefreshAccessToken();// Wait if token is refreshingawait this.waitForCondition();if ((!getCookie("accessToken") || !getCookie("refreshToken")) &&!window.location.pathname.match(ALLOWED_ALL_TIME_ROUTES)) {this.logout();return Promise.reject({ status: 401, data: "Invalid Token" });}return new Promise((resolve, reject) => {this.service.request(config).then(({ data }) => {resolve(data);}).catch((err) => {if (err?.response) {const { status, data } = err.response;if (status === UNAUTHORIZED_STATUS_CODE) {this.logout();}reject({ status, data });}})});}
Here we are using Axios but it can vary as per the project requirement. Hope you found this blog helpful in making your applications more secure!
Related content
Auriga: Leveling Up for Enterprise Growth!
Auriga’s journey began in 2010 crafting products for India’s