JSON Web Token module:
npm install jwt-simple
Not pre-requisites. Makes UI development convenient.
* ui-router
* sweetAlert
For persistence of token even when the browser is refreshed, $window.sessionStorage would suffice, no need to use ng-storage. However, it's better to use $window.localStorage, $window.sessionStorage is for single tab only; if we open the same app in another tab, it won't be able to read the sessionStorage from the original tab. $window.localStorage can be accessed between tabs.
Authentication consist mainly of these parts:
Server-side:
* Auth.ts -- username and password authentication goes here
* CanBe.ts -- authorization goes here
* app.ts -- entry point of a node app
This is how an API can be authorized in nodejs's app.js. Insert the CanBe.accessedBy node RequestHandler before the API to be called, e.g.,
Without authorization:
app.get('/api/item', itemApi.search);
With authorization:
app.get('/api/item-secured', canBe.accessedBy(['rockstar']), itemApi.search);
Shared by server-side and client-side:
* ILoginDto.ts -- username and password transmitted from client-side to server-side
* ILoginResponseDto.ts -- Auth.ts's authentication response
* ITokenPayload.ts -- Not exactly used by client-side, if there's a need for the client-side to decode the middle part of the JWT, it goes in this data structure. The first part and middle part of JWT are publicly-available information, and as such, ITokenPayload should contain no sensitive information, e.g., password
Client-side:
* init.ts -- called by main.js. main.js is the conventional name for the app's entry point in a RequireJS-based apps.
* AuthService.ts -- called by login UI, to decouple the login controller if there will be changes in authentication mechanism
* HttpInterceptor.ts -- the money shot. this is where to make an angular application automatically send a header everytime an $http or $resource call happens
Auth.ts:
import express = require('express');
import jwt = require('jwt-simple');
export function verify(req: express.Request, res:express.Response, next: Function) : any {
var loginDto = <shared.ILoginDto> req.body;
var loginResponseDto = <shared.ILoginResponseDto> {};
var isValidUser = dbAuthenticate(loginDto.username, loginDto.password);
if (isValidUser) {
var roles = dbGetUserRoles(loginDto.username);
var tokenPayload = <shared.ITokenPayload> { username: loginDto.username, roles: roles };
var payloadWithSignature = jwt.encode(tokenPayload, "safeguardThisSuperSecretKey");
loginResponseDto.isUserValid = true;
loginResponseDto.theAuthorizationToUse = "Bearer " + payloadWithSignature;
res.json(loginResponseDto);
}
else {
loginResponseDto.isUserValid = false;
res.status(401).json(loginResponseDto);
}
}
function dbAuthenticate(username: string, password: string): any {
if (username == "Thom" && password == "Yorke") {
return true;
}
if (username == "Kurt" && password == "Cobain") {
return true;
}
return false;
}
function dbGetUserRoles(username: string): string[] {
if (username == "Thom") {
return ["rockstar", "composer"];
}
if (username == "Kurt") {
return ["composer"];
}
return [];
}
CanBe.ts:
import express = require('express');
import jwt = require('jwt-simple');
export function accessedBy(roles: string[]):any {
accessedByBind.bind(roles);
}
function accessedByBind(roles:string[], req:express.Request, res:express.Response, next:Function): any {
function unauthorized(): any {
return res.sendStatus(401);
}
// === accessedByBind starts here ===
var theAuthorizationToUse = req.headers["authorization"];
if (theAuthorizationToUse === undefined) {
return unauthorized();
}
var payloadWithSignature = theAuthorizationToUse.substr("Bearer ".length);
var tokenPayload = <shared.ITokenPayload> jwt.decode(payloadWithSignature, "safeguardThisSuperSecretKey");
if (tokenPayload == undefined || tokenPayload.username == "") {
return unauthorized();
}
if (tokenPayload.roles.filter(memberRole => roles.filter(allowedRole => memberRole == allowedRole).length > 0 ).length > 0) {
return next();
}
return res.status(500).json({customError: "Unknown Error"});
}
ILoginDto.ts:
module shared {
export interface ILoginDto {
username: string;
password: string;
}
}
ILoginResponseDto.ts:
module shared {
export interface ILoginResponseDto {
isUserValid: boolean;
theAuthorizationToUse: string;
}
}
ITokenPayload.ts:
module shared {
export interface ITokenPayload {
username: string;
roles: string[];
}
}
init.js
// app initialization and dependency injections goes here
var angie:ng.IAngularStatic = require('angular');
var mod:ng.IModule = angie.module('niceApp', [
'ui.router', 'ngResource', 'scs.couch-potato', 'ngFileUpload',
'ngSanitize', 'ui.pagedown', 'ngTagsInput', 'puElasticInput', 'ui.bootstrap']);
mod.service('AuthService', ['$http', '$q', '$window', InhouseLib.AuthService]);
mod.factory('HttpInterceptor', ['$q', '$window', '$injector', 'TheSweetAlert', '$location', InhouseLib.HttpInterceptor]);
mod.config(['$httpProvider', ($httpProvider:ng.IHttpProvider) => {
$httpProvider.interceptors.push('HttpInterceptor');
}]);
AuthService.ts:
module InhouseLib {
export class AuthService {
constructor(public $http:ng.IHttpService, public $q:ng.IQService, public $window: ng.IWindowService) {
}
verify(username:string, password:string):ng.IPromise<ng.IHttpPromiseCallbackArg<shared.ILoginResponseDto>> {
var deferred = this.$q.defer();
var u = <shared.ILoginDto>{};
u.username = username;
u.password = password;
this.$http.post<shared.ILoginResponseDto>('/api/member/verify', u)
.then(success => {
this.$window.localStorage["theToken"] = success.data.theAuthorizationToUse;
// 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.$window.localStorage["theToken"];
}
}
}
HttpInterceptor.ts:
module InhouseLib {
export function HttpInterceptor($q:ng.IQService,
$window:ng.IWindowService,
$injector:ng.auto.IInjectorService,
TheSweetAlert,
$location:ng.ILocationService)
{
return {
request: (config):any => {
config.headers = config.headers || {};
var theToken = $window.localStorage["theToken"];
if (theToken !== undefined) {
// token already include the word "Bearer "
config.headers.Authorization = theToken;
}
return config;
},
responseError: (response):any => {
if (response.status === 401 || response.status === 403) {
// http://stackoverflow.com/questions/25495942/circular-dependency-found-http-templatefactory-view-state
// ui-router's $state
// can use the $location.path(paramHere) to change the url. but since we are using ui-router
// it's better to use its $state component to route things around.
var $state:any = $injector.get('$state');
TheSweetAlert({title: "Oops", text: "Not allowed. Will redirect you to login page\n" +
"This is your current url: " + $location.path()}, () =>
{
$state.go('root.login');
});
}
return $q.reject(response);
}
};
}// function HttpInterceptor
}
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