Search code examples
javascriptreact-nativelocal-storagetoken

Need help trying to refresh my token in react native


I thought it was a simple task, storing my token, setting a timer and fetching the token whenever the timer expired, i was so wrong, after watching and reading several articles to how to approach this, i'm super lost right now, i need help on both storing my token (or both, username data plus token? not sure anymore), and refreshing the token whenever expires.

Yes i've seen quite a few questions related to this on stack overflow, but many of these are related to specific issues and not how to do it.

my app connects to office 365 via microsoft graph from an api (net core 2.0).

on my app i got this code to fetch the data from the api passing the parameters my username and password

  async ApiLogin(loginRequestObject: LoginRequest) {
    var serviceResult = new ServiceResult();
    await NetInfo.fetch().then(async state => {
      var param = JSON.stringify(loginRequestObject);
      if (state.isConnected) {
        try {
          await fetch(ServiceProperties.URLauthentication, {
            method: 'POST',
            headers: {
              Accept: 'application/json',
              'Content-Type': 'application/json',
            },
            body: param,
          })
            .then(response => {
              return response.json();
            })
            .then(responseJson => {
              if (JSON.stringify(responseJson) != null) {
                serviceResult.Success = true;
                serviceResult.Message = 'Service Authentication ok';
                serviceResult.ResponseBody = responseJson;
                serviceResult.StatusCode = 0;
              } else {
                serviceResult.Success = false;
                serviceResult.Message = 'Service Authentication not ok';
                serviceResult.ResponseBody = null;
                serviceResult.StatusCode = -100;
              }
            });
        } catch (error) {
          serviceResult.Success = false;
          serviceResult.Message = 'Service Authentication not ok';
          serviceResult.ResponseBody = null;
          serviceResult.StatusCode = -999;
        }
      } else {
        serviceResult.Success = false;
        serviceResult.Message = 'Service internet not ok';
        serviceResult.ResponseBody = null;
        serviceResult.StatusCode = -1;
      }
    });
    console.log(JSON.parse(serviceResult.ResponseBody));
    return serviceResult;
  }

the result is this.

{"Username":"sensitive data","DisplayName":"sensitive data","GivenName":"sensitive data","SurName":"sensitive data","Email":"sensitive data","Token":"ZSI6Im42aGRfdDVGRHhrSzBEVklJUXpxV09DWHZ4dWc0RlhWVkI4ZVJ6dEFsWDAiLCJhbGciOiJSUzI1NiIsIng1dCI6IlNzWnNCTmhaY0YzUTlTNHRycFFCVEJ5TlJSSSIsImtpZCI6IlNzWnNCTmhaYm5ldC8zOTBmODU5NS1kZTFlLTRmNmQtYTk1NC0yNWY2N5MjkwMTYsImV4cCI6MTU5MjkzMjkxNiButVBqe3E3QwcBr1P0G_dWyC9ASQU0psGDPnsQPHp0T070ROZ_mcPitgquNfsO5JZ8-o056l_aePhXSMO7bHWmUBbVn7TA1UoYIz3lAoOzvE6juadve4aU3goeaBj8PIrhG0M2zEEfKgOL1Al9MSU1GGUmRW9dBofeA4e1cGmlGQrUKnt73n0sHap6","PhotoBase64":null}

this is pretty much all i got, currently, i've used async storage on this app, but only to store an object with "useless" data to say the least, i'm not sure if async storage is the way to go with this or not, if not, what can i do?

EDIT: after reading some more, i discovered that i need to ask for a second token, the refresh token from microsoft graph https://massivescale.com/microsoft-v2-endpoint-primer/ still need help on how to store the data and refresh the token whenever expires,

EDIT 2: unfortunately i'm not getting neither the refresh token or the expiresAt value from the api


Solution

  • I can not help with that specific authentication provider (never worked with office 365) but this is general steps that you need to follow:

    1. Send request(s) to get access and refresh tokens
    2. Store tokens in a storage that persist data through reloads/restarts (for web it would be localStorage, for RN sqlite or asyncstorage or whatever do you use)
    3. Save token and authentication state that it's available for all your components (Redux, Context API or even your own solution). This is needed to show/hide parts of application when user authenticates, unauthenticates or token is expired
    4. You need to know somehow when token will be expired (can't say how to do it but API docs should have some info) and use setTimeout in order to refresh
    5. When you refreshed token, you should persist it (see n.2) and update global auth state (see n.3)
    6. When app just (re)started, check if you have access/refresh tokens persisted in storage (see n.2) and update global auth state accordingly (see n.3)
    7. You routes should react to auth state changes (see docs to your routing library, something about protected/authenticated routes). Your components that display sensitive content also should react to auth state changes.

    Here is my auth solution for Reactjs (do not have RN example, unfortunately) that authenticates client against my own API using JWT. Access token in this scenario is refresh token as well. I use an approach without Redux, just pure React and JS. I hope this would help you.

    import { useCallback, useState, useEffect } from "react";
    import JWT from "jsonwebtoken";
    import { ENV } from "../config";
    import { useLanguageHeaders } from "./i18n";
    
    const decodeToken = (token) =>
      typeof token === "string" ? JWT.decode(token) : null;
    
    //This class is responsible for authentication, 
    //refresh and global auth state parts
    //I create only one instance of AuthProvider and export it, 
    //so it's kind of singleton
    class AuthProvider {
      //Getter for _authStatus
      get authStatus() {
        return this._authStatus;
      }
    
      constructor({ tokenEndpoint, refreshEndpoint, refreshLeeway = 60 }) {
        this._tokenEndpoint = tokenEndpoint;
        this._refreshEndpoint = refreshEndpoint;
        this._refreshLeeway = refreshLeeway;
        //When app is loaded, I load token from local storage
        this._loadToken();
        //And start refresh function that checks expiration time each second
        //and updates token if it will be expired in refreshLeeway seconds
        this._maybeRefresh();
      }
    
      //This method is called in login form
      async authenticate(formData, headers = {}) {
        //Making a request to my API
        const response = await fetch(this._tokenEndpoint, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            ...headers,
          },
          redirect: "follow",
          body: JSON.stringify(formData),
        });
        const body = await response.json();
        if (response.status === 200) {
          //Authentication successful, persist token and update _authStatus
          this._updateToken(body.token);
        } else {
          //Error happened, replace stored token (if any) with null 
          //and update _authStatus
          this._updateToken(null);
          throw new Error(body);
        }
      }
    
      //This method signs user out by replacing token with null
      unauthenticate() {
        this._updateToken(null);
      }
    
      //This is needed so components and routes are able to 
      //react to changes in _authStatus
      addStatusListener(listener) {
        this._statusListeners.push(listener);
      }
    
      //Components need to unsubscribe from changes when they unmount
      removeStatusListener(listener) {
        this._statusListeners = this._statusListeners.filter(
          (cb) => cb !== listener
        );
      }
    
      _storageKey = "jwt";
      _refreshLeeway = 60;
      _tokenEndpoint = "";
      _refreshEndpoint = "";
      _refreshTimer = undefined;
      //This field holds authentication status
      _authStatus = {
        isAuthenticated: null,
        userId: null,
      };
      _statusListeners = [];
    
      //This method checks if token refresh is needed, performs refresh 
      //and calls itself again in a second
      async _maybeRefresh() {
        clearTimeout(this._refreshTimer);
    
        try {
          const decodedToken = decodeToken(this._token);
    
          if (decodedToken === null) {
            //no token - no need to refresh
            return;
          }
    
          //Note that in case of JWT expiration date is built-in in token
          //itself, so I do not need to make requests to check expiration
          //Otherwise you might want to store expiration date in _authStatus
          //and localStorage
          if (
            decodedToken.exp * 1000 - new Date().valueOf() >
            this._refreshLeeway * 1000
          ) {
            //Refresh is not needed yet because token will not expire soon
            return;
          }
    
          if (decodedToken.exp * 1000 <= new Date().valueOf()) {
            //Somehow we have a token that is already expired
            //Possible when user loads app after long absence
            this._updateToken(null);
            throw new Error("Token is expired");
          }
    
          //If we are not returned from try block earlier, it means 
          //we need to refresh token
          //In my scenario access token itself is used to get new one
          const response = await fetch(this._refreshEndpoint, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            redirect: "follow",
            body: JSON.stringify({ token: this._token }),
          });
          const body = await response.json();
          if (response.status === 401) {
            //Current token is bad, replace it with null and update _authStatus
            this._updateToken(null);
            throw new Error(body);
          } else if (response.status === 200) {
            //Got new token, replace existing one
            this._updateToken(body.token);
          } else {
            //Network error, maybe? I don't care unless its 401 status code
            throw new Error(body);
          }
        } catch (e) {
          console.log("Something is wrong when trying to refresh token", e);
        } finally {
          //Finally block is executed even if try block has return statements
          //That's why I use it to schedule next refresh try
          this._refreshTimer = setTimeout(this._maybeRefresh.bind(this), 1000);
        }
      }
    
      //This method persist token and updates _authStatus
      _updateToken(token) {
        this._token = token;
        this._saveCurrentToken();
    
        try {
          const decodedToken = decodeToken(this._token);
    
          if (decodedToken === null) {
            //No token
            this._authStatus = {
              ...this._authStatus,
              isAuthenticated: false,
              userId: null,
            };
          } else if (decodedToken.exp * 1000 <= new Date().valueOf()) {
            //Token is expired
            this._authStatus = {
              ...this._authStatus,
              isAuthenticated: false,
              userId: null,
            };
          } else {
            //Token is fine
            this._authStatus = {
              ...this._authStatus,
              isAuthenticated: true,
              userId: decodedToken.id,
            };
          }
        } catch (e) {
          //Token is so bad that can not be decoded (malformed)
          this._token = null;
          this._saveCurrentToken();
          this._authStatus = {
            ...this._authStatus,
            isAuthenticated: false,
            userId: null,
          };
          throw e;
        } finally {
          //Notify subscribers that _authStatus is updated
          this._statusListeners.forEach((listener) => listener(this._authStatus));
        }
      }
    
      //Load previously persisted token (called in constructor)
      _loadToken() {
        this._updateToken(window.localStorage.getItem(this._storageKey));
      }
    
      //Persist token
      _saveCurrentToken() {
        if (typeof this._token === "string") {
          window.localStorage.setItem(this._storageKey, this._token);
        } else {
          window.localStorage.removeItem(this._storageKey);
        }
      }
    }
    
    //Create authProvider instance
    const authProvider = new AuthProvider(ENV.auth);
    
    //This hook gives a component a function to authenticate user
    export const useAuthenticate = () => {
      const headers = useLanguageHeaders();
    
      return useCallback(
        async (formData) => {
          await authProvider.authenticate(formData, headers);
        },
        [headers]
      );
    };
    
    //This hook gives a function to unauthenticate
    export const useUnauthenticate = () => {
      return useCallback(() => authProvider.unauthenticate(), []);
    };
    
    //This hook allows components to get authentication status 
    //and react to changes
    export const useAuthStatus = () => {
      const [authStatus, setAuthStatus] = useState(authProvider.authStatus);
    
      useEffect(() => {
        authProvider.addStatusListener(setAuthStatus);
    
        return () => {
          authProvider.removeStatusListener(setAuthStatus);
        };
      }, []);
    
      return authStatus;
    };
    

    This line of code inside of functional component allows to know if user is authenticated or not: const { isAuthenticated } = useAuthStatus();