I have a Redux app and a remote API that serves as an OAuth server. Based on typical routine, a user exchanges their credentials to a token that is then used by the app to fetch data and do some stuff on the server. This token is stored into the store and also into sessionStorage
.
Now, sometimes an access token would expire, but since a refresh token has been received, it's better to attempt to refresh first, and only if it goes wrong, sign the user off.
I completely understand the signing-off part, which technically means simply dispatching a certain action. But how can I simplify the token refreshment routine?
I tried redux-saga, but it's very verbose. I literally have to partially duplicate the code for every action that depends on remote API, to make sure every request like that would first check access token and whether it hasn't yet expired, and manage to refresh it otherwise.
Another thing I tried to do is a middleware that would expect a certain type of action with a request to remote API wrapped into Promise. This kind of works, but I'm curious if there's another way to do.
Has anybody ever implemented this (pretty generic) kind of thing? Any ideas how to automate token refreshment and not get mad with increased amount of code? Maybe higher-order component?
Calling APIs with OAuth2 Access Tokens – The Easy Way! Scenario: Your API needs to call another REST API – or your Console App or Web Job needs to call some other REST API. You can acquire an access token to that API from an OAuth2 Security Token Service such as Duende Identity Server, Okta, Auth0 or Azure Active Directory.
Then we will use a simple redux subscriber to store our auth tokens in the browser localStorage. Using a subscriber will keep it synced so that anytime the auth token in the redux state changes it updates it in the localStorage.
An audience (as described in the OIDC Core spec for ID Tokens) is assigned to every access token. The API gateway is validating audience information maps to the requested API.
Scenario: Your API needs to call another REST API – or your Console App or Web Job needs to call some other REST API. You can acquire an access token to that API from an OAuth2 Security Token Service such as Duende Identity Server, Okta, Auth0 or Azure Active Directory.
For code that needs to happen repeatedly and for something that needs to be seamless and generic, middlewares are usually the way to go. It can be as simple as adding two lines of code to include the middleware when creating the store and writing up a simple function that will handle the token logic for you.
Let's say you would create your store as such:
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from './reducers';
import { browserHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux';
import tokenMiddleware from './middleware/token';
const finalCreateStore = compose(
applyMiddleware(
routerMiddleware(browserHistory),
tokenMiddleware,
),
window.devToolsExtension ? window.devToolsExtension() : f => f,
)(createStore);
Then you would call this function from somewhere, with the initial state.
const store = finalCreateStore(rootReducer, initialState);
This will enable you to do something with all the actions that pass through the store. Since it is very common to have a middleware that handles API calls using promises, some people prefer to re-use that for this purpose as well and bundle the two together.
A typical middleware will look something like this:
export const tokenMiddleware = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') { // pass along
return action(dispatch, getState);
}
// so let's say you have a token that's about to expire
// and you would like to refresh it, let's write so pseudo code
const currentState = getState();
const userObj = state.authentication.user;
if (userObj.token && userObj.token.aboutToExpire) {
const config = getSomeConfigs();
// some date calculation based on expiry time that we set in configs
const now = new Date();
const refreshThreshold = config.token.refreshThreshold;
if (aboutToExpireAndIsBelowThresholdToRefresh) {
// refreshTheToken can be an action creator
// from your auth module for example
// it should probably be a thunk so that you can handle
// an api call and a promise within once you get the new token
next(refreshTheToken(userObj, someOtherParams);
}
}
....
return next(action);
}
Your refresh token thunk could be something similar to this:
function refreshToken(user, maybeSomeOtherParams) {
const config = getSomeConfigs;
return dispatch => {
makeAPostCallWithParamsThatReturnsPromise
.then(result => dispatch(saveNewToken({
result,
...
})))
.catch(error => dispatch({
type: uh_oh_token_refresh_failed_action_type,
error,
}));
};
Another alternative that you could possibly go for would be to handle this when changing routes.
Let's say you would have a top level route somewhere for the routes that need authentication and a valid user to be present in the system. Let's call them authenticated routes
.
You could wrap these authenticated routes
with a top level route, that defines an onChange
handler function. Something like this:
<Route onChange={authEntry}>
<Route ... /> // authenticated routes
<Route ... />
</Route>
When creating these routes and setting up your store, once you have created the store, you could bind it to this function called checkAuth
.
const authEntry = checkAuth.bind(null, store)
Another way would be to wrap the route definitions in a function and pass the store into it, and then you would have access just the same, but I found that to be not as clean as this (personal preference).
Now what would this checkAuth
do?
Something like this:
export function checkAuth (store, previous, next, replace, callback) {
const currentUser = store.getState().auth.user
// can possibly dispatch actions from here too
// store.dispatch(..).then(() => callback())..
// so you could possibly refresh the token here using an API call
// if it is about to expire
// you can also check if the token did actually expire and/or
// there's no logged in user trying to access the route, so you can redirect
if (!currentUser || !isLoggedIn(currentUser)) {
replace('/yourLoginRouteHere')
}
callback() // pass it along
}
Both of these should be generic enough so that they provide you with re-usable code in a centralized location. Hope you will find these helpful.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With