Since I clearly cannot get enough of JWT authentication, here's a look at how to use it to authenticate your shiny new Phoenix API with a React + Redux front-end application, using React Router 4.
In this post, we'll cover:
- Using React Router 4 to set up both regular and authenticated routes.
- Using React Router's
routerMiddleware
to teach our store how to handle actions provided to us by React router. - Building a simple Phoenix API endpoint for authentication with the help of Comeonin and Guardian.
- Using React to establish a connection to a Phoenix websocket and channel.
- Using a Guardian Plug to authenticate incoming API requests from React using the JWT.
Configuring The Routes
First things first, we'll configure our routes and append that configuration to the DOM to render our component tree.
For the purposes of this article, let's say that we're building a chatting application in which users can visit an index of chatrooms, /chats
, and enter a chatroom, chats/:id
, to start chatting
# web/static/js/routes/index.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom'
import App from '../containers/app';
import Navigation from '../views/shared/nav';
import RegistrationsNew from '../views/registrations/new';
import SessionsNew from '../views/sessions/new';
import Chats from '../views/chats';
import Actions from '../actions/sessions';
export default function configRoutes() {
return (
<div>
<Navigation />
<Route exact path="/" component={App} />
<Route path="/sign_up" component={RegistrationsNew} />
<Route path="/sign_in" component={SessionsNew} />
<AuthenticatedRoute path="/chats" component={Chats} />
</div>
);
}
const AuthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
localStorage.getItem('phoenixAuthToken') ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/sign_in',
state: { from: props.location }
}}/>
)
)}/>
)
If you're familiar with earlier versions of React Router, much of this code probably looks familiar.
We've defined a function configRoutes
, that uses React Router DOM's Route
component to define a set of routes. We map each path to a component to render, and we import our components at the top of the file.
We've defined the following routes:
-
/
, the root path, which points to our container component,App
. -
/sign_up
, which points to the component that houses our registration form. -
/sign_in
, pointing to the component that houses our sign in form. -
/chats
, pointing to the chat index component. This route is our protected, or authenticated route.
Let's take a closer look at that authenticated route now.
Defining an Authenticated Route
Our authenticated route is really just a functional component. It is invoked with props
that include a key of component
, set to the Chats
component that we passed in.
Our functional component returns a Route
component. The render()
function of this Route
component is responsible for rendering the Chats
component from props, or redirecting.
Let's take a closer look at this render()
function:
props => (
localStorage.getItem('phoenixAuthToken') ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/sign_in',
state: { from: props.location }
}}/>
)
)
Our function determines whether or not we have an authenticated user based on the presence or absence of the phoenixAuthToken
key in localStorage
. Later, we'll build out the functionality of storing the JWT we receive from Phoenix in localStorage
.
If a token is present, we'll go ahead and call the component that was passed into our Route
as a prop, the Chats
component.
If no token is found, we'll use the Redirect
component from React Router DOM to enact a redirect.
And that's it! Now, we'll take our route configuration and append it to the DOM with ReactDOM, thereby appending our component tree to the DOM.
Configuring The Store and Router Component
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux'
import { Provider} from 'react-redux'
import thunk from 'redux-thunk'
import createHistory from 'history/createBrowserHistory'
import {
ConnectedRouter as Router,
routerMiddleware
} from 'react-router-redux'
import {
Route,
Link
} from 'react-router-dom'
import configRoutes from './routes'
import rootReducer from './reducers'
const history = createHistory()
const rMiddleware = routerMiddleware(history)
const store = createStore(
rootReducer,
applyMiddleware(thunk, rMiddleware)
)
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<div>
{configRoutes()}
</div>
</Router>
</Provider>,
document.getElementById('main_container')
);
There are a few things to point out here.
First, we're using React Router's routerMiddleware
. React Router gives us access to a set of action creator functions with which to manipulate browser history:
push(location)
replace(location)
go(number)
goBack()
goForward()
We'll use push
later to redirect after we sign in a user.
Out of the box, however, the Redux store doesn't know how to handle the dispatch of these actions. That's where the routerMiddleware
comes in. We create an instance of our routerMiddleware
by invoking the routerMiddleware
function with an argument of our browser history instance.
Then, we pass this middleware instance to our store via the applyMiddlware
function. Now, when we dispatch any of the actions listed above, the store will handle them by applying them to our browser history.
It's important to note that we still need to pass our browser history instance to our Router
. This will make sure that our routes sync up with the browser history's location and the store at the same time.
Now that we have our routes set up, let's build the authorization flow.
The Sign In Component
Our sign in form will live in our sessions/new.js
component. Let's build it out:
# /views/sessions/new.js
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import Actions from '../../actions/sessions';
class SessionsNew extends React.Component {
handleSubmit(e) {
e.preventDefault();
const { dispatch } = this.props;
const data = {
email: this.refs.email.value,
password: this.refs.password.value
};
dispatch(Actions.signIn(data));
}
render() {
const { errors } = this.props;
return (
<div className="container">
<div className="container">
<form
className="form-horizontal"
onSubmit={::this.handleSubmit}>
<fieldset>
<legend>Sign In</legend>
<div className="form-group">
<label className="col-lg-2">email</label>
<div className="col-lg-10">
<input
className="form-control"
ref="email"
id="user_email"
type="text"
placeholder="email" required={true} />
</div>
</div>
<div className="form-group">
<label className="col-lg-2">password</label>
<div className="col-lg-10">
<input
className="form-control"
ref="password"
id="user_password"
type="password"
placeholder="password" required={true} />
</div>
</div>
<br/>
<button type="submit">Sign in</button>
</fieldset>
</form>
<Link to="/sign_up">Sign up</Link>
</div>
</div>
);
}
}
export default connect()(SessionsNew)
Our form is pretty simple, it has a field for the user's email and a field for the user's password. On the submission of the form, we dispatch an action that will send a POST
request to the sign in route of our Phoenix API.
Let's build out that action now.
The Sign In Action
# /actions/sessions.js
import { push } from 'react-router-redux';
import Constants from '../constants';
import { Socket } from 'phoenix';
import { httpPost } from '../utils';
const Actions = {
signIn: (creds) => {
return dispatch => {
const data = {
session: creds,
};
httpPost('/api/v1/sessions', data)
.then((response) => {
localStorage.setItem('phoenixAuthToken',
response.jwt);
setCurrentUser(dispatch, response.user);
dispatch(push('/challenges'));
})
.catch((error) => {
error.response.json()
.then((errorJSON) => {
dispatch({
type: Constants.SESSIONS_ERROR,
error: errorJSON.error,
});
});
});
};
}
}
export default Actions
Here, we define our Actions
constant to implement a function, signIn()
. We also use this same file to define a helper function, setCurrentUser()
.
The signIn()
function relies on a tool we defined in another file, httpPost()
, to make our POST
request to the sign in endpoint of our Phoenix API.
The httpPost()
function relies on Fetch to make web requests:
# web/utils/index.js
import fetch from 'isomorphic-fetch';
import { polyfill } from 'es6-promise';
const defaultHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
function headers() {
const jwt = localStorage.getItem('phoenixAuthToken');
return { ...defaultHeaders, Authorization: jwt };
}
export function checkStatus(response) {
if (response.ok) {
return response;
} else {
var error = new Error(response.statusText);
error.response = response;
throw error;
}
}
export function parseJSON(response) {
return response.json();
}
export function httpPost(url, data) {
const body = JSON.stringify(data);
return fetch(url, {
method: 'post',
headers: headers(),
body: body,
}).then(checkStatus)
.then(parseJSON);
}
Note: This file will grow to include all of our HTTP requests to our API, and rely on the headers()
function to build authentication headers using the token we will store in localStorage
once we authenticate our user.
So, we use the httpPost
function to make our authentication request to the API, and if that request is a success, we grab the jwt
key from the response body and store it in localStorage
. We'll actually build out this endpoint soon, but for now we will assume that it exists and returns a successful response body of:
{
jwt: <some token>,
user: <serialized user>
}
Let's take a closer look at the code in our signIn()
function that is responsible for this action:
localStorage.setItem('phoenixAuthToken', response.jwt);
setCurrentUser(dispatch, response.user);
dispatch(push('/challenges'));
After we set the phoenixAuthToken
in localStorage
, we invoke our helper function, setCurrentUser
, and use the dispatch
function to invoke a route change. This route change is enacted with the help of the push
action creator function from React Router Redux. (Remember when we used the routerMiddleware
to enable our store to handle the push
action?)
We're almost ready to take a closer look at the setCurrentUser()
function. But first, let's build out the authentication endpoint of our Phoenix API.
The Sign In API Endpoint
Phoenix Authorization Dependencies
In order to authenticate users, we'll use the Comeonin library. In order to generate a JWT token for our user, we'll rely on the Guardian library.
Let's add these dependencies to our mix.exs
file and make sure to start up the Comeonin application when our app starts.
# mix.exs
...
def application do
[
mod: {PhoenixPair, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, :phoenix_ecto, :postgrex, :comeonin]
]
end
...
defp deps do
[{:phoenix, "~> 1.2.1"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.6"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:comeonin, "~> 2.0"},
{:guardian, "~> 0.9.0"}]
end
Defining the Route
We'll scope our API endpoints under /api/v1
, and define our sign in route like this:
# /web/router.ex
scope "/api", PhoenixPair do
pipe_through :api
scope "/v1" do
post "/sessions", SessionsController, :create
end
end
Defining the Controller
The SessionsController
will implement a create function, that contains the code for authorizing the user.
# web/controllers/api/v1/sessions_controller.ex
defmodule PhoenixPair.SessionsController do
use PhoenixPair.Web, :controller
alias PhoenixPair.{Repo, User}
plug :scrub_params, "session" when action in [:create]
def create(conn, %{"session" => session_params}) do
case PhoenixPair.Session.authenticate(session_params) do
{:ok, user} ->
{:ok, jwt, _full_claims} = user
|> Guardian.encode_and_sign(:token)
conn
|> put_status(:created)
|> render("show.json", jwt: jwt, user: user)
:error ->
conn
|> put_status(:unprocessable_entity)
|> render("error.json")
end
end
def unauthenticated(conn, _params) do
conn
|> put_status(:forbidden)
|> render(PhoenixPair.SessionsView, "forbidden.json",
error: "Not Authenticated!")
end
end
Authenticating the User
Our create
function relies on a helper module, PhoenixPair.Session
to authenticate the user given the email and password present in params.
# web/services/session.ex
defmodule PhoenixPair.Session do
alias PhoenixPair.{Repo, User}
def authenticate(%{"email" => e, "password" => p}) do
case Repo.get_by(User, email: e) do
nil ->
:error
user ->
case verify_password(p, user.encrypted_password) do
true ->
{:ok, user}
_ ->
:error
end
end
end
defp verify_password(password, pw_hash) do
Comeonin.Bcrypt.checkpw(password, pw_hash)
end
end
This module implements a function, authenticate/1
, which expects to be invoked with an argument of a map that pattern matches to a map with keys of "email"
and "password"
.
It uses the email to look up the user via:
Repo.get_by(User, email: email)
If no user is found our case statement with execute the nil ->
clause and return the atom :error
.
If a user is found, we'll call our verify_password
helper function. This function uses Comeonin.Bcrypt.checkpw
to validate the password. If this validation is successful, we will return the tuple {:ok, user}
, where user
is the User struct returned by our Repo.get_by
query.
Generating a JWT
Back in our controller, if the call to .Session.authenticate
returns the success tuple, {:ok, user}
, we'll use Guardian to generate a JWT.
...
{:ok, jwt, _full_claims} = user
|> Guardian.encode_and_sign(:token)
conn
|> put_status(:created)
|> render("show.json", jwt: jwt, user: user)
If our call to Guardian.encode_and_sign(user, :token)
was successful, we'll use our Session View to render the following JSON payload:
{jwt: jwt, user: user}
# web/views/sessions_view.ex
defmodule PhoenixPair.SessionsView do
use PhoenixPair.Web, :view
def render("show.json", %{jwt: jwt, user: user}) do
%{
jwt: jwt,
user: user
}
end
def render("error.json", _) do
%{error: "Invalid email or password"}
end
def render("forbidden.json", %{error: error}) do
%{error: error}
end
end
If the call to .Session.authenticate
was not successful, or if our attempt to use Guardian to generate a token was not successful, we will render an error instead.
Now that our endpoint is up and running, let's return to our React app and discuss how we will set the current user with a successful payload.
Setting the Current User
What does it mean to set the current user in a React and Phoenix app? We want to leverage the power of Phoenix channels to build real-time communication features for our user. So, when we "set the current user", we will need to establish a socket connection for that user, and connect that user to their very own Phoenix channel.
On the React side, we will store the current user's information in state, under the session
key, under a key of currentUser
:
# state
{
session:
currentUser: {
name: <a href="http://beatscodeandlife.ghost.io/">"Antoin Campbell"</a>,
email: "antoin5@5antoins.com"
},
...
...
}
So, our setCurrentUser()
function, called in our signIn()
action, should handle both of these responsibilities.
Establishing the Current User's Socket Connection
We'll import Socket
from Phoenix, and use the Socket API to establish our user's socket connection.
import { Socket } from 'phoenix';
export function setCurrentUser(dispatch, user) {
const socket = new Socket('/socket', {
params: {token: localStorage.getItem('phxAuthToken') },
logger: (kind, msg, data) => { console.log(`${kind}:
${msg}`, data); },
});
socket.connect();
const channel = socket.channel(`users:${user.id}`);
if (channel.state != 'joined') {
channel.join().receive('ok', () => {
dispatch({
type: Constants.SOCKET_CONNECTED,
currentUser: user,
socket: socket,
channel: channel,
});
});
}
};
Let's break this down.
- First, we instantiate a new instance of
Socket
via:
const socket = new Socket('/socket', {
params: {token: localStorage.getItem('phxAuthToken')},
logger: (kind, msg, data) => { console.log(`${kind}:
${msg}`, data);
}
Then, we invoke the connect
function on that instance:
socket.connect()
This has the effect of invoking the connect
function of our UserSocket
, with params of %{"token" => token}
. We'll need to define that socket to implement the connect
function:
web/channels/user_socket.ex
defmodule PhoenixPair.UserSocket do
use Phoenix.Socket
alias PhoenixPair.{Repo, User, GuardianSerializer, Session}
## Channels
channel "users:*", PhoenixPair.UsersChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
transport :longpoll, Phoenix.Transports.LongPoll
def connect(%{"token" => token}, socket) do
case Guardian.decode_and_verify(token) do
{:ok, claims} ->
case GuardianSerializer.from_token(claims["sub"]) do
{:ok, user} ->
{:ok, assign(socket, :current_user, user)}
{:error, _reason} ->
:error
end
{:error, _reason} ->
:error
end
end
def connect(_params, socket), do: :error
def id(socket) do
"users_socket:{socket.assigns.current_user.id}"
end
end
Our connect
function uses Guardian to decode the JWT from params. If the decode was successful, we'll use Guardian again to pluck out the User struct from the deserialized token payload. Then, we'll assign that struct to the key of :current_user
within our socket's storage system. This socket is shared by all additional channels we might open for this user. So, any future channels we build on this socket can access the current user via socket.assigns.current_user
.
Our UserSocket
also implements a connect
function that does not match the pattern of expected params. This function will simply return :error
.
def connect(_params, socket), do: :error
Lastly, we define an id
function, which returns the designation of this socket, named with the ID of our user:
def id(socket) do:
"users_socket:#{socket.assigns.current_user.id}"
end
The socket id will allow us to identify all sockets for a given user, and therefore broadcast events through a specific user's socket. For example:
PhoenixPair.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
Now that our User Socket knows how to handle the calls to connect, let's go back to our React app's setCurrentUser()
function and connect to the UsersChannel
.
Connecting to the Users Channel
We'll define our UsersChannel
to respond to a join
function, and return the socket connection if the join was successful.
# web/channels/users_channel.ex
defmodule PhoenixPair.UsersChannel do
use PhoenixPair.Web, :channel
def join("users:" <> user_id, _params, socket) do
{:ok, socket}
end
end
Then, we'll have our setCurrentUser
function in React send a message to join this channel:
export function setCurrentUser(dispatch, user) {
...
const channel = socket.channel(`users:${user.id}`);
if (channel.state != 'joined') {
channel.join().receive('ok', () => {
dispatch({
type: Constants.SOCKET_CONNECTED,
currentUser: user,
socket: socket,
channel: channel
});
});
}
}
We get our channel instance via
socket.channel(
users:${user.id})
. Then, we join the channel by calling channel.join()
. This fires the join
function we defined in our UsersChannel
.
On to that function invocation, we chain a call to receive
. The receive
function which will be invoked when we get the "ok" response from our channel.
Once the channel has been successfully joined, we're ready to dispatch an action to our reducer to update state with our current user, as well as the socket and channel. We want to store these last two items in our React application's state so that we can use them to enact channel communications later on as we build out our chatting app.
Making Authenticated API Requests
Now that we're properly storing our current user in our React app's state, and our current user's JWT in localStorage
, let's take a look at how we will make subsequent authenticated requests to our Phoenix API.
We've already defined a set of helper functions in web/static/js/utils/index.js
that use Fetch to make API requests. These functions rely on a helper method, headers
, to set the authorization header using the token from localStorage
:
import React from 'react';
import fetch from 'isomorphic-fetch';
import { polyfill } from 'es6-promise';
const defaultHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
function headers() {
const jwt = localStorage.getItem('phoenixAuthToken');
return { ...defaultHeaders, Authorization: jwt };
}
export function checkStatus(response) {
if (response.ok) {
return response;
} else {
var error = new Error(response.statusText);
error.response = response;
throw error;
}
}
export function parseJSON(response) {
return response.json();
}
export function httpGet(url) {
return fetch(url, {
headers: headers(),
})
.then(checkStatus)
.then(parseJSON);
}
export function httpPost(url, data) {
const body = JSON.stringify(data);
return fetch(url, {
method: 'post',
headers: headers(),
body: body,
})
.then(checkStatus)
.then(parseJSON);
}
...
So, all of the requests we make to our Phoenix API using the functions we've defined here, httpPost
, httpGet
, etc., will include the JWT in the authorization header.
Now we have to teach our Phoenix controllers to authorize incoming requests using this header. Luckily, Guardian does a lot of this work for us.
Let's take a look at our ChatsController
.
defmodule PhoenixPair.ChatsController do
use PhoenixPair.Web, :controller
plug Guardian.Plug.EnsureAuthenticated, handler: PhoenixPair.SessionsController
alias PhoenixPair.{Repo, User, Challenge}
def index(conn, _params) do
challenges = Repo.all(Chat)
render(conn, "index.json", chats: chats)
end
end
This is the line that has all the authorization magic:
plug Guardian.Plug.EnsureAuthenticated, handler: PhoenixPair.SessionsController
This plug checks for a valid JWT in the authorization header.
If one isn't found, it invokes the unauthenticated
function in the handler module. In our case, this is the PhoenixPair.SessionsController.unauthenticated
function that we defined earlier.
We can add this plug to any and all authenticated controllers as we build out our app.
Conclusion
So far, I've found that React and Phoenix play really well together. I definitely approached this authentication feature with a little trepidation, not having worked with React Router 4 before or done any token-based auth in Phoenix.
However, integrating JWT authentication between our React front-end and our Phoenix API back-end was pretty seamless thanks to the tools provided by React Router and Guardian.
Happy coding!
Top comments (1)
Thanks Sophie!
I'm refactoring one of my Flatiron projects to use Routes instead of A LOT of conditional formatting - the authenticated route part was exactly what I needed!