Let’s Build |> a Slack Clone with Elixir, Phoenix, and React (part 3— Frontend Authentication)

Live Demo — GitHub Repo
Part 1 — Part 2 — Part 3 — Part 4 — Part 5 — Part 6 — Part 7

In part 2 we added the API endpoints needed for authentication. Now we can create our signup/login forms and authenticate from the frontend.

A quick note on styles before we begin. In React projects, I like scoping styles to components as much as possible, keeping CSS in my JS, and using inline styles. I try to keep global styles limited, using them for resets, base elements styles, and libraries like Twitter Bootstrap.

There are many methods for using CSS in JS…CSS Modules, Radium, Styled-Components, or plain javascript objects. In this project I will be using Aphrodite.

I’m jumping ahead and linking to this git diff to show how I’m setting up global styles in this project. I downloaded the most recent versions of bootstrap and font-awesome, created an index.css file for some basic styles, and imported them all in our app entry point.

We need to create two new routes, one for the /login page and one for /signup. Let’s start by adding these routes to the main App container.

sling/web/src/containers/App/index.js

Our Login and Signup containers are both very similar. They are responsible for some basic layout, and passing data from a child form to an action.

sling/web/src/containers/Signup/index.js
sling/web/src/containers/Login/index.js

You’ll notice we’re importing a Navbar component, which just makes our page look a little nicer.

sling/web/src/components/Navbar/index.js

Another quick note on react-router: In React projects, I’ve always used react-router-redux. It allows you to redirect users during an action with dispatch(push(/login)). Right now, it’s not yet compatible with react-router v4, so to dispatch redirects, we have to pass around the value of this.context.router through to our actions.

Similar to the Signup and Login containers, the SignupForm and LoginForm are both pretty similar.

sling/web/src/components/SignupForm/index.js
sling/web/src/components/LoginForm/index.js

Both of these forms are using redux-form, which is how we’re getting data from each input. By wrapping the handleSubmit action in this.props.handleSubmit, a special prop supplied by redux-form, we get the data from each Field based on it’s name. The submitting prop is also a value from redux-form that evaluates to true if you pass an onSubmit prop to your form that returns a Promise.

With each Field component, you can can use component=”input” to get a basic text input, but here we’re using a custom Input component to help display errors.

sling/web/src/components/Input/index.js

Both Signup and Login containers are importing actions from a session.js file, so let’s create that next.

sling/web/src/actions/session.js

To make http requests easier to work with in redux actions, I usually create wrapper functions around the Fetch API and store them in an API utility file that you can carry around between projects. Here’s the implementation of that file:

Using these helper methods, you can just call api.post(/url, data) in a redux action and receive the json success or error response. Also, each request will include the jwt used for authentication in an Authorization: Bearer header if it is present in localStorage.

create-react-app, which we used to scaffold the project, ships with dotenv support. It let’s you create a .env file the project root, any variables that start with REACT_APP_ will be picked up and available on process.env.REACT_APP_*.

sling/web/.env

When a user successfully signs up or logs in, we dispatch an ‘AUTHENTICATION_SUCCESS’ action type. We need to create a reducer to listen for this action so we can save the user data in redux state.

sling/web/src/reducers/session.js

We also need to wire up this session reducer to the main reducer:

sling/web/src/reducers/index.js

The session reducer will respond to AUTHENTICATION_SUCCESS and LOGOUT actions, changing the value of isAuthenticated and currentUser. Let’s connect to that state in the Home container so we can see if a user is logged in.

sling/web/src/containers/Home/index.js

Our Home container is now connect to redux, and we will show the current user’s username if isAuthenticated is true. We also added some links to our signup and login pages. However, at this point if you try to sign up, you will probably get an error like “No ‘Access-Control-Allow-Origin’ header is present on the requested resource.” This is because we do not have Phoenix set up to accept http requests from different domains.

To fix this, we can install cors_plug. To do this, add {:cors_plug, “~> 1.1”} to your list of dependencies, and run mix deps.get. Then you will also have to add plug CORSPlug to sling/api/sling/endpoint.ex right about plug Sling.Router. Then restart your Phoenix server.

At this point, our Login and Signup forms are fully functioning. And if you login, you will see the current user’s name on the home page with a button that logs you out.

However, we have some problems. If you refresh the page, you will be logged out. Also, unauthenticated users can access the home page, and authenticated users can access the login and signup pages. Let’s fix that.

Here’s a git commit with what we’ve accomplished so far

Persisting User Sessions

We previously created an api endpoint at /sessions/refresh, so let’s create an action that will POST there as soon as any page is loaded. It makes sense to place this in the App container because that is our root component.

sling/web/src/containers/App/index.js

Our component lifecycle method componentDidMount checks for a token stored in localStorage, if present it will dispatch an authenticate function. Let’s add that function to the bottom of our session.js actions file.

sling/web/src/actions/session.js

Here our api.post(‘/sessions/refresh’) function is not sending along any data, but the wrapper function in sling/web/src/api/index.js will include the token in the Authorization: Bearer header, which is where Guardian will look for the token and authenticate it. If that fails for some reason, we catch the error, remove the token from localStorage so we do not try authenticating again with the invalid token, and redirect the user to the login page.

Now you should be able to login, refresh the page, and still be logged in.

Git commit

Protecting Routes

For our app, we want the home page to only be visible to logged in users, and the login/signup pages to only be visible to unauthenticated users.

In previous React projects, I’ve accomplished this by wrapping components I want protected in Higher Order Components which are connected to the redux store. Those components would have access to a value like isAuthenticated, and redirect users if that value is false.

React-router v4 provides us some new functionality where you can render a <Redirect /> component. So instead of just using a <Match /> component for our routes, we can pass some props to a Stateless component and conditionally render a <Match /> or a <Redirect />. Check out the official docs.

Here is my implementation of a <MatchAuthenticated /> and <RedirectAuthenticated /> component.

sling/web/src/components/MatchAuthenticated/index.js
sling/web/src/components/RedirectAuthenticated/index.js

In the process of building these components, I found I needed to pass something like a willAuthenticate prop and return null if true. This is because in the moment after the page is loaded but the react app has not authenticated with the API, this component would redirect the user because isAuthenticated is currently false.

Now we can use these components instead of <Match /> when we define the routes in the App container.

sling/web/src/containers/App/index.js

Here we’ve replace Match with our new components, and pass in the necessary authentication props. I know it looks silly to destructure isAuthenticated and willAuthenticate from this.props, only to recollect them as authProps, but I feel like it helps being explicit with what props we have to pass to those authentication components.

Notice we’ve also added an unauthenticate() function. This will help us change the value of willAuthenticate when authentication fails. Let’s take a look at the new implementation of these actions.

sling/web/src/actions/session.js

In our authentication flow, we begin by dispatching ‘AUTHENTICATION_REQUEST’, the setCurrentUser function dispatches an ‘AUTHENTICATION_SUCCESS’ action type on success, and unauthenticate dispatches ‘AUTHENTICATION_FAILURE’. Now we can update state accordingly in the session reducer.

sling/web/src/reducers/session.js

Once again, all of our code is in working order. You should now be able to login/logout and only access the appropriate routes.

Git commit with all of our authentication working.

That’s it for part 3! In part 4, we will begin diving into the core of our application and allow users to create chat rooms.

Read part 4 or view the live demo