├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── dist ├── http-auth-interceptor.js └── http-auth-interceptor.min.js ├── package.json └── src └── http-auth-interceptor.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | bower_components 4 | components 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012 Witold Szczerba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HTTP Auth Interceptor Module 2 | ============================ 3 | for AngularJS 4 | ------------- 5 | 6 | This is the implementation of the concept described in 7 | [Authentication in AngularJS (or similar) based application](http://espeo.eu/blog/authentication-in-angularjs-or-similar-based-application/). 8 | 9 | There are releases for both AngularJS **1.0.x** and **1.2.x**, 10 | see [releases](https://github.com/witoldsz/angular-http-auth/releases). 11 | 12 | Launch demo [here](http://witoldsz.github.com/angular-http-auth/) 13 | or switch to [gh-pages](https://github.com/witoldsz/angular-http-auth/tree/gh-pages) 14 | branch for source code of the demo. 15 | 16 | Usage 17 | ------ 18 | 19 | - Install via bower: `bower install --save angular-http-auth` 20 | - ...or via npm: `npm install --save angular-http-auth` 21 | - Include as a dependency in your app: `angular.module('myApp', ['http-auth-interceptor'])` 22 | 23 | Manual 24 | ------ 25 | 26 | This module installs $http interceptor and provides the `authService`. 27 | 28 | The $http interceptor does the following: 29 | the configuration object (this is the requested URL, payload and parameters) 30 | of every HTTP 401 response is buffered and everytime it happens, the 31 | `event:auth-loginRequired` message is broadcasted from $rootScope. 32 | 33 | The `authService` has only 2 methods: `loginConfirmed()` and `loginCancelled()`. 34 | 35 | You are responsible to invoke `loginConfirmed()` after user logs in. You may optionally pass in 36 | a data argument to this method which will be passed on to the loginConfirmed 37 | $broadcast. This may be useful, for example if you need to pass through details of the user 38 | that was logged in. The `authService` will then retry all the requests previously failed due 39 | to HTTP 401 response. 40 | 41 | You are responsible to invoke `loginCancelled()` when authentication has been invalidated. You may optionally pass in 42 | a data argument to this method which will be passed on to the loginCancelled 43 | $broadcast. The `authService` will cancel all pending requests previously failed and buffered due 44 | to HTTP 401 response. 45 | 46 | In the event that a requested resource returns an HTTP 403 response (i.e. the user is 47 | authenticated but not authorized to access the resource), the user's request is discarded and 48 | the `event:auth-forbidden` message is broadcast from $rootScope. 49 | 50 | #### Ignoring the 401 interceptor 51 | 52 | Sometimes you might not want the interceptor to intercept a request even if one returns 401 or 403. In a case like this you can add `ignoreAuthModule: true` to the request config. A common use case for this would be, for example, a login request which returns 401 if the login credentials are invalid. 53 | 54 | ### Typical use case: 55 | 56 | * somewhere (some service or controller) the: `$http(...).then(function(response) { do-something-with-response })` is invoked, 57 | * the response of that requests is a **HTTP 401**, 58 | * `http-auth-interceptor` captures the initial request and broadcasts `event:auth-loginRequired`, 59 | * your application intercepts this to e.g. show a login dialog: 60 | * DO NOT REDIRECT anywhere (you can hide your forms), just show login dialog 61 | * once your application figures out the authentication is OK, call: `authService.loginConfirmed()`, 62 | * your initial failed request will now be retried and when proper response is finally received, 63 | the `function(response) {do-something-with-response}` will fire, 64 | * your application will continue as nothing had happened. 65 | 66 | ### Advanced use case: 67 | 68 | #### Sending data to listeners: 69 | You can supply additional data to observers across your application who are listening for `event:auth-loginConfirmed` and `event:auth-loginCancelled`: 70 | 71 | $scope.$on('event:auth-loginConfirmed', function(event, data){ 72 | $rootScope.isLoggedin = true; 73 | $log.log(data) 74 | }); 75 | 76 | $scope.$on('event:auth-loginCancelled', function(event, data){ 77 | $rootScope.isLoggedin = false; 78 | $log.log(data) 79 | }); 80 | 81 | Use the `authService.loginConfirmed([data])` and `authService.loginCancelled([data])` methods to emit data with your login and logout events. 82 | 83 | #### Updating [$http(config)](https://docs.angularjs.org/api/ng/service/$http): 84 | Successful login means that the previous request are ready to be fired again, however now that login has occurred certain aspects of the previous requests might need to be modified on the fly. This is particularly important in a token based authentication scheme where an authorization token should be added to the header. 85 | 86 | The `loginConfirmed` method supports the injection of an Updater function that will apply changes to the http config object. 87 | 88 | authService.loginConfirmed([data], [Updater-Function]) 89 | 90 | //application of tokens to previously fired requests: 91 | var token = response.token; 92 | 93 | authService.loginConfirmed('success', function(config){ 94 | config.headers["Authorization"] = token; 95 | return config; 96 | }) 97 | 98 | The initial failed request will now be retried, all queued http requests will be recalculated using the Updater-Function. 99 | 100 | It is also possible to stop specific request from being retried, by returning ``false`` from the Updater-Function: 101 | 102 | authService.loginConfirmed('success', function(config){ 103 | if (shouldSkipRetryOnSuccess(config)) 104 | return false; 105 | return config; 106 | }) 107 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Witold Szczerba", 3 | "name": "angular-http-auth", 4 | "homepage": "https://github.com/witoldsz/angular-http-auth", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/witoldsz/angular-http-auth.git" 8 | }, 9 | "main": "./src/http-auth-interceptor.js", 10 | "dependencies": { 11 | "angular": "^1.2" 12 | }, 13 | "ignore": [ 14 | "package.json" 15 | ], 16 | "license": "MIT" 17 | } 18 | -------------------------------------------------------------------------------- /dist/http-auth-interceptor.js: -------------------------------------------------------------------------------- 1 | /*global angular:true, browser:true */ 2 | 3 | /** 4 | * @license HTTP Auth Interceptor Module for AngularJS 5 | * (c) 2012 Witold Szczerba 6 | * License: MIT 7 | */ 8 | 9 | (function () { 10 | 'use strict'; 11 | 12 | angular.module('http-auth-interceptor', ['http-auth-interceptor-buffer']) 13 | 14 | .factory('authService', ['$rootScope','httpBuffer', function($rootScope, httpBuffer) { 15 | return { 16 | /** 17 | * Call this function to indicate that authentication was successful and trigger a 18 | * retry of all deferred requests. 19 | * @param data an optional argument to pass on to $broadcast which may be useful for 20 | * example if you need to pass through details of the user that was logged in 21 | * @param configUpdater an optional transformation function that can modify the 22 | * requests that are retried after having logged in. This can be used for example 23 | * to add an authentication token. It must return the request. 24 | */ 25 | loginConfirmed: function(data, configUpdater) { 26 | var updater = configUpdater || function(config) {return config;}; 27 | $rootScope.$broadcast('event:auth-loginConfirmed', data); 28 | httpBuffer.retryAll(updater); 29 | }, 30 | 31 | /** 32 | * Call this function to indicate that authentication should not proceed. 33 | * All deferred requests will be abandoned or rejected (if reason is provided). 34 | * @param data an optional argument to pass on to $broadcast. 35 | * @param reason if provided, the requests are rejected; abandoned otherwise. 36 | */ 37 | loginCancelled: function(data, reason) { 38 | httpBuffer.rejectAll(reason); 39 | $rootScope.$broadcast('event:auth-loginCancelled', data); 40 | } 41 | }; 42 | }]) 43 | 44 | /** 45 | * $http interceptor. 46 | * On 401 response (without 'ignoreAuthModule' option) stores the request 47 | * and broadcasts 'event:auth-loginRequired'. 48 | * On 403 response (without 'ignoreAuthModule' option) discards the request 49 | * and broadcasts 'event:auth-forbidden'. 50 | */ 51 | .config(['$httpProvider', function($httpProvider) { 52 | $httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function($rootScope, $q, httpBuffer) { 53 | return { 54 | responseError: function(rejection) { 55 | var config = rejection.config || {}; 56 | if (!config.ignoreAuthModule) { 57 | switch (rejection.status) { 58 | case 401: 59 | var deferred = $q.defer(); 60 | var bufferLength = httpBuffer.append(config, deferred); 61 | if (bufferLength === 1) 62 | $rootScope.$broadcast('event:auth-loginRequired', rejection); 63 | return deferred.promise; 64 | case 403: 65 | $rootScope.$broadcast('event:auth-forbidden', rejection); 66 | break; 67 | } 68 | } 69 | // otherwise, default behaviour 70 | return $q.reject(rejection); 71 | } 72 | }; 73 | }]); 74 | }]); 75 | 76 | /** 77 | * Private module, a utility, required internally by 'http-auth-interceptor'. 78 | */ 79 | angular.module('http-auth-interceptor-buffer', []) 80 | 81 | .factory('httpBuffer', ['$injector', function($injector) { 82 | /** Holds all the requests, so they can be re-requested in future. */ 83 | var buffer = []; 84 | 85 | /** Service initialized later because of circular dependency problem. */ 86 | var $http; 87 | 88 | function retryHttpRequest(config, deferred) { 89 | function successCallback(response) { 90 | deferred.resolve(response); 91 | } 92 | function errorCallback(response) { 93 | deferred.reject(response); 94 | } 95 | $http = $http || $injector.get('$http'); 96 | $http(config).then(successCallback, errorCallback); 97 | } 98 | 99 | return { 100 | /** 101 | * Appends HTTP request configuration object with deferred response attached to buffer. 102 | * @return {Number} The new length of the buffer. 103 | */ 104 | append: function(config, deferred) { 105 | return buffer.push({ 106 | config: config, 107 | deferred: deferred 108 | }); 109 | }, 110 | 111 | /** 112 | * Abandon or reject (if reason provided) all the buffered requests. 113 | */ 114 | rejectAll: function(reason) { 115 | if (reason) { 116 | for (var i = 0; i < buffer.length; ++i) { 117 | buffer[i].deferred.reject(reason); 118 | } 119 | } 120 | buffer = []; 121 | }, 122 | 123 | /** 124 | * Retries all the buffered requests clears the buffer. 125 | */ 126 | retryAll: function(updater) { 127 | for (var i = 0; i < buffer.length; ++i) { 128 | var _cfg = updater(buffer[i].config); 129 | if (_cfg !== false) 130 | retryHttpRequest(_cfg, buffer[i].deferred); 131 | } 132 | buffer = []; 133 | } 134 | }; 135 | }]); 136 | })(); 137 | 138 | /* commonjs package manager support (eg componentjs) */ 139 | if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ 140 | module.exports = 'http-auth-interceptor'; 141 | } 142 | -------------------------------------------------------------------------------- /dist/http-auth-interceptor.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";angular.module("http-auth-interceptor",["http-auth-interceptor-buffer"]).factory("authService",["$rootScope","httpBuffer",function($rootScope,httpBuffer){return{loginConfirmed:function(data,configUpdater){var updater=configUpdater||function(config){return config};$rootScope.$broadcast("event:auth-loginConfirmed",data),httpBuffer.retryAll(updater)},loginCancelled:function(data,reason){httpBuffer.rejectAll(reason),$rootScope.$broadcast("event:auth-loginCancelled",data)}}}]).config(["$httpProvider",function($httpProvider){$httpProvider.interceptors.push(["$rootScope","$q","httpBuffer",function($rootScope,$q,httpBuffer){return{responseError:function(rejection){var config=rejection.config||{};if(!config.ignoreAuthModule)switch(rejection.status){case 401:var deferred=$q.defer(),bufferLength=httpBuffer.append(config,deferred);return 1===bufferLength&&$rootScope.$broadcast("event:auth-loginRequired",rejection),deferred.promise;case 403:$rootScope.$broadcast("event:auth-forbidden",rejection)}return $q.reject(rejection)}}}])}]),angular.module("http-auth-interceptor-buffer",[]).factory("httpBuffer",["$injector",function($injector){function retryHttpRequest(config,deferred){function successCallback(response){deferred.resolve(response)}function errorCallback(response){deferred.reject(response)}$http=$http||$injector.get("$http"),$http(config).then(successCallback,errorCallback)}var $http,buffer=[];return{append:function(config,deferred){return buffer.push({config:config,deferred:deferred})},rejectAll:function(reason){if(reason)for(var i=0;i dist/http-auth-interceptor.js", 31 | "version": "npm run build && git add -A dist", 32 | "postversion": "git push && git push --tags" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/http-auth-interceptor.js: -------------------------------------------------------------------------------- 1 | /*global angular:true, browser:true */ 2 | 3 | /** 4 | * @license HTTP Auth Interceptor Module for AngularJS 5 | * (c) 2012 Witold Szczerba 6 | * License: MIT 7 | */ 8 | 9 | (function () { 10 | 'use strict'; 11 | 12 | angular.module('http-auth-interceptor', ['http-auth-interceptor-buffer']) 13 | 14 | .factory('authService', ['$rootScope','httpBuffer', function($rootScope, httpBuffer) { 15 | return { 16 | /** 17 | * Call this function to indicate that authentication was successful and trigger a 18 | * retry of all deferred requests. 19 | * @param data an optional argument to pass on to $broadcast which may be useful for 20 | * example if you need to pass through details of the user that was logged in 21 | * @param configUpdater an optional transformation function that can modify the 22 | * requests that are retried after having logged in. This can be used for example 23 | * to add an authentication token. It must return the request. 24 | */ 25 | loginConfirmed: function(data, configUpdater) { 26 | var updater = configUpdater || function(config) {return config;}; 27 | $rootScope.$broadcast('event:auth-loginConfirmed', data); 28 | httpBuffer.retryAll(updater); 29 | }, 30 | 31 | /** 32 | * Call this function to indicate that authentication should not proceed. 33 | * All deferred requests will be abandoned or rejected (if reason is provided). 34 | * @param data an optional argument to pass on to $broadcast. 35 | * @param reason if provided, the requests are rejected; abandoned otherwise. 36 | */ 37 | loginCancelled: function(data, reason) { 38 | httpBuffer.rejectAll(reason); 39 | $rootScope.$broadcast('event:auth-loginCancelled', data); 40 | } 41 | }; 42 | }]) 43 | 44 | /** 45 | * $http interceptor. 46 | * On 401 response (without 'ignoreAuthModule' option) stores the request 47 | * and broadcasts 'event:auth-loginRequired'. 48 | * On 403 response (without 'ignoreAuthModule' option) discards the request 49 | * and broadcasts 'event:auth-forbidden'. 50 | */ 51 | .config(['$httpProvider', function($httpProvider) { 52 | $httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function($rootScope, $q, httpBuffer) { 53 | return { 54 | responseError: function(rejection) { 55 | var config = rejection.config || {}; 56 | if (!config.ignoreAuthModule) { 57 | switch (rejection.status) { 58 | case 401: 59 | var deferred = $q.defer(); 60 | var bufferLength = httpBuffer.append(config, deferred); 61 | if (bufferLength === 1) 62 | $rootScope.$broadcast('event:auth-loginRequired', rejection); 63 | return deferred.promise; 64 | case 403: 65 | $rootScope.$broadcast('event:auth-forbidden', rejection); 66 | break; 67 | } 68 | } 69 | // otherwise, default behaviour 70 | return $q.reject(rejection); 71 | } 72 | }; 73 | }]); 74 | }]); 75 | 76 | /** 77 | * Private module, a utility, required internally by 'http-auth-interceptor'. 78 | */ 79 | angular.module('http-auth-interceptor-buffer', []) 80 | 81 | .factory('httpBuffer', ['$injector', function($injector) { 82 | /** Holds all the requests, so they can be re-requested in future. */ 83 | var buffer = []; 84 | 85 | /** Service initialized later because of circular dependency problem. */ 86 | var $http; 87 | 88 | function retryHttpRequest(config, deferred) { 89 | function successCallback(response) { 90 | deferred.resolve(response); 91 | } 92 | function errorCallback(response) { 93 | deferred.reject(response); 94 | } 95 | $http = $http || $injector.get('$http'); 96 | $http(config).then(successCallback, errorCallback); 97 | } 98 | 99 | return { 100 | /** 101 | * Appends HTTP request configuration object with deferred response attached to buffer. 102 | * @return {Number} The new length of the buffer. 103 | */ 104 | append: function(config, deferred) { 105 | return buffer.push({ 106 | config: config, 107 | deferred: deferred 108 | }); 109 | }, 110 | 111 | /** 112 | * Abandon or reject (if reason provided) all the buffered requests. 113 | */ 114 | rejectAll: function(reason) { 115 | if (reason) { 116 | for (var i = 0; i < buffer.length; ++i) { 117 | buffer[i].deferred.reject(reason); 118 | } 119 | } 120 | buffer = []; 121 | }, 122 | 123 | /** 124 | * Retries all the buffered requests clears the buffer. 125 | */ 126 | retryAll: function(updater) { 127 | for (var i = 0; i < buffer.length; ++i) { 128 | var _cfg = updater(buffer[i].config); 129 | if (_cfg !== false) 130 | retryHttpRequest(_cfg, buffer[i].deferred); 131 | } 132 | buffer = []; 133 | } 134 | }; 135 | }]); 136 | })(); 137 | 138 | /* commonjs package manager support (eg componentjs) */ 139 | if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ 140 | module.exports = 'http-auth-interceptor'; 141 | } 142 | --------------------------------------------------------------------------------