Sunday, September 6, 2015

Custom Basic Authentication and Authorization with AngularJS, Node Express, TypeScript, Massive DAL and PostgreSQL

Design:
    • On successful login, the server shall send back the authentication header that will be saved to angular's $http pipeline
    • Authorization is roles-based
    • Member's role(s) are stored as text array in the database

Prerequisites:
    • Data Access Layer node module
        ○ npm install massive
    • Basic Authentication node module
        ○ npm install basic-auth



I'll go owl on this and just do a code dump and briefly explain each part and their relation to other parts.


What will be created:
    
    • Login screen:
        1. /public/app-dir/Login/Template.html
        2. /public/app-dir/Login/Controller.ts
        
    • Login and Response DTOs shared by client-side and server-side
    
        3. /shared/dto/LoginDto.ts
        4. /shared/dto/LoginResponseDto.ts
        
    • Authentication service for Angular
        5. /public/inhouse-lib/AuthService.ts
    
    • Authentication node service
        6. /api/member.ts
        7. /db/loginVerify.sql

    • Authorization for node REST APIs
        8. /server/CanBe.ts
        9. /db/verifyRoleAccess.sql

    • Member table
        10. server/ddl.sql

    
What need to be changed:
        11. Nodejs's app.ts
            i. Add initialization of massive DAL
            ii. Add authentication url to node
            iii. Authorize a REST API




1. public/app-dir/Login/Template.html
<style>

    .block label { display: inline-block; width: 140px; text-align: left; }

</style>

<div class="col-md-2 col-md-offset-5">
    <form ng-submit="c.login()">
        <div class="block">
            <label>Username</label>
            <input type="text" ng-model="c.username"/>
        </div>

        <div class="block">
            <label>Password</label>
            <input type="password" ng-model="c.password"/>
        </div>

        <div class="block">
            <label></label>
            <input type="submit" value="Login"/>
        </div>

    </form>
</div>

2. public/app-dir/Login/Controller.ts
///<reference path="../../lib-inhouse/doDefine.ts"/>
///<reference path="../../../typings/angularjs/angular.d.ts"/>
///<reference path="../../lib-inhouse/AuthService.ts"/>
///<reference path="../../../typings/sweetalert/sweetalert.d.ts"/>
///<reference path="../../shared-state/AppWide.ts"/>

module App.Login {

    export class Controller {


        username:string;
        password:string;


        // Dependency-injected sweet alert, so it can be easily mocked

        constructor(public authService:InhouseLib.AuthService, public $http:ng.IHttpService, public $state:any,
                    public TheSweetAlert:SweetAlert.SweetAlertStatic,
                    public appWide:SharedState.AppWide) {
        }


        login():void {


            this.authService.verify(this.username, this.password)
                .then(success => {
                    this.appWide.isUploadVisible = true;
                    this.$state.go('root.app.home');
                }, error => {
                    this.appWide.isUploadVisible = false;
                    this.TheSweetAlert(error.status.toString(), "Not authorized", "error");
                });


        }

    }
}


doDefine(require => {

    var mod:angular.IModule = require('eat');


    require('/shared/dto/LoginDto.js');


    mod["registerController"]('LoginController', ['AuthService', '$http', '$state', 'TheSweetAlert', 'AppWide',
        App.Login.Controller]);

});


authService.verify line 27, is where the setting and clearing of Basic Authentication information to Angular's $http pipeline happens, authService.verify returns a promise if the authentication is successful, if the user is not authenticated it goes to the promise's error callback parameter.


public/lib-inhouse/doDefine.ts
///<reference path="../../typings/requirejs/require.d.ts"/>

// the doDefine will be disabled during unit test

function doDefine(func: (cb) => any) {

    define(func);

}

3. shared/dto/LoginDto.ts
///<reference path="../../typings/node/node.d.ts"/>


module Dto {

    export class LoginDto {

        username: string;
        password: string;

    }

}



4. shared/dto/LoginResponseDto.ts
///<reference path="../../typings/node/node.d.ts"/>

module Dto {

    export class LoginResponseDto {

        isValidUser: boolean;

        theBasicAuthHeaderToUse: string;

    }

}

The theBasicAuthHeaderToUse is the header that will be received by Angular and shall be assigned to Angular's $http pipeline. Its format is:

Basic base64encodeOf(username + ': ' + password)

Sample format:
Basic VGhvbSBZb3JrZTpjcmVlcA==


5. public/lib-inhouse/AuthService.ts
///<reference path="../../typings/angularjs/angular.d.ts"/>
///<reference path="../../shared/dto/LoginDto.ts"/>
///<reference path="../../shared/dto/LoginResponseDto.ts"/>

module InhouseLib {

    export class AuthService {


        constructor(public $http:ng.IHttpService, public $q:ng.IQService) {
        }

        verify(username:string, password:string):ng.IPromise<ng.IHttpPromiseCallbackArg<Dto.LoginResponseDto>> {

            var deferred = this.$q.defer();

            var u = new Dto.LoginDto();
            u.username = username;
            u.password = password;



            this.$http.post<Dto.LoginResponseDto>('/api/member/verify', u)
                .then(success => {


                    this.$http.defaults.headers["common"]["Authorization"] = success.data.theBasicAuthHeaderToUse;

                    // This goes to then's success callback parameter
                    deferred.resolve(success);

                }, error => {

                    this.logout();

                    // this goes to then's error callback parameter
                    deferred.reject(error);

                });

            return deferred.promise;

        }

        logout(): void {
            delete this.$http.defaults.headers["common"]["Authorization"];
        }

    }

}



6. Authentication module. Do the base64 encoding of Basic username:passwordHere at the server.

api/member.ts
/// <reference path="../typings/express/express.d.ts"/>
/// <reference path="../typings/extend/extend.d.ts"/>
/// <reference path="../shared/dto/LoginDto.ts"/>
///<reference path="../shared/dto/LoginResponseDto.ts"/>


import express = require("express");

import extend = require('extend');


export function verify(req:express.Request, res:express.Response, next:Function):any {


    var app:express.Application = req["app"];
    var db = app.get('db');


    var loginDto = <Dto.LoginDto> req.body;
    console.log(loginDto.username);

    var loginResponseDto = <Dto.LoginResponseDto>{};

    db.loginVerify([loginDto.username, loginDto.password], (err, result) => {


        if (result.length == 0) {

            loginResponseDto.isValidUser = false;
            loginResponseDto.theBasicAuthHeaderToUse = "Ha! Are you expecting a returned value?!";
            res.status(401).json(loginResponseDto);

        }
        else {

            var member = result[0];
            var salted_password = member.salted_password;

            loginResponseDto.isValidUser = true;
            loginResponseDto.theBasicAuthHeaderToUse = 
                "Basic " + new Buffer(loginDto.username + ":" + loginDto.password).toString("base64");
            res.json(loginResponseDto);
        }

    });

}



7. db/loginVerify.sql

This is called by the Authentication module.

For a primer of bcrypt mechanism: http://www.ienablemuch.com/2014/10/bcrypt-primer.html

select member_name, salted_password
from member
where member_name = $1 and salted_password = crypt($2, salted_password);

This is where the authorization happens.


8. Authorization module

server/CanBe.ts
/// <reference path="../typings/express/express.d.ts"/>

import express = require('express');

var basicAuth = require('basic-auth');


export function accessedBy(roles:string[]):express.RequestHandler {
    return accessedByBind.bind(undefined, roles);
}


function accessedByBind(roles:string[], req:express.Request, res:express.Response, next:Function): any {

    function unauthorized(): any {
        res.set('WWW-Authenticate', 'Basic realm=Authorization Required');
        return res.sendStatus(401);
    }


    // === accessedByBind starts here ===

    var app:express.Application = req["app"];
    var db = app.get('db');


    var user = basicAuth(req);

    if (!user || !user.name || !user.pass) {
        return unauthorized();
    }

    var username = user.name;
    var password = user.pass;

    db.verifyRoleAccess([username, password, roles], (err, result) => {

        if (err == null) {
            var isAllowed = result[0].is_allowed;

            if (isAllowed)
                return next();
            else {
                return unauthorized();
            }
        }
        else {
            return res.status(500).json({customError: 'Unknown Error'});
        }

    });


}



9. db/verifyRoleAccess.sql

This is called by the CanBe Authorization module.

select exists(

select null
from member
where
member_name = $1 and salted_password = crypt($2, salted_password)
and roles && $3 -- check if $3 (arrayOfRoles) passed from canBe.accessedBy(arrayOfRolesHere) is in member.roles.

) as is_allowed;



10. server/ddl.sql
create table member
(
    member_id serial primary key,
    member_name citext not null unique,
    salted_password text not null, -- using bcrypt
    roles text[]
);

insert into member(member_name, salted_password, roles) values
('Thom Yorke', crypt('creep', gen_salt('bf')), '{"rockstar"}')


11. app.ts
var massive = require('massive'); // DAL
import memberApi = require('./api/member'); // authentication 
import canBe = require('./server/CanBe'); // authorization


var massiveInstance = massive.connectSync({connectionString: 'postgres://postgres:yourPasswordHere@localhost/commerce'});
app.set('db', massiveInstance);

app.post('/api/member/verify', memberApi.verify);

// unsecured API
app.get('/api/item', itemApi.search);

// just insert canBe.accessedBy before the itemApi.search RequestHandler REST API to secure the REST API
app.get('/api/item-secured', canBe.accessedBy(['rockstar']), itemApi.search); 

No comments:

Post a Comment