I can not help with that specific authentication provider (never worked with office 365) but this is general steps that you need to follow:
- Send request(s) to get access and refresh tokens
- 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)
- 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
- 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
- When you refreshed token, you should persist it (see n.2) and update global auth state (see n.3)
- 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)
- 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();