J Cole Morrison
J Cole Morrison

J Cole Morrison

Developer Advocate @HashiCorp, DevOps Enthusiast, Startup Lover, Teaching at awsdevops.io



Complete Guides:

React

React and Redux Sagas Authentication App Tutorial Part 2

Posted by J Cole Morrison on .

React and Redux Sagas Authentication App Tutorial Part 2

Posted by J Cole Morrison on .

Overview

This is a continuation of the React and Redux Sagas Authentication App Tutorial. In the previous section knocked out a great deal:

  • Scaffolded out the React application
  • Setup Redux, Redux Form, React Router and Redux Saga
  • Modeled and created our Global State
  • Setup the "Client" state, which will be our placeholder for Auth
  • Created our Signup Component, Reducer and Actions

In this post we'll be making the Login and Authentication flow. We'll do some of authorization and loopback around to more of that in part 3 when we actually create the routes and resources that need it. The API already has authorization built into the actual data models, so all we really need to do is deal with handling what the client sees.

Again, this is part of a 3 (maybe 4) part series:

Part 1 - Setting up the base project and Signing up our Users

Part 2 (This one) - Making the Login and Authentication/Authorization flow

Part 3 - Working with protected resources

Additionally, we also use the API that I cover extensively how to build and setup using Node, Express and Loopback.

All of the code for this portion of the series can be found here:

Github Part 2 Branch

Table of Contents

  1. Picking Back Up
  2. Constructing the Login State
  3. Another Redux Aside
  4. Logging in with Redux Saga
  5. Authorizing Our Widgets
  6. Summary

Pick Up Where We Left Off

In the last post we ended right as we gained the ability to signup our clients (the reason why they're called clients and not users is explained in the api post). So obviously the next logical point is logging in.

If you're just picking this up, I highly Going through Part 1, however the entire starting point for this code base can be found at:

Github Part 1 Branch

Let's dive in.

Constructing the Login State

0. Make sure you're in your code/src/ folder from last time

This is the folder where we scaffolded out our original project.

Small successes again! We'll work from here, so I'm going to omit saying "In your code/src/ folder." All of our work will be in the code/src/ folder.

Just in case your skimming - all paths I'm mentioning are relative to the code/src/ folder

1. Open up the login/reducer.js file

As with our Signup and Client areas, let's begin with the end and setup the state of our Login component and related logic.

And... this is going to be really easy. We're going to use the exact same State layout as our Signup process.

Remember, redux state, not react state. It's like the two gave 0 blanks about the fact each of them used the same word for a core concept. At least they didn't call it this.

Modify the login/reducer.js file to be the following:

const initialState = {  
  requesting: false,
  successful: false,
  messages: [],
  errors: [],
}

Like with Signup, we'll have some flags to handle the state of our API request and two arrays for us to push messages and errors to. From a front end perspective, this can be a little confusing to start with the data end of things, but beyond all of the "engineering" benefits of doing so there's the psychological benefits of...a clear, simple target.

This is what our state deals with - sending a request, tracking it and showing errors/messages to the user. What actions can the user take to reduce my app's state?

Well, we know they'll probably

  • request a login
  • succeed in the login
  • fail in the login (error)
  • or return with a token prior and get logged in automatically
  • logout

So let's go ahead and make the constants to represent these actions.

Remember, constants are convention to make sure we don't misspell or modify consistent values that get referenced by other areas. You could just as well have your reducers and actions pass around 'RAW_STRING_ACTIONS'. But why risk the typos and side effects?

2. Open up the login/constants.js and add the following:

export const LOGIN_REQUESTING = 'LOGIN_REQUESTING'  
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'  
export const LOGIN_ERROR = 'LOGIN_ERROR'  

These are the types of actions we can take to reduce our Login's State.

But wait.. what about something like LOGOUT and LOGIN_EXISTING? Didn't we agree that a user would want that?

Yes totally. HOWEVER. We've actually already set that up in our previous tutorial. What is logging in and logging out? For a client-thick, token api based application, it's just the presence of the access token that allows .. well .. access to the API.

Therefore the "spirit" of our LOGOUT, LOGIN_EXISTING and really the raw idea of LOGIN is embodied by both our CLIENT_SET and CLIENT_UNSET actions we've already setup.

These separated actions allow us to leverage authentication elsewhere without being bound to just our Login component and reducer. (i.e. imagine being in a store, a real e-store, process and wanting to login midway through purchase).

3. Open up login/actions.js and add the following:

import {  
  LOGIN_REQUESTING,
} from './constants'

// In order to perform an action of type LOGIN_REQUESTING
// we need an email and password
const loginRequest = function loginRequest ({ email, password }) {  
  return {
    type: LOGIN_REQUESTING,
    email,
    password,
  }
}

// Since it's the only one here
export default loginRequest  

This is the only action we'll deal with and dispatch directly. This would honestly be better called action-creator since it's creating an action with the type of LOGIN_REQUESTING. There's nothing stopping us from just calling dispatch({type: LOGIN_REQUESTING, email, password}) from inside of our component and other areas.

4. Finally, let's go ahead and open up login/reducer.js and wrap it all together:

import {  
  LOGIN_REQUESTING,
  LOGIN_SUCCESS,
  LOGIN_ERROR,
} from './constants'

const initialState = {  
  requesting: false,
  successful: false,
  messages: [],
  errors: [],
}

const reducer = function loginReducer (state = initialState, action) {  
  switch (action.type) {
    // Set the requesting flag and append a message to be shown
    case LOGIN_REQUESTING:
      return {
        requesting: true,
        successful: false,
        messages: [{ body: 'Logging in...', time: new Date() }],
        errors: [],
      }

    // Successful?  Reset the login state.
    case LOGIN_SUCCESS:
      return {
        errors: [],
        messages: [],
        requesting: false,
        successful: true,
      }

    // Append the error returned from our api
    // set the success and requesting flags to false
    case LOGIN_ERROR:
      return {
        errors: state.errors.concat([{
          body: action.error.toString(),
          time: new Date(),
        }]),
        messages: [],
        requesting: false,
        successful: false,
      }

    default:
      return state
  }
}

export default reducer  

Learn how to deploy production-ready Node and React applications on AWS.

Get notified when my next AWS DevOps Workshop opens:

Another Redux and "State" of Mind Aside

Okay okay. So if you're a seasoned Redux'er, go ahead and move to the next step. However, if you're newer to Redux (and possibly React) right now you may feel a bit...

How do we know this...

... with respect to us just magically saying THIS IS WHAT OUR STATE SHALL LOOK LIKE HAHAHA.

How do we know?

Here's the thing - we didn't.

We decided.

The way to think about this, is that we're agreeing upon what actions and states our Login component will work with.

Think of it like going to build a house. Well, I've never built one, but think of the concept of building a house. We wouldn't just, dive right in, hammer and nail would we?

Nope.

We'd decide upon a blueprint. The kitchen will go here, the bedroom here and when we flip a switch here, the electricity will pop on. Now when actually building, we'll have much more clarity of how things fit together.

This "blueprint" is essentially what we've just setup. We're saying that:

a) if our Login component dispatches an action with type of LOGIN_REQUESTING

then

b) they should also call it with an object that contains an email and password

And

c) we'll change our state, and thus the props on the Login component, to flag this.props.requesting as true.

Hopefully this makes sense - we've just made it easier on ourselves for when we begin developing the component. We know that when we want to login, we'll just call the loginRequest({email: 'email@example', password: 'superSecure'}).

Obviously this simple login request doesn't exemplify the true power of this directional thinking. However, when the states are huge and the UI super complex, boiling it down to this gives tremendous focus.

It also helps split up work between team members. If we we're a front end developer working with a back end developer, we'd do this exact same flow. We'd agree that, when the front-end hits the backend, that it will receive a payload in a particular fashion and format. This makes it easy to develop in parallel and gives focus. We've technically already done that in this series since the backend is prebuilt!

Alright, back to the action.

5. Open up index-reducer.js and let's add in our login reducer:

import { combineReducers } from 'redux'  
import { reducer as form } from 'redux-form'  
import client from './client/reducer'  
import signup from './signup/reducer'  
import login from './login/reducer'

const IndexReducer = combineReducers({  
  signup,
  client,
  login,
  form,
})

export default IndexReducer  

Now to move onto actually coding up the component.

Creating the Login Component

This will be refreshingly straight forward because it's almost identical to our Signup component. One might as.. "Why don't we just combine them then?" WE could, but rarely would we mix both of these together. Even though WE won't be, what if we wanted to nest our login form into a dropdown? We'll we could easily reuse it, restyle it, etc, and not worry about messing up signup.

YES, we could abstract and abstract and make everything in both forms reusable. But hugely and insanely abstracted components, where it's obvious that they've been forced into becoming a Swiss army knife, are a hotbed for bugs and errors.

5. Open up login/index.js and modify it to be:

import React, { Component, PropTypes } from 'react'  
import { reduxForm, Field } from 'redux-form'  
import { connect } from 'react-redux'  
import { Link } from 'react-router'

import Messages from '../notifications/Messages'  
import Errors from '../notifications/Errors'

import loginRequest from './actions'

// If you were testing, you'd want to export this component
// so that you can test your custom made component and not
// test whether or not Redux and Redux Form are doing their jobs
class Login extends Component {  
  // Pass the correct proptypes in for validation
  static propTypes = {
    handleSubmit: PropTypes.func,
    loginRequest: PropTypes.func,
    login: PropTypes.shape({
      requesting: PropTypes.bool,
      successful: PropTypes.bool,
      messages: PropTypes.array,
      errors: PropTypes.array,
    }),
  }

  // Remember, Redux Form passes the form values to our handler
  // In this case it will be an object with `email` and `password`
  submit = (values) => {
    this.props.loginRequest(values)
  }

  render () {
    const {
      handleSubmit, // remember, Redux Form injects this into our props
      login: {
        requesting,
        successful,
        messages,
        errors,
      },
    } = this.props

    return (
      <div className="login">
        <form className="widget-form" onSubmit={handleSubmit(this.submit)}>
          <h1>LOGIN</h1>
          <label htmlFor="email">Email</label>
          {/*
            Our Redux Form Field components that bind email and password
            to our Redux state's form -> login piece of state.
          */}
          <Field
            name="email"
            type="text"
            id="email"
            className="email"
            component="input"
          />
          <label htmlFor="password">Password</label>
          <Field
            name="password"
            type="password"
            id="password"
            className="password"
            component="input"
          />
          <button action="submit">LOGIN</button>
        </form>
        <div className="auth-messages">
          {/* As in the signup, we're just using the message and error helpers */}
          {!requesting && !!errors.length && (
            <Errors message="Failure to login due to:" errors={errors} />
          )}
          {!requesting && !!messages.length && (
            <Messages messages={messages} />
          )}
          {requesting && <div>Logging in...</div>}
          {!requesting && !successful && (
            <Link to="/signup">Need to Signup? Click Here »</Link>
          )}
        </div>
      </div>
    )
  }
}

// Grab only the piece of state we need
const mapStateToProps = state => ({  
  login: state.login,
})

// make Redux state piece of `login` and our action `loginRequest`
// available in this.props within our component
const connected = connect(mapStateToProps, { loginRequest })(Login)

// in our Redux's state, this form will be available in 'form.login'
const formed = reduxForm({  
  form: 'login',
})(connected)

// Export our well formed login component
export default formed  

Comments are included to explain the code again. But really and truly it should all look familiar. At it's core it allows us to:

a) input an email and password

b) submit the email and password to ReduxForm and thus our custom this.submit function

c) make a call to our loginRequest action that we've already defined and made available to our component's props via connect.

6. Navigate to localhost:3000/login to see our amazingly bland login form!

login component with state

As we can see, our entire component and Login piece of state are now wired together. Redux Form has also started tracking our individual form fields and passes them to our custom submit function whenever we submit.

Now let's dive into the juicy part - logging in with Redux Saga.

Learn how to deploy production-ready Node and React applications on AWS.

Get notified when my next AWS DevOps Workshop opens:

Logging in with Redux Saga

While this part will be somewhat similar, we're going to leverage a few more advanced concepts from both Redux Saga AND generators in general. Again, if you haven't done so, read up on the basics of generator functions and the usage in iterators!

7. Open up login/sagas.js

As with our previous signup saga, let's begin with the end in mind. What's the goal of this saga? Well, it will be to WATCH for the action LOGIN_REQUESTING and begin the login process.

In addition to that basic watcher, we're also going to watch for a LOGIN_ERROR and also a CLIENT_UNSET as well. Annnnnnd. We're going to do all of that in just one generator function!

import { take, fork, cancel, call, put, cancelled } from 'redux-saga/effects'

// We'll use this function to redirect to different routes based on cases
import { browserHistory } from 'react-router'

// Helper for api errors
import { handleApiErrors } from '../lib/api-errors'

// Our login constants
import {  
  LOGIN_REQUESTING,
  LOGIN_SUCCESS,
  LOGIN_ERROR,
} from './constants'

// So that we can modify our Client piece of state
import {  
  setClient,
  unsetClient,
} from '../client/actions'

import {  
  CLIENT_UNSET,
} from '../client/constants'

function loginApi (email, password) {}

function* logout () {}

function* loginFlow (email, password) {}

// Our watcher (saga).  It will watch for many things.
function* loginWatcher () {

  // Generators halt execution until their next step is ready/occurring
  // So it's not like this loop is firing in the background 1000/sec
  // Instead, it says, "okay, true === true", and hits the first step...
  while (true) {
    //
    // ... and in this first it sees a yield statement with `take` which
    // pauses the loop.  It will sit here and WAIT for this action.
    //
    // yield take(ACTION) just says, when our generator sees the ACTION
    // it will pull from that ACTION's payload that we send up, its
    // email and password.  ONLY when this happens will the loop move
    // forward...
    const { email, password } = yield take(LOGIN_REQUESTING)

    // ... and pass the email and password to our loginFlow() function.
    // The fork() method spins up another "process" that will deal with
    // handling the loginFlow's execution in the background!
    // Think, "fork another process".
    //
    // It also passes back to us, a reference to this forked task
    // which is stored in our const task here.  We can use this to manage
    // the task.
    //
    // However, fork() does not block our loop.  It's in the background
    // therefore as soon as our loop executes this it mores forward...
    const task = yield fork(loginFlow, email, password)

    // ... and begins looking for either CLIENT_UNSET or LOGIN_ERROR!
    // That's right, it gets to here and stops and begins watching
    // for these tasks only.  Why would it watch for login any more?
    // During the life cycle of this generator, the user will login once
    // and all we need to watch for is either logging out, or a login
    // error.  The moment it does grab either of these though it will
    // once again move forward...
    const action = yield take([CLIENT_UNSET, LOGIN_ERROR])

    // ... if, for whatever reason, we decide to logout during this
    // cancel the current action.  i.e. the user is being logged
    // in, they get impatient and start hammering the logout button.
    // this would result in the above statement seeing the CLIENT_UNSET
    // action, and down here, knowing that we should cancel the
    // forked `task` that was trying to log them in.  It will do so
    // and move forward...
    if (action.type === CLIENT_UNSET) yield cancel(task)

    // ... finally we'll just log them out.  This will unset the client
    // access token ... -> follow this back up to the top of the while loop
    yield call(logout)
  }
}

export default loginWatcher  

A TON of explanation is in the code comments. Let's walk through it though.

What we're doing here is

a. Starting a loop.

This doesn't mean that we're running something that will be running 1000 times a second! A lesser loop would run through everything, but we have a loop inside of a generator function. That means, just like any other generator function, it will stop at every yield statement and WAIT for that statement to "yield" it a value.

That means it will hit the:

const { email, password } = yield take(LOGIN_REQUESTING)  

and...

b. Begin watching for the LOGIN_REQUESTING action

Our loop hits our first yield statement and, since it's a generator function, will wait for our Redux Saga's take method to receive a LOGIN_REQUESTING value.

Let me reiterate that - it will PAUSE at that first take and the loop will stay there and wait for the action. Once LOGIN_REQUESTING is dispatched from our component, this saga will take the email and password off of the action and...

c. Fork a background task that will run loginFlow()

const task = yield fork(loginFlow, email, password)  

The saga says, "Oh I got an action that I care about!" It takes the email and password and then forks a background task that runs our loginFlow function and passes it the email and password.

Now it's important to realize it's forking a background task because that means it will continue on with execution. It will not halt as it did with the take. Therefore it begins the loginFlow and then...

d. Begin watching for CLIENT_UNSET and LOGIN_ERROR

const action = yield take([CLIENT_UNSET, LOGIN_ERROR])  

It forks the tasks and moves forward and hits another take. We can see here that take can also accept an Array of actions to watch for. And in this case it's CLIENT_UNSET and LOGIN_ERROR. Just as previously, our generator function will sit here and watch for those two action types. When our component (or any component) dispatches either of these two actions, it will take the action payload, set it to the const action and continue on and...

e. If the action.type is CLIENT_UNSET cancel our forked task of trying to login

This is more of precaution to deal with a case where our user has begun the login process, but, mid way, decides to logout. If that occurs, we'll use the reference to the task which represents that background process and cancel it.

moving on, regardless of which action has been taken from the the dispatch...

f. We log out the by calling to our logout function

Once we code that out, it will simply dispatch a CLIENT_UNSET action that will remove the data from our state, remove the token and redirect them to the login screen.

But we're not done... once we've logged them back out...

g. Go back to (a)

Yep that's right, the loop begins again and starts watching for LOGIN_REQUESTING! Because while(true) is surprisingly true, we start right back at the top and begin again.

Here's the condensed code without comments:

import { take, fork, cancel, call, put, cancelled } from 'redux-saga/effects'

import { browserHistory } from 'react-router'

import { handleApiErrors } from '../lib/api-errors'

import {  
  LOGIN_REQUESTING,
  LOGIN_SUCCESS,
  LOGIN_ERROR,
} from './constants'

import {  
  setClient,
  unsetClient,
} from '../client/actions'

import {  
  CLIENT_UNSET,
} from '../client/constants'

function* loginFlow (email, password) {}

function* logout () {}

function* loginWatcher () {  
  while (true) {
    const { email, password } = yield take(LOGIN_REQUESTING)

    const task = yield fork(loginFlow, email, password)

    const action = yield take([CLIENT_UNSET, LOGIN_ERROR])

    if (action.type === CLIENT_UNSET) yield cancel(task)

    yield call(logout)
  }
}

export default loginWatcher  

So this is all well and good, but now we need to deal with actually coding the loginFlow function. Thankfully, this is going to be QUITE similar to the signupFlow we created in the previous article because it's the same pattern - hit an API with a request, do something with the response.

8. In our login/sagas.js file, modify the loginFlow function to be:

// ...
function* loginFlow (email, password) {  
  let token
  try {
    // try to call to our loginApi() function.  Redux Saga
    // will pause here until we either are successful or
    // receive an error
    token = yield call(loginApi, email, password)

    // inform Redux to set our client token, this is non blocking so...
    yield put(setClient(token))

    // .. also inform redux that our login was successful
    yield put({ type: LOGIN_SUCCESS })

    // set a stringified version of our token to localstorage on our domain
    localStorage.setItem('token', JSON.stringify(token))

    // redirect them to WIDGETS!
    browserHistory.push('/widgets')
  } catch (error) {
    // error? send it to redux
    yield put({ type: LOGIN_ERROR, error })
  } finally {
    // No matter what, if our `forked` `task` was cancelled
    // we will then just redirect them to login
    if (yield cancelled()) {
      browserHistory.push('/login')
    }
  }

  // return the token for health and wealth
  return token
}
// ...

This should seem familiar as per our signup saga we created. We...

a. (pauses until done) attempt to login

b. (continues on) calls to the setClient action creator to dispatch our token

note: this is an example of using action creators. We could make an action creator for every login action, but (a) the login success/error actions only deal with the login view we won't reuse them and (b) the client actions will be reused elsewhere

c. (continues on) dispatch the LOGIN_SUCCESS action

d. (continues on) set our token to local storage

e. (continues on) redirect to the /widgets route

If it fails, we'll dispatch the LOGIN_ERROR action with the relevant error.

Also, remember how below we have that condition of "What if someone tries to logout in the process of logging in?" We decided to cancel the current login process if that was the case - therefore we called yield cancel(task).

Our task is this current running loginFlow(). The finally block in a try/catch is always run, therefore if it sees the cancelled() it will just stop.

Beyond all of that we'll return the token.

So again, I know we're working backwards, but the better way to think about it is that we're beginning with the end in mind. The target. We said,

What are we watching for?

and then

What are we going to do once we see what we've been watching for?

Now we need to actually accomplish the API login task. This is almost identical to the signupApi function we wrote earlier with the exception that it uses a different endpoint.

9. In our login/sagas.js modify the loginApi function to be:

// ...
// Different LOGIN endpoint
const loginUrl = `${process.env.REACT_APP_API_URL}/api/Clients/login`

function loginApi (email, password) {  
  return fetch(loginUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, password }),
  })
    .then(handleApiErrors)
    .then(response => response.json())
    .then(json => json)
    .catch((error) => { throw error })
}
// ...

There's not any real description in this, because it's identical to the signupApi function. If you'd really like to abstract it out, go for it. But it's separated out just in case, in the future, we need a more robust loginApi function.

One thing I forgot to mention in the first post - it's called REACT_APP_* because create-react-app only makes environment variables prefixed with this available in our application.

10. In our login/sagas.js modify the logout function to be:

// ..
function* logout () {  
  // dispatches the CLIENT_UNSET action
  yield put(unsetClient())

  // remove our token
  localStorage.removeItem('token')

  // redirect to the /login screen
  browserHistory.push('/login')
}
// ..

With that, our entire login saga is completed! The full file looks like:

import { take, fork, cancel, call, put, cancelled } from 'redux-saga/effects'

// We'll use this function to redirect to different routes based on cases
import { browserHistory } from 'react-router'

// Helper for api errors
import { handleApiErrors } from '../lib/api-errors'

// Our login constants
import {  
  LOGIN_REQUESTING,
  LOGIN_SUCCESS,
  LOGIN_ERROR,
} from './constants'

// So that we can modify our Client piece of state
import {  
  setClient,
  unsetClient,
} from '../client/actions'

import {  
  CLIENT_UNSET,
} from '../client/constants'

const loginUrl = `${process.env.REACT_APP_API_URL}/api/Clients/login`

function loginApi (email, password) {  
  return fetch(loginUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, password }),
  })
    .then(handleApiErrors)
    .then(response => response.json())
    .then(json => json)
    .catch((error) => { throw error })
}

function* logout () {  
  // dispatches the CLIENT_UNSET action
  yield put(unsetClient())

  // remove our token
  localStorage.removeItem('token')

  // redirect to the /login screen
  browserHistory.push('/login')
}

function* loginFlow (email, password) {  
  let token
  try {
    // try to call to our loginApi() function.  Redux Saga
    // will pause here until we either are successful or
    // receive an error
    token = yield call(loginApi, email, password)

    // inform Redux to set our client token, this is non blocking so...
    yield put(setClient(token))

    // .. also inform redux that our login was successful
    yield put({ type: LOGIN_SUCCESS })

    // set a stringified version of our token to localstorage on our domain
    localStorage.setItem('token', JSON.stringify(token))

    // redirect them to WIDGETS!
    browserHistory.push('/widgets')
  } catch (error) {
    // error? send it to redux
    yield put({ type: LOGIN_ERROR, error })
  } finally {
    // No matter what, if our `forked` `task` was cancelled
    // we will then just redirect them to login
    if (yield cancelled()) {
      browserHistory.push('/login')
    }
  }

  // return the token for health and wealth
  return token
}

// Our watcher (saga).  It will watch for many things.
function* loginWatcher () {  
  // Generators halt execution until their next step is ready/occurring
  // So it's not like this loop is firing in the background 1000/sec
  // Instead, it says, "okay, true === true", and hits the first step...
  while (true) {
    // ... and in this first it sees a yield statement which
    // pauses the loop.  It will sit here and WAIT for this action.
    //
    // yield take(ACTION) just says, when our generator sees the ACTION
    // it will pull from that ACTION's payload that we send up, its
    // email and password.  ONLY when this happens will the loop move
    // forward...
    const { email, password } = yield take(LOGIN_REQUESTING)

    // ... and pass the email and password to our loginFlow() function
    // the fork() method spins up another "process" that will deal with
    // handling the loginFlow's execution in the background!
    // Think, "fork another process".
    //
    // It also passes back to us, a reference to this forked task
    // which is stored in our const task here.  We can use this to manage
    // the task.
    //
    // However, fork() does not block our loop.  It's in the background
    // therefore as soon as our loop executes this it mores forward...
    const task = yield fork(loginFlow, email, password)

    // ... and begins looking for either CLIENT_UNSET or LOGIN_ERROR!
    // That's right, it gets to here and stops and begins watching
    // for these tasks only.  Why would it watch for login any more?
    // During the life cycle of this generator, the user will login once
    // and all we need to watch for is either logging out, or a login
    // error.  The moment it does grab either of these though it will
    // once again move forward...
    const action = yield take([CLIENT_UNSET, LOGIN_ERROR])

    // ... if, for whatever reason, we decide to logout during this
    // cancel the current action.  i.e. the user is being logged
    // in, they get impatient and start hammering the logout button.
    // this would result in the above statement seeing the CLIENT_UNSET
    // action, and down here, knowing that we should cancel the
    // forked `task` that was trying to log them in.  It will do so
    // and move forward...
    if (action.type === CLIENT_UNSET) yield cancel(task)

    // ... finally we'll just log them out.  This will unset the client
    // access token ... -> follow this back up to the top of the while loop
    yield call(logout)
  }
}

export default loginWatcher  

10.5. Add the login saga to the index-sagas.js file:

thanks Craig Cannon for catching this!

import SignupSaga from './signup/sagas'  
import LoginSaga from './login/sagas'

export default function* IndexSaga () {  
  yield [
    SignupSaga(),
    LoginSaga(),
  ]
}

One more step...

In order to see our login process actual work though, we need to render() our widgets index. Right now our saga redirects to that component, but we haven't setup a view yet. This results in an error.

11. Open widgets/index.js and modify it to be:

import React, { Component } from 'react'

class Widgets extends Component {  
  render () {
    return (<div>WIDGETS</div>)
  }
}

export default Widgets  

Alrighty! Now we can login and see that our access token is being set in our state and that we're being redirected:

Before:

before logging in

After:

before logging in

Error:

before logging in

Caveat here - if we logged in, and then navigate back to login (without refreshing the page or directly url changing), login will just hang if we try to login again. Why? Because our redux saga is no longer listening for LOGIN_REQUESTING. Its switched to listening for our logout events. We could easily account for that, but out of sake of time we'll move on to greater things..

One last adventure. Now we should deal with re-authenticating users when they return and authorizing them to our widgets view.

Learn how to deploy production-ready Node and React applications on AWS.

Get notified when my next AWS DevOps Workshop opens:

Authorizing Our Widgets

In the final frontier of our saga we will deal with authorizing our Widget view based on whether or not our user is authenticated AND remembering that they're authenticated upon return.

We'll have 3 primary checks:

1) Does our user have a valid token? Great, set it to the local redux state and redirect them to /widgets.

2) Does our user have a valid token that's expired? Let's clear the token and redirect them to /login

3) Does our user have no token at all? Redirect them to /login.

12. Open up index.js

As usual we'll begin with the end in mind. Although this will "break" our app, mentally it will give us a far better idea of what's going on.

On each React Router <Route> we can set an onEnter property and pass it a function. As one might suspect, it will run this function on entering the route. What we want to do is run our authentication / authorization checks at this point.

Let's modify index.js to include all of our new helpers and modify the routes:

import React from 'react'  
import ReactDOM from 'react-dom'  
import { applyMiddleware, createStore, compose } from 'redux'  
import { Provider } from 'react-redux'  
import createSagaMiddleware from 'redux-saga'  
import { Router, Route, browserHistory, IndexRoute } from 'react-router'  
// add IndexRoute above and the helpers below
import {  
  checkIndexAuthorization,
  checkWidgetAuthorization,
} from './lib/check-auth'

// .. middle of file collapsed for brevity....

ReactDOM.render(  
  <Provider store={store}>
    <Router history={browserHistory}>
      <Route path="/" component={App} >
        <IndexRoute onEnter={checkIndexAuthorization(store)} />
        <Route path="/login" component={Login} />
        <Route path="/signup" component={Signup} />
        <Route onEnter={checkWidgetAuthorization(store)} path="/widgets" component={Widgets} />
      </Route>
    </Router>
  </Provider>,
  document.getElementById('root'),
)

note: the middle of the file is there, I'm just not showing it in this snippet since none of it has changed.

The only 3 things that have changed here are:

a. We included IndexRoute - this is a React Router helper that allows us to reference that top level <Route path="/" component={app}> portion.

If we were to put our check directly on the <Route path="/" component={app}>, the checkIndexAuthorization method would get run on EVERY nested route. Instead we only want it on the / one.

b. We include our helpers checkIndexAuthorization and checkWidgetAuthorization. Obviously we haven't made either of these yet. But we're beginning with the end in mind. We're stating how we want things to work.

c. We set the helpers on our different routes. We pass them our redux store so that they can interact with our state and actions via dispatch.

Note here, the reason we're calling a function within this ,(checkWidgetAuthorization(store) vs. checkWidgetAuthorization) is so that we can have access to the store. From this we'll return the actual function that the router will use.

Excellent. Now let's go make these helpers.

13. Open up lib/check-auth.js and let's create our checkIndexAuthorization function:

export function checkIndexAuthorization ({ dispatch }) {  
  // by having a function that returns a function we satisfy 2 goals:
  //
  // 1. grab access to our Redux Store and thus Dispatch to call actions
  // 2. Return a function that includes all the proper .. properties that
  //    React Router expects for us to include and use
  //
  // `nextState` - the next "route" we're navigating to in the router
  // `replace` - a helper to change the route
  // `next` - what we call when we're done messing around
  //
  return (nextState, replace, next) => {
    // we'll make this in a minute - remember begin with the end!
    // If we pass the authentication check, go to widgets
    if (checkAuthorization(dispatch)) {
      replace('widgets')

      return next()
    }

    // Otherwise let's take them to login!
    replace('login')
    return next()
  }
}

The description of this rather straight forward function is in the code above. The only real caveat here is that we're returning a function to React Router in order to have access to both the helpers from React Router AND Redux.

Yes yes, I know we haven't made the checkAuthorization helper yet, but that will come soon enough... soon enough...

14. While still in lib/check-auth.js let's create the checkWidgetAuthorization function:

// .. our checkIndexAuthorization function is above this

export function checkWidgetAuthorization ({ dispatch, getState }) {  
  // Same format - we do this to have the Redux State available.
  // The difference is that this time we also pull in the helper
  // `getState` which will allow us to.....
  // ....
  // get the state.
  //
  return (nextState, replace, next) => {
    // reference to the `client` piece of state
    const client = getState().client

    // is it defined and does it have a token? good, go ahead to widgets
    if (client && client.token) return next()

    // not set yet?  Let's try and set it and if so, go ahead to widgets
    if (checkAuthorization(dispatch)) return next()

    // nope?  okay back to login ya go.
    replace('login')
    return next()
  }
}

Pretty much the same format, so let's move on.

15. Let's finally create the almighty checkAuthorization function in our lib/check-auth.js file:

import { setClient } from '../client/actions'

function checkAuthorization (dispatch) {  
  // attempt to grab the token from localstorage
  const storedToken = localStorage.getItem('token')

  // if it exists
  if (storedToken) {
    // parse it down into an object
    const token = JSON.parse(storedToken)

    // this just all works to compare the total seconds of the created
    // time of the token vs the ttl (time to live) seconds
    const createdDate = new Date(token.created)
    const created = Math.round(createdDate.getTime() / 1000)
    const ttl = 1209600
    const expiry = created + ttl

    // if the token has expired return false
    if (created > expiry) return false

    // otherwise, dispatch the token to our setClient action
    // which will update our redux state with the token and return true
    dispatch(setClient(token))
    return true
  }

  return false
}

// .. rest of file

All we're doing is checking to see if they have a token and seeing if it's expired. If they have a token, we'll update our redux state with it and let them move on. If not, we'll return false. Both of the other authorizers, upon false, will redirect them to the login page.

The file in its final form:

import { setClient } from '../client/actions'

function checkAuthorization (dispatch) {  
  // attempt to grab the token from localstorage
  const storedToken = localStorage.getItem('token')

  // if it exists
  if (storedToken) {
    // parse it down into an object
    const token = JSON.parse(storedToken)

    // this just all works to compare the total seconds of the created
    // time of the token vs the ttl (time to live) seconds
    const createdDate = new Date(token.created)
    const created = Math.round(createdDate.getTime() / 1000)
    const ttl = 1209600
    const expiry = created + ttl

    // if the token has expired return false
    if (created > expiry) return false

    // otherwise, dispatch the token to our setClient action
    // which will update our redux state with the token and return true
    dispatch(setClient(token))
    return true
  }

  return false
}

export function checkIndexAuthorization ({ dispatch }) {  
  // by having a function that returns a function we satisfy 2 goals:
  //
  // 1. grab access to our Redux Store and thus Dispatch to call actions
  // 2. Return a function that includes all the proper .. properties that
  //    React Router expects for us to include and use
  //
  // `nextState` - the next "route" we're navigating to in the router
  // `replace` - a helper to change the route
  // `next` - what we call when we're done messing around
  //
  return (nextState, replace, next) => {
    // we'll make this in a minute - remember begin with the end!
    // If we pass the authentication check, go to widgets
    if (checkAuthorization(dispatch)) {
      replace('widgets')

      return next()
    }

    // Otherwise let's take them to login!
    replace('login')
    return next()
  }
}

export function checkWidgetAuthorization ({ dispatch, getState }) {  
  // Same format - we do this to have the Redux State available.
  // The difference is that this time we also pull in the helper
  // `getState` which will allow us to.....
  // ....
  // get the state.
  //
  return (nextState, replace, next) => {
    // reference to the `client` piece of state
    const client = getState().client

    // is it defined and does it have a token? good, go ahead to widgets
    if (client && client.token) return next()

    // not set yet?  Let's try and set it and if so, go ahead to widgets
    if (checkAuthorization(dispatch)) return next()

    // nope?  okay back to login ya go.
    replace('login')
    return next()
  }
}

AWESOME.

Now if you don't have a token set and try to navigate to the /widgets route you'll automatically be redirected to /login. Additionally, if you DO have a token set and navigate to the / route, you'll be taken to the /widgets route!

If you'd like the /login route to behave the same way, just modify it to be:

<Route onEnter={checkIndexAuthorization(store)} path="/login" component={Login} />  

We'd then need to add the following to the checkIndexAuthorization function"

if (nextState.location.pathname !== '/login') replace('login')  
return next()  

Of course a better name would probably be in store for it. I left it off of the route just in case you'd like to go back to /login and refresh your token, view it, etc.

note: you can delete your token by opening up your developer console panel, navigating to application, clicking on Local Storage in the side panel, clicking on our localhost:3000 and deleting the token

note: if you want to test the unauthed process easily, just open up an incognito window which will have no token.

Summary

What'd we accomplish?

  1. Modeling our Login State
  2. Setting up our Login Actions and Constants
  3. Stepping through and creating our Login Saga
  4. Learning about the fun loop feature of Sagas
  5. Handling authorization!

You can see the entirety of this code base at:

https://github.com/jcolemorrison/redux-sagas-authentication-app

And the code specific to this section at:

https://github.com/jcolemorrison/redux-sagas-authentication-app/tree/login

In Part 3

We'll setup with creating and fetching Widgets as an authenticated user!


As usual, if you find any technical glitches or hiccups PLEASE leave a comment or hit me up on twitter or with a message!

Enjoy Posts Like These? Sign up to my mailing list!

My Tech Guides and Thoughts Mailing List


More from the blog

J Cole Morrison

J Cole Morrison

http://start.jcolemorrison.com

Developer Advocate @HashiCorp, DevOps Enthusiast, Startup Lover, Teaching at awsdevops.io

View Comments...
J Cole Morrison

J Cole Morrison

Developer Advocate @HashiCorp, DevOps Enthusiast, Startup Lover, Teaching at awsdevops.io



Complete Guides: