1

Environment: This is code I'm trying to implement on top of the tried and tested React-boilerplate. While this boilerplate is wonderful, it doesn't contain authentication. I'm trying to implement my own authentication logic in a way that meshes with the existing logic of the boilerplate.

Context:

I have a service/utility function that is used to send the server requests with a JWT authentication header and return its response.

Goal:

I'm trying to implement logic where when a request is made using an expired token, the access token is automatically refreshed before the request is sent to the server.

What's stopping me:

  1. I have a saga that handles the access token refresh. It works perfectly when it is called from within a container. But because this is a service, it is unaware of the redux store. For this reason I am unable to dispatch actions.

  2. The react-boilerplate structure works by injecting reducers and sagas on the fly. This means I'd need to inject the authentication saga and reducer from within the service (kinda feels wrong no?). However, the saga and reducer injectors are React side-effects and can, therefore, only be used inside of React components.

I feel like the task I'm trying to achieve is quite trivial (and I'm sure it was implementing a million times already), and yet, I can't think of a solution that doesn't seem to go against the entire logic of why use Redux or Sagas to begin with.

Can anyone offer some insights? In the attached image, the red text is the part I'm struggling to implement enter image description here

See code below:

/**
 * Requests a URL, returning a promise
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch".
 *
 * @return {object}           The response data
 */
export default function request( url, options ) {

    const token = makeSelectAccessToken();
    if (!token) throw new Error('No access token found.');

    // Refresh access token if it's expired.
    if (new Date(token.expires) - Date.now() <= 0) {
      // TODO: Attempt to use refresh token to get new access token before continuing
      /** dispatch(REFRESH_ACCESS_TOKEN) **/

      // PROBLEM: can't dispatch actions because the store is not exposed to this service.
      // Secondary challenge: Can't inject Saga or Reducer because the React-boilerplate injectors are React side-effects.
    }

  options = {
    ...options,
    Authorization: `Bearer ${token.token}`, // Adding the JWT token to the request
  };

  return fetch(url, options)
}
Michael Seltenreich
  • 3,013
  • 2
  • 29
  • 55

2 Answers2

1

There is multiple way to do that

1- Export the store

create new store.js

import { createStore } from 'redux';
import reducer from './reducer';
//you can add your middleware, sagas, ...
const store = createStore(reducer);

export default store;

import it to your service and use it everywhere but you’ll end up with a single store for all of your users

  • Cons: you’ll end up with a single store for all of your users(if your app is using Server Side Rendering don't use this method)

  • Pros: Easy to use

2- You can add your func to middleware and intercept an action

edit your create store code and add new middleware for authorization

const refreshAuthToken = store => next => action => {
  
  const token = makeSelectAccessToken();
  if (!token) throw new Error('No access token found.');

  if(new Date(token.expires) - Date.now() <= 0) {
    // you can dispatch inside store
    this.store.dispatch(REFRESH_ACCESS_TOKEN);
  }

  // continue processing this action
  return next(action);
}

const store = createStore(
  ... //add to your middleware list
  applyMiddleware(refreshAuthToken)
);
  • Cons: Maybe you can need redux-thunk library if this middleware not solve your problem (its easy job. You can't say this even a con)

  • Pros: Best and safer way for your need and it will work everytime you call an action. It will works like a charm on SSR apps

3- or you can send store as parameter to your request method from component

  • Cons: Not the best way it will run after your components initialize and you can make easily mistake or can forget to add to your component.

  • Pros: At least you can do what you want

Onur Gelmez
  • 1,044
  • 7
  • 9
  • This is wonderfully helpful. Is one way better than another? Can you flush out the prose and cons of each technique? – Michael Seltenreich Jun 27 '22 at 16:43
  • 1
    Second way safer for me because if you use first way you must use only one store for all clients and you don't want that. So the best way to do it is try to put it in a thunk or other middleware so that it doesn’t have to reference the store directly. For me the best way is middlewares for your job. If you want you can search for "Redux Thunk" – Onur Gelmez Jun 27 '22 at 19:48
  • Thank you for your detailed response! I'll go for option 2 as you suggest – Michael Seltenreich Jun 27 '22 at 20:45
  • 1
    @MichaelSeltenreich Have a nice day and coding – Onur Gelmez Jun 27 '22 at 20:52
-1

Maybe a better approach would be to store the token on the localStorage so that you don't need to access the Redux store to obtain it. Just fetch the token and store it this way:

localStorage.setItem('token', JSON.stringify(token))

Later in your service, just retrieve like this:

const token = JSON.parse(localStorage.getItem('token'));

If you don’t feel safe storing the token in the local storage, there's also the secure-ls package, that allows encryption for the data saved to the local storage.

Fernando Bravo Diaz
  • 551
  • 1
  • 4
  • 11
  • 1
    Thanks for you answer. Apologies if my question isn't clear. I'm not asking how to read/store the token. I'm asking how to dispatch the action that refreshes the token (or any action for that matter) from within a service. I have a Saga that uses the refresh token in order to obtain a fresh access token. But I can't figure out how to dispatch the saga from my request service. – Michael Seltenreich Jun 26 '22 at 20:58
  • 1
    Sorry about that, I misunderstood your question, but you might want to take a look at [this other answer](https://stackoverflow.com/questions/50962628/is-it-possible-to-trigger-a-redux-action-from-outside-a-component) about dispatching an action from outside a React component or a Saga – Fernando Bravo Diaz Jun 26 '22 at 21:26