├── .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 [](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 |
--------------------------------------------------------------------------------