1.0.7 • Published 4 years ago

@gamesolutionslab/server_authentication v1.0.7

Weekly downloads
10
License
-
Repository
-
Last release
4 years ago

About this package

The aim of this package is to provide an easy way to authenticate users on node.js servers.
Authentication for browser to server can happen via Google and JWT.
Authentication for application to server can happen via JWT.

Installation

Install via npm i @gamesolutionslab/server_authentication --save.

Dependancies

  • A MongoDB for saving user data

Usage

Install needed packages

For both Google and JWT you need the following packages:

  • dotenv
  • express
  • express-session
  • mongoose

For Google you need the following extra package:

  • connect-ensure-login

Install via the usual way:
npm i [packagename] --save

Require the packages (exept connect-ensure-login) in your main server file (usually server.js):

require('dotenv').config();
var express = require('express');
var expressSession = require('express-session');
var mongoose = require('mongoose');

Create .env file

Create a new file called .env if your project doen't have one already.
A .env file is a file where you usually keep secrets and passwords. Therefore it is important to NEVER commit this file to source control.

Add the following key and replace its value:

EXPRESS_SESSION_SECRET=THIS IS USED FOR EXPRESS SESSION, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING

For JWT, add the following key and replace its value:

JWT_SECRET=THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING

For Google, follow the following steps:

  • Go to the google developer console
  • Create a new project
  • Go to credentials
  • Select create credentials > OAuth client ID
  • Select web application
  • Enter a name
  • In Authorised JavaScript origins enter your server url (for example: http://localhost:8080)
  • In Authorised redirect URIs enter your callback url (for example: http://localhost:8080/google/return)
  • Save
  • Select your credential in the OAuth 2.0 client IDs list

At the top of the page you will see your Client ID and Client Secret. Add the following keys to your .env file and paste the Client ID and Client Secret behind the Client ID and Client Secret Key and add the Callback URL you entered behind the Callback URL key:

GOOGLE_CLIENT_ID=[Client ID]
GOOGLE_CLIENT_SECRET=[Client Secret]
GOOGLE_CALLBACK_URL=[Callback url, in example: /google/return]

Add and configure middleware

Configure the express-session middleware in your main server file (usually server.js). Please refer to the express-session npm page for all available options and explanations.

app.use(expressSession({
    secret: process.env.EXPRESS_SESSION_SECRET,
    resave: true,
    saveUninitialized: true,
    cookie: {
        maxAge: 1000 * 60 * 60 * 24
    }
}));

For JWT, import and use the jwt_error_handler middleware as well:

var errorHandler = require('@gamesolutionslab/server_authentication').jwt_error_handler;
app.use(errorHandler);

Create a user

A user needs to be saved in a database.
If you don't have one already, create a MongoDB. Use the following code in your main server file (usually server.js) to use mongoose to connect to your MongoDB if you don't have a connection already:

//Replace [MongoDB connection string] with your own
var db = mongoose.connection;
mongoose.connect([MongoDB connection string]);
db.on('error', console.error.bind(console, 'connection error: '));
db.once('open', () => {
    console.log("Db connection opened: " + [MongoDB connection string]);
});

The first user needs to be created by hand, or before you secure your routes.
A user has the following fields:

FieldTypeFor GoogleFor JWTDefaultExplanation
passwordStringoptionalrequired""password for login in and getting JWT token
nameStringrequiredrequired""(user)name of the user
emailStringrequiredoptional""email of the user
hdStringoptionaloptional""the part of the email after the @ sign (for example gmail.com)
roleStringrequiredrequiredUserrole of the user, see Secure your routes
activeBooleanoptionaloptionaltruethe active state of the user account, see Create log in

To create a user via code:

var usermodel = require('@gamesolutionslab/server_authentication').user_model;

function CreateUser()
{
    new usermodel({
        password: "password",
        name: "name",
        email: "email",
        hd: "hd",
        role: "role",
        active: true
    }).save((error, result) => {
        if (error) {
            console.log("Error: " + error);
        }
        else {
            console.log("Succes: " + result);
        }
    });
}

Create log in

Now that we have a user, we have to create a way for that user to log in.
The flow for JWT and Google is the same:

  • Get the user credentials
  • Check the user credentials in the database
  • When in the database and correct, return JWT token (JWT) or save user in session (Google)
  • When not in the database or incorrect, throw an error

For JWT you will need to send the user credentials in the body of the log in request. Below an example on how to log in with a simple HTML page:

<!-- Simple HTML page with username and password input fields and a button to log in-->
<!DOCTYPE html>
<html>
<body>
    <form action="#">
        <input type="text" id="txt_username">
        <input type="text" id="txt_password">
        <button type="button" id="btn_login">
            Log in
        </button>
    </form>
</body>
</html>
//Simple JS script to send an AJAX request when the log in button has been pressed
$(document).ready(function () {
    $('#btn_login').click(function () {
        username = $('#txt_username').val();
        password = $('#txt_password').val();

        var aData = {};
        aData.username = username;
        aData.password = password;

        $.ajax({
            method: 'POST',
            url: '/jwt/authenticate',
            data: aData,
            error: function (error) {
                console.log("error");
            }
        }).done(function (token) {
            console.log(token);
        });
    });
}

The above script calls the route /jwt/authenticate, the code snippet below is an example on how to create this route:

var express = require('express');
var router = express.Router();
var userService = require('@gamesolutionslab/server_authentication').jwt_user_service;

router.post('/authenticate', authenticate);

module.exports = router;

function authenticate(req, res, next) {
    userService.authenticate(req.body)
        .then((token) => {
            if (token)
            {
                //OPTIONAL: Check if the logged in user has an active account
                if (req.user.active) {
                    //The account is active, redirect to desired page or do other actions here
                    res.redirect('/');
                }
                else {
                    //Account is inactive
                    res.redirect('/error');
                }

                res.json(token)
            }
            else{
                res.status(400).json({ message: 'Username or password is incorrect' })
            }
        })
        .catch(err => next(err));
}

This userService.authenticate method checks if the username and password combination is correct and if it is correct, it generates a JWT token.
To be able to use this route, the above exported router needs to be imported into your main server file (usually server.js):

var express = require('express');
var app = express();

//Replace [Script with router] with the name of your script with the exported router.
//Due to using the path '/jwt', all routes from the router will be prefixed with '/jwt'.
//In this example you get '/jwt/authenticate' as the full route path.
app.use('/jwt', require('./[Script with router]'));

It is up to you on how to proceed further with the token the user gets returned. In case of a HTML page, you can save it in Localstorage (WARNING: This is not a secure way to save it) or somewhere else to your liking. In case of a application you can simply save it in a variable or write it to a file. Keep in mind that the returned token has te be kept secret and thus needs to be stored away securely.

Google comes with its own webpage for logging in, so there's no need to make one yourself. The code below shows how to show this webpage and handle the callback it gives:

var express = require('express');
var router = express.Router();

var passport = require('@gamesolutionslab/server_authentication').google_passport;

router.use(passport.initialize());
router.use(passport.session());

router.get('/login', google_auth);
router.get(process.env.GOOGLE_CALLBACK_URL, passport.authenticate('google', { failureRedirect: '/error' }), callback);
router.get('/error', loginError);

module.exports = router;

function google_auth(req, res, next) {
    //Shows the Google webpage for logging in
    passport.authenticate('google', function (err, user, info) {
        if (err) {
            console.log(err);
            return res.status(401).json(err);
        }
        if (user) {
            console.log("user: " + JSON.stringify(user));
        }
        else {
            res.status(401).json(info);
        }
    })(req, res)
}

//The callback is called when a user has succesfully logged in
function callback(req, res, next) {
    //OPTIONAL: Check if the logged in user has an active account
    if (req.user.active) {
        //The account is active, redirect to desired page or do other actions here
        res.redirect('/');
    }
    else {
        //Account is inactive
        res.redirect('/google/error');
    }
}

function loginError(req, res, next) {
    //Handle log in errors
    res.redirect('/google/error');
}

To be able to use these routes, the above exported router needs to be imported into your main server file (usually server.js):

var express = require('express');
var app = express();

//Replace [Script with router] with the name of your script with the exported router.
//Due to using the path '/google', all routes from the router will be prefixed with '/google'.
//In this example you get '/google/login', '/google/return' and '/google/error' as the full route paths.
app.use('/google', require('./[Script with router]'));

Create log out

In most cases, you want your users to be able to log out after logging in. To do this, you need to create a logout route.
When using JWT, you simply have to stop sending the JWT token to have a logged out user. If you save the token somewhere, it is a good practice to delete it now.
For Google you simply need to create a new route in the router created in Create log in:

router.get('/logout', logout);

function logout(req, res, next) {
    req.logout();
    //Redirect or do other actions after logout
}

Secure your routes

Now that your users are able to log in and out, it is time to secure your routes. There are two ways to secure your routes:

  • Only logged in users have access
  • Only logged in users with the correct roles have access

For both ways you will need to pass along the JWT token of the user to the secured route when using JWT.

To secure a route with JWT without a role you will need to use the authorize middleware in your routes and tell it you are using JWT:

var authorize = require('@gamesolutionslab/server_authentication').authorize;

router.get('/data', authorize("JWT"), exportData);

function exportData(req, res, next) {
    res.status(200).send("data send: secured data");
}

When using Google, you wil need to use the connect-ensure-login package in your routes.

//Ensures the user is logged in, when not the case, redirect to /google/login
var ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn({ redirectTo: '/google/login' });

router.get('/data', ensureLoggedIn, exportData);

function exportData(req, res, next) {
    res.status(200).send("data send: secured data");
}

If you want to use roles, you will need to create a file where you define the roles, something like this:

module.exports = {
    Admin: 'Admin',
    Moderator: 'Moderator'
}

For JWT you then simply pass the roles that you want to have access to the route to the authorize middleware:

//Replace [role file name] with the name of the file where you have defined your roles
const Role = require('/[role file name]');
var authorize = require('@gamesolutionslab/server_authentication').authorize;

//A single role can be passed along like so
router.get('/data', authorize("JWT", Role.Admin), exportData);
//Multiple roles have to be passed as an array
router.get('/data', authorize("JWT", [Role.Admin, Role.Moderator]), exportData);

function exportData(req, res, next) {
    res.status(200).send("data send: secured data");
}

For Google you will need to use the connect-ensure-login package along with the authorize middleware. Tell the middleware you are using Google and pass along the roles you want to have acces to the route:

//Replace [role file name] with the name of the file where you have defined your roles
const Role = require('/[role file name]');
//Ensures the user is logged in, when not the case, redirect to /google/login
var ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn({ redirectTo: '/google/login' });
var authorize = require('@gamesolutionslab/server_authentication').authorize;

//A single role can be passed along like so
router.get('/data', ensureLoggedIn, authorize("Google", Role.Admin), exportData);
//Multiple roles have to be passed as an array
router.get('/data', ensureLoggedIn, authorize("Google", [Role.Admin, Role.Moderator]), exportData);

function exportData(req, res, next) {
    res.status(200).send("data send: secured data");
}

Secure your HTML pages and other files

Now that all your routes are secured, you might want to secure your HTML pages or other files. This is very similar to securing your routes.
To secure a HTML page or a file, create a route that serves that HTML page or file. Then secure it just like you've done with your other routes:

var express = require('express');
var router = express.Router();
//No need to install this package, you already have it by default
var path = require('path');

var passport = require('@gamesolutionslab/server_authentication').google_passport;
var ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn({ redirectTo: '/google/login' 
});
var authorize = require('@gamesolutionslab/server_authentication').authorize;

router.use(passport.initialize());
router.use(passport.session());

//JWT - no roles
router.get('/', authorize("JWT"), fileRoute);
//JWT - with roles
router.get('/', authorize("JWT", Role.Admin), fileRoute);
//Google - no roles
router.get('/', ensureLoggedIn, fileRoute);
//Google - with roles
router.get('/', ensureLoggedIn, authorize("Google", Role.Admin), fileRoute);

function fileRoute(req, res, next) {
    //Replace [path to folder] with the path to the folder where your HTML page or file is located
    //Replace [file name] with the file name and extension of the file you want to serve
    res.sendFile(path.join(__dirname, '[path to folder]', '[file name]'));
}

Troubleshooting

I can't authenticate my users
I'm getting a Bad request token error
Stuck on Google Authentication Screen (infinite loading)
Please check if your server calls arrive correctly. If they do, it's highly likely that the problem lies with the database. Make sure you have a connection and make sure you only have one version of mongoose installed. You can check this by going into your node_modules folder. There should be a single mongoose folder. If this is not the case, delete all but one. If this is not the case, got to the node_modules folder and find the @gamesolutionslab > server_authentication > node_modules folder. Check if this folder contains a mongoose folder. If it does, delete it.