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);
UPDATE:
Some code above has
Cargo Culting in it, it uses $q.defer unnecessarily. Haven't properly learned the promise's fundamentals before :) Read:
https://www.codelord.net/2015/09/24/%24q-dot-defer-youre-doing-it-wrong/