0

I am hoping to get some advice regarding my authentication workflow. I feel that I may be overthinking things. Maybe there is a different approach that would be better suited for this application.

I am building an app in React Native using expo (iOS & Android) with the following requirements: The app must implement a persistent login - user only has to authenticate from same device once. (Only under rare circumstances require new login.) App must utilize silent token refresh and refresh token rotation. (Refresh tokens are 1 time use, and exchanged for new upon renewing access token)

I am struggling with deciding how to handle renewing the refresh token dynamically, as traditionally an expired refresh token would warrant the user to re-authenticate. Obviously I’m trading some security for convenience here. Although, I see many other apps that operate this way, but I’m not clear on how they’re handling it, or what the best recommended practices are..

My first thought is I need a way to verify the request to renew the refresh token is coming from a trusted device. I was planning on providing a token binding proof using a unique client identifier upon renewing the refresh token so the request is effectively signed by the client. To accomplish this I was going to exchange asymmetric keys between the client and server, one being used by the client to sign binding, the other would be stored in the database to decrypt & verify the fingerprint prior to renewal.

Does this make any sense here or am I way off the mark? I’m new to this and ChatGPT generally tends to agree with all my ideas lol.. so I just wanted to get a fresh perspective. Is there a better or maybe even a simpler way to do this? I would generally prefer to avoid paid services such as Auth0 if possible.

I have not yet implemented this. Requesting advice prior to implementation.

1 Answers1

0

Hi the below code will help you to automatically refresh token when needed. I am using axios, redux. What I am doing is, in every api call I extract token from redux store and add to request header of axios api call. If token get expired, I raise flag that token is refreshing meanwhile other api call wait till flag become false. Once token is refreshed, I saved it to redux.

Make sure you export modified axios instance and use it everywhere.

Note : I have also logged api call request & response to keep track on it. You may remove if you don't need it.

import axios from "axios"
import { Store } from "../Store"
import ActionType from "../Store/action-type"
import { getRefreshTokenUrl } from "./api-helper"


export const SimpleAxiosInstance = axios.create()




let IS_REFRESHING = false


async function refreshToken () {
    IS_REFRESHING = true
    let refreshUrl = getRefreshTokenUrl(Store.getState().login?.refreshToken)
    console.log(".\n\nAXIOS :: Refreshing Token \n\n.", refreshUrl);
    let res = await SimpleAxiosInstance.get(refreshUrl)
    Store.dispatch({
        type: ActionType.REFRESH_TOKEN,
        payload: {
            accessToken: res.data.object.id_token,
            refreshToken : res.data.object.refresh_token
        }
    })
    IS_REFRESHING = false
}

function canProceed () {
    return new Promise((resolve, reject) => {
        if(!IS_REFRESHING) resolve(true)
        let intervalInstance = null
        let elapsedDuration = 0
        intervalInstance = setInterval(() => {
            console.log(".\n\nAXIOS :: waiting for token refresh\n\n.");
            if(!IS_REFRESHING){
                resolve(true)
                clearInterval(intervalInstance)
            }
            if(elapsedDuration > 10000){
                reject(false)
                clearInterval(intervalInstance)
            }
            (++elapsedDuration) * 1000
        }, 1000)
    })
}

const AxiosInstance = axios.create()

AxiosInstance.interceptors.request.use(
    async config => {
        // console.log("Store \n."+config.url+".\n\n\n\n\n\n.", Store.getState().login, ".\n\n\n\n\n.");
        if(IS_REFRESHING){
            await canProceed()
        }
        config.headers = {
            Authorization: `Bearer ${Store.getState().login?.token}`,
        };
        // console.log(".\n\nAxiosInstance :: request : \nURL :: ", config.url,".\n\nHeader :: ", config?.headers?.Authorization)
        return config;
    },
    error => {
        // console.log("AxiosInstance :: request error :", error);
        Promise.reject("AXIOS :: request interceptor : error --", error);
    })

AxiosInstance.interceptors.response.use(
    response  => {
        // console.log(".\n\nAxiosInstance :: request : \nURL :: ", response.config.url,".\n\nHeader :: ", response.config?.headers?.Authorization)
        console.log(".\n\nAxiosInstance :: request : \nURL :: ", response.config.url)
        console.log("body  ", response.config?.data);
        console.log(".\nAXIOS :: Response : ", JSON.stringify(response.data))
        return response;
    },
    async error => {
        try {
            console.warn(".\n\nAxiosInstance :: request ERROR : \nURL :: ", error?.config.url,".\n\nHeader :: ", error.config?.headers?.Authorization)
            console.warn("body  ", error.config?.data);
            console.warn(".\nAXIOS :: Response ERROR : ", JSON.stringify(error.response?.data))
            // console.log("AXIOS INSTA errror   ", error);
            const originalRequest = error.config;
            if (!error.response) {
                return Promise.reject('Network Error');
            }
            if (error.response.status === 401 && !originalRequest._retry) {
                originalRequest._retry = true;
                await refreshToken()
                return AxiosInstance(error.config);
            }
            return Promise.reject(error);
        } catch (error) {
            if(IS_REFRESHING) IS_REFRESHING = false
            return Promise.reject(error)
        }
    }
)



export default AxiosInstance
Alok Prakash
  • 191
  • 1
  • 9