react-redux-universal-hot-example icon indicating copy to clipboard operation
react-redux-universal-hot-example copied to clipboard

How to include JWT authentication in the middleware

Open nicolabortignon opened this issue 8 years ago • 57 comments

I'm trying to include JWT ( http://jwt.io/ ) token authentication in the boilerplate. Let's pretend for a second that the autentication process serverside is already there. What I do is calling API/login passing username and password and I return a token. From that point on, I'd like the middleware to attach this to every http calls.

 headers: {
                    'Authorization': `Bearer *TOKEN* `
 }

Where TOKEN is this.store.token variable defined in the redux auth.js.

How can I do that ?

nicolabortignon avatar Nov 23 '15 21:11 nicolabortignon

@nicolabortignon i made a library for just that and am using it in combination with this boilerplate: https://github.com/bdefore/express-jwt-proxy

sorry there's very little documentation at this point.

bdefore avatar Nov 25 '15 00:11 bdefore

I'm currently passing token header manually for every request that need authorization header. First, I modified the ApiClient Class, add token parameter, and set the header when token is present

this[method] = (path, { params, data, token }

...

        if (token) {
          request.set('authorization', 'Bearer ' + token);
        }

And pass the token value manually to the action

export function changePassword({password, verifyToken}) {
  return {
    types: [CHANGEPASSWORD, CHANGEPASSWORD_SUCCESS, CHANGEPASSWORD_FAIL],
    promise: (client) => client.post('/user/newPassword', {
      data: {
        password,
      },
      token: verifyToken
    })
  };
}

tyao1 avatar Nov 25 '15 08:11 tyao1

Hi @xiaobuu , I did implement pretty much a similar approach. I changed the middleware to check the store for the token, and in case is available, add in the header at any API call.

What it does puzzle me right now is how you retain the token after the user left your app. In order to maintain the isomorphism you want to save this as a cookie (local storage wouldn't work). Then I guess you need at bootstrap phase load the cookie, and if there is a token, save it in the newly create store. This, that in theory sounds easy, looks to me quite hard to implement.

@bdefore : i'll have a look at your library! thanks!

nicolabortignon avatar Nov 25 '15 17:11 nicolabortignon

@nicolabortignon part of the consideration for building my library is to store the token in redis with express-session (which identifies the client by a session cookie). i think that addresses what you're describing above.

this article is worth reading regarding why it's better not to store the token on the client: http://alexbilbie.com/2014/11/oauth-and-javascript/

bdefore avatar Nov 25 '15 18:11 bdefore

I'm using react-cookie to save the JWT Token and that is a isomorphic(universal) implementation.

So you can read the token from the server

import cookie from 'react-cookie';
app.get('*', (req, res) => {
  cookie.plugToRequest(req, res);
  //...
}

rfranco avatar Nov 26 '15 13:11 rfranco

@nicolabortignon I'm doing exactly what you said. I stored the token in the cookie, and in the App.js I fired a load action in fetchData to load the token from server, then I just implemented a reducer to keep the token somewhere in the state tree.

tyao1 avatar Nov 26 '15 14:11 tyao1

@nicolabortignon - check out https://github.com/GetExpert/redux-blog-example It solves that retain-token problem you've described by adding a small layer that saves the JWT to a cookie.

The basic solution they describe in this example AFAICT works as follows;

  • /signup (or /login)
    1. the server should return a JWT in the response body (i.e. not a cookie)
    2. the app saves the JWT inside a cookie, setting an (cookie) expiry
    3. the app dispatches a LOGIN_SUCCESS with the same token (which is then also saved to the store)
  • authenticated API request
    1. grab token from state
    2. pop it in the request header i.e. { Authorization: 'Bearer ' + token } ( so no cookie action, just standard redux store )
  • logout
    1. unset cookie
    2. dispatch a LOGOUT action that resets the store
  • bootstrapping
    • initialize the store with a token by reading from the cookie, if there is one
    • (or it initializes the store using SSR as below)
  • full page refresh (SSR)
    • the browser will send through the Cookie in the request header
    • parses request header (i.e. get cookie), hydrate the store with the auth token (from the cookie).
    • ( pop that on window.INITIAL_STATE to be picked up by the bootstrapping code)

Of course this strategy means all of this needs to happen over https!

It would be great to have something around JWT and/or cookies like this included in this starter boilerplate app.

glennr avatar Nov 27 '15 16:11 glennr

Good discussion. I am trying to figure out if there is a way to use localStorage instead of cookies; however, I am aware there is no way to get the localStorage from the client on to the server for SSR. Is there?

Dindaleon avatar Dec 03 '15 00:12 Dindaleon

@Dindaleon - correct. If you have the JWT in a cookie, then as a side benefit the server will receive that cookie along with the request, which is pretty neat from a SSR perspective.

glennr avatar Dec 04 '15 15:12 glennr

Also @Dindaleon check this article out https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/ - TL;DR store your JWTs inside a cookie.

glennr avatar Dec 04 '15 15:12 glennr

@nicolabortignon can you post the middleware you use to add in the token if possible ?

anomaly44 avatar Dec 06 '15 14:12 anomaly44

@feesjah

https://github.com/TracklistMe/ReactClient/blob/master/src/helpers/ApiClient.js#L23

here what I've done. Said so, I still feel really uncomfortable about this solution. I struggle in have a proper understanding of the SSR integration, although the comments above provide good 'food for thoughts'.

Would be great if someone can actually add to the boilerplate a JWT over Cookie, SSR compatible authentication model.

nicolabortignon avatar Dec 07 '15 17:12 nicolabortignon

Hey I've been following this discussion and implemented a solution based on react-cookie (thanks @rfranco) that ends up being very simple and feels pretty solid. I'll outline the changes to files here, @nicolabortignon if you like it maybe you can incorporate it into your example. I changed some of the code below a bit from my working code to make it match the boilerplate (my project has diverged considerably now) so there might be some typos.

install react-cookie: npm install react-cookie --save

Add to ApiClient.js:

import cookie from 'react-cookie';
...
//in the constructor method creators, load the auth_token from your cookie:
class _ApiClient {
  constructor(req) {
    methods.forEach((method) =>
      this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => {
        ...
        if (cookie && cookie.load('loginResult')) {
          request.set('authorization', 'Bearer ' + cookie.load('loginResult').auth_token);
        }
  ...

In your auth.js reducer:

import cookie from 'react-cookie';
...
case LOGIN_SUCCESS:
   // save the auth token into the cookie - may differ depending how your api returns the auth token
   // no need to save the auth token into the redux state. Instead just depend on the cookie as the single source of truth for the token. But save anything else you need from your login response into the redux state, such as the user name.
   cookie.save('loginResult', action.result);
   return {
     ...state,
     loggingIn: false,
     user: action.result.user
   };
// Add a new action to load the authentication state from your cookie
case LOAD_AUTH_COOKIE:
  const loginResult = cookie.load('loginResult');
  const user = loginResult ? loginResult.user : null;
  return {
    ...state,
    user
  };
...

export function loadAuthCookie() {
  return {
    type: LOAD_AUTH_COOKIE
  };
}

In server.js, configure react-cookie to add cookies sent from the client to the server side rendering environment:

import cookie from 'react-cookie';
...
app.use((req, res) => {
  cookie.plugToRequest(req, res);
...

In App.js - use fetch data to load your auth cookie. This will run both on the server for SSR or on the client if you manage to refresh the app on the client without a server side render. (I don't know if that is actually possible but maybe it is? In any case it seems a good place to trigger the check cookie logic. Maybe if your app doesn't need to check if you're logged in at all routes, you could do this fetch data in a different container and then it would be likely to get triggered on the client).

import {loadAuthCookie} from 'redux/modules/auth';
...
function fetchData(getState, dispatch) {
  bindActionCreators({loadAuthCookie}, dispatch).loadAuthCookie();
}
@connectData(fetchData)
...
export default class App extends Component {

adailey14 avatar Dec 07 '15 20:12 adailey14

@adailey14 thanks for this!

I'm currently working on a fork of the boilerplate that unfortunately went way too far from the current mainline. If I get your code to work I'll send over a PR to be included back in the boilerplate.

Regarding where to place the bootstrap part (the lookup of the cookie), initially I was including it in the router.js file, at the moment of the store creation. I don't see any benefit compare with your solution, so I'm happy to move it to the main container (app.js)

nicolabortignon avatar Dec 07 '15 21:12 nicolabortignon

+1

I am also about to begin with JWT and cookies on my project. It would be great to get either a PR or the steps that worked for the both of you in this boilerplate!

oyeanuj avatar Dec 08 '15 01:12 oyeanuj

has anyone read this blog post ? http://alexbilbie.com/2014/11/oauth-and-javascript/ The author suggests using a separate proxy to encrypt/sign requests. At the moment I still don't understand fully why a proxy is needed, why cant the encryption / signing all happen on the API side ?

and @adailey14 could you perhaps post the code where you create the token too ? sorry for the trouble :)

anomaly44 avatar Dec 08 '15 11:12 anomaly44

@feesjah Actually I am using a Rails powered API so my JWT generation is in ruby, using the JWT ruby gem. I followed this blog post for how to get that set up: http://adamalbrecht.com/2015/07/20/authentication-using-json-web-tokens-using-rails-and-react/

So sorry someone else will have to share their javascript JWT generation code.

Regarding the blog post, I have found oauth to be way too complicated for my needs, and hard to reason about. For my application (and 99% of apps probably) I just need to do 'simple' user authentication (email / password => userId) so I don't think his situation applies to me. I think in the blog's case, the proxy is necessary because he is using a 'global' client_secret that provides 'keys to the kingdom', which you wouldn't want users to access, so you shouldn't store that on the client side.

Below is how I think about security with the JWT setup. I don't see why a proxy would be needed in this case (but I'm really not a security expert!):

  1. Important - Force your app to use https with an SSL certificate, this keeps all traffic between the client and server encrypted so an attacker can't snoop out the user name and password or other sensitive data being sent back and forth.
  2. Ask a client for username / password to authenticate, and provide them a JWT (JSON Web Token) in response. The JWT should not be fakeable due to it's encryption with a server-side secret, and an attacker should not be able to steal the JWT because communication is going over https.
  3. Send the JWT with all future requests to the API, and the server checks that it is valid, and uses the information within the JWT (i.e. userId) as a basis for fetching any secure data. I.E. don't let someone with user1's JWT access user2's data.
  4. Make sure not to keep your server side secret in code anywhere, it should be set in some environment variable on the server or other secure storage mechanism. Also hash your user's passwords on the server, so there is no way to know what their password is, there is only a way to check if a supplied password is correct.
  5. Add some additional anti CSRF measures to keep your cookie from being hi-jacked. A CSRF attack is where an attacker sends you an email with a link to a site where you're already signed in (so you already have a cookie with JWT), and that link makes you do something bad with that site's API. In Rails there was CSRF protection automatically baked in so I have to learn more about how to do that in javascript. With my API I can't think of anything too useful a CSRF attacker could achieve... but I'm sure there is something.
  6. In general don't trust any user input to the API. Don't render anything you got from user input back out to the user as HTML, as this could allow for code-injection attacks.
  7. Probably some additional stuff... security is difficult!

adailey14 avatar Dec 08 '15 15:12 adailey14

@adailey14 What about social logins? How would you change your approach in that case?

oyeanuj avatar Dec 08 '15 15:12 oyeanuj

@oyeanuj Take this with a grain of salt because I don't really know: I think when you start talking about social logins you may have to do something more advanced, and start getting into the details of OAuth flows and such. However even in that case you are usually using OAuth as a 'consumer', rather than a 'provider'. So you just have to figure out how to fit in with the OAuth flow that facebook / linkedIn / etc provides. Sometimes this is as simple as adding some snippet to your client side code that allows the user to login to facebook, and then pulling facebook data into your client side code (so nothing fancy on your sever side). But if you want to persist their social login more permanently in your database and such, then you have to better understand the OAuth flows, and probably be careful about where you expose the various secret keys involved. I read somewhere that it would take a developer a dedicated month of study to understand OAuth well enough to implement it with confidence (and I've never dedicated a month to that!).

Of course if you are trying to be like Facebook yourself, allowing other sites to let users sign in to your site through their site... that's when you have to become an OAuth provider / expert yourself.

adailey14 avatar Dec 08 '15 17:12 adailey14

@adailey14 your snippets worked like a charm!

I have couple of thoughts that I'd like to pick your mind on:

1- every API call (via the apiclient) we invoke cookie.load('loginResult'). Do you know if this is a state read (memory) or a cookie file read? (just for curiosity, I'm not actually concerned about reading time).

2- how would you handle the redirect on login success? I like the approach of Redux-History-Transition (https://github.com/johanneslumpe/redux-history-transitions), but it seams in counter tendency with how the boiler plate is currently handling the redirection (in the router.js file)

@feesjah Here you can find the server side code that i use for generating the token. My server side is on nodejs + express. Both client (based on this boilerplate) and server are opensource, so feel free to have a look around.

nicolabortignon avatar Dec 08 '15 20:12 nicolabortignon

@nicolabortignon glad the snippets worked. Sorry these answers may not be very satisfying:

  1. I don't know... probably this depends on how each browser implements cookies. My guess is that most browsers have the cookies for the current page cached in memory, because the cookies get used a lot. They are included in the header of every request sent from the page, both AJAX and regular. I figure the cookie is going to be included anyway in the header, so pulling out the token and adding it to the header too can't be too big a deal performance-wise.
  2. I don't know... sorry I haven't gotten to this part of my app yet! The approach in redux-history-transitions seems ok, though it feels strange to have to "enhance" the store to get this behavior. It feels like it belongs in some kind of wrapper component maybe? A google search just led me to this: https://github.com/joshgeller/react-redux-jwt-auth-example. I think I will follow that path of creating an authenticated component wrapper. The example includes redirecting after login to the path you were trying to get to.

adailey14 avatar Dec 08 '15 22:12 adailey14

@adailey14 regarding point 2: having the 'redirecting after login to the path you were trying to get' seams a desired feature to have so I'd probably try to implement something similar as well. Having a wrapper component seams legit although a bit forced. In angular it would be a service, but I'm not quite sure how it does translate in the react-redux world.

@erikras thoughts?

nicolabortignon avatar Dec 08 '15 22:12 nicolabortignon

Yeah I'm not clear yet on what is and is not forced in the react-redux world, but I think react-router seems to accomplish all of its magic using wrapper components, so it feels to me like the approach in that link fits in with that, and it allows you to specify which routes require authentication right in the same routes.js file, which seems like probably the right place for it. Let us know what you end up with and how it works out.

adailey14 avatar Dec 08 '15 22:12 adailey14

@nicolabortignon I just noticed on the redux-router readme it has an example of doing login redirection using redux-rx so you might want to take a look at that.

adailey14 avatar Dec 10 '15 19:12 adailey14

@adailey14 At the end I went with the wrapper components. I'm not 100% confident in having redux doing the routing part, so at the end I let the route component handle that part.

The final implementation works well. I'm just concerned how little is self contained as solutions (at the end I've been editing 7 files).

nicolabortignon avatar Dec 10 '15 19:12 nicolabortignon

@adailey14 , @nicolabortignon Thanks, I have it up and running aswell. However there is still one problem i can't seem to get fixed, loading the auth cookie and setting the user. When I do it in App.js fetchData, I see that the action gets executed but It seems like it's not connected to the state, since it doesnt actually change

function fetchData(getState, dispatch) {
  // bindActionCreators({loadAuthCookie}, dispatch).loadAuthCookie();
  const promises = [];
  if (!isInfoLoaded(getState())) {
    promises.push(dispatch(loadInfo()));
  }
  if (!isAuthLoaded(getState())) {
    promises.push(dispatch(loadAuth()));
    promises.push(dispatch(loadAuthCookie()));
  }
  return Promise.all(promises);
}

@connectData(fetchData)
@connect(
  state => ({user: state.auth.user, agency: state.agency.agency}),
  {logout, pushState})
export default class App extends Component {
...

must be doing something wrong.

anomaly44 avatar Dec 13 '15 13:12 anomaly44

Hi @feesjah nothing obviously wrong in your code. When something doesn't work as I expect in this setup I've been doing "old school" console.log("Am I here?", someObjectToLookAt) to see where the code isn't doing what I expect. So you should probably do that, see if your loadAuthCookie() is being called, see if the dispatched action is being reduced in your reducer, etc, see where the break in the chain is.

Also I wanted to mention since I added the snippets above, I ended up moving the dispatch(loadAuthCookie()) stuff into App.js's ComponentWillMount, because fetchData was causing some infinite loops when paired with routing / redirection logic, and loadAuthCookie doesn't actually do anything asynchronous, so doesn't need to be in fetchData.

adailey14 avatar Dec 13 '15 16:12 adailey14

@adailey14 , I did that, and the function is being executed and the cookie loaded, but when i log the state, it shows nothing, so I think somehow it's not connected. I m gonna try and see if I get better results with ComponentWillMount.

anomaly44 avatar Dec 13 '15 16:12 anomaly44

in ComponentWillMount it works like a charm, only problem is, when I do a refresh, the app will show as not logged in for like half a second, and then rerender as logged in. So I'm thinking about reading the cookie on the server, and dispatching an action to load the user on the server.

anomaly44 avatar Dec 13 '15 16:12 anomaly44

The way it works in the snippets I pasted above is that the cookie gets plugged in on the server-side and the code in either fetch data or component will mount is valuable to read that cookie. It sounds to me like you're missing that piece in your code. Fetch data will only run on the server side, while component will mount runs on both server and client.

On Sunday, December 13, 2015, feesjah [email protected] wrote:

in ComponentWillMount it works like a charm, only problem is, when I do a refresh, the app will show as not logged in for like half a second, and then rerender as logged in. So I'm thinking about reading the cookie on the server, and dispatching an action to load the user on the server.

— Reply to this email directly or view it on GitHub https://github.com/erikras/react-redux-universal-hot-example/issues/608#issuecomment-164276206 .

adailey14 avatar Dec 13 '15 19:12 adailey14