diff --git a/angular2-jwt.spec.ts b/angular2-jwt.spec.ts index 226e1fe8..e38ed9c7 100644 --- a/angular2-jwt.spec.ts +++ b/angular2-jwt.spec.ts @@ -1,5 +1,5 @@ import "core-js"; -import {AuthConfig, AuthHttp, tokenNotExpired, JwtHelper} from "./angular2-jwt"; +import {AuthConfig, AuthHttp, tokenNotExpired, JwtHelper, AuthHttpError} from "./angular2-jwt"; import {Observable} from "rxjs"; import {Base64} from "js-base64"; @@ -173,4 +173,26 @@ describe("AuthHttp", () => { }); }); }); -}); \ No newline at end of file +}); + +describe("AuthHttpError", () => { + + 'use strict'; + + it("constructor fails to set the error message", () => { + const message = 'This is an error'; + let error = new AuthHttpError(message); + expect(error.message).toBe(''); + }); +}); + +describe("Error", () => { + + 'use strict'; + + it("constructor should set the error message", () => { + const message = 'This is an error'; + let error = new Error(message); + expect(error.message).toBe(message); + }); +}); diff --git a/angular2-jwt.ts b/angular2-jwt.ts index 7776afbb..eef49d07 100644 --- a/angular2-jwt.ts +++ b/angular2-jwt.ts @@ -103,7 +103,7 @@ export class AuthHttp { if (!tokenNotExpired(undefined, token)) { if (!this.config.noJwtError) { return new Observable((obs: any) => { - obs.error(new AuthHttpError('No JWT present or has expired')); + obs.error(new Error('No JWT present or has expired')); }); } } else { diff --git a/karma.conf.js b/karma.conf.js index 89aae235..4c8d04bf 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -6,14 +6,14 @@ module.exports = function (config) { frameworks: ["jasmine"], // list of files / patterns to load in the browser files: [ - {pattern: 'angular2-jwt.spec.ts', watched: false} + {pattern: '*.spec.ts', watched: false} ], // list of files / patterns to exclude exclude: [], preprocessors: { - 'angular2-jwt.spec.ts': [ 'webpack', 'sourcemap'] + '*.spec.ts': [ 'webpack', 'sourcemap'] }, @@ -35,18 +35,18 @@ module.exports = function (config) { logLevel: config.LOG_INFO, // enable / disable watching file and executing tests whenever any file changes - autoWatch: false, + autoWatch: true, browsers: [ //"Firefox", - //"Chrome", + "Chrome", //"IE", - "PhantomJS" + //"PhantomJS" ], // Continuous Integration mode // if true, it capture browsers, run tests and exit - singleRun: true, + singleRun: false, reporters: ['progress'], diff --git a/token.spec.ts b/token.spec.ts new file mode 100644 index 00000000..7dc3cb5a --- /dev/null +++ b/token.spec.ts @@ -0,0 +1,110 @@ +import { Token } from "./token"; +import { Base64 } from 'js-base64'; + +const expiredToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjB9.m2OKoK5-Fnbbg4inMrsAQKsehq2wpQYim8695uLdogk"; +const validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTl9.K_lUwtGbvjCHP8Ff-gW9GykydkkXzHKRPbACxItvrFU"; +const noExpiryToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; +const tokenFormatString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.{0}.m2OKoK5-Fnbbg4inMrsAQKsehq2wpQYim8695uLdogk"; + + +describe("Token", () => { + + 'use strict'; + + it("constructor with null value should throw", () => { + let createToken = function () { new Token(null); }; + expect(createToken).toThrow(); + }); + + it("constructor with invalid format value should throw", () => { + let createToken = function () { new Token('InvalidFormat'); }; + expect(createToken).toThrow(); + }); + + it("constructor with blank value should throw", () => { + let createToken = function () { new Token(''); }; + expect(createToken).toThrow(); + }); + + it("with same value are equal", () => { + let token1 = new Token(expiredToken); + let token2 = new Token(expiredToken); + expect(token1.equals(token2)).toBe(true); + }); + + it("with different value are not equal", () => { + let token1 = new Token(expiredToken); + let token2 = new Token(noExpiryToken); + expect(token1.equals(token2)).toBe(false); + }); + + it("null is not a token", () => { + expect(Token.isToken(null)).toBe(false); + }); + + it("blank string is not a token", () => { + expect(Token.isToken('')).toBe(false); + }); + + it("null is not a token", () => { + expect(Token.isToken(null)).toBe(false); + }); + + it("validtoken is a token", () => { + expect(Token.isToken(validToken)).toBe(true); + }); + + describe("Payload", () => { + + const unexpiredObject = { name: "angular2", exp: 9999999999 }; + const expiredObject = { name: "angular2", exp: 10 }; + const expirelessObject = { name: "angular2" }; + + it("with expireless instance should decode", () => { + //arrange + let ePayload = Base64.encode(JSON.stringify(expirelessObject)); + let tokenValue = tokenFormatString.replace('{0}', ePayload); + const token = new Token(tokenValue); + + //act + let payload = token.decodePayLoad(); + + //assert + expect(payload.name).toBe(expirelessObject.name); + expect(payload.exp).toBeUndefined(); + expect(payload.getExpirationDate()).toBe(null); + expect(payload.isExpired()).toBe(false); + }); + + it("with unexpired instance should decode", () => { + let encoded = Base64.encode(JSON.stringify(unexpiredObject)); + let tokenValue = tokenFormatString.replace('{0}', encoded); + const token = new Token(tokenValue); + + //act + let payload = token.decodePayLoad(); + + //assert + expect(payload.name).toBe(unexpiredObject.name); + expect(payload.exp).toBe(unexpiredObject.exp); + expect(payload.isExpired()).toBe(false); + }); + + it("with expired instance should decode", () => { + let encoded = Base64.encode(JSON.stringify(expiredObject)); + let tokenValue = tokenFormatString.replace('{0}', encoded); + const token: Token = new Token(tokenValue); + + //act + let payload = token.decodePayLoad(); + + //assert + expect(payload.name).toBe(expiredObject.name); + expect(payload.exp).toBe(expiredObject.exp); + expect(payload.isExpired()).toBe(true); + }); + + }); + +}); + diff --git a/token.ts b/token.ts new file mode 100644 index 00000000..8faa9914 --- /dev/null +++ b/token.ts @@ -0,0 +1,82 @@ +import {JwtHelper} from "./angular2-jwt"; + +export class Token { + + private _value: string = null; + private _payload: IPayload = null; + + public get value(): string { + return this._value; + } + + constructor(value: string) { + + Token._decodeInternal(value); + this._value = value; + } + + public static isToken(value: string): boolean { + try { + Token._decodeInternal(value); + return true; + } + catch (e) { + }; + return false; + } + + public equals(token: Token): boolean { + if (token.value === this.value) return true; + return false; + } + + public decodePayLoad(): any { + return this._payload = this._payload || Token._decodeInternal(this._value); + } + + private static _decodeInternal(value: string): IPayload { + + let parts = value.split('.'); + + if (parts.length !== 3) { + throw new Error('Failed to decode. A JWT token must contain 3 parts.'); + } + + let decoded = new JwtHelper().urlBase64Decode(parts[1]); + if (!decoded) { + throw new Error('Failed to decode. The token must be base64 encoded.'); + } + + var instance = JSON.parse(decoded); + this.makePayLoadImplementation(instance); + return instance; + } + + private static makePayLoadImplementation(instance: any) { + instance.getExpirationDate = function() { + if (!instance.hasOwnProperty('exp')) { + return null; + } + let date = new Date(0); // The 0 here is the key, which sets the date to the epoch + date.setUTCSeconds(instance.exp); + + return date; + }; + instance.isExpired = function(offsetSeconds?: number) { + let date = instance.getExpirationDate(); + offsetSeconds = offsetSeconds || 0; + + if (date == null) { + return false; + } + + // Token expired? + return !(date.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000))); + }; + }; +} + +interface IPayload { + exp: string; + getExpirationDate(): boolean; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index bafbd0a7..fffd2315 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,8 @@ "files": [ "angular2-jwt.ts", "angular2-jwt.spec.ts", + "token.ts", + "token.spec.ts", "custom.d.ts" ], "exclude": [