• 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/
No comments:
Post a Comment