6

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

Nicolas Silva
  • 566
  • 1
  • 10
  • 28

2 Answers2

7

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

Gennady Dogaev
  • 5,902
  • 1
  • 15
  • 23
0

I'm not familiar with that api, but I can tell you, in general terms, how I do it in my react native app.

First of all, as you said, you need both the access token and the refresh token. From the documentation:

Refresh Tokens are only returned when you include offline_access in your first scopes list.

So basically you need to add the offline_access scope in your authentication request and then the response should include a refresh token token prop:

ApiLogin({
    ...otherProps,
    scope: "my other scopes offline_access"
})

Once you get the response you should store the tokens data. I's not safe to do it with async storage. I would advise you to store that information in a secure way. There are some libraries to do it.

After that you can keep track of the expiration date in the client and then perform the refresh token request, as you said. You can also keep doing requests as usual and when the api returns a 401 error response (unauthorized, happens on token expiration) you can try to get a new token with the refresh token (if it fails it probably means the refresh token has also expired and you can redirect the user to the login screen). From the documentation you provided:

To exorcise your Refresh Token, we need to make another HTTP POST back to the provider. The POST’s body must be encoded as application/x-www-form-urlencoded. This body will be POSTed up to https://login.microsoftonline.com/common/oauth2/v2.0/token. The prototype for this call should look like:

https://login.microsoftonline.com/common/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token=[REFRESH TOKEN]&
client_id=[APPLICATION ID]&
client_secret=[PASSWORD]&
scope=[SCOPE]&
redirect_uri=[REDIRECT URI]

With that you should be able to get a new access token that you should use until it expires and then repeat the process.

Let me know if my response was helpful or any doubts you might have. Again, I'm not familiar with the api and the things I posted here are only my interpretation of the docs, so they might need some adaptations.

Carlos Jiménez
  • 486
  • 2
  • 8
  • unfortunately, i no longer have access to the api config, all i get is the authentication token, so my only way to solve this, is to authenticate the user everytime the token expires, also, i'm using keychain but that only works with username, password data storage – Nicolas Silva Jun 29 '20 at 04:49
  • I'm not sure what you mean, sorry. I think you just have to add the scope offline_access to your body params. – Carlos Jiménez Jun 29 '20 at 09:50
  • are you sure i only need to add the scope on the fetch? that's all? no config from the api itself?, can you please elaborate more on your answer based on my code? – Nicolas Silva Jun 30 '20 at 02:22
  • @CarlosJiménez hi carlos, are you familiar with react navigation? – kd12345 Feb 17 '21 at 13:20