NodeJS-React-Redux Tutorial - Part 5: JWT / Authentication / Encryption + Comments

In part 5 of this tutorial we’ll add authentication to our app and build a comments system.

1.8 JWT, Login, Registration

In order to build our Login and Registration we need a way of authenticating the user. To do this we are going to use JWT. JWT (JSON Web Token) is an open standard that defines a way of securely transmitting data between two parties as a JSON object.

With it we can have the server generate an encrypted token for the client, which in further requests the client can use to prove it’s identity.

JWT’s are composed of 3 sections, separated by dots.

(header.payload.signature).

In our application we will use JWT as follows:

  1. Our client sends a login request.
  2. The server checks the details provided, if they are correct it will create a JWT containing the user’s id and username, using a secretKey. This JWT will then be returned back to the client and placed into localstorage.
  3. Each time an API request is made that requires us to authenticate we will pass the JWT in the authorization header. On our server we will apply a middleware authentication check to API routes that we wish to authenticate. This middleware will verify and decode the JWT, extract the username and id and then include this username/id within the request for our API to use in the request.

To begin, lets install jsonwebtoken and bcrypt

npm install jsonwebtoken bcrypt --save

In our .env config file we’ll add our secret key:

...
JWTSECRET = rogueowlseverywhere
...

This will get loaded in with our other env variables (Although I have included the file in the repo it is best not to commit this file)

Next we need to define our user model.

var mongoose = require('mongoose');
var bcrypt = require('bcrypt');

var SALT_WORK_FACTOR = 10;

const UserSchema = new mongoose.Schema({
    username: {
        type: String,
        index: { unique: true }
    },
    password: String,
    created: {
        type: Date,
        required: true,
        default: new Date()
    }  
});


UserSchema.methods.comparePassword = function comparePassword(password, callback) {
     bcrypt.compare(password, this.password, callback);
};

// On save, hash the password
UserSchema.pre('save', function saveHook(next) {
    var user = this;

    if(!user.isModified('password')){
        return next();
    }

    return bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt){
        if(err){ return next(err); }

        return bcrypt.hash(user.password, salt, function(hashError, hash){
            if(hashError){ 
                return next(hashError); 
            }
            user.password = hash;
            return next();
        });
    });
});


module.exports = mongoose.model('User', UserSchema);

Within our UserSchema we defined a method which will compare the password from a login attempt with our users hashed password.

We also created a hook so that when a new user is created, the password they provided is encrypted using bcrypt before being saved.

Next, In server/controller create a new file called AuthController.js

var bcrypt = require('bcrypt');
var jwt = require('jsonwebtoken');

var User = require('../models/User')

module.exports = {

    login: function(username, password, callback){

        User.findOne({ username: username }, function(err, user) {
            if(err){
                callback(err, null);
                return;
            }

            if(!user){
                //User not found
                callback(err, null);
            }else{
                user.comparePassword(password, function(err, isMatch) {
                    if(err){
                        callback(err, null);
                        return
                    }

                    if(isMatch){
                        var authToken = jwt.sign({ username: user.username, _id: user._id}, process.env.JWTSECRET);
                        callback(null, authToken);
                    }else{
                        callback(err, null);
                    }
                });
            };

        });
    },
    register: function(username, password, callback){
        
        var newUser = new User({username,password});

        newUser.save(function(err, user) {
            if(err){
                callback(err, null);
                return;
            }              

            var authToken = jwt.sign({ username: user.username, _id: user._id}, process.env.JWTSECRET);
            callback(null, authToken);
        });
    }
}

This contains our login and register methods. Login looks up the passed in username, if it exists it then calls our password compare method. If the password is correct we create a signed JWT using our secret key and then fire off our callback. Register takes the provided username and password (for the purposes of the tutorial we are not validating ) , creates a user, creates a new JWT token for them and returns it in the callback.

Next we’ll create a new route in /routes/auth.js to handle our login and register and call the auth controller.

var express = require('express')
var router = express.Router()
var authController = require('../controllers/authController')

router.post('/:username', function(req, res, next) {
    authController.login(req.body.username, req.body.password, function(err, result){
        if(err){  
            console.log(err);
            res.status(500).json({
                success: 0,
                error: err
            })
            return;
        }

        if(result){
            res.status(200).json({
                success: 1,
                data: {tokenID: result, username: req.body.username}
            });
        }else{
            res.status(401).json({
                success: 0,
                data: result
            });
        }
    });
});

router.post('/', function(req, res, next) {
    authController.register(req.body.username, req.body.password, function(err, result){
        if(err){  
            console.log(err);
            res.status(500).json({
                success: 0,
                error: err
            })
            return;
        }
        if(result){
            res.status(200).json({
                success: 1,
                data: {tokenID: result, username: req.body.username}
            });
        }else{
            res.status(401).json({
                success: 0,
                data: result
            });
        }
    });

});

module.exports = router

Now we will alter /server/app.js to make all requests to ‘/user’ go to our new auth router

...
var authRoute = require('./routes/auth');
...
app.use('/user', authRoute);
app.use('/news', newsRoute);
...

We now have the API for login and registering, each function returning the token and username back to the browser on success.

Now lets do some alterations to the client to organize things a bit better

First, rename actions/actions.js to newsActions.js.

In News.js , NewsArticle.js, newsItemDetail.js, and NewsSubmit.js change the references to use the renamed newsActions.js.

Next create some new constants in constants/actionTypes.js

export default {
    NEWS_RECEIVED: 'NEWS_RECEIVED',
    NEWSITEM_RECEIVED: 'NEWSITEM_RECEIVED',
    NEWSITEM_LOADING: 'NEWSITEM_LOADING',
    USER_REGISTERED: 'USER_REGISTERED',
    USER_LOGGEDIN: 'USER_LOGGEDIN',
    USER_LOGOUT: 'USER_LOGOUT'
}

We’ll now create a new file actions/authActions.js and add our new authentication functions

import actionTypes from '../constants/actionTypes';

function userLoggedIn(username){
    return {
        type: actionTypes.USER_REGISTERED,
        username: username
    }
}

function userRegistered(username){
    return {
        type: actionTypes.USER_LOGGEDIN,
        username: username
    }
}

function logout(){
    return {
        type: actionTypes.USER_LOGOUT
    }
}

export function submitLogin(data){
    return dispatch => {
        return fetch(`/user/${data.username}`, { 
                method: 'POST', 
                 headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                  },
                body: JSON.stringify(data), 
                mode: 'cors'})
            .then( (response) => {
                if (!response.ok) {
                    throw Error(response.statusText);
                }
                return response.json();
            })
            .then( (data) => {
                localStorage.setItem('username', data.data.username);
                localStorage.setItem('token', data.data.tokenID);

                dispatch(userLoggedIn(data.data.username));
            })        
            .catch( (e) => console.log(e) );
    }    
}

export function submitRegister(data){
    return dispatch => {
        return fetch('/user/', { 
            method: 'POST', 
             headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
              },
            body: JSON.stringify(data), 
            mode: 'cors'})
            .then( (response) => {
                if (!response.ok) {
                    throw Error(response.statusText);
                }
                return response.json();
            })
            .then( (data) => {

                localStorage.setItem('username', data.data.username);
                localStorage.setItem('token', data.data.tokenID);

                dispatch(userLoggedIn(data.data.username));
            })        
            .catch( (e) => console.log(e) );
    }    
}

export function logoutUser() {
    return dispatch => {
        localStorage.removeItem('username');
        localStorage.removeItem('token');
        dispatch(logout());
    }
}

On both login and register, if the response is ok, we take the token and username from the response and place it into localstorage.

We then dispatch a userLoggedIn action

Next, In clients/src/presentation create the following two files:

Login.js

import React, { Component } from 'react';
import { submitLogin } from '../../actions/authActions';
import { connect } from 'react-redux';

class Login extends Component {

    constructor(){
        super();

        this.state = {
            details:{
            }
        };
    }

    updateDetails(event){
        let updateDetails = Object.assign({}, this.state.details);

        updateDetails[event.target.id] = event.target.value;
        this.setState({
            details: updateDetails   
        });
    }

    login(){
        this.props.dispatch(submitLogin(this.state.details));    
    }

    render(){
        return (
            <div>
                <h3>Login</h3>
                Username <input onChange={this.updateDetails.bind(this)} id="username" type="text" placeholder= "Username"/><br/>
                Password <input onChange={this.updateDetails.bind(this)} id="password" type="password" placeholder= "Password"/><br/>
                <button onClick={this.login.bind(this)}>Go</button>
            </div>
        )
    }
}

const mapStateToProps = state => {
    return {
    }
}

export default connect(mapStateToProps)(Login);

Register.js

import React, { Component} from 'react';
import { submitRegister } from '../../actions/authActions';
import { connect } from 'react-redux';

class Register extends Component {
    
    constructor(){
        super();

        this.state = {
            details:{
            }
        };
    }
    
    updateDetails(event){
        let updateDetails = Object.assign({}, this.state.details);

        updateDetails[event.target.id] = event.target.value;
        this.setState({
            details: updateDetails   
        });
    }

    register(){
        this.props.dispatch(submitRegister(this.state.details));    
    }

    render(){
        return (
            <div>
                <h3>Register</h3>
                Username <input onChange={this.updateDetails.bind(this)} id="username" type="text" placeholder= "Username"/><br/>
                Password <input onChange={this.updateDetails.bind(this)} id="password" type="password" placeholder= "Password"/><br/>

                <button onClick={this.register.bind(this)}>Go</button>
            </div>
        )
    }
}

const mapStateToProps = state => {
    return {
    }
}

export default connect(mapStateToProps)(Register);

Now we’ll create a container to hold our Login and Registration components

import React, { Component} from 'react';
import { connect } from 'react-redux'
import Login from '../presentation/Login';
import Register from '../presentation/Register';
import { logoutUser } from '../../actions/authActions';

class Authentication extends Component {

    constructor(){
        super();

        this.state = {
            toggleReg: false
        };
    }

    componentDidMount(){
        
    }

    showLogin(){
        this.setState({
            toggleReg: false   
        });        
    }    

    showReg(){
        this.setState({
            toggleReg: true   
        });
    }    

    logout(){
        this.props.dispatch(logoutUser());
    }    

    render(){

        const userNotLoggedIn = (
            <div>
                <button onClick={this.showLogin.bind(this)}>Login</button><button onClick={this.showReg.bind(this)}>Register</button>
                { this.state.toggleReg ? <Register /> : <Login /> }
            </div>
        );
        const userLoggedIn = (<div>Logged in as: {this.props.username} <button onClick={this.logout.bind(this)}>Logout</button></div>);

        return (
            <div>
                {this.props.loggedIn ? userLoggedIn : userNotLoggedIn}
            </div>
        )
    }
}

const mapStateToProps = state => {
    return {
        loggedIn: state.auth.loggedIn,
        username: state.auth.username
    }
}

export default connect(mapStateToProps)(Authentication)

Now lets make a new reducer for our authentication. in src/reducers create authReducer.js

import constants from '../constants/actionTypes'

var initialState = {
    loggedIn: localStorage.getItem('token') ? true : false,
    username: localStorage.getItem('username') ? localStorage.getItem('username') : ''
}

export default (state = initialState, action) => {

  var updated = Object.assign({}, state);

  switch(action.type) {

    case constants.USER_REGISTERED:
      updated['loggedIn'] = true;
      updated['username'] = action.username;

      return updated;

    case constants.USER_LOGGEDIN:
      updated['loggedIn'] = true;
      updated['username'] = action.username;
      return updated;

    case constants.USER_LOGOUT:
      updated['loggedIn'] = false;
      updated['username'] = '';
      return updated;

    default:
      return state;
    }
}

Although our reducer function itself is should be a pure function, we can set our default state using the values from localstorage so that on page refresh our logged in state is maintained.

Now we’ll add the new reducer into our store in /stores/store.js

import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import newsReducer from '../reducers/newsReducer';
import authReducer from '../reducers/authReducer';

const store = createStore(
  combineReducers({
    news: newsReducer,
    auth: authReducer
  }),
  applyMiddleware(
    thunk
  )
);

export default store;

To complete our authentication we need to add our middleware for checking for the presence of the Authorization token on secure routes.

const jwt = require('jsonwebtoken');
const User = require('../models/User');


module.exports = (req, res, next) => {

    if (!req.headers.authorization) {
        return res.status(401).end();
    }

    const token = req.headers.authorization.split(' ')[1];

    return jwt.verify(token, process.env.JWTSECRET, (err, decoded) => {

        if (err) { 
            return res.status(401).end(); 
        }

        req.userData = {};
        req.userData.tokenID  = token;
        req.userData.userid = decoded._id;
        req.userData.username = decoded.username;

        return next();


    });

};

Our middleware verifys the token using our secret and then sets variables in req.userData for our secure requests to use.

At this stage we now have our server side authentication API for login and register. On the client we can login and register, with it storing the JWT token and username in localstorage.

We’ll now extend the API to include the ability for logged in users to post comments..

1.9 Comments

Lets start with a new Schema for our comment

/models/Comment.js

var mongoose = require('mongoose');  


var CommentSchema = new mongoose.Schema({  
    username: String,
    body: String,
    status: {
        type: Number,
        default: 1
      },
    created: {
        type: Date,
        required: true,
        default: new Date()
    }
});

mongoose.model('Comment', CommentSchema);

module.exports = mongoose.model('Comment');

Then we’ll add this new comment schema to our News schema as a subdocument.

const mongoose = require('mongoose');  
const CommentSchema = require('./Comment').schema;  

const NewsSchema = new mongoose.Schema({  
    title: String,
    teaser: String,
    body: String,
    status: {
        type: Number,
        default: 1
      },
    created: {
        type: Date,
        required: true,
        default: new Date()
    },
    comments: [CommentSchema]
});

mongoose.model('News', NewsSchema);

module.exports = mongoose.model('News');

We then need to add some changes to our NewsController to create a new comment and add it to the news items comments.

NewsController.js

...
const Comment = require('../models/Comment')
...
    createComment: function(id, username, body, callback){
        News.findById(id, function(err, result){
            if(err){
                callback(err, null);
                return;
            }
            
            var comment = new Comment({username: username, body: body});

            result.comments.push(comment);
            
            result.save(function(err, commentResult){
                if(err){
                    callback(err, null);
                    return;
                }

                callback(null, commentResult);
            });
        });

    }
...

We now need to import our authCheckMiddle and apply it to our route /news/:id/comment

App.js integration of auth check

...
const authCheckMiddleware = require('./middleware/authCheck');
...
app.use('/', routes);

app.use('/user', authRoute);

app.use('/news/:id/comment', authCheckMiddleware);
app.use('/news', newsRoute);
...

Next we’ll create a new route within the news router for calling our newsController’s createComment method, passing in the req.userData.username that the authCheckMiddleware provided

router.post('/:id/comment', function(req, res, next) {
    const id = req.params.id;

    newsController.createComment(id, req.userData.username, req.body.body, function(err, result){
        if(err){  
            console.log(err);
            res.json({
                success: 0,
                error: err
            })
            return;
        }

        res.json({
            success: 1,
            data: result
        });
    });

});

Lets again switch back to the client and create some components for our comments functionality.

First our CommentsPanel container in containers/CommentsPanel.js

import React, { Component} from 'react';
import CommentElement from '../presentation/CommentElement';
import CreateComment from '../presentation/CreateComment';
import { fetchNews } from '../../actions/newsActions'

class CommentsPanel extends Component {

    render(){
        const commentItems = this.props.comments.map( (comment, i) => {
            return ( <li key={i}><CommentElement data = {comment} /></li> );
        });

        return (
            <div>
                <h2>Comments</h2>
                <ul>
                    {(this.props.comments.length > 0) ? <ul>{commentItems}</ul> : <div>No comments</div>}
                </ul>
                <CreateComment newsItemID={this.props.id}/>
            </div>
        )
    }
}

export default CommentsPanel

Next in /presentation lets create two Components for our Comment element and also our create comment form

CommentElement.js

import React, { Component} from 'react';
import PropTypes from 'prop-types';

class CommentElement extends Component {
    render(){
        return (
            <div>
                <div><b>{this.props.data.username}</b></div>
                <div>{this.props.data.body}</div>
            </div>
        )
    }
}


CommentElement.propTypes = {
    data: PropTypes.shape({
        username: PropTypes.string.isRequired,
        body: PropTypes.string.isRequired
    })
};

export default CommentElement

CreateComment.js

import React, { Component} from 'react';
import { submitComment } from '../../actions/newsActions';
import { connect } from 'react-redux';

class CreateComment extends Component {
    
    constructor(){
        super();

        this.state = {
            comment: ''
        };
    }
    
    updateComment(event){
        this.setState({
            comment: event.target.value  
        });
    }

    submitComment(){
        this.props.dispatch(submitComment(this.props.newsItemID, this.props.username, {body: this.state.comment}));    

        this.setState({
            comment: ''
        });        
    }

    render(){
        return (
            <div>
                <h3>Post comment</h3>
                <textarea value={this.state.comment} onChange={this.updateComment.bind(this)} id="body" type="text">

                </textarea><br/>

                <button onClick={this.submitComment.bind(this)}>Post</button>
            </div>
        )
    }
}

const mapStateToProps = state => {
    return {
        username: state.auth.username        
    }
}

export default connect(mapStateToProps)(CreateComment);

In our NewsArticle container we’ll add in our CommentsPanel

NewsArticle.js

import React, { Component} from 'react';
import NewsItemDetail from '../presentation/NewsItemDetail';
import CommentsPanel from './CommentsPanel';
import { connect } from 'react-redux'
import { fetchNewsItem } from '../../actions/newsActions'

class NewsArticle extends Component {


    componentDidMount(){
        this.props.dispatch(fetchNewsItem(this.props.match.params.id));
    }

    render(){

        return (
            <div>
                <h2>News Story</h2>
                <ul>
                    { !this.props.newsItemLoading ? <div><NewsItemDetail data={this.props.newsItem} /> <CommentsPanel comments={this.props.comments} id={this.props.newsItem._id} /></div> : <div>Loading</div>}
                </ul>
            </div>
        )
    }
}

const mapStateToProps = state => {
    return {
        newsItem: state.news.newsItem,
        comments: state.news.newsItem.comments,
        newsItemLoading: state.news.newsItemLoading
    }
}

export default connect(mapStateToProps)(NewsArticle)

By adding the comments to our mapStateToProps function, the news story should re-render when a new comment is added

For our add comment action we need to first add the constant NEWS_ADDCOMMENT

export default {
    NEWS_RECEIVED: 'NEWS_RECEIVED',
    NEWSITEM_RECEIVED: 'NEWSITEM_RECEIVED',
    NEWSITEM_LOADING: 'NEWSITEM_LOADING',
    USER_REGISTERED: 'USER_REGISTERED',
    USER_LOGGEDIN: 'USER_LOGGEDIN',
    USER_LOGOUT: 'USER_LOGOUT',
    NEWS_ADDCOMMENT: 'NEWS_ADDCOMMENT'
}

Next, In the function which makes the request for adding the comment we will get the JWT from localstorage and pass it in the Authorization header.

newsActions.js

...
function addComment(username, body){
    return {
        type: actionTypes.NEWS_ADDCOMMENT,
        username: username,
        body: body
    }
}
...
export function submitComment(newsItemID, username, data){
    var token = localStorage.getItem('token') || null;

    return dispatch => {
        return fetch(`/news/${newsItemID}/comment`, { 
            method: 'POST', 
             headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization' : `Bearer ${token}`
              },
            body: JSON.stringify(data), 
            mode: 'cors'})
            .then( (response) => {
                if (!response.ok) {
                    throw Error(response.statusText);
                }else{

                    dispatch(addComment(username, data.body))
                }
            })
            .catch( (e) => console.log(e) );
    }    
}
...

With the response returned from the API, we dispatch an addComment action which will add the new comment to our page without requiring us to re-request the news story data.

To do this we need to make changes to news Reducer to append the comments details to the news stories comments array.

import constants from '../constants/actionTypes'

var initialState = {
    news: [],
    newsItem: {},
    newsItemLoading: true
}

export default (state = initialState, action) => {

  var updated = Object.assign({}, state);

  switch(action.type) {

    case constants.NEWS_RECEIVED:
      updated['news'] = action.news;
      return updated;

    case constants.NEWSITEM_RECEIVED:
      updated['newsItem'] = action.newsItem;
      updated['newsItemLoading'] = false;
      return updated;

    case constants.NEWSITEM_LOADING:
      updated['newsItemLoading'] = true;
      return updated

    case constants.NEWS_ADDCOMMENT:
        var updatedComments = Object.assign([], updated['newsItem'].comments);
        updatedComments.push({"username": action.username, "body": action.body});
        updated['newsItem'].comments = updatedComments;
        return updated

    default:
      return state;
    }
}

With that done we now have a functioning comments system.

In the next part we’ll do some tidying up and validation… To be continued..