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