After a bit of research, JWT is commonly used for login authentication because of its compact nature and easiness to parse. I have settled on using JWT. However, my question is on how to embed this in my redux paradigm. Assuming we have a sign up form, when a user fills in his or her credentials and clicks a submit button, this will invoke an action to create an action to create a JWT. Now, this action goes to the back-end of my application and the back-end of my application calls the JWT API? So this action is an asynchronous/rpc call? Also, how does routing happen exactly? I have used react-router before, but using a boilerplate. I am building this web app from scratch and so I am a bit confused on where to deal with the routing and where do I pass this token exactly that I obtain from the server the first time? Is the token used every time a user does a request? How does the client know about this token every time it does the request so that it would keep a user authenticated?
3 Answers
When a user submits his credentials (email/password) your backend authenticates that for the first time and only this time does the backend use these credentials. On authentication your backend will create a JWT with some of the user information, usually just the user ID. There are plenty of JWT Libraries and even jwt-decode for javascript to do this. The backend will respond with this JWT where the front-end will save it (ie, localStorage.setItem('authToken', jwt)) for every subsequent request.
The user will send a request with the JWT in the request header under the Authorization key. Something like:
function buildHeaders() {
const token = localStorage.getItem('authToken')
return {
"Accept": "application/json",
"Content-Type": "application/json"
"Authorization": `${token}`
}
}
Your backend will now decode and authenticate the JWT. If it's a valid JWT the request continues, if not it's rejected.
Now with React-Router you can protect authenticated routes with the onEnter function. The function you provide does any necessary checks (check localStorage for JWT and if a current user). Typically I've done this:
const _ensureAuthenticated = (nextState, replace) => {
const { dispatch } = store
const { session } = store.getState()
const { currentUser } = session
const token = localStorage.getItem("phoenixAuthToken")
if (!currentUser && token) { // if no user but token exist, still verify
dispatch(Actions.currentUser())
} else if (!token) { // if no token at all redirect to sign-in
replace({
pathname: "/sign-in",
state: { nextPathname: nextState.location.pathname}
})
}
}
You can use this function in any route like so:
<Route path="/secret-path" onEnter={_ensureAuthenticated} />
Check out jwt.io for more information on JWT's and the react-router auth-flow example for more information on authentication with react-router.
Comments
I personally use Redux saga for async API calls, and I'll show You the flow I've been using for JWT authorization:
- Dispatch
LOG_INaction with username and password - In your saga You dispatch
LOGGING_IN_PROGRESSaction to show e.x. spinner - Make API call
- Retrieved token save e.x. in localstorage
- Dispatch
LOG_IN_SUCCESSorLOG_IN_FAILEDto inform application what response did You get
Now, I always used a separate function to handle all my requests, which looks like this:
import request from 'axios';
import {get} from './persist'; // function to get something from localstorage
export const GET = 'GET';
export const POST = 'POST';
export const PUT = 'PUT';
export const DELETE = 'DELETE';
const service = (requestType, url, data = {}, config = {}) => {
request.defaults.headers.common.Authorization = get('token') ? `Token ${get('token')}` : '';
switch (requestType) {
case GET: {
return request.get(url, data, config);
}
case POST: {
return request.post(url, data, config);
}
case PUT: {
return request.put(url, data, config);
}
case DELETE: {
return request.delete(url, data, config);
}
default: {
throw new TypeError('No valid request type provided');
}
}
};
export default service;
Thanks to this service, I can easily set request data for every API call from my app (can be setting locale also).
The most interesting part of it should be this line:
request.defaults.headers.common.Authorization = get('token') ? `Token ${get('token')}` : '';`
It sets JWT token on every request or leave the field blank.
If the Token is outdated or is invalid, Your backend API should return a response with 401 status code on any API call. Then, in the saga catch block, you can handle this error any way You want.
Comments
I recently had to implement registration and login with React & Redux as well.
Below are a few of the main snippets that implement the login functionality and setting of the http auth header.
This is my login async action creator function:
function login(username, password) {
return dispatch => {
dispatch(request({ username }));
userService.login(username, password)
.then(
user => {
dispatch(success(user));
history.push('/');
},
error => {
dispatch(failure(error));
dispatch(alertActions.error(error));
}
);
};
function request(user) { return { type: userConstants.LOGIN_REQUEST, user } }
function success(user) { return { type: userConstants.LOGIN_SUCCESS, user } }
function failure(error) { return { type: userConstants.LOGIN_FAILURE, error } }
}
This is the login function of the user service that handles the api call:
function login(username, password) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
};
return fetch('/users/authenticate', requestOptions)
.then(response => {
if (!response.ok) {
return Promise.reject(response.statusText);
}
return response.json();
})
.then(user => {
// login successful if there's a jwt token in the response
if (user && user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
}
return user;
});
}
And this is a helper function used to set the Authorization header for http requests:
export function authHeader() {
// return authorization header with jwt token
let user = JSON.parse(localStorage.getItem('user'));
if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token };
} else {
return {};
}
}
For the full example and working demo you can go to this blog post