├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── angular-oauth2.js └── angular-oauth2.min.js ├── gulpfile.js ├── karma.conf.js ├── package.json ├── src ├── angular-oauth2.js ├── config │ └── oauth-config.js ├── interceptors │ └── oauth-interceptor.js └── providers │ ├── oauth-provider.js │ └── oauth-token-provider.js └── test ├── mocks └── angular-cookies.mock.js └── unit ├── config └── oauth-config.spec.js ├── interceptors └── oauth-interceptor.spec.js └── providers ├── oauth-provider.spec.js └── oauth-token-provider.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Configuration File 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise": true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase": false, // true: Identifiers must be in camelCase 10 | "curly": true, // true: Require {} for every new block or scope 11 | "eqeqeq": true, // true: Require triple equals (===) for comparison 12 | "forin": true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze": true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed": true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent": 2, // {int} Number of spaces to use for indentation 16 | "latedef": false, // true: Require variables/functions to be defined before being used 17 | "newcap": true, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg": true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty": true, // true: Prohibit use of empty blocks 20 | "nonbsp": true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew": false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus": false, // true: Prohibit use of `++` & `--` 23 | "quotmark": "single", // Quotation mark consistency 24 | "undef": true, // true: Require all non-global variables to be declared (prevents global leaks) 25 | "unused": true, // true: Require all defined variables be used 26 | "strict": false, // true: Requires all functions run in ES5 Strict Mode 27 | "maxparams": false, // {int} Max number of formal params allowed per function 28 | "maxdepth": false, // {int} Max depth of nested blocks (within functions) 29 | "maxstatements": false, // {int} Max number statements per function 30 | "maxcomplexity": false, // {int} Max cyclomatic complexity per function 31 | "maxlen": false, // {int} Max number of characters per line 32 | 33 | // Relaxing 34 | "asi": false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 35 | "boss": false, // true: Tolerate assignments where comparisons would be expected 36 | "debug": false, // true: Allow debugger statements e.g. browser breakpoints. 37 | "eqnull": false, // true: Tolerate use of `== null` 38 | "es5": false, // true: Allow ES5 syntax (ex: getters and setters) 39 | "esnext": true, // true: Allow ES.next (ES6) syntax (ex: `const`) 40 | "moz": false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 41 | "evil": false, // true: Tolerate use of `eval` and `new Function()` 42 | "expr": false, // true: Tolerate `ExpressionStatement` as Programs 43 | "funcscope": false, // true: Tolerate defining variables inside control statements 44 | "globalstrict": false, // true: Allow global "use strict" (also enables 'strict') 45 | "iterator": false, // true: Tolerate using the `__iterator__` property 46 | "lastsemic": false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 47 | "laxbreak": false, // true: Tolerate possibly unsafe line breakings 48 | "laxcomma": false, // true: Tolerate comma-first style coding 49 | "loopfunc": false, // true: Tolerate functions being defined in loops 50 | "multistr": false, // true: Tolerate multi-line strings 51 | "noyield": false, // true: Tolerate generator functions with no yield statement in them. 52 | "notypeof": false, // true: Tolerate invalid typeof operator values 53 | "proto": false, // true: Tolerate using the `__proto__` property 54 | "scripturl": false, // true: Tolerate script-targeted URLs 55 | "shadow": false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 56 | "sub": false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 57 | "supernew": false, // true: Tolerate `new function () { ... };` and `new Object;` 58 | "validthis": false, // true: Tolerate using this in a non-constructor function 59 | 60 | // Environments 61 | "browser": true, // Web Browser (window, document, etc) 62 | "browserify": true, // Browserify (node.js code in the browser) 63 | "node": true, // Node.js 64 | 65 | // Custom Globals 66 | "globals": { 67 | "angular": false 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | 9 | git: 10 | depth: 10 11 | 12 | node_js: 13 | - 4.3 14 | 15 | cache: 16 | directories: 17 | - node_modules 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### 4.2.0 / 2018-01-24 4 | - [#127](https://github.com/oauthjs/angular-oauth2/pull/127) Prevent loading angular multiple times with webpack (@erhardos) 5 | 6 | ### 4.1.1 / 2017-04-03 7 | - [#118](https://github.com/oauthjs/angular-oauth2/pull/118) Validate object property access (@hitmanmcc) 8 | - [#107](https://github.com/oauthjs/angular-oauth2/pull/107) Updated readme (@anteriovieira) 9 | 10 | ### 4.1.0 / 2016-11-03 11 | - [#87](https://github.com/oauthjs/angular-oauth2/pull/87) allow overriding oauth base config using options (@lionelB) 12 | - [#96](https://github.com/oauthjs/angular-oauth2/pull/96) Specify the type of grant used (#96) (@Timokasse) 13 | 14 | ### 4.0.0 / 2016-02-12 15 | - [#80](https://github.com/oauthjs/angular-oauth2/pull/80) Reintroduce authorization header to be overridden (@ruipenso) 16 | 17 | ### 3.1.1 / 2016-02-12 18 | - [#79](https://github.com/oauthjs/angular-oauth2/pull/79) Update dependencies (@ruipenso) 19 | 20 | ### 3.1.0 / 2016-02-10 21 | - [#78](https://github.com/oauthjs/angular-oauth2/pull/78) Update OAuth methods to allow `data` and `options` override (@ruipenso) 22 | - [#71](https://github.com/oauthjs/angular-oauth2/pull/71) Add `client_id` and `client_secret` on revoke_token (@tinogomes) 23 | - [#77](https://github.com/oauthjs/angular-oauth2/pull/77) Update interceptor to allow authorization header to be overridden (@ruipenso) 24 | - [#75](https://github.com/oauthjs/angular-oauth2/pull/75) Update README dependencies (@ruipenso) 25 | - [#76](https://github.com/oauthjs/angular-oauth2/pull/76) Update `package.json` (@ruipenso) 26 | 27 | ### 3.0.1 / 2015-06-01 28 | - [#27](https://github.com/oauthjs/angular-oauth2/pull/27) Add travis-ci configuration (@seegno) 29 | 30 | ### 3.0.0 / 2015-06-01 31 | - [#49](https://github.com/oauthjs/angular-oauth2/pull/49) Add changelog task (@ruipenso) 32 | - [#48](https://github.com/oauthjs/angular-oauth2/pull/48) Update readme (@ruipenso) 33 | - [#25](https://github.com/oauthjs/angular-oauth2/pull/25) Replace ipCookie with ngCookies (@seegno) 34 | - [#28](https://github.com/oauthjs/angular-oauth2/pull/28) Add methods to get/set `token` property (@seegno) 35 | - [#47](https://github.com/oauthjs/angular-oauth2/pull/47) Add unauthorized error interception (@ruipenso) 36 | 37 | ### 2.1.1 / 2015-05-28 38 | - [#40](https://github.com/oauthjs/angular-oauth2/pull/40) Fix missing dependency on readme example (@ruipenso) 39 | 40 | ### 2.1.0 / 2015-03-09 41 | - [#15](https://github.com/oauthjs/angular-oauth2/pull/15) Add `clientSecret` as optional (@ruipenso) 42 | - [#18](https://github.com/oauthjs/angular-oauth2/pull/18) Remove npm postinstall script (@ruipenso) 43 | - [#14](https://github.com/oauthjs/angular-oauth2/pull/14) Fix readme configuration (@ruipenso) 44 | 45 | ### 2.0.0 / 2015-02-04 46 | - [#11](https://github.com/oauthjs/angular-oauth2/pull/11) Add options to `ipCookie.remove()` on OAuthToken (@ruipenso) 47 | - [#10](https://github.com/oauthjs/angular-oauth2/pull/10) Update `oauthInterceptor` responseError handling (@ruipenso) 48 | - [#7](https://github.com/oauthjs/angular-oauth2/pull/7) Fix readme typo of download url (@seegno) 49 | - [#6](https://github.com/oauthjs/angular-oauth2/pull/6) README.md: typo fix (@AdirAmsalem) 50 | 51 | ### 1.0.2 / 2015-01-19 52 | - [#4](https://github.com/oauthjs/angular-oauth2/pull/4) Add support for higher dependencies versions (@seegno) 53 | 54 | ### 1.0.1 / 2015-01-19 55 | - [#3](https://github.com/oauthjs/angular-oauth2/pull/3) Fix bower ignore list (@ruipenso) 56 | 57 | ### 1.0.0 / 2015-01-18 58 | - [#2](https://github.com/oauthjs/angular-oauth2/pull/2) Fix indentation in README examples (@fixe) 59 | - [#1](https://github.com/oauthjs/angular-oauth2/pull/1) Add project structure (@ruipenso) 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Seegno 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-oauth2 [![Build Status](https://travis-ci.org/seegno/angular-oauth2.svg)](https://travis-ci.org/seegno/angular-oauth2) 2 | 3 | AngularJS OAuth2 authentication module written in ES6. 4 | 5 | Currently `angular-oauth2` only uses the [Resouce Owner Password Credential Grant](https://tools.ietf.org/html/rfc6749#section-4.3), i.e, using a credentials combination (username, password), we'll request an access token (using `grant_type='password'`) which, in case of success, will typically return a response such as: 6 | 7 | ``` 8 | { 9 | "access_token": "foobar", 10 | "token_type": "Bearer", 11 | "expires_in": 3600, 12 | "refresh_token": "foobiz" 13 | } 14 | ``` 15 | Internally we'll automatically store it as a cookie and it will be used in every request adding an `Authorization` header: `Authorization: 'Bearer foobar'`. 16 | 17 | --- 18 | 19 | ## Installation 20 | 21 | Choose your preferred method: 22 | 23 | * Bower: `bower install angular-oauth2` 24 | * NPM: `npm install --save angular-oauth2` 25 | * Download: [angular-oauth2](https://raw.github.com/seegno/angular-oauth2/master/dist/angular-oauth2.min.js) 26 | 27 | ## Usage 28 | 29 | ###### 1. Download `angular-oauth2` dependencies. 30 | 31 | * [angular](https://github.com/angular/bower-angular) 32 | * [angular-cookies](https://github.com/angular/bower-angular-cookies) 33 | * [query-string](https://github.com/sindresorhus/query-string) 34 | 35 | If you're using `bower` they will be automatically downloaded upon installing this library. 36 | 37 | ###### 2. Include `angular-oauth2` and dependencies. 38 | 39 | ```html 40 | 41 | 42 | 43 | 44 | ``` 45 | 46 | ###### 3. Configure `OAuth` (optional) and `OAuthToken` (optional): 47 | 48 | ```js 49 | angular.module('myApp', ['angular-oauth2']) 50 | .config(['OAuthProvider', function(OAuthProvider) { 51 | OAuthProvider.configure({ 52 | baseUrl: 'https://api.website.com', 53 | clientId: 'CLIENT_ID', 54 | clientSecret: 'CLIENT_SECRET' // optional 55 | }); 56 | }]); 57 | ``` 58 | 59 | You can also configure `OAuth` service in a `.run()` block, in case you retrieve the Oauth server configuration from a ajax request. 60 | 61 | ```js 62 | angular.module('myApp', ['angular-oauth2']) 63 | .run(['OAuth', function(OAuth) { 64 | OAuth.configure({ 65 | baseUrl: 'https://api.website.com', 66 | clientId: 'CLIENT_ID', 67 | clientSecret: 'CLIENT_SECRET' // optional 68 | }); 69 | }]); 70 | ``` 71 | 72 | ###### 4. Catch `OAuth` errors and do something with them (optional): 73 | 74 | ```js 75 | angular.module('myApp', ['angular-oauth2']) 76 | .run(['$rootScope', '$window', 'OAuth', function($rootScope, $window, OAuth) { 77 | $rootScope.$on('oauth:error', function(event, rejection) { 78 | // Ignore `invalid_grant` error - should be catched on `LoginController`. 79 | if ('invalid_grant' === rejection.data.error) { 80 | return; 81 | } 82 | 83 | // Refresh token when a `invalid_token` error occurs. 84 | if ('invalid_token' === rejection.data.error) { 85 | return OAuth.getRefreshToken(); 86 | } 87 | 88 | // Redirect to `/login` with the `error_reason`. 89 | return $window.location.href = '/login?error_reason=' + rejection.data.error; 90 | }); 91 | }]); 92 | ``` 93 | 94 | ## API 95 | 96 | #### OAuthProvider 97 | 98 | Configuration defaults: 99 | 100 | ```js 101 | OAuthProvider.configure({ 102 | baseUrl: null, 103 | clientId: null, 104 | clientSecret: null, 105 | grantPath: '/oauth2/token', 106 | revokePath: '/oauth2/revoke' 107 | }); 108 | ``` 109 | 110 | #### OAuth 111 | 112 | Update configuration defaults: 113 | 114 | ```js 115 | OAuth.configure({ 116 | baseUrl: null, 117 | clientId: null, 118 | clientSecret: null, 119 | grantPath: '/oauth2/token', 120 | revokePath: '/oauth2/revoke' 121 | }); 122 | 123 | ``` 124 | Check authentication status: 125 | 126 | ```js 127 | /** 128 | * Verifies if the `user` is authenticated or not based on the `token` 129 | * cookie. 130 | * 131 | * @return {boolean} 132 | */ 133 | 134 | OAuth.isAuthenticated(); 135 | ``` 136 | 137 | Get an access token: 138 | 139 | ```js 140 | /** 141 | * Retrieves the `access_token` and stores the `response.data` on cookies 142 | * using the `OAuthToken`. 143 | * 144 | * @param {object} user - Object with `username` and `password` properties. 145 | * @param {object} config - Optional configuration object sent to `POST`. 146 | * @return {promise} A response promise. 147 | */ 148 | 149 | OAuth.getAccessToken(user, options); 150 | ``` 151 | 152 | Refresh access token: 153 | 154 | ```js 155 | /** 156 | * Retrieves the `refresh_token` and stores the `response.data` on cookies 157 | * using the `OAuthToken`. 158 | * 159 | * @return {promise} A response promise. 160 | */ 161 | 162 | OAuth.getRefreshToken() 163 | ``` 164 | 165 | Revoke access token: 166 | 167 | ```js 168 | /** 169 | * Revokes the `token` and removes the stored `token` from cookies 170 | * using the `OAuthToken`. 171 | * 172 | * @return {promise} A response promise. 173 | */ 174 | 175 | OAuth.revokeToken() 176 | ``` 177 | 178 | **NOTE**: An *event* `oauth:error` will be sent everytime a `responseError` is emitted: 179 | 180 | * `{ status: 400, data: { error: 'invalid_request' }` 181 | * `{ status: 400, data: { error: 'invalid_grant' }` 182 | * `{ status: 401, data: { error: 'invalid_token' }` 183 | * `{ status: 401, headers: { 'www-authenticate': 'Bearer realm="example"' } }` 184 | 185 | #### OAuthTokenProvider 186 | 187 | `OAuthTokenProvider` uses [angular-cookies](https://github.com/angular/bower-angular-cookies) to store the cookies. Check the [available options](https://code.angularjs.org/1.4.0/docs/api/ngCookies/service/$cookies). 188 | 189 | Configuration defaults: 190 | 191 | ```js 192 | OAuthTokenProvider.configure({ 193 | name: 'token', 194 | options: { 195 | secure: true 196 | } 197 | }); 198 | ``` 199 | 200 | #### OAuthToken 201 | 202 | If you want to manage the `token` yourself you can use `OAuthToken` service. 203 | Please check the [OAuthToken](https://github.com/seegno/angular-oauth2/blob/master/src/providers/oauth-token-provider.js#L45) source code to see all the available methods. 204 | 205 | ## Contributing & Development 206 | 207 | #### Contribute 208 | 209 | Found a bug or want to suggest something? Take a look first on the current and closed [issues](https://github.com/seegno/angular-oauth2/issues). If it is something new, please [submit an issue](https://github.com/seegno/angular-oauth2/issues/new). 210 | 211 | #### Develop 212 | 213 | It will be awesome if you can help us evolve `angular-oauth2`. Want to help? 214 | 215 | 1. [Fork it](https://github.com/seegno/angular-oauth2). 216 | 2. `npm install`. 217 | 3. Do your magic. 218 | 4. Run the tests: `gulp test`. 219 | 5. Build: `gulp build` 220 | 6. Create a [Pull Request](https://github.com/seegno/angular-oauth2/compare). 221 | 222 | *The source files are written in ES6.* 223 | 224 | ## Reference 225 | 226 | * http://tools.ietf.org/html/rfc2617 227 | * http://tools.ietf.org/html/rfc6749 228 | * http://tools.ietf.org/html/rfc6750 229 | * https://tools.ietf.org/html/rfc7009 230 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-oauth2", 3 | "version": "4.2.0", 4 | "description": "AngularJS OAuth2", 5 | "main": "./dist/angular-oauth2.js", 6 | "authors": [ 7 | "Seegno " 8 | ], 9 | "keywords": [ 10 | "AngularJS", 11 | "Authentication", 12 | "OAuth2" 13 | ], 14 | "license": "MIT", 15 | "homepage": "https://github.com/seegno/angular-oauth2", 16 | "ignore": [ 17 | "**/.*", 18 | "bower_components", 19 | "gulpfile.js", 20 | "karma.conf.js", 21 | "node_modules", 22 | "package.json", 23 | "test" 24 | ], 25 | "dependencies": { 26 | "angular": "1.5.9", 27 | "angular-cookies": "1.5.9", 28 | "query-string": "^1.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dist/angular-oauth2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-oauth2 - Angular OAuth2 3 | * @version v4.1.1 4 | * @link https://github.com/seegno/angular-oauth2 5 | * @license MIT 6 | */ 7 | (function(root, factory) { 8 | if (typeof define === "function" && define.amd) { 9 | define([ "angular", "angular-cookies", "query-string" ], factory); 10 | } else if (typeof exports === "object") { 11 | module.exports = factory(require("angular"), require("angular-cookies"), require("query-string")); 12 | } else { 13 | root.angularOAuth2 = factory(root.angular, "ngCookies", root.queryString); 14 | } 15 | })(this, function(angular, ngCookies, queryString) { 16 | var ngModule = angular.module("angular-oauth2", [ ngCookies ]).config(oauthConfig).factory("oauthInterceptor", oauthInterceptor).provider("OAuth", OAuthProvider).provider("OAuthToken", OAuthTokenProvider); 17 | function oauthConfig($httpProvider) { 18 | $httpProvider.interceptors.push("oauthInterceptor"); 19 | } 20 | oauthConfig.$inject = [ "$httpProvider" ]; 21 | var _createClass = function() { 22 | function defineProperties(target, props) { 23 | for (var i = 0; i < props.length; i++) { 24 | var descriptor = props[i]; 25 | descriptor.enumerable = descriptor.enumerable || false; 26 | descriptor.configurable = true; 27 | if ("value" in descriptor) descriptor.writable = true; 28 | Object.defineProperty(target, descriptor.key, descriptor); 29 | } 30 | } 31 | return function(Constructor, protoProps, staticProps) { 32 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 33 | if (staticProps) defineProperties(Constructor, staticProps); 34 | return Constructor; 35 | }; 36 | }(); 37 | function _classCallCheck(instance, Constructor) { 38 | if (!(instance instanceof Constructor)) { 39 | throw new TypeError("Cannot call a class as a function"); 40 | } 41 | } 42 | var defaults = { 43 | baseUrl: null, 44 | clientId: null, 45 | clientSecret: null, 46 | grantPath: "/oauth2/token", 47 | revokePath: "/oauth2/revoke" 48 | }; 49 | var requiredKeys = [ "baseUrl", "clientId", "grantPath", "revokePath" ]; 50 | function OAuthProvider() { 51 | var _this = this; 52 | var sanitizeConfigParams = function sanitizeConfigParams(params) { 53 | if (!(params instanceof Object)) { 54 | throw new TypeError("Invalid argument: `config` must be an `Object`."); 55 | } 56 | var config = angular.extend({}, defaults, params); 57 | angular.forEach(requiredKeys, function(key) { 58 | if (!config[key]) { 59 | throw new Error("Missing parameter: " + key + "."); 60 | } 61 | }); 62 | if ("/" === config.baseUrl.substr(-1)) { 63 | config.baseUrl = config.baseUrl.slice(0, -1); 64 | } 65 | if ("/" !== config.grantPath[0]) { 66 | config.grantPath = "/" + config.grantPath; 67 | } 68 | if ("/" !== config.revokePath[0]) { 69 | config.revokePath = "/" + config.revokePath; 70 | } 71 | return config; 72 | }; 73 | this.configure = function(params) { 74 | _this.defaultConfig = sanitizeConfigParams(params); 75 | }; 76 | this.$get = function($http, OAuthToken) { 77 | var OAuth = function() { 78 | function OAuth(config) { 79 | _classCallCheck(this, OAuth); 80 | this.config = config; 81 | } 82 | _createClass(OAuth, [ { 83 | key: "configure", 84 | value: function configure(params) { 85 | this.config = sanitizeConfigParams(params); 86 | } 87 | }, { 88 | key: "isAuthenticated", 89 | value: function isAuthenticated() { 90 | return !!OAuthToken.getToken(); 91 | } 92 | }, { 93 | key: "getAccessToken", 94 | value: function getAccessToken(data, options) { 95 | data = angular.extend({ 96 | client_id: this.config.clientId, 97 | grant_type: "password" 98 | }, data); 99 | if (null !== this.config.clientSecret) { 100 | data.client_secret = this.config.clientSecret; 101 | } 102 | data = queryString.stringify(data); 103 | options = angular.extend({ 104 | headers: { 105 | Authorization: undefined, 106 | "Content-Type": "application/x-www-form-urlencoded" 107 | } 108 | }, options); 109 | return $http.post("" + this.config.baseUrl + this.config.grantPath, data, options).then(function(response) { 110 | OAuthToken.setToken(response.data); 111 | return response; 112 | }); 113 | } 114 | }, { 115 | key: "getRefreshToken", 116 | value: function getRefreshToken(data, options) { 117 | data = angular.extend({ 118 | client_id: this.config.clientId, 119 | grant_type: "refresh_token", 120 | refresh_token: OAuthToken.getRefreshToken() 121 | }, data); 122 | if (null !== this.config.clientSecret) { 123 | data.client_secret = this.config.clientSecret; 124 | } 125 | data = queryString.stringify(data); 126 | options = angular.extend({ 127 | headers: { 128 | Authorization: undefined, 129 | "Content-Type": "application/x-www-form-urlencoded" 130 | } 131 | }, options); 132 | return $http.post("" + this.config.baseUrl + this.config.grantPath, data, options).then(function(response) { 133 | OAuthToken.setToken(response.data); 134 | return response; 135 | }); 136 | } 137 | }, { 138 | key: "revokeToken", 139 | value: function revokeToken(data, options) { 140 | var refreshToken = OAuthToken.getRefreshToken(); 141 | data = angular.extend({ 142 | client_id: this.config.clientId, 143 | token: refreshToken ? refreshToken : OAuthToken.getAccessToken(), 144 | token_type_hint: refreshToken ? "refresh_token" : "access_token" 145 | }, data); 146 | if (null !== this.config.clientSecret) { 147 | data.client_secret = this.config.clientSecret; 148 | } 149 | data = queryString.stringify(data); 150 | options = angular.extend({ 151 | headers: { 152 | "Content-Type": "application/x-www-form-urlencoded" 153 | } 154 | }, options); 155 | return $http.post("" + this.config.baseUrl + this.config.revokePath, data, options).then(function(response) { 156 | OAuthToken.removeToken(); 157 | return response; 158 | }); 159 | } 160 | } ]); 161 | return OAuth; 162 | }(); 163 | return new OAuth(this.defaultConfig); 164 | }; 165 | this.$get.$inject = [ "$http", "OAuthToken" ]; 166 | } 167 | var _createClass = function() { 168 | function defineProperties(target, props) { 169 | for (var i = 0; i < props.length; i++) { 170 | var descriptor = props[i]; 171 | descriptor.enumerable = descriptor.enumerable || false; 172 | descriptor.configurable = true; 173 | if ("value" in descriptor) descriptor.writable = true; 174 | Object.defineProperty(target, descriptor.key, descriptor); 175 | } 176 | } 177 | return function(Constructor, protoProps, staticProps) { 178 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 179 | if (staticProps) defineProperties(Constructor, staticProps); 180 | return Constructor; 181 | }; 182 | }(); 183 | function _classCallCheck(instance, Constructor) { 184 | if (!(instance instanceof Constructor)) { 185 | throw new TypeError("Cannot call a class as a function"); 186 | } 187 | } 188 | function OAuthTokenProvider() { 189 | var config = { 190 | name: "token", 191 | options: { 192 | secure: true 193 | } 194 | }; 195 | this.configure = function(params) { 196 | if (!(params instanceof Object)) { 197 | throw new TypeError("Invalid argument: `config` must be an `Object`."); 198 | } 199 | angular.extend(config, params); 200 | return config; 201 | }; 202 | this.$get = function($cookies) { 203 | var OAuthToken = function() { 204 | function OAuthToken() { 205 | _classCallCheck(this, OAuthToken); 206 | } 207 | _createClass(OAuthToken, [ { 208 | key: "setToken", 209 | value: function setToken(data) { 210 | return $cookies.putObject(config.name, data, config.options); 211 | } 212 | }, { 213 | key: "getToken", 214 | value: function getToken() { 215 | return $cookies.getObject(config.name); 216 | } 217 | }, { 218 | key: "getAccessToken", 219 | value: function getAccessToken() { 220 | var _ref = this.getToken() || {}; 221 | var access_token = _ref.access_token; 222 | return access_token; 223 | } 224 | }, { 225 | key: "getAuthorizationHeader", 226 | value: function getAuthorizationHeader() { 227 | var tokenType = this.getTokenType(); 228 | var accessToken = this.getAccessToken(); 229 | if (!tokenType || !accessToken) { 230 | return; 231 | } 232 | return tokenType.charAt(0).toUpperCase() + tokenType.substr(1) + " " + accessToken; 233 | } 234 | }, { 235 | key: "getRefreshToken", 236 | value: function getRefreshToken() { 237 | var _ref2 = this.getToken() || {}; 238 | var refresh_token = _ref2.refresh_token; 239 | return refresh_token; 240 | } 241 | }, { 242 | key: "getTokenType", 243 | value: function getTokenType() { 244 | var _ref3 = this.getToken() || {}; 245 | var token_type = _ref3.token_type; 246 | return token_type; 247 | } 248 | }, { 249 | key: "removeToken", 250 | value: function removeToken() { 251 | return $cookies.remove(config.name, config.options); 252 | } 253 | } ]); 254 | return OAuthToken; 255 | }(); 256 | return new OAuthToken(); 257 | }; 258 | this.$get.$inject = [ "$cookies" ]; 259 | } 260 | function oauthInterceptor($q, $rootScope, OAuthToken) { 261 | return { 262 | request: function request(config) { 263 | config.headers = config.headers || {}; 264 | if (!config.headers.hasOwnProperty("Authorization") && OAuthToken.getAuthorizationHeader()) { 265 | config.headers.Authorization = OAuthToken.getAuthorizationHeader(); 266 | } 267 | return config; 268 | }, 269 | responseError: function responseError(rejection) { 270 | if (!rejection) { 271 | return $q.reject(rejection); 272 | } 273 | if (400 === rejection.status && rejection.data && ("invalid_request" === rejection.data.error || "invalid_grant" === rejection.data.error)) { 274 | OAuthToken.removeToken(); 275 | $rootScope.$emit("oauth:error", rejection); 276 | } 277 | if (401 === rejection.status && rejection.data && "invalid_token" === rejection.data.error || rejection.headers && rejection.headers("www-authenticate") && 0 === rejection.headers("www-authenticate").indexOf("Bearer")) { 278 | $rootScope.$emit("oauth:error", rejection); 279 | } 280 | return $q.reject(rejection); 281 | } 282 | }; 283 | } 284 | oauthInterceptor.$inject = [ "$q", "$rootScope", "OAuthToken" ]; 285 | return ngModule; 286 | }); -------------------------------------------------------------------------------- /dist/angular-oauth2.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"function"==typeof define&&define.amd?define(["angular","angular-cookies","query-string"],t):"object"==typeof exports?module.exports=t(require("angular"),require("angular-cookies"),require("query-string")):e.angularOAuth2=t(e.angular,"ngCookies",e.queryString)}(this,function(e,t,n){function r(e){e.interceptors.push("oauthInterceptor")}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(){var t=this,r=function(t){if(!(t instanceof Object))throw new TypeError("Invalid argument: `config` must be an `Object`.");var n=e.extend({},f,t);return e.forEach(h,function(e){if(!n[e])throw new Error("Missing parameter: "+e+".")}),"/"===n.baseUrl.substr(-1)&&(n.baseUrl=n.baseUrl.slice(0,-1)),"/"!==n.grantPath[0]&&(n.grantPath="/"+n.grantPath),"/"!==n.revokePath[0]&&(n.revokePath="/"+n.revokePath),n};this.configure=function(e){t.defaultConfig=r(e)},this.$get=function(t,i){var a=function(){function a(e){o(this,a),this.config=e}return s(a,[{key:"configure",value:function(e){this.config=r(e)}},{key:"isAuthenticated",value:function(){return!!i.getToken()}},{key:"getAccessToken",value:function(r,o){return r=e.extend({client_id:this.config.clientId,grant_type:"password"},r),null!==this.config.clientSecret&&(r.client_secret=this.config.clientSecret),r=n.stringify(r),o=e.extend({headers:{Authorization:void 0,"Content-Type":"application/x-www-form-urlencoded"}},o),t.post(""+this.config.baseUrl+this.config.grantPath,r,o).then(function(e){return i.setToken(e.data),e})}},{key:"getRefreshToken",value:function(r,o){return r=e.extend({client_id:this.config.clientId,grant_type:"refresh_token",refresh_token:i.getRefreshToken()},r),null!==this.config.clientSecret&&(r.client_secret=this.config.clientSecret),r=n.stringify(r),o=e.extend({headers:{Authorization:void 0,"Content-Type":"application/x-www-form-urlencoded"}},o),t.post(""+this.config.baseUrl+this.config.grantPath,r,o).then(function(e){return i.setToken(e.data),e})}},{key:"revokeToken",value:function(r,o){var a=i.getRefreshToken();return r=e.extend({client_id:this.config.clientId,token:a?a:i.getAccessToken(),token_type_hint:a?"refresh_token":"access_token"},r),null!==this.config.clientSecret&&(r.client_secret=this.config.clientSecret),r=n.stringify(r),o=e.extend({headers:{"Content-Type":"application/x-www-form-urlencoded"}},o),t.post(""+this.config.baseUrl+this.config.revokePath,r,o).then(function(e){return i.removeToken(),e})}}]),a}();return new a(this.defaultConfig)},this.$get.$inject=["$http","OAuthToken"]}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(){var t={name:"token",options:{secure:!0}};this.configure=function(n){if(!(n instanceof Object))throw new TypeError("Invalid argument: `config` must be an `Object`.");return e.extend(t,n),t},this.$get=function(e){var n=function(){function n(){o(this,n)}return s(n,[{key:"setToken",value:function(n){return e.putObject(t.name,n,t.options)}},{key:"getToken",value:function(){return e.getObject(t.name)}},{key:"getAccessToken",value:function(){var e=this.getToken()||{},t=e.access_token;return t}},{key:"getAuthorizationHeader",value:function(){var e=this.getTokenType(),t=this.getAccessToken();if(e&&t)return e.charAt(0).toUpperCase()+e.substr(1)+" "+t}},{key:"getRefreshToken",value:function(){var e=this.getToken()||{},t=e.refresh_token;return t}},{key:"getTokenType",value:function(){var e=this.getToken()||{},t=e.token_type;return t}},{key:"removeToken",value:function(){return e.remove(t.name,t.options)}}]),n}();return new n},this.$get.$inject=["$cookies"]}function u(e,t,n){return{request:function(e){return e.headers=e.headers||{},!e.headers.hasOwnProperty("Authorization")&&n.getAuthorizationHeader()&&(e.headers.Authorization=n.getAuthorizationHeader()),e},responseError:function(r){return r?(400!==r.status||!r.data||"invalid_request"!==r.data.error&&"invalid_grant"!==r.data.error||(n.removeToken(),t.$emit("oauth:error",r)),(401===r.status&&r.data&&"invalid_token"===r.data.error||r.headers&&r.headers("www-authenticate")&&0===r.headers("www-authenticate").indexOf("Bearer"))&&t.$emit("oauth:error",r),e.reject(r)):e.reject(r)}}}var c=e.module("angular-oauth2",[t]).config(r).factory("oauthInterceptor",u).provider("OAuth",i).provider("OAuthToken",a);r.$inject=["$httpProvider"];var s=function(){function e(e,t){for(var n=0;n = factory(root.angular, 'ngCookies', root.queryString); 37 | } 38 | }(this, function(angular, ngCookies, queryString) { 39 | <% if (exports) { %> 40 | <%= contents %> 41 | return <%= exports %>; 42 | <% } else { %> 43 | return <%= contents %>; 44 | <% } %> 45 | })); 46 | ` 47 | }, 48 | banner: ['/**', 49 | ' * <%= pkg.name %> - <%= pkg.description %>', 50 | ' * @version v<%= pkg.version %>', 51 | ' * @link <%= pkg.homepage %>', 52 | ' * @license <%= pkg.license %>', 53 | ' */', 54 | ''].join('\n') 55 | }; 56 | 57 | /** 58 | * Scripts task. 59 | */ 60 | 61 | gulp.task('scripts', ['scripts-lint'], function() { 62 | return gulp.src(config.src) 63 | .pipe(babel({ modules: 'ignore', blacklist: ['useStrict'] })) 64 | .pipe(concat(config.name)) 65 | .pipe(wrapUmd(config.umd)) 66 | .pipe(uglify({ 67 | mangle: false, 68 | output: { beautify: true }, 69 | compress: false 70 | })) 71 | .pipe(header(config.banner, { pkg: pkg })) 72 | .pipe(gulp.dest(config.dest)); 73 | }); 74 | 75 | gulp.task('scripts-minify', ['scripts'], function() { 76 | return gulp.src(config.dest + '/' + config.name) 77 | .pipe(uglify()) 78 | .pipe(rename(function(path) { 79 | path.extname = '.min.js'; 80 | })) 81 | .pipe(gulp.dest(config.dest)); 82 | }); 83 | 84 | gulp.task('scripts-lint', function() { 85 | return gulp.src(config.src) 86 | .pipe(jshint()) 87 | .pipe(jshint.reporter('jshint-stylish')) 88 | .pipe(jshint.reporter('fail')); 89 | }); 90 | 91 | /** 92 | * Test task. 93 | */ 94 | 95 | gulp.task('test', ['scripts'], function() { 96 | var server = new karma({ 97 | configFile: __dirname + '/karma.conf.js', 98 | singleRun: true 99 | }, function(code) { 100 | console.log('Karma has exited with code', code); 101 | }); 102 | 103 | return server.start(); 104 | }); 105 | 106 | /** 107 | * Main tasks. 108 | */ 109 | 110 | gulp.task('build', ['scripts-minify']); 111 | gulp.task('default', ['test']); 112 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var argv = require('yargs').argv; 7 | 8 | /** 9 | * Karma. 10 | */ 11 | 12 | module.exports = function(config) { 13 | config.set({ 14 | basePath: './', 15 | browsers: [argv.browsers || 'Chrome'], 16 | files: [ 17 | 'node_modules/angular/angular.js', 18 | 'node_modules/angular-cookies/angular-cookies.js', 19 | 'node_modules/query-string/query-string.js', 20 | 'node_modules/lodash/lodash.js', 21 | 'node_modules/angular-mocks/angular-mocks.js', 22 | 'dist/angular-oauth2.js', 23 | 'test/mocks/**/*.mock.js', 24 | 'test/unit/**/*.spec.js' 25 | ], 26 | frameworks: [ 27 | 'browserify', 28 | 'mocha', 29 | 'should', 30 | 'sinon' 31 | ], 32 | plugins: [ 33 | 'karma-browserify', 34 | 'karma-chrome-launcher', 35 | 'karma-firefox-launcher', 36 | 'karma-mocha', 37 | 'karma-mocha-reporter', 38 | 'karma-should', 39 | 'karma-sinon' 40 | ], 41 | reporters: ['mocha'] 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-oauth2", 3 | "version": "4.2.0", 4 | "description": "Angular OAuth2", 5 | "main": "./dist/angular-oauth2.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/seegno/angular-oauth2.git" 9 | }, 10 | "keywords": [ 11 | "AngularJS", 12 | "Authentication", 13 | "OAuth2" 14 | ], 15 | "author": { 16 | "name": "Seegno", 17 | "email": "projects@seegno.com" 18 | }, 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/seegno/angular-oauth2/issues" 22 | }, 23 | "homepage": "https://github.com/seegno/angular-oauth2", 24 | "dependencies": {}, 25 | "devDependencies": { 26 | "angular": "^1.5.9", 27 | "angular-cookies": "^1.5.9", 28 | "angular-mocks": "1.5.9", 29 | "browserify": "^14.5.0", 30 | "github-changes": "^1.0.0", 31 | "gulp": "^3.8.10", 32 | "gulp-babel": "^5.3.0", 33 | "gulp-concat": "^2.4.3", 34 | "gulp-header": "^1.2.2", 35 | "gulp-jshint": "^1.9.0", 36 | "gulp-rename": "^1.2.0", 37 | "gulp-uglify": "^1.0.2", 38 | "gulp-wrap-umd": "^0.2.1", 39 | "jshint-stylish": "^1.0.0", 40 | "karma": "^0.13.0", 41 | "karma-browserify": "^5.1.2", 42 | "karma-chrome-launcher": "^0.1.7", 43 | "karma-firefox-launcher": "^0.1.4", 44 | "karma-mocha": "^0.1.10", 45 | "karma-mocha-reporter": "^0.3.1", 46 | "karma-should": "0.0.1", 47 | "karma-sinon": "^1.0.4", 48 | "lodash": "^4.0.0", 49 | "mocha": "^2.4.5", 50 | "query-string": "^1.0.0", 51 | "should": "^4.6.0", 52 | "sinon": "^1.17.3", 53 | "watchify": "^3.9.0", 54 | "yargs": "^3.6.0" 55 | }, 56 | "scripts": { 57 | "changelog": "./node_modules/.bin/github-changes -o oauthjs -r angular-oauth2 -a --only-pulls --use-commit-body --title 'Changelog' --date-format '/ YYYY-MM-DD'", 58 | "test": "./node_modules/.bin/gulp test --browsers Firefox" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/angular-oauth2.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | import angular from 'angular'; 7 | import OAuthProvider from './providers/oauth-provider'; 8 | import OAuthTokenProvider from './providers/oauth-token-provider'; 9 | import oauthConfig from './config/oauth-config'; 10 | import oauthInterceptor from './interceptors/oauth-interceptor'; 11 | import ngCookies from 'angular-cookies'; 12 | 13 | var ngModule = angular.module('angular-oauth2', [ 14 | ngCookies 15 | ]) 16 | .config(oauthConfig) 17 | .factory('oauthInterceptor', oauthInterceptor) 18 | .provider('OAuth', OAuthProvider) 19 | .provider('OAuthToken', OAuthTokenProvider) 20 | ; 21 | 22 | /** 23 | * Export `angular-oauth2`. 24 | */ 25 | 26 | export default ngModule; 27 | -------------------------------------------------------------------------------- /src/config/oauth-config.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * OAuth config. 4 | */ 5 | 6 | function oauthConfig($httpProvider) { 7 | $httpProvider.interceptors.push('oauthInterceptor'); 8 | } 9 | 10 | oauthConfig.$inject = ['$httpProvider']; 11 | 12 | /** 13 | * Export `oauthConfig`. 14 | */ 15 | 16 | export default oauthConfig; 17 | -------------------------------------------------------------------------------- /src/interceptors/oauth-interceptor.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * OAuth interceptor. 4 | */ 5 | 6 | function oauthInterceptor($q, $rootScope, OAuthToken) { 7 | return { 8 | request: function(config) { 9 | config.headers = config.headers || {}; 10 | 11 | // Inject `Authorization` header. 12 | if (!config.headers.hasOwnProperty('Authorization') && OAuthToken.getAuthorizationHeader()) { 13 | config.headers.Authorization = OAuthToken.getAuthorizationHeader(); 14 | } 15 | 16 | return config; 17 | }, 18 | responseError: function(rejection) { 19 | if (!rejection) { 20 | return $q.reject(rejection); 21 | } 22 | 23 | // Catch `invalid_request` and `invalid_grant` errors and ensure that the `token` is removed. 24 | if (400 === rejection.status && rejection.data && 25 | ('invalid_request' === rejection.data.error || 'invalid_grant' === rejection.data.error) 26 | ) { 27 | OAuthToken.removeToken(); 28 | 29 | $rootScope.$emit('oauth:error', rejection); 30 | } 31 | 32 | // Catch `invalid_token` and `unauthorized` errors. 33 | // The token isn't removed here so it can be refreshed when the `invalid_token` error occurs. 34 | if (401 === rejection.status && 35 | (rejection.data && 'invalid_token' === rejection.data.error) || 36 | (rejection.headers && rejection.headers('www-authenticate') && 0 === rejection.headers('www-authenticate').indexOf('Bearer')) 37 | ) { 38 | $rootScope.$emit('oauth:error', rejection); 39 | } 40 | 41 | return $q.reject(rejection); 42 | } 43 | }; 44 | } 45 | 46 | oauthInterceptor.$inject = ['$q', '$rootScope', 'OAuthToken']; 47 | 48 | /** 49 | * Export `oauthInterceptor`. 50 | */ 51 | 52 | export default oauthInterceptor; 53 | -------------------------------------------------------------------------------- /src/providers/oauth-provider.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | import angular from 'angular'; 7 | import queryString from 'query-string'; 8 | 9 | var defaults = { 10 | baseUrl: null, 11 | clientId: null, 12 | clientSecret: null, 13 | grantPath: '/oauth2/token', 14 | revokePath: '/oauth2/revoke' 15 | }; 16 | 17 | var requiredKeys = [ 18 | 'baseUrl', 19 | 'clientId', 20 | 'grantPath', 21 | 'revokePath' 22 | ]; 23 | 24 | /** 25 | * OAuth provider. 26 | */ 27 | 28 | function OAuthProvider() { 29 | 30 | /** 31 | * @private 32 | * sanitize configuration parameters 33 | * @param {object} an `object` of params to sanitize 34 | * @return {object} an sanitize version of the params 35 | */ 36 | const sanitizeConfigParams = (params) => { 37 | if (!(params instanceof Object)) { 38 | throw new TypeError('Invalid argument: `config` must be an `Object`.'); 39 | } 40 | 41 | // Extend default configuration. 42 | const config = angular.extend({}, defaults, params); 43 | 44 | // Check if all required keys are set. 45 | angular.forEach(requiredKeys, (key) => { 46 | if (!config[key]) { 47 | throw new Error(`Missing parameter: ${key}.`); 48 | } 49 | }); 50 | 51 | // Remove `baseUrl` trailing slash. 52 | if ('/' === config.baseUrl.substr(-1)) { 53 | config.baseUrl = config.baseUrl.slice(0, -1); 54 | } 55 | 56 | // Add `grantPath` facing slash. 57 | if ('/' !== config.grantPath[0]) { 58 | config.grantPath = `/${config.grantPath}`; 59 | } 60 | 61 | // Add `revokePath` facing slash. 62 | if ('/' !== config.revokePath[0]) { 63 | config.revokePath = `/${config.revokePath}`; 64 | } 65 | 66 | return config; 67 | }; 68 | 69 | /** 70 | * Configure. 71 | * 72 | * @param {object} params - An `object` of params to extend. 73 | */ 74 | this.configure = (params) => { 75 | this.defaultConfig = sanitizeConfigParams(params); 76 | }; 77 | 78 | /** 79 | * OAuth service. 80 | */ 81 | 82 | this.$get = function($http, OAuthToken) { 83 | class OAuth { 84 | 85 | /** 86 | * Check if `OAuthProvider` is configured. 87 | */ 88 | 89 | constructor(config) { 90 | this.config = config; 91 | } 92 | 93 | /** 94 | * Configure OAuth service during runtime 95 | * 96 | * @param {Object} params - An object of params to extend 97 | */ 98 | configure(params) { 99 | this.config = sanitizeConfigParams(params); 100 | } 101 | 102 | 103 | /** 104 | * Verifies if the `user` is authenticated or not based on the `token` 105 | * cookie. 106 | * 107 | * @return {boolean} 108 | */ 109 | 110 | isAuthenticated() { 111 | return !!OAuthToken.getToken(); 112 | } 113 | 114 | /** 115 | * Retrieves the `access_token` and stores the `response.data` on cookies 116 | * using the `OAuthToken`. 117 | * 118 | * @param {object} data - Request content, e.g., `username` and `password`. 119 | * @param {object} options - Optional configuration. 120 | * @return {promise} A response promise. 121 | */ 122 | 123 | getAccessToken(data, options) { 124 | data = angular.extend({ 125 | client_id: this.config.clientId, 126 | grant_type: 'password' 127 | }, data); 128 | 129 | if (null !== this.config.clientSecret) { 130 | data.client_secret = this.config.clientSecret; 131 | } 132 | 133 | data = queryString.stringify(data); 134 | 135 | options = angular.extend({ 136 | headers: { 137 | 'Authorization': undefined, 138 | 'Content-Type': 'application/x-www-form-urlencoded' 139 | } 140 | }, options); 141 | 142 | return $http.post(`${this.config.baseUrl}${this.config.grantPath}`, data, options).then((response) => { 143 | OAuthToken.setToken(response.data); 144 | 145 | return response; 146 | }); 147 | } 148 | 149 | /** 150 | * Retrieves the `refresh_token` and stores the `response.data` on cookies 151 | * using the `OAuthToken`. 152 | * 153 | * @param {object} data - Request content. 154 | * @param {object} options - Optional configuration. 155 | * @return {promise} A response promise. 156 | */ 157 | 158 | getRefreshToken(data, options) { 159 | data = angular.extend({ 160 | client_id: this.config.clientId, 161 | grant_type: 'refresh_token', 162 | refresh_token: OAuthToken.getRefreshToken(), 163 | }, data); 164 | 165 | if (null !== this.config.clientSecret) { 166 | data.client_secret = this.config.clientSecret; 167 | } 168 | 169 | data = queryString.stringify(data); 170 | 171 | options = angular.extend({ 172 | headers: { 173 | 'Authorization': undefined, 174 | 'Content-Type': 'application/x-www-form-urlencoded' 175 | } 176 | }, options); 177 | 178 | return $http.post(`${this.config.baseUrl}${this.config.grantPath}`, data, options).then((response) => { 179 | OAuthToken.setToken(response.data); 180 | 181 | return response; 182 | }); 183 | } 184 | 185 | /** 186 | * Revokes the `token` and removes the stored `token` from cookies 187 | * using the `OAuthToken`. 188 | * 189 | * @param {object} data - Request content. 190 | * @param {object} options - Optional configuration. 191 | * @return {promise} A response promise. 192 | */ 193 | 194 | revokeToken(data, options) { 195 | var refreshToken = OAuthToken.getRefreshToken(); 196 | 197 | data = angular.extend({ 198 | client_id: this.config.clientId, 199 | token: refreshToken ? refreshToken : OAuthToken.getAccessToken(), 200 | token_type_hint: refreshToken ? 'refresh_token' : 'access_token' 201 | }, data); 202 | 203 | if (null !== this.config.clientSecret) { 204 | data.client_secret = this.config.clientSecret; 205 | } 206 | 207 | data = queryString.stringify(data); 208 | 209 | options = angular.extend({ 210 | headers: { 211 | 'Content-Type': 'application/x-www-form-urlencoded' 212 | } 213 | }, options); 214 | 215 | return $http.post(`${this.config.baseUrl}${this.config.revokePath}`, data, options).then((response) => { 216 | OAuthToken.removeToken(); 217 | 218 | return response; 219 | }); 220 | } 221 | } 222 | 223 | return new OAuth(this.defaultConfig); 224 | }; 225 | 226 | this.$get.$inject = ['$http', 'OAuthToken']; 227 | } 228 | 229 | /** 230 | * Export `OAuthProvider`. 231 | */ 232 | 233 | export default OAuthProvider; 234 | -------------------------------------------------------------------------------- /src/providers/oauth-token-provider.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | import angular from 'angular'; 7 | 8 | /** 9 | * Token provider. 10 | */ 11 | 12 | function OAuthTokenProvider() { 13 | var config = { 14 | name: 'token', 15 | options: { 16 | secure: true 17 | } 18 | }; 19 | 20 | /** 21 | * Configure. 22 | * 23 | * @param {object} params - An `object` of params to extend. 24 | */ 25 | 26 | this.configure = function(params) { 27 | // Check if is an `object`. 28 | if (!(params instanceof Object)) { 29 | throw new TypeError('Invalid argument: `config` must be an `Object`.'); 30 | } 31 | 32 | // Extend default configuration. 33 | angular.extend(config, params); 34 | 35 | return config; 36 | }; 37 | 38 | /** 39 | * OAuthToken service. 40 | */ 41 | 42 | this.$get = function($cookies) { 43 | class OAuthToken { 44 | 45 | /** 46 | * Set token. 47 | */ 48 | 49 | setToken(data) { 50 | return $cookies.putObject(config.name, data, config.options); 51 | } 52 | 53 | /** 54 | * Get token. 55 | */ 56 | 57 | getToken() { 58 | return $cookies.getObject(config.name); 59 | } 60 | 61 | /** 62 | * Get accessToken. 63 | */ 64 | 65 | getAccessToken() { 66 | const { access_token } = this.getToken() || {}; 67 | 68 | return access_token; 69 | } 70 | 71 | /** 72 | * Get authorizationHeader. 73 | */ 74 | 75 | getAuthorizationHeader() { 76 | const tokenType = this.getTokenType(); 77 | const accessToken = this.getAccessToken(); 78 | 79 | if (!tokenType || !accessToken) { 80 | return; 81 | } 82 | 83 | return `${tokenType.charAt(0).toUpperCase() + tokenType.substr(1)} ${accessToken}`; 84 | } 85 | 86 | /** 87 | * Get refreshToken. 88 | */ 89 | 90 | getRefreshToken() { 91 | const { refresh_token } = this.getToken() || {}; 92 | 93 | return refresh_token; 94 | } 95 | 96 | /** 97 | * Get tokenType. 98 | */ 99 | 100 | getTokenType() { 101 | const { token_type } = this.getToken() || {}; 102 | 103 | return token_type; 104 | } 105 | 106 | /** 107 | * Remove token. 108 | */ 109 | 110 | removeToken() { 111 | return $cookies.remove(config.name, config.options); 112 | } 113 | } 114 | 115 | return new OAuthToken(); 116 | }; 117 | 118 | this.$get.$inject = ['$cookies']; 119 | } 120 | 121 | /** 122 | * Export `OAuthTokenProvider`. 123 | */ 124 | 125 | export default OAuthTokenProvider; 126 | -------------------------------------------------------------------------------- /test/mocks/angular-cookies.mock.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Angular cookies mock. 4 | */ 5 | 6 | angular.module('angular-cookies.mock', []) 7 | .provider('$cookies', function() { 8 | this.$get = function() { 9 | var cookieStore = {}; 10 | 11 | return { 12 | getObject: function(key) { 13 | return cookieStore[key]; 14 | }, 15 | putObject: function(key, value, options) { 16 | cookieStore[key] = value; 17 | }, 18 | remove: function(key) { 19 | delete cookieStore[key]; 20 | } 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /test/unit/config/oauth-config.spec.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Test `oauthConfig`. 4 | */ 5 | 6 | describe('oauthConfig', function() { 7 | it('should push `oauthInterceptor` into `$httpProvider`', function() { 8 | var httpProvider; 9 | 10 | angular.module('angular-oauth2.test', []) 11 | .config(function($httpProvider) { 12 | httpProvider = $httpProvider; 13 | }); 14 | 15 | angular.mock.module('angular-oauth2', 'angular-oauth2.test'); 16 | 17 | angular.mock.inject(function() { 18 | httpProvider.interceptors.should.containEql('oauthInterceptor'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/interceptors/oauth-interceptor.spec.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Test `oauthInterceptor`. 4 | */ 5 | 6 | describe('oauthInterceptor', function() { 7 | beforeEach(function() { 8 | angular.mock.module('angular-oauth2', 'angular-cookies.mock'); 9 | }); 10 | 11 | afterEach(inject(function(OAuthToken) { 12 | OAuthToken.removeToken(); 13 | })); 14 | 15 | it('should not inject `Authorization` header if `token` is empty', inject(function($http, $httpBackend) { 16 | $httpBackend.expectGET('https://website.com', function(headers) { 17 | headers.should.not.have.property('Authorization'); 18 | 19 | return headers; 20 | }).respond(200); 21 | 22 | $http.get('https://website.com'); 23 | $httpBackend.flush(); 24 | })); 25 | 26 | it('should inject `Authorization` header if `token` exists', inject(function($http, $httpBackend, OAuthToken) { 27 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 28 | 29 | $httpBackend.expectGET('https://website.com', function(headers) { 30 | headers.should.have.property('Authorization'); 31 | headers.Authorization.should.match(/Bearer/) 32 | 33 | return headers; 34 | }).respond(200); 35 | 36 | $http.get('https://website.com'); 37 | $httpBackend.flush(); 38 | })); 39 | 40 | it('should not inject `Authorization` header if it already exists', inject(function($http, $httpBackend, OAuthToken) { 41 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 42 | 43 | $httpBackend.expectGET('https://website.com', function(headers) { 44 | headers.Authorization = undefined; 45 | 46 | return headers; 47 | }).respond(200); 48 | 49 | $http.get('https://website.com').then(function(response) { 50 | response.config.headers.should.have.property('Authorization'); 51 | (undefined === response.config.headers.Authorization).should.be.true; 52 | }).catch(function() { 53 | should.fail(); 54 | }); 55 | 56 | $httpBackend.flush(); 57 | 58 | $httpBackend.verifyNoOutstandingExpectation(); 59 | $httpBackend.verifyNoOutstandingRequest(); 60 | })); 61 | 62 | it('should remove `token` if an `invalid_request` error occurs', inject(function($http, $httpBackend, OAuthToken) { 63 | sinon.spy(OAuthToken, 'removeToken'); 64 | 65 | $httpBackend.expectGET('https://website.com').respond(400, { error: 'invalid_request' }); 66 | 67 | $http.get('https://website.com') 68 | .catch(() => { }); 69 | 70 | $httpBackend.flush(); 71 | 72 | OAuthToken.removeToken.callCount.should.equal(1); 73 | OAuthToken.removeToken.restore(); 74 | })); 75 | 76 | it('should emit `oauth:error` event if an `invalid_request` error occurs', inject(function($http, $httpBackend, $rootScope) { 77 | sinon.spy($rootScope, '$emit'); 78 | 79 | $httpBackend.expectGET('https://website.com').respond(400, { error: 'invalid_request' }); 80 | 81 | $http.get('https://website.com') 82 | .catch(() => { }); 83 | 84 | $httpBackend.flush(); 85 | 86 | $rootScope.$emit.callCount.should.equal(1); 87 | $rootScope.$emit.firstCall.args[0].should.eql('oauth:error'); 88 | $rootScope.$emit.firstCall.args[1].should.have.property('status', 400); 89 | $rootScope.$emit.firstCall.args[1].should.have.property('data', { error: 'invalid_request' }); 90 | $rootScope.$emit.restore(); 91 | })); 92 | 93 | it('should remove `token` if an `invalid_grant` error occurs', inject(function($http, $httpBackend, OAuthToken) { 94 | sinon.spy(OAuthToken, 'removeToken'); 95 | 96 | $httpBackend.expectGET('https://website.com').respond(400, { error: 'invalid_grant' }); 97 | 98 | $http.get('https://website.com') 99 | .catch(() => { }); 100 | 101 | $httpBackend.flush(); 102 | 103 | OAuthToken.removeToken.callCount.should.equal(1); 104 | OAuthToken.removeToken.restore(); 105 | })); 106 | 107 | it('should emit `oauth:error` event if an `invalid_grant` error occurs', inject(function($http, $httpBackend, $rootScope) { 108 | sinon.spy($rootScope, '$emit'); 109 | 110 | $httpBackend.expectGET('https://website.com').respond(400, { error: 'invalid_grant' }); 111 | 112 | $http.get('https://website.com') 113 | .catch(() => { }); 114 | 115 | $httpBackend.flush(); 116 | 117 | $rootScope.$emit.callCount.should.equal(1); 118 | $rootScope.$emit.firstCall.args[0].should.eql('oauth:error'); 119 | $rootScope.$emit.firstCall.args[1].should.have.property('status', 400); 120 | $rootScope.$emit.firstCall.args[1].should.have.property('data', { error: 'invalid_grant' }); 121 | $rootScope.$emit.restore(); 122 | })); 123 | 124 | it('should emit `oauth:error` event if an `invalid_token` error occurs', inject(function($http, $httpBackend, $rootScope) { 125 | sinon.spy($rootScope, '$emit'); 126 | 127 | $httpBackend.expectGET('https://website.com').respond(401, { error: 'invalid_token' }); 128 | 129 | $http.get('https://website.com') 130 | .catch(() => { }); 131 | 132 | $httpBackend.flush(); 133 | 134 | $rootScope.$emit.callCount.should.equal(1); 135 | $rootScope.$emit.firstCall.args[0].should.eql('oauth:error'); 136 | $rootScope.$emit.firstCall.args[1].should.have.property('status', 401); 137 | $rootScope.$emit.firstCall.args[1].should.have.property('data', { error: 'invalid_token' }); 138 | $rootScope.$emit.restore(); 139 | })); 140 | 141 | it('should emit `oauth:error` event if an `unauthorized` error occurs', inject(function($http, $httpBackend, $rootScope) { 142 | sinon.spy($rootScope, '$emit'); 143 | 144 | $httpBackend.expectGET('https://website.com').respond(401, null, { 'www-authenticate': 'Bearer realm="example"' }); 145 | 146 | $http.get('https://website.com') 147 | .catch(() => { }); 148 | 149 | $httpBackend.flush(); 150 | 151 | $rootScope.$emit.callCount.should.equal(1); 152 | $rootScope.$emit.firstCall.args[0].should.eql('oauth:error'); 153 | $rootScope.$emit.firstCall.args[1].should.have.property('status', 401); 154 | $rootScope.$emit.firstCall.args[1].should.have.property('headers'); 155 | $rootScope.$emit.firstCall.args[1].headers('www-authenticate').should.equal('Bearer realm="example"'); 156 | $rootScope.$emit.restore(); 157 | })); 158 | }); 159 | -------------------------------------------------------------------------------- /test/unit/providers/oauth-provider.spec.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Test `OAuthProvider`. 4 | */ 5 | 6 | describe('OAuthProvider', function() { 7 | var defaults = { 8 | baseUrl: 'https://api.website.com', 9 | clientId: 'CLIENT_ID', 10 | grantPath: '/oauth2/token', 11 | revokePath: '/oauth2/revoke', 12 | clientSecret: 'CLIENT_SECRET' 13 | }; 14 | 15 | describe('configure()', function() { 16 | var provider; 17 | 18 | beforeEach(function() { 19 | angular.module('angular-oauth2.test', []) 20 | .config(function(OAuthProvider) { 21 | provider = OAuthProvider; 22 | }); 23 | 24 | angular.mock.module('angular-oauth2', 'angular-oauth2.test'); 25 | 26 | angular.mock.inject(function() {}); 27 | }); 28 | 29 | it('should throw an error if configuration is not an object', function() { 30 | try { 31 | provider.configure(false); 32 | 33 | should.fail(); 34 | } catch(e) { 35 | e.should.be.an.instanceOf(TypeError); 36 | e.message.should.match(/config/); 37 | } 38 | }); 39 | 40 | it('should throw an error if `baseUrl` param is empty', function() { 41 | try { 42 | provider.configure(_.omit(defaults, 'baseUrl')); 43 | 44 | should.fail(); 45 | } catch(e) { 46 | e.should.be.an.instanceOf(Error); 47 | e.message.should.match(/baseUrl/); 48 | } 49 | }); 50 | 51 | it('should throw an error if `clientId` param is empty', function() { 52 | try { 53 | provider.configure(_.omit(defaults, 'clientId')); 54 | 55 | should.fail(); 56 | } catch(e) { 57 | e.should.be.an.instanceOf(Error); 58 | e.message.should.match(/clientId/); 59 | } 60 | }); 61 | 62 | it('should not throw an error if `clientSecret` param is empty', function() { 63 | try { 64 | provider.configure(_.omit(defaults, 'clientSecret')); 65 | should.not.fail(); 66 | } catch(e) {} 67 | }); 68 | 69 | it('should throw an error if `grantPath` param is empty', function() { 70 | try { 71 | provider.configure(_.defaults({ grantPath: null }, defaults)); 72 | 73 | should.fail(); 74 | } catch(e) { 75 | e.should.be.an.instanceOf(Error); 76 | e.message.should.match(/grantPath/); 77 | } 78 | }); 79 | 80 | it('should remove trailing slash from `baseUrl`', function() { 81 | provider.configure(_.defaults({ 82 | baseUrl: 'https://api.website.com/' 83 | }, defaults)); 84 | 85 | provider.defaultConfig.baseUrl.should.equal('https://api.website.com'); 86 | }); 87 | 88 | it('should add facing slash from `grantPath`', function() { 89 | provider.configure(_.defaults({ 90 | grantPath: 'oauth2/token' 91 | }, defaults)); 92 | 93 | provider.defaultConfig.grantPath.should.equal('/oauth2/token'); 94 | }); 95 | 96 | it('should throw an error if `revokePath` param is empty', function() { 97 | try { 98 | provider.configure(_.defaults({ revokePath: null }, defaults)); 99 | 100 | should.fail(); 101 | } catch(e) { 102 | e.should.be.an.instanceOf(Error); 103 | e.message.should.match(/revokePath/); 104 | } 105 | }); 106 | 107 | it('should add facing slash from `revokePath`', function() { 108 | provider.configure(_.defaults({ 109 | revokePath: 'oauth2/revoke' 110 | }, defaults)); 111 | 112 | provider.defaultConfig.revokePath.should.equal('/oauth2/revoke'); 113 | }); 114 | }); 115 | 116 | describe('$get()', function() { 117 | beforeEach(function() { 118 | angular.module('angular-oauth2.test', ['angular-cookies.mock']) 119 | .config(function(OAuthProvider) { 120 | OAuthProvider.configure(defaults); 121 | }); 122 | 123 | angular.mock.module('angular-oauth2', 'angular-oauth2.test'); 124 | }); 125 | 126 | afterEach(inject(function(OAuthToken) { 127 | OAuthToken.removeToken(); 128 | })); 129 | describe('construtor', function() { 130 | it('should set initialize config with data passed in configure', inject(function(OAuth) { 131 | OAuth.config.should.eql(defaults); 132 | })) 133 | }) 134 | 135 | describe('configure()', function() { 136 | it('should throw an error if configuration is not an object', inject(function(OAuth) { 137 | try { 138 | OAuth.configure(false); 139 | 140 | should.fail(); 141 | } catch(e) { 142 | e.should.be.an.instanceOf(TypeError); 143 | e.message.should.match(/config/); 144 | } 145 | })); 146 | 147 | it('should throw an error if `baseUrl` param is empty', inject(function(OAuth) { 148 | try { 149 | OAuth.configure(_.omit(defaults, 'baseUrl')); 150 | 151 | should.fail(); 152 | } catch(e) { 153 | e.should.be.an.instanceOf(Error); 154 | e.message.should.match(/baseUrl/); 155 | } 156 | })); 157 | 158 | it('should throw an error if `clientId` param is empty', inject(function(OAuth) { 159 | try { 160 | OAuth.configure(_.omit(defaults, 'clientId')); 161 | 162 | should.fail(); 163 | } catch(e) { 164 | e.should.be.an.instanceOf(Error); 165 | e.message.should.match(/clientId/); 166 | } 167 | })); 168 | 169 | it('should not throw an error if `clientSecret` param is empty', inject(function(OAuth) { 170 | try{ 171 | OAuth.configure(_.omit(defaults, 'clientSecret')); 172 | 173 | should.not.fail(); 174 | } catch(e) {} 175 | })); 176 | 177 | it('should throw an error if `grantPath` param is empty', inject(function(OAuth) { 178 | try { 179 | OAuth.configure(_.defaults({ grantPath: null }, defaults)); 180 | 181 | should.fail(); 182 | } catch(e) { 183 | e.should.be.an.instanceOf(Error); 184 | e.message.should.match(/grantPath/); 185 | } 186 | })); 187 | 188 | it('should remove trailing slash from `baseUrl`', inject(function(OAuth) { 189 | OAuth.configure(_.defaults({ 190 | baseUrl: 'https://api.website.com/' 191 | }, defaults)); 192 | 193 | OAuth.config.baseUrl.should.equal('https://api.website.com'); 194 | })); 195 | 196 | it('should add facing slash from `grantPath`', inject(function(OAuth) { 197 | OAuth.configure(_.defaults({ 198 | grantPath: 'oauth2/token' 199 | }, defaults)); 200 | 201 | OAuth.config.grantPath.should.equal('/oauth2/token'); 202 | })); 203 | 204 | it('should throw an error if `revokePath` param is empty', inject(function(OAuth) { 205 | try { 206 | OAuth.configure(_.defaults({ revokePath: null }, defaults)); 207 | 208 | should.fail(); 209 | } catch(e) { 210 | e.should.be.an.instanceOf(Error); 211 | e.message.should.match(/revokePath/); 212 | } 213 | })); 214 | 215 | it('should add facing slash from `revokePath`', inject(function(OAuth) { 216 | OAuth.configure(_.defaults({ 217 | revokePath: 'oauth2/revoke' 218 | }, defaults)); 219 | 220 | OAuth.config.revokePath.should.equal('/oauth2/revoke'); 221 | })); 222 | }); 223 | 224 | describe('isAuthenticated()', function() { 225 | it('should be true when there is a stored `token` cookie', inject(function(OAuth, OAuthToken) { 226 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 227 | 228 | OAuth.isAuthenticated().should.be.true; 229 | })); 230 | 231 | it('should be false when there is no stored `token` cookie', inject(function(OAuth) { 232 | OAuth.isAuthenticated().should.be.false; 233 | })); 234 | }); 235 | 236 | describe('getAccessToken()', function() { 237 | var data = queryString.stringify({ 238 | client_id: defaults.clientId, 239 | grant_type: 'password', 240 | username: 'foo', 241 | password: 'bar', 242 | client_secret: defaults.clientSecret 243 | }); 244 | 245 | it('should call `queryString.stringify`', inject(function(OAuth) { 246 | sinon.spy(queryString, 'stringify'); 247 | 248 | OAuth.getAccessToken({ 249 | username: 'foo', 250 | password: 'bar' 251 | }); 252 | 253 | queryString.stringify.callCount.should.equal(1); 254 | queryString.stringify.firstCall.args.should.have.lengthOf(1); 255 | queryString.stringify.firstCall.args[0].should.eql({ 256 | client_id: defaults.clientId, 257 | grant_type: 'password', 258 | username: 'foo', 259 | password: 'bar', 260 | client_secret: defaults.clientSecret 261 | }); 262 | queryString.stringify.restore(); 263 | })); 264 | 265 | it('should return an error if user credentials are invalid', inject(function($httpBackend, OAuth) { 266 | $httpBackend.expectPOST(defaults.baseUrl + defaults.grantPath, data) 267 | .respond(400, { error: 'invalid_grant' }); 268 | 269 | OAuth.getAccessToken({ 270 | username: 'foo', 271 | password: 'bar' 272 | }).then(function() { 273 | should.fail(); 274 | }).catch(function(response) { 275 | response.status.should.equal(400); 276 | response.data.error.should.equal('invalid_grant'); 277 | }); 278 | 279 | $httpBackend.flush(); 280 | 281 | $httpBackend.verifyNoOutstandingExpectation(); 282 | $httpBackend.verifyNoOutstandingRequest(); 283 | })); 284 | 285 | it('should retrieve and store `token` if request is successful', inject(function($httpBackend, OAuth, OAuthToken) { 286 | $httpBackend.expectPOST(defaults.baseUrl + defaults.grantPath, data) 287 | .respond({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 288 | 289 | OAuth.getAccessToken({ 290 | username: 'foo', 291 | password: 'bar' 292 | }).then(function(response) { 293 | OAuthToken.getToken().should.eql(response.data); 294 | }).catch(function() { 295 | should.fail(); 296 | }); 297 | 298 | $httpBackend.flush(); 299 | 300 | $httpBackend.verifyNoOutstandingExpectation(); 301 | $httpBackend.verifyNoOutstandingRequest(); 302 | })); 303 | }); 304 | 305 | describe('refreshToken()', function() { 306 | var data = { 307 | client_id: defaults.clientId, 308 | grant_type: 'refresh_token', 309 | refresh_token: 'bar', 310 | client_secret: defaults.clientSecret 311 | }; 312 | 313 | it('should call `queryString.stringify`', inject(function(OAuth, OAuthToken) { 314 | sinon.spy(queryString, 'stringify'); 315 | 316 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 317 | 318 | OAuth.getRefreshToken(); 319 | 320 | queryString.stringify.callCount.should.equal(1); 321 | queryString.stringify.firstCall.args.should.have.lengthOf(1); 322 | queryString.stringify.firstCall.args[0].should.eql({ 323 | client_id: defaults.clientId, 324 | grant_type: 'refresh_token', 325 | refresh_token: 'bar', 326 | client_secret: defaults.clientSecret 327 | }); 328 | queryString.stringify.restore(); 329 | })); 330 | 331 | it('should return an error if `refresh_token` is missing', inject(function($httpBackend, OAuth) { 332 | $httpBackend.expectPOST(defaults.baseUrl + defaults.grantPath, queryString.stringify(_.assign({}, data, { 'refresh_token': undefined }))) 333 | .respond(400, { error: 'invalid_request' }); 334 | 335 | OAuth.getRefreshToken().then(function() { 336 | should.fail(); 337 | }).catch(function(response) { 338 | response.status.should.equal(400); 339 | response.data.error.should.equal('invalid_request'); 340 | }); 341 | 342 | $httpBackend.flush(); 343 | 344 | $httpBackend.verifyNoOutstandingExpectation(); 345 | $httpBackend.verifyNoOutstandingRequest(); 346 | })); 347 | 348 | it('should return an error if `refresh_token` is invalid', inject(function($httpBackend, OAuth, OAuthToken) { 349 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 350 | 351 | $httpBackend.expectPOST(defaults.baseUrl + defaults.grantPath, queryString.stringify(data)) 352 | .respond(400, { error: 'invalid_grant' }); 353 | 354 | OAuth.getRefreshToken().then(function() { 355 | should.fail(); 356 | }).catch(function(response) { 357 | response.status.should.equal(400); 358 | response.data.error.should.equal('invalid_grant'); 359 | }); 360 | 361 | $httpBackend.flush(); 362 | 363 | $httpBackend.verifyNoOutstandingExpectation(); 364 | $httpBackend.verifyNoOutstandingRequest(); 365 | })); 366 | 367 | it('should retrieve and store `refresh_token` if request is successful', inject(function($httpBackend, OAuth, OAuthToken) { 368 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 369 | 370 | $httpBackend.expectPOST(defaults.baseUrl + defaults.grantPath, queryString.stringify(data)) 371 | .respond({ token_type: 'bearer', access_token: 'qux', expires_in: 3600, refresh_token: 'biz' }); 372 | 373 | OAuth.getRefreshToken().then(function(response) { 374 | response.data.should.eql({ 375 | token_type: 'bearer', 376 | access_token: 'qux', 377 | expires_in: 3600, 378 | refresh_token: 'biz' 379 | }); 380 | }); 381 | 382 | $httpBackend.flush(); 383 | 384 | $httpBackend.verifyNoOutstandingExpectation(); 385 | $httpBackend.verifyNoOutstandingRequest(); 386 | })); 387 | }); 388 | 389 | describe('revokeToken()', function () { 390 | it('should call `queryString.stringify`', inject(function(OAuth, OAuthToken) { 391 | sinon.spy(queryString, 'stringify'); 392 | 393 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 394 | 395 | OAuth.revokeToken(); 396 | 397 | queryString.stringify.callCount.should.equal(1); 398 | queryString.stringify.firstCall.args.should.have.lengthOf(1); 399 | queryString.stringify.firstCall.args[0].should.eql({ 400 | client_id: defaults.clientId, 401 | token: 'bar', 402 | token_type_hint: 'refresh_token', 403 | client_secret: defaults.clientSecret 404 | }); 405 | queryString.stringify.restore(); 406 | })); 407 | 408 | it('should call `queryString.stringify` with `access_token` if `refresh_token` is not available', inject(function(OAuth, OAuthToken) { 409 | sinon.spy(queryString, 'stringify'); 410 | 411 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600 }); 412 | 413 | OAuth.revokeToken(); 414 | 415 | queryString.stringify.callCount.should.equal(1); 416 | queryString.stringify.firstCall.args.should.have.lengthOf(1); 417 | queryString.stringify.firstCall.args[0].should.eql({ 418 | client_id: defaults.clientId, 419 | token: 'foo', 420 | token_type_hint: 'access_token', 421 | client_secret: defaults.clientSecret 422 | }); 423 | queryString.stringify.restore(); 424 | })); 425 | 426 | it('should return an error if `token` is missing', inject(function($httpBackend, OAuth) { 427 | var data = queryString.stringify({ 428 | client_id: defaults.clientId, 429 | token: undefined, 430 | token_type_hint: 'access_token', 431 | client_secret: defaults.clientSecret 432 | }); 433 | 434 | $httpBackend.expectPOST(defaults.baseUrl + defaults.revokePath, data) 435 | .respond(400, { error: 'invalid_request' }); 436 | 437 | OAuth.revokeToken().then(function() { 438 | should.fail(); 439 | }).catch(function(response) { 440 | response.status.should.equal(400); 441 | }); 442 | 443 | $httpBackend.flush(); 444 | 445 | $httpBackend.verifyNoOutstandingExpectation(); 446 | $httpBackend.verifyNoOutstandingRequest(); 447 | })); 448 | 449 | it('should revoke and remove `token` if request is successful', inject(function($httpBackend, OAuth, OAuthToken) { 450 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 451 | 452 | var data = queryString.stringify({ 453 | client_id: defaults.clientId, 454 | token: 'bar', 455 | token_type_hint: 'refresh_token', 456 | client_secret: defaults.clientSecret 457 | }); 458 | 459 | $httpBackend.expectPOST(defaults.baseUrl + defaults.revokePath, data) 460 | .respond(200); 461 | 462 | OAuth.revokeToken().then(function() { 463 | (undefined === OAuthToken.getToken()).should.be.true; 464 | }).catch(function() { 465 | should.fail(); 466 | }); 467 | 468 | $httpBackend.flush(); 469 | 470 | $httpBackend.verifyNoOutstandingExpectation(); 471 | $httpBackend.verifyNoOutstandingRequest(); 472 | })); 473 | }); 474 | }); 475 | }); 476 | -------------------------------------------------------------------------------- /test/unit/providers/oauth-token-provider.spec.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Test `OAuthTokenProvider`. 4 | */ 5 | 6 | describe('OAuthTokenProvider', function() { 7 | describe('configure()', function() { 8 | var provider; 9 | 10 | beforeEach(function() { 11 | angular.module('angular-oauth2.test', []) 12 | .config(function(OAuthTokenProvider) { 13 | provider = OAuthTokenProvider; 14 | }); 15 | 16 | angular.mock.module('angular-oauth2', 'angular-oauth2.test'); 17 | 18 | angular.mock.inject(function() {}); 19 | }); 20 | 21 | it('should throw an error if configuration is not an object', function() { 22 | try { 23 | provider.configure(false); 24 | 25 | should.fail(); 26 | } catch(e) { 27 | e.should.be.an.instanceOf(TypeError); 28 | e.message.should.match(/config/); 29 | } 30 | }); 31 | }); 32 | 33 | describe('$get()', function() { 34 | beforeEach(function() { 35 | angular.module('angular-oauth2.test', ['angular-cookies.mock']) 36 | .config(function(OAuthProvider) { 37 | OAuthProvider.configure({ 38 | baseUrl: 'https://api.website.com', 39 | clientId: 'CLIENT_ID' 40 | }); 41 | }); 42 | 43 | angular.mock.module('angular-oauth2', 'angular-oauth2.test'); 44 | 45 | angular.mock.inject(function(OAuthToken) { 46 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'foo', expires_in: 3600, refresh_token: 'bar' }); 47 | }); 48 | 49 | }); 50 | 51 | afterEach(inject(function(OAuthToken) { 52 | OAuthToken.removeToken(); 53 | })); 54 | 55 | it('getAuthorizationHeader()', inject(function(OAuthToken) { 56 | OAuthToken.getAuthorizationHeader().should.eql('Bearer foo'); 57 | })); 58 | 59 | it('getAccessToken()', inject(function(OAuthToken) { 60 | OAuthToken.getAccessToken().should.eql('foo'); 61 | })); 62 | 63 | it('getRefreshToken()', inject(function(OAuthToken) { 64 | OAuthToken.getRefreshToken().should.eql('bar'); 65 | })); 66 | 67 | it('setToken()', inject(function(OAuthToken) { 68 | OAuthToken.setToken({ token_type: 'bearer', access_token: 'qux', expires_in: 3600, refresh_token: 'biz' }); 69 | 70 | OAuthToken.getToken().should.eql({ 71 | token_type: 'bearer', 72 | access_token: 'qux', 73 | expires_in: 3600, 74 | refresh_token: 'biz' 75 | }); 76 | })); 77 | 78 | it('getToken()', inject(function(OAuthToken) { 79 | OAuthToken.getToken().should.eql({ 80 | token_type: 'bearer', 81 | access_token: 'foo', 82 | expires_in: 3600, 83 | refresh_token: 'bar' 84 | }); 85 | })); 86 | 87 | it('getTokenType()', inject(function(OAuthToken) { 88 | OAuthToken.getTokenType().should.eql('bearer'); 89 | })); 90 | 91 | it('removeToken()', inject(function(OAuthToken) { 92 | OAuthToken.removeToken(); 93 | 94 | (undefined === OAuthToken.getToken()).should.true; 95 | })); 96 | }); 97 | }); 98 | --------------------------------------------------------------------------------