/// /// let expect = chai.expect; let seededChance = new Chance(1); let fixtures = { user : { _self: '/users/1', userId: 1, email: 'joe.bloggs@example.com', firstName: seededChance.first(), lastName: seededChance.last(), password: 'password', phone: seededChance.phone() }, get userResponse(){ return _.omit(fixtures.user, 'password'); }, get authBasic(){ return 'Basic '+btoa(fixtures.user.email+':'+fixtures.user.password) }, buildToken: (overrides = {}) => { let defaultConfig = { header: { alg: 'RS256', typ: 'JWT' }, data: { iss: 'api.spira.io', aud: 'spira.io', sub: fixtures.user.userId, iat: Number(moment().format('X')), exp: Number(moment().add(1, 'hours').format('X')), jti: 'random-hash', '#user': fixtures.userResponse, }, signature: 'this-is-the-signed-hash' }; let token:NgJwtAuth.IJwtToken = _.defaults(overrides, defaultConfig); return btoa(JSON.stringify(token.data)) + '.' + btoa(JSON.stringify(token.data)) + '.' + token.signature ; }, get token(){ return fixtures.buildToken(); //no customisations } }; let defaultAuthServiceProvider:NgJwtAuth.NgJwtAuthServiceProvider; describe('Default configuration', function () { let defaultAuthService:NgJwtAuth.NgJwtAuthService; beforeEach(() => { module('ngJwtAuth', (_ngJwtAuthServiceProvider_) => { defaultAuthServiceProvider = _ngJwtAuthServiceProvider_; //register injection of service provider }); }); it('should have the default endpoints', () => { expect((defaultAuthServiceProvider).config.apiEndpoints.base).to.equal('/api/auth'); expect((defaultAuthServiceProvider).config.apiEndpoints.login).to.equal('/login'); expect((defaultAuthServiceProvider).config.apiEndpoints.refresh).to.equal('/refresh'); }); beforeEach(()=>{ inject(function(_ngJwtAuthService_){ defaultAuthService = _ngJwtAuthService_; }) }); it('should have the default login endpoint', function() { expect((defaultAuthService).getLoginEndpoint()).to.equal('/api/auth/login'); }); it('should have the default token exchange endpoint', function() { expect((defaultAuthService).getTokenExchangeEndpoint()).to.equal('/api/auth/token'); }); it('should have the default refresh endpoint', function() { expect((defaultAuthService).getRefreshEndpoint()).to.equal('/api/auth/refresh'); }); }); describe('Custom configuration', function () { let authServiceProvider:NgJwtAuth.NgJwtAuthServiceProvider; let customAuthService:NgJwtAuth.NgJwtAuthService; let partialCustomConfig:NgJwtAuth.INgJwtAuthServiceConfig = { tokenLocation: 'token-custom', tokenUser: '#user-custom', apiEndpoints: { base: '/api/auth-custom', login: '/login-custom', tokenExchange: '/token-custom', refresh: '/refresh-custom', }, //storageKeyName: 'NgJwtAuthToken-custom', //intentionally commented out as this will be tested to be the default }; beforeEach(() => { module('ngJwtAuth', (_ngJwtAuthServiceProvider_) => { authServiceProvider = _ngJwtAuthServiceProvider_; //register injection of service provider authServiceProvider.configure(partialCustomConfig); }); }); it('should be able to partially configure the service provider', () => { expect((authServiceProvider).config.apiEndpoints).to.deep.equal(partialCustomConfig.apiEndpoints); //assert that the custom value has come across expect((authServiceProvider).config.storageKeyName).to.deep.equal((authServiceProvider).config.storageKeyName); //assert that the default was not overridden }); beforeEach(()=>{ inject((_ngJwtAuthService_) => { customAuthService = _ngJwtAuthService_; }) }); it('should have the configured login endpoint', function() { expect((customAuthService).getLoginEndpoint()).to.equal('/api/auth-custom/login-custom'); }); }); describe('Service tests', () => { let $httpBackend:ng.IHttpBackendService; let ngJwtAuthService:NgJwtAuth.NgJwtAuthService; beforeEach(()=>{ module('ngJwtAuth'); inject((_$httpBackend_, _ngJwtAuthService_) => { if (!ngJwtAuthService){ //dont rebind, so each test gets the singleton $httpBackend = _$httpBackend_; ngJwtAuthService = _ngJwtAuthService_; //register injected of service provider } }); ngJwtAuthService.init(); }); afterEach(() => { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); describe('Initialisation', () => { it('should be an injectable service', () => { return expect(ngJwtAuthService).to.be.an('object'); }); it('should not be logged in initially', () => { return expect(ngJwtAuthService.loggedIn).to.be.false; }); it('should not be able to retrieve a user on init', () => { return expect(ngJwtAuthService.getUser()).to.be.undefined; }); }); describe('Authentication', () => { it('should process a token and return a user', () => { $httpBackend.expectGET('/api/auth/login').respond({token: fixtures.token}); let authPromise = ngJwtAuthService.authenticateCredentials(fixtures.user.email, fixtures.user.password); expect(authPromise).to.eventually.deep.equal(fixtures.userResponse); $httpBackend.flush(); }); it('should be able to get user info once authenticated', () => { let user = ngJwtAuthService.getUser(); let userPromise = ngJwtAuthService.getPromisedUser(); expect(user).to.deep.equal(fixtures.userResponse); expect(userPromise).eventually.to.deep.equal(fixtures.userResponse); }); it('should have saved the jwt to localstorage', () => { let storageKey = (ngJwtAuthService).config.storageKeyName; expect(window.localStorage.getItem(storageKey)).to.equal(fixtures.token); }); it('should set a default authorisation header for subsequent requests', () => { $httpBackend.expectGET('/any', (headers) => { return headers['Authorization'] == 'Bearer '+fixtures.token; }).respond('foobar'); (ngJwtAuthService).$http.get('/any'); $httpBackend.flush(); }); it('should be able to log out and clear token data', () => { ngJwtAuthService.logout(); expect(ngJwtAuthService.getUser()).to.be.null; $httpBackend.expectGET('/any', (headers) => { return !_.contains(headers, 'Authorization'); //Authorization header has been unset }).respond('foobar'); (ngJwtAuthService).$http.get('/any'); $httpBackend.flush(); return expect(ngJwtAuthService.loggedIn).to.be.false; }) }); describe('Failed authentication', () => { it('should fail promise when server response with an error code', () => { $httpBackend.expectGET('/api/auth/login').respond(404); let authPromise = ngJwtAuthService.authenticateCredentials(fixtures.user.email, fixtures.user.password); expect(authPromise).to.eventually.be.rejectedWith(NgJwtAuth.NgJwtAuthException); $httpBackend.flush(); }); it('should fail promise when authentication fails', () => { $httpBackend.expectGET('/api/auth/login').respond(401); let authPromise = ngJwtAuthService.authenticateCredentials(fixtures.user.email, fixtures.user.password); expect(authPromise).to.eventually.be.rejectedWith(NgJwtAuth.NgJwtAuthException); $httpBackend.flush(); }); it('should fail promise when returned token is invalid', () => { $httpBackend.expectGET('/api/auth/login').respond({token: 'invalid_token'}); let authPromise = ngJwtAuthService.authenticateCredentials(fixtures.user.email, fixtures.user.password); expect(authPromise).to.eventually.be.rejectedWith(NgJwtAuth.NgJwtAuthException); $httpBackend.flush(); }); it('should pass through any http errors that are not unauthorised', () => { $httpBackend.expectGET('/any').respond(403); let $http = (ngJwtAuthService).$http; //get the injected http method let httpResponse = $http.get('/any'); //try to get a resource expect(httpResponse).to.eventually.be.rejected; $httpBackend.flush(); }); }); describe('Require login', () => { it('should throw an exception when a credential promise factory is not set', () => { let testCredentialPromiseFactoryFn = () => { ngJwtAuthService.getPromisedUser(); }; expect(testCredentialPromiseFactoryFn).to.throw(NgJwtAuth.NgJwtAuthException); }); it('should be able to set a credential promise factory', () => { let $q = (ngJwtAuthService).$q; //set credential promise factory ngJwtAuthService.registerCredentialPromiseFactory((currentUser:NgJwtAuth.IUser) : ng.IPromise => { let credentials:NgJwtAuth.ICredentials = { username: fixtures.user.email, password: fixtures.user.password, }; return $q.when(credentials); //immediately resolve }); }); it('should not be able to re-set a credential promise factory', () => { let $q = (ngJwtAuthService).$q; //set credential promise factory let setFactoryFn = () => { ngJwtAuthService.registerCredentialPromiseFactory((currentUser:NgJwtAuth.IUser):ng.IPromise => { let credentials:NgJwtAuth.ICredentials = { username: fixtures.user.email, password: fixtures.user.password, }; return $q.when(credentials); //immediately resolve }); }; expect(setFactoryFn).to.throw(NgJwtAuth.NgJwtAuthException); }); it('should prompt a login promise to be resolved when a 401 occurs, then retry the method', () => { $httpBackend.expectGET('/any').respond(401); let $http = (ngJwtAuthService).$http; //get the injected http method $http.get('/any'); //try to get a resource $httpBackend.expectGET('/api/auth/login', (headers) => { return headers['Authorization'] == fixtures.authBasic; }).respond({token: fixtures.token}); $httpBackend.expectGET('/any').respond('ok'); $httpBackend.flush(); }); it('should be able to wait for a user to authenticate to get a user object', () => { ngJwtAuthService.logout(); //make sure that the service is not logged in. $httpBackend.expectGET('/api/auth/login', (headers) => { return headers['Authorization'] == fixtures.authBasic; }).respond({token: fixtures.token}); let userPromise = ngJwtAuthService.getPromisedUser(); expect(userPromise).to.eventually.deep.equal(fixtures.userResponse); $httpBackend.flush(); }); }); describe('Authenticate with token', () => { beforeEach(() => { ngJwtAuthService.logout(); //make sure that the service is not logged in. }); it ('should be able to authenticate with an arbitrary token', () => { let token = 'abc123'; $httpBackend.expectGET('/api/auth/token', (headers) => { return headers['Authorization'] == 'Token '+token; }).respond({token: fixtures.token}); let authPromise = ngJwtAuthService.exchangeToken(token); expect(authPromise).to.eventually.deep.equal(fixtures.userResponse); $httpBackend.flush(); }); it ('should be able to re-authenticate with an existing token', () => { let refreshFn = () => { ngJwtAuthService.refreshToken(); }; expect(refreshFn).to.throw(NgJwtAuth.NgJwtAuthException); //if not logged it, exception should be thrown on attempt to refresh $httpBackend.expectGET('/api/auth/login').respond({token: fixtures.token}); ngJwtAuthService.authenticateCredentials(fixtures.user.email, fixtures.user.password); $httpBackend.flush(); let updatedToken = fixtures.buildToken({signature:'update-hash'}); $httpBackend.expectGET('/api/auth/refresh', (headers) => { return headers['Authorization'] == 'Bearer '+fixtures.token; }).respond({token: updatedToken}); let refreshPromise = ngJwtAuthService.refreshToken(); expect(refreshPromise).to.eventually.be.fulfilled; refreshPromise.then(()=>{ expect(ngJwtAuthService.rawToken).to.equal(updatedToken); }); $httpBackend.flush(); }); }); }); describe('Service Reloading', () => { let $httpBackend:ng.IHttpBackendService; let ngJwtAuthService:NgJwtAuth.NgJwtAuthService; beforeEach(()=>{ module('ngJwtAuth'); inject((_$httpBackend_, _ngJwtAuthService_) => { $httpBackend = _$httpBackend_; ngJwtAuthService = _ngJwtAuthService_; //register injected of service provider }); let $q = (ngJwtAuthService).$q; ngJwtAuthService.registerCredentialPromiseFactory((currentUser:NgJwtAuth.IUser):ng.IPromise => { let credentials:NgJwtAuth.ICredentials = { username: fixtures.user.email, password: fixtures.user.password, }; return $q.when(credentials); //immediately resolve }); }); afterEach(() => { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); describe('User reloaded before expiry', () => { let clock:Sinon.SinonFakeTimers = sinon.useFakeTimers(); after(() => { clock.restore(); }); it('should use the token from storage on init', () => { window.localStorage.setItem((defaultAuthServiceProvider).config.storageKeyName, fixtures.token); ngJwtAuthService.init(); let userPromise = ngJwtAuthService.getPromisedUser(); expect(userPromise).to.eventually.deep.equal(fixtures.userResponse); return expect(ngJwtAuthService.loggedIn).to.be.true; }); it('should refresh the token when it is about to expire', () => { let tokenExpirySeconds = 60 * 20; //20 mins let expiringToken = fixtures.buildToken({ data: { exp: moment().add(tokenExpirySeconds, 'seconds').format('X') }, signature: 'nearly-expired-token' }); window.localStorage.setItem((ngJwtAuthService).config.storageKeyName, expiringToken); let tickIntervalSeconds = (ngJwtAuthService).config.checkExpiryEverySeconds, refreshBeforeSeconds = (ngJwtAuthService).config.refreshBeforeSeconds, intervalsToRun = (tokenExpirySeconds / tickIntervalSeconds) + 10 //make sure at least the expiry period is ticked over ; ngJwtAuthService.init(); //initialise with the default token $httpBackend.expectGET('/api/auth/refresh', (headers) => { return headers['Authorization'] == 'Bearer '+expiringToken; }).respond({token: fixtures.token}); //as angular's $interval does not seem to be overidden by sinon's clock they both have to be ticked independently for (let i=0; i<=intervalsToRun;i++){ //add clock.tick(1000 * tickIntervalSeconds); //fast forward clock by the configured seconds (ngJwtAuthService).$interval.flush(1000 * tickIntervalSeconds); //fast forward intervals by the configured seconds let latestRefresh = moment((ngJwtAuthService).tokenData.data.exp * 1000).subtract(refreshBeforeSeconds, 'seconds'), nextRefreshOpportunity = moment().add(tickIntervalSeconds) ; if (latestRefresh <= nextRefreshOpportunity){ //after the interval that the token should have refreshed, flush the http request $httpBackend.flush(); } } return expect(ngJwtAuthService.loggedIn).to.be.true; }); it('should not attempt to refresh the token over time when the user has never logged in', () => { ngJwtAuthService.logout(); //make sure user is logged out let tickIntervalSeconds = (ngJwtAuthService).config.checkExpiryEverySeconds, hoursToRun = 4, intervalsToRun = (hoursToRun*60*60) / tickIntervalSeconds ; ngJwtAuthService.init(); //initialise without a token //as angular's $interval does not seem to be overidden by sinon's clock they both have to be ticked independently for (let i=0; i <= intervalsToRun;i++){ //add clock.tick(1000 * tickIntervalSeconds); //fast forward clock by the configured seconds (ngJwtAuthService).$interval.flush(1000 * tickIntervalSeconds); //fast forward intervals by the configured seconds } return expect(ngJwtAuthService.loggedIn).to.be.false; }); }); describe('User reloaded after expiry', () => { let expiredToken = fixtures.buildToken({ data: { exp: moment().subtract(1, 'hour').format('X') }, signature: 'expired-token' }); before(()=>{ window.localStorage.setItem((defaultAuthServiceProvider).config.storageKeyName, expiredToken); }); it('should prompt the user to log in when the loaded token has expired on init', () => { ngJwtAuthService.init(); //after prompt the credentials are immediately supplied, triggering a new auth request $httpBackend.expectGET('/api/auth/login', (headers) => { return headers['Authorization'] == fixtures.authBasic; }).respond({token: fixtures.token}); $httpBackend.flush(); let user = ngJwtAuthService.getUser(); expect(user).to.deep.equal(fixtures.userResponse); expect(ngJwtAuthService.rawToken).to.not.equal(expiredToken); }); }); });