Part 1 — Create React App & Rails API Authentication with JWT Tokens and Redux
Git Repo: https://github.com/christinetran825/SelfCare-C
Git Repo: https://github.com/christinetran825/YourSelfCare-API
Boy, this was a huge issue for me. I had a hard time understanding JWT Tokens. A lot of resources had different takes on how to authenticate and authorize a user. Some used services like Auth0, but I wanted to try sticking to more of an internal setup with fetch
. I’m not sure if this is the ‘correct’ way of doing things, but I made it work. Let’s start by defining Authentication and Authorization.
- Identification: Who you claim to be (email, name, etc)
- Authentication: The process of verifying you are who you claim to be and assigning your associated role based on your identity
- Access Policy: Actions the assigned roles are allowed to perform
- Authorization: Access privileges granted to a user based on their Access Policy
Here’s how it works.
- Go to a website & submit your email, username, and/or password. These are your credentials
- The website’s servers verifies (authenticates) if your credentials (identity) are correct.
- If credentials are correct, the server will issue a cookie to your browser. A common use of cookies is for login. Cookies can also be used to store data in a user’s browsers. That cookie is stored into the browser like an ID badge (Access Policy). This badge allows you to visit other areas of the website without any issues depending on your access privileges (authorization).
- If your credentials aren’t correct, you’ll get an alert message notifying you that your credentials are incorrect.
Understanding this process we’ll start this post on Identification and Authentication.
Rails API Authentication Set Up
- Install Gems:
gem 'knock'
&gem 'jwt'
- Run
bundle install
- I moved the
user_token_controller.rb
to my/controllers/api
directory. I ensured the code reads the Api directory
class Api::UserTokenController < Knock::AuthTokenController
end
- In my
users_controller.rb
, I added another method calledfind
to allow the server to find a user’s by their email after submitting their password.
def find
@user = User.find_by(email: params[:user][:email])
if @user
render json: @user
else
@errors = @user.errors.full_messages
render json: @errors
end
end
- In my
routes.rb
, I movedpost ‘user_token’ => ‘user_token#create’
to my namespace of:api
. I also added a POST to myfind
method of myusers_controller
. Here’s the code:
namespace :api do
resources :users, :medications, :insurances
post 'user_token' => 'user_token#create'
post 'find_user' => 'users#find'
end
React API Authentication Set Up — ACTIONS
Per the Redux doc — Actions are payloads of information that send data from your client to your store. They are the only source of information for the store. You send them to the store using store.dispatch()
.
Per the Redux doc — The Store is the object that brings together actions and reducers. The store has the following responsibilities:
- Holds application state;
- Allows access to state via
getState()
; - Allows state to be updated via
dispatch(action);
- Registers listeners via
subscribe(listener)
; - Handles unregistering of listeners via the function returned by
subscribe(listener)
.
Essentially, Actions pass data from your client to the server through the store. So what are our Actions? Let’s think about the actions a user will take when signing up and logging in through a client. We should also keep in mind of the server’s state, too (this will help us with the Reducers).
Sign Up
- User creates an account by entering their credentials and clicks submit.
- The client (web browser) communicates with the server (Rails API) by sending those credentials. Here the client is Requesting Authentication (Action) and the server is Authenticating (state) a user to determine if their credentials meet the validation rules (like meeting the character limit for a password).
- If the credentials don’t meet the validation rules, the client is sent an Authentication Failure (Action) and the server has an error (state) so the user can correct their credentials.
- If the server determines the validation is correct, the client is sent an Authentication Success (Action) and the server has Authenticated (state) the user. The user is then given a token in the server. That token is another form of identification. Once the token is provided, the user is now the current user and is redirected to the next webpage according to the Rails API Routes.
Log In
- User enters their credentials and clicks submit.
- The client (web browser) communicates with the server (Rails API) by sending those credentials to be verified. Here the client is Requesting Authentication (Action) and the server is Authenticating the user by finding them in the User database based on their email or whatever params the User Controller in the API defined.
- If the credentials are not correct, the client is sent an Authentication Failure (Action) and the server has an error (state) so the user can correct their credentials.
- If the credentials are correct, the token is sent to the client notifying it that there was an Authentication Success (Action) and the server has Authenticated the user. The user’s token allows the user to be the current user and they are redirected to the next webpage according to the Rails API Routes.
Based on our understanding of how a user signs up and logs in through a client (web browser), the user experiences the following action types:
- Authentication Request
- Authentication Success
- Authentication Failure
I defined these action types in my actionTypes.js
file that will be imported and referenced in the authActions.js
file with a types.
before the type name.
//Authentication Typesexport const AUTHENTICATION_REQUEST = "AUTHENTICATION_REQUEST"
export const AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS"
export const AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE"
export const LOGOUT = "LOGOUT"
Each of these actions were also passed or received either credentials, a token
, or errors
. These things are our objects. The credentials when correct helps us find our user
. We defined these action types with a constant function.
import fetch from 'isomorphic-fetch';
import { API_URL } from './apiUrl'
import * as types from './actionTypes'//this function has an authentication request action typeconst authRequest = () => {
return {
type: types.AUTHENTICATION_REQUEST
}
}
//this function has an authentication success action type. When there's a success in correct credentials, the server passes a user and token.const authSuccess = (user, token) => {
return {
type: types.AUTHENTICATION_SUCCESS,
user: user,
token: token
}
}//this function has an authentication failure action type. When there are incorrect credentials, the server passes errors.const authFailure = (errors) => {
return {
type: types.AUTHENTICATION_FAILURE,
errors: errors
}
}
These action types can now be referenced into our actions that will be called in our handleSubmit
of our Sign Up and Log In forms along with our Log Out button. So let’s define these actions.
Fetch
Note: I highly recommend reading through the Fetch MDN to understand how it works — https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
If Actions are payloads of information that send data from your client to your store, then where do our data come from? It comes from our Rails API database. So each Action must fetch
data from our Rails API database routes.
Fetch sets a parameter to the path to the resource you want to fetch with, in this case it ishttps://localhost:3001/api
with the routes append to the end. The fetch then returns a promise containing the response (an HTTP Response
object). We want our data to be converted into JSON, so to get the JSON body content from the response, we use the json()
method. Fetch can also optionally set a second parameter, which is an init
object that allows us to POST JSON-encoded data.
To make the code DRY, I added a .env
file in my root directory with a constant leading to the resource.
REACT_APP_API_URL=http://localhost:3001/api
In my src/actions
directory I add the file apiURL.js
the will process the .env
file. It can now be referenced in all my Actions through the const API_URL
.
export const API_URL = process.env.REACT_APP_API_URL;
Defining the Actions — Signing Up a user
signup
action:
- the client passes a
user
object to the server. We define this object in aconst newUser
- we fetch the
/users
resource and pass in ourinit
object - We define the
init
object with a method of"POST"
since we’re creating a user. The headers denote that we are acceptingjson
data and the content-type should bejson
data. Next we define our body as aJSON
object by passing in ouruser
object. - THEN, we’ll receive a response and we convert that response into
json
- THEN, we take that converted
json
response that now includes ouruser
object and dispatch it to ourauthenticate
action. We will authenticate that user by defining their name, email, and password to the params. - IF the authentication in the
authenticate
action can’t be completed,errors
object will be returned and dispatched to theauthFailure
action type.
export const signup = (user) => {
const newUser = user
return dispatch => {
return fetch(`${API_URL}/users`, {
method: "POST",
headers: {
"Accept":"application/json",
"Content-Type":"application/json"
},
body: JSON.stringify({user: user})
})
.then(response => response.json())
.then(jresp => {
dispatch(authenticate({
name: newUser.name,
email: newUser.email,
password: newUser.password})
);
})
.catch((errors) => {
dispatch(authFailure(errors))
})
};
}
authenticate
action:
- the client passes the user’s credentials and dispatches the action type of
authRequest()
as we’re making a request to authenticate the credentials - at this point, we are defining or creating a user’s
token
. we’llfetch
our/user_token
resource and pass in ourinit
object - We define the
init
object with a method of"POST"
since we’re creating a token. The headers denote that the content-type should bejson
data. Next we define our body as aJSON
object by passing in the user’s credentials to be authenticated - THEN, we’ll receive a response and we convert that response into
json
- THEN, we take that converted
json
response and convert it as ajwt
token. We’ll define this token intoconst token
. Thetoken
object will now be stored or.setItem
into thelocalStorage
. The credentials that the response held onto will be passed into ourgetUser
action. The action will find the user associated to the token that was stored. - IF the user in the
getUser
action can be found, theuser
object that was carried over from thesignup
action will be passed to theauthSuccess
action type along with the newly stored tokenlocalStorage.token
. - IF the user in the
getUser
action can’t be found,errors
object will be returned and dispatched to theauthFailure
action type. ThelocalStorage
will clear itself since no tokens were passed.
export const authenticate = (credentials) => {
return dispatch => {
dispatch(authRequest())
return fetch(`${API_URL}/user_token`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({auth: credentials})
})
.then(res => res.json())
.then((response) => {
const token = response.jwt;
localStorage.setItem('token', token);
return getUser(credentials)
})
.then((user) => {
console.log(user)
dispatch(authSuccess(user, localStorage.token))
})
.catch((errors) => {
dispatch(authFailure(errors))
localStorage.clear()
})
}
}
getUser
action:
- the credentials were passed from
authenticate
action - we define a
const request
to point to anew Request
to our resource of/find_user
and pass in ourinit
object - we define the
init
object with a method of"POST"
since we’re creating a user. The headers denote that the content-type should bejson
data along with an authorization where we’ll associate thetoken
object to aBearer
. Next we define our body as aJSON
object by passing in the user’s credentials - now, we make a
fetch
to the definedconst request
- THEN, we’ll receive a response and we convert that response into
json
- THEN, we take that converted
json
response and return it as ouruser
object. Thisuser
object is passed back toauthenticate
action and is dispatched to theauthSuccess
action type passing in theuser
object and itstoken
. - IF the user can’t be found, then an
error
object is returned.
export const getUser = (credentials) => {
const request = new Request(`${API_URL}/find_user`, {
method: "POST",
headers: new Headers({
"Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.token}`,
}),
body: JSON.stringify({user: credentials})
})
return fetch(request)
.then(response => response.json())
.then(userJson => {return userJson})
.catch(error => {
return error;
});
}
WHOA… That’s a lot to define when signing up a user. What happens when we log in a user? Luckily, we already defined all the actions needed for it.
Defining the Actions — Logging In a user
Since a user that wants to log in through a client, they’ve already have a token associated with their credentials. All that needs to be done is have the client pass their credentials into the authenticate
action. The cycle continues as before. When the credentials get passed into the getUser
action, the request
will go to the resource /find_user
to find the user based on their credentials and pass their existing token
back to the authenticate
action where it and the user
will be dispatched to authSuccess
action type.
Defining the Actions — Logging Out a user
We simply dispatch a call to clear the localStorage
of the token that was passed to client to the LOGOUT
action type.
export const logout = () => {
return dispatch => {
localStorage.clear();
return dispatch({
type: types.LOGOUT
});
}
}
Referencing the actions in our components
SignUp component
handleSubmit = (e) => {
e.preventDefault();
if (this.props.signup(this.state)) {
this.props.history.push('/user_profile')
window.alert("Thank you for signing up.")
} else {
window.alert("We're having issues creating your account.")
}
}
LogIn component
handleSubmit = (e) => {
e.preventDefault();
if (this.props.authenticate(this.state)) {
this.props.history.push('/user_profile')
window.alert("You're Logged In!")
} else {
window.alert("Sorry, something went wrong. Please try logging in again.")
}
}
Navigation component
handleLogout = (e) => {
e.preventDefault();
this.props.logout();
this.props.history.push('/')
}
WHOA….. that was A LOT to take in. I hope you have a better understanding about the Rails API setup and the React ACTIONS setup. Take a moment to digest what you’ve learned and when you’re ready, we’ll continue with Reducers, Access Policy, and Authorization in Part 2.
Resources: