Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 885
Nicolas Silva Avatar asked Jun 23 '20 17:06

Nicolas Silva


People also ask

How do you handle token expiration in react native?

We need to do 2 steps: – Create a component with react-router subscribed to check JWT Token expiry. – Render it in the App component. In src folder, create common/AuthVerify.

How do I refresh refresh tokens?

To refresh your access token as well as an ID token, you send a token request with a grant_type of refresh_token . Be sure to include the openid scope when you want to refresh the ID token. If the refresh token is valid, then you get back a new access and the refresh token.

How can I get OAuth refresh token?

Because OAuth2 access expires after a limited time, an OAuth2 refresh token is used to automatically renew OAuth2 access. Click the tab for the programming language you're using, and follow the instructions to generate an OAuth2 refresh token and set up the configuration file for your client.


1 Answers

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();

like image 87
Gennady Dogaev Avatar answered Sep 18 '22 14:09

Gennady Dogaev