├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── dist ├── jquery.oauth.js └── jquery.oauth.min.js ├── examples ├── amd.html ├── amd.js └── assets │ └── resource.json ├── gulpfile.js ├── package.json └── src └── jquery.oauth.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Esben Petersen 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jQuery OAuth 2 | ============ 3 | A $.ajax wrapper for OAuth 2 access and refresh token management for use in a SPA. 4 | 5 | # What 6 | This is a library for storing access token client side and use it for $.ajax requests. Secondly, it is a library for 7 | requesting new access tokens upon expiration. 8 | 9 | # Guide 10 | 11 | I have written an extensive blog post about writing a web app using Lumen that utilizes this library for client side 12 | management of tokens. 13 | 14 | [Building a web app with Lumen web API and OAuth2 authentication](http://esbenp.github.io/2015/05/26/lumen-web-api-oauth-2-authentication/) 15 | 16 | # Dependencies 17 | * [jQuery](https://github.com/jquery/jquery) 1.5+ 18 | * [store.js](https://github.com/marcuswestin/store.js) 1.3.17 19 | 20 | # Features 21 | * Stores access token client side using store.js for persistence between refreshes 22 | * Adds authorization header to ajax requests 23 | * Adds CSRF token to header requests if provided to protect access token 24 | * When access token expires all 401 requests are buffered and fired after a new access token is generated by using 25 | refresh token server side 26 | * Works with AMD modules 27 | 28 | # Installation 29 | Can be installed via bower package 30 | ``` 31 | bower install --save jquery-oauth 32 | ``` 33 | 34 | ... or by cloning the repository 35 | ``` 36 | git clone git@github.com:esbenp/jquery-oauth.git 37 | ``` 38 | ... or by grabbing a zip of the latest release 39 | 40 | # Usage 41 | OAuth has to be implemented server side. If using Laravel, I recommend the great library 42 | [oauth-server-laravel](https://github.com/lucadegasperi/oauth2-server-laravel) by 43 | [Luca Degasperi](https://github.com/lucadegasperi) ([@lucadegasperi](https://twitter.com/lucadegasperi)). 44 | 45 | Have an endpoint that issues access tokens. Remember do NOT send ```client_id``` and ```client_secret``` with the request. 46 | Send the request to a proxy endpoint that sends the request to the OAuth endpoint. Remember do NOT save the refresh 47 | token client side. Save this as an encrypted httponly cookie and use a proxy to send this to the OAuth endpoint when 48 | refreshing access tokens. 49 | 50 | ## Example of resource owner implementation in a SPA 51 | #### Somewhere where things are initialised 52 | ```javascript 53 | define([/*other dependencies,*/ "jquery-oauth", function(jqOAuth){ 54 | /* other initialisation */ 55 | 56 | var csrfToken = $("input[name='_token']").val(); 57 | 58 | var auth = new jqOAuth({ 59 | csrfToken: csrfToken, 60 | events: { 61 | login: function() { 62 | // User is hereby logged in and the 63 | // access token will be added to subsequent 64 | // $.ajax calls, until a new token cannot 65 | // be acquired or auth.logout() is called. 66 | }, 67 | logout: function() { 68 | // auth.logout() has been called and the 69 | // authorization headers are removed from 70 | // $.ajaxSettings. Tokens are removed from 71 | // localStorage. 72 | }, 73 | tokenExpiration: function() { 74 | // this event is fired when 401 calls are 75 | // received from the server. Has to return 76 | // an ajax promise. 77 | // New tokens are set with auth.setAccessToken() 78 | 79 | return $.post("/refresh-token-proxy-endpoint").success(function(response){ 80 | auth.setAccessToken(response.accessToken, response.accessTokenExpiration); 81 | }); 82 | } 83 | } 84 | }); 85 | }); 86 | ``` 87 | 88 | #### Login form (when form is submitted) 89 | ```javascript 90 | $.ajax({ 91 | url: "/access-token-proxy-endpoint", 92 | method: "POST", 93 | data: { 94 | username: "username-from-a-form", 95 | password: "password-from-a-form" 96 | }, 97 | statusCode: { 98 | 200: function(response) { 99 | auth.login(response.accessToken, response.accessTokenExpiration); 100 | }, 101 | 401: function() { 102 | alert("The username or password were not correct. Try again."); 103 | } 104 | } 105 | }); 106 | ``` 107 | 108 | #### Logout 109 | ```javascript 110 | auth.logout(); 111 | ``` 112 | 113 | # API 114 | These calls are available through auth 115 | 116 | ```javascript 117 | define(['jquery-oauth'], function(auth){ 118 | // The access token and expiration in seconds 119 | // Sets authorization headers and stores tokens 120 | auth.login(accessToken, accessTokenExpiration); 121 | 122 | // Removes headers from $.ajax and tokens from localStorage 123 | auth.logout(); 124 | 125 | // Initialize library 126 | var auth = new jqOAuth({ 127 | csrfToken: "token" //CSRF token, 128 | events: { 129 | logout: function(){}, 130 | login: function(){}, 131 | tokenExpiration: function(){} 132 | } 133 | }); 134 | 135 | // Sets new tokens - should be used with tokenExpiration event 136 | auth.setAccessToken(accessToken, accessTokenExpiration); 137 | 138 | // Checks for tokens 139 | auth.hasAccessToken(); 140 | auth.hasAccessTokenExpiration(); 141 | }); 142 | ``` 143 | 144 | # Thank you 145 | The library addresses OAuth problems in general. Many of these are outlined in this great article: 146 | [OAuth2 with Angular the right way](http://jeremymarc.github.io/2014/08/14/oauth2-with-angular-the-right-way/) 147 | 148 | Also, the refresh token functionality takes great inspiration in this great AngularJS library: 149 | [angular-http-auth](https://github.com/witoldsz/angular-http-auth) 150 | 151 | So thank you [Jeremy Marc](https://github.com/jeremymarc) ([@jeremymarc](https://twitter.com/jeremymarc)) and 152 | [Witold Szczerba](https://github.com/witoldsz) ([@witoldsz](https://twitter.com/witoldsz)) 153 | 154 | # License 155 | Copyright © 2015 [Esben Petersen](http://github.com/esbenp) & Contributors 156 | 157 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with 158 | the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 159 | 160 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 161 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 162 | language governing permissions and limitations under the License. 163 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-oauth", 3 | "version": "2.1.3", 4 | "homepage": "https://github.com/esbenp/jquery-oauth", 5 | "authors": [ 6 | "Esben Petersen " 7 | ], 8 | "description": "A $.ajax wrapper for OAuth 2 access and refresh token management for use in a SPA", 9 | "main": "src/jquery.oauth.js", 10 | "keywords": [ 11 | "oauth", 12 | "ajax", 13 | "jquery" 14 | ], 15 | "license": "MIT", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "public/vendor", 21 | "test", 22 | "tests" 23 | ], 24 | "devDependencies": { 25 | "requirejs": "~2.1.16" 26 | }, 27 | "dependencies": { 28 | "jquery": ">=1.5", 29 | "store-js": "~1.3.17" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dist/jquery.oauth.js: -------------------------------------------------------------------------------- 1 | ;(function(root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(['jquery', 'store'], factory); 4 | } else if (typeof exports === 'object') { 5 | module.exports = factory(require('jquery'), require('store')); 6 | } else { 7 | root.jqOAuth = factory(root.jQuery, root.store); 8 | } 9 | }(this, function($, storage) { 10 | var jqOAuth = function jqOAuth(options) { 11 | this.options = {}; 12 | this.data = {}; 13 | this.intercept = false; 14 | this.refreshing = false; 15 | this.buffer = []; 16 | this.currentRequests = []; 17 | 18 | this._resetOptions(); 19 | this._setupInterceptor(); 20 | 21 | $.extend(this.options, options); 22 | 23 | if (this._hasStoredData()) { 24 | this._getStoredData(); 25 | 26 | if (this.hasAccessToken()) { 27 | this.login(this.data.accessToken); 28 | } 29 | } else { 30 | this._resetData(); 31 | this._updateStorage(); 32 | } 33 | 34 | if (options.csrfToken !== null) { 35 | this._setCsrfHeader(); 36 | } 37 | } 38 | 39 | // Public methods 40 | 41 | jqOAuth.prototype.getAccessToken = function getAccessToken() { 42 | return this.data.accessToken; 43 | }; 44 | 45 | jqOAuth.prototype.hasAccessToken = function hasAccessToken() { 46 | return this.data.accessToken !== null; 47 | }; 48 | 49 | jqOAuth.prototype.logout = function logout() { 50 | this._resetData(); 51 | this._updateStorage(); 52 | this._deactivateInterceptor(); 53 | this._removeAjaxHeader("Authorization"); 54 | this._fireEvent("logout"); 55 | }; 56 | 57 | jqOAuth.prototype.login = function login(accessToken) { 58 | this.setAccessToken(accessToken); 59 | 60 | this._activateInterceptor(); 61 | this._fireEvent("login"); 62 | }; 63 | 64 | jqOAuth.prototype.setAccessToken = function setAccessToken(accessToken) { 65 | this.data.accessToken = accessToken; 66 | 67 | this._setAuthorizationHeader(); 68 | this._updateStorage(); 69 | }; 70 | 71 | jqOAuth.prototype.setCsrfToken = function setCsrfToken(csrfToken) { 72 | this.options.csrfToken = csrfToken; 73 | 74 | this._setCsrfHeader(); 75 | }; 76 | 77 | // Private methods 78 | 79 | jqOAuth.prototype._activateInterceptor = function _activateInterceptor() { 80 | this.intercept = true; 81 | }; 82 | 83 | jqOAuth.prototype._addToBuffer = function _addToBuffer(settings, deferred) { 84 | this.buffer.push({ 85 | deferred: deferred, 86 | settings: settings 87 | }); 88 | }; 89 | 90 | jqOAuth.prototype._clearBuffer = function _clearBuffer() { 91 | this.buffer = []; 92 | }; 93 | 94 | jqOAuth.prototype._deactivateInterceptor = function _deactivateInterceptor() { 95 | this.intercept = false; 96 | }; 97 | 98 | jqOAuth.prototype._fireBuffer = function _fireBuffer() { 99 | var self = this; 100 | var deferred; 101 | var promises = []; 102 | 103 | for(var i in this.buffer) { 104 | deferred = this.buffer[i].deferred; 105 | 106 | this.buffer[i].settings.refreshRetry = true; 107 | this.buffer[i].settings.headers["Authorization"] = $.ajaxSettings.headers["Authorization"]; 108 | 109 | promises.push($.ajax(this.buffer[i].settings).then(deferred.resolve, deferred.reject)); 110 | } 111 | 112 | this._clearBuffer(); 113 | 114 | $.when.apply($, promises) 115 | .done(function() { 116 | self._setRefreshingFlag(false); 117 | }) 118 | .fail(function(){ 119 | self._setRefreshingFlag(false); 120 | self.logout(); 121 | }); 122 | }; 123 | 124 | jqOAuth.prototype._fireEvent = function _fireEvent(eventType) { 125 | if (this._hasEvent(eventType)) { 126 | return this.options.events[eventType](); 127 | } 128 | }; 129 | 130 | jqOAuth.prototype._getStoredData = function _getStoredData() { 131 | $.extend(this.data, storage.get(this.options.tokenName)); 132 | }; 133 | 134 | jqOAuth.prototype._hasEvent = function _hasEvent(eventType) { 135 | return this.options.events[eventType] !== undefined && typeof this.options.events[eventType] === "function"; 136 | }; 137 | 138 | jqOAuth.prototype._hasStoredData = function _hasStoredData() { 139 | return storage.get(this.options.tokenName) !== undefined; 140 | }; 141 | 142 | jqOAuth.prototype._isAjaxHeadersInitialized = function _isAjaxHeadersInitialized() { 143 | return $.ajaxSettings.headers !== undefined; 144 | }; 145 | 146 | jqOAuth.prototype._removeAjaxHeader = function _removeAjaxHeader(header) { 147 | if (!this._isAjaxHeadersInitialized()) { 148 | return true; 149 | } 150 | $.ajaxSettings.headers[header] = undefined; 151 | }; 152 | 153 | jqOAuth.prototype._removeAllAjaxHeaders = function _removeAllAjaxHeaders() { 154 | this._removeAjaxHeader("Authorization"); 155 | this._removeAjaxHeader("X-CSRF-Token"); 156 | }; 157 | 158 | jqOAuth.prototype._resetData = function _resetData() { 159 | this.data = { 160 | accessToken: null 161 | }; 162 | }; 163 | 164 | jqOAuth.prototype._resetOptions = function _resetOptions() { 165 | this.options = { 166 | bufferInterval: 25, 167 | bufferWaitLimit: 500, 168 | csrfToken: null, 169 | events: {}, 170 | tokenName: 'jquery.oauth' 171 | }; 172 | 173 | this._removeAllAjaxHeaders(); 174 | }; 175 | 176 | jqOAuth.prototype._setAjaxHeader = function _setAjaxHeader(header, value) { 177 | if (!this._isAjaxHeadersInitialized()) { 178 | $.ajaxSettings.headers = {}; 179 | } 180 | 181 | $.ajaxSettings.headers[header] = value; 182 | }; 183 | 184 | jqOAuth.prototype._setAuthorizationHeader = function _setAuthorizationHeader() { 185 | this._setAjaxHeader("Authorization", "Bearer " + this.data.accessToken); 186 | }; 187 | 188 | jqOAuth.prototype._setCsrfHeader = function _setCsrfHeader() { 189 | this._setAjaxHeader("X-CSRF-Token", this.options.csrfToken); 190 | }; 191 | 192 | jqOAuth.prototype._setRefreshingFlag = function _setRefreshingFlag(newFlag) { 193 | this.refreshing = newFlag; 194 | }; 195 | 196 | jqOAuth.prototype._setupInterceptor = function _setupInterceptor() { 197 | var self = this; 198 | 199 | // Credits to gnarf @ http://stackoverflow.com/a/12446363/602488 200 | $.ajaxPrefilter(function(options, originalOptions, jqxhr) { 201 | if (options.refreshRetry === true) { 202 | return; 203 | } 204 | 205 | var deferred = $.Deferred(); 206 | 207 | self.currentRequests.push(options.url); 208 | jqxhr.always(function(){ 209 | self.currentRequests.splice($.inArray(options.url, self.currentRequests), 1); 210 | }); 211 | jqxhr.done(deferred.resolve); 212 | jqxhr.fail(function() { 213 | var args = Array.prototype.slice.call(arguments); 214 | 215 | if (self.intercept && jqxhr.status === 401 && self._hasEvent("tokenExpiration")) { 216 | self._addToBuffer(options, deferred); 217 | 218 | if (!self.refreshing) { 219 | self._setRefreshingFlag(true); 220 | self._fireEvent("tokenExpiration") 221 | .success(function () { 222 | // Setup buffer interval that waits for all sent requests to return 223 | var waited = 0; 224 | self.interval = setInterval(function(){ 225 | waited += self.options.bufferInterval; 226 | 227 | // All requests have returned 401 and have been buffered 228 | if (self.currentRequests.length === 0 || waited >= self.options.bufferWaitLimit) { 229 | clearInterval(self.interval); 230 | self._fireBuffer(); 231 | } 232 | }, self.options.bufferInterval); 233 | }) 234 | .fail(function () { 235 | self.logout(); 236 | }); 237 | } 238 | } else { 239 | deferred.rejectWith(jqxhr, args); 240 | } 241 | }); 242 | 243 | return deferred.promise(jqxhr); 244 | }); 245 | }; 246 | 247 | jqOAuth.prototype._updateStorage = function _updateStorage() { 248 | storage.set(this.options.tokenName, this.data); 249 | }; 250 | 251 | return jqOAuth; 252 | })); 253 | -------------------------------------------------------------------------------- /dist/jquery.oauth.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"function"==typeof define&&define.amd?define(["jquery","store"],e):"object"==typeof exports?module.exports=e(require("jquery"),require("store")):t.jqOAuth=e(t.jQuery,t.store)}(this,function(t,e){var r=function(e){this.options={},this.data={},this.intercept=!1,this.refreshing=!1,this.buffer=[],this.currentRequests=[],this._resetOptions(),this._setupInterceptor(),t.extend(this.options,e),this._hasStoredData()?(this._getStoredData(),this.hasAccessToken()&&this.login(this.data.accessToken)):(this._resetData(),this._updateStorage()),null!==e.csrfToken&&this._setCsrfHeader()};return r.prototype.getAccessToken=function(){return this.data.accessToken},r.prototype.hasAccessToken=function(){return null!==this.data.accessToken},r.prototype.logout=function(){this._resetData(),this._updateStorage(),this._deactivateInterceptor(),this._removeAjaxHeader("Authorization"),this._fireEvent("logout")},r.prototype.login=function(t){this.setAccessToken(t),this._activateInterceptor(),this._fireEvent("login")},r.prototype.setAccessToken=function(t){this.data.accessToken=t,this._setAuthorizationHeader(),this._updateStorage()},r.prototype.setCsrfToken=function(t){this.options.csrfToken=t,this._setCsrfHeader()},r.prototype._activateInterceptor=function(){this.intercept=!0},r.prototype._addToBuffer=function(t,e){this.buffer.push({deferred:e,settings:t})},r.prototype._clearBuffer=function(){this.buffer=[]},r.prototype._deactivateInterceptor=function(){this.intercept=!1},r.prototype._fireBuffer=function(){var e,r=this,o=[];for(var i in this.buffer)e=this.buffer[i].deferred,this.buffer[i].settings.refreshRetry=!0,this.buffer[i].settings.headers.Authorization=t.ajaxSettings.headers.Authorization,o.push(t.ajax(this.buffer[i].settings).then(e.resolve,e.reject));this._clearBuffer(),t.when.apply(t,o).done(function(){r._setRefreshingFlag(!1)}).fail(function(){r._setRefreshingFlag(!1),r.logout()})},r.prototype._fireEvent=function(t){return this._hasEvent(t)?this.options.events[t]():void 0},r.prototype._getStoredData=function(){t.extend(this.data,e.get(this.options.tokenName))},r.prototype._hasEvent=function(t){return void 0!==this.options.events[t]&&"function"==typeof this.options.events[t]},r.prototype._hasStoredData=function(){return void 0!==e.get(this.options.tokenName)},r.prototype._isAjaxHeadersInitialized=function(){return void 0!==t.ajaxSettings.headers},r.prototype._removeAjaxHeader=function(e){return this._isAjaxHeadersInitialized()?void(t.ajaxSettings.headers[e]=void 0):!0},r.prototype._removeAllAjaxHeaders=function(){this._removeAjaxHeader("Authorization"),this._removeAjaxHeader("X-CSRF-Token")},r.prototype._resetData=function(){this.data={accessToken:null}},r.prototype._resetOptions=function(){this.options={bufferInterval:25,bufferWaitLimit:500,csrfToken:null,events:{},tokenName:"jquery.oauth"},this._removeAllAjaxHeaders()},r.prototype._setAjaxHeader=function(e,r){this._isAjaxHeadersInitialized()||(t.ajaxSettings.headers={}),t.ajaxSettings.headers[e]=r},r.prototype._setAuthorizationHeader=function(){this._setAjaxHeader("Authorization","Bearer "+this.data.accessToken)},r.prototype._setCsrfHeader=function(){this._setAjaxHeader("X-CSRF-Token",this.options.csrfToken)},r.prototype._setRefreshingFlag=function(t){this.refreshing=t},r.prototype._setupInterceptor=function(){var e=this;t.ajaxPrefilter(function(r,o,i){if(r.refreshRetry!==!0){var s=t.Deferred();return e.currentRequests.push(r.url),i.always(function(){e.currentRequests.splice(t.inArray(r.url,e.currentRequests),1)}),i.done(s.resolve),i.fail(function(){var t=Array.prototype.slice.call(arguments);e.intercept&&401===i.status&&e._hasEvent("tokenExpiration")?(e._addToBuffer(r,s),e.refreshing||(e._setRefreshingFlag(!0),e._fireEvent("tokenExpiration").success(function(){var t=0;e.interval=setInterval(function(){t+=e.options.bufferInterval,(0===e.currentRequests.length||t>=e.options.bufferWaitLimit)&&(clearInterval(e.interval),e._fireBuffer())},e.options.bufferInterval)}).fail(function(){e.logout()}))):s.rejectWith(i,t)}),s.promise(i)}})},r.prototype._updateStorage=function(){e.set(this.options.tokenName,this.data)},r}); -------------------------------------------------------------------------------- /examples/amd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Examples 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/amd.js: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | baseUrl: '../bower_components', 3 | paths: { 4 | assets: '../examples/assets', 5 | dist: '../dist', 6 | 7 | jquery: 'jquery/dist/jquery', 8 | 'jquery-oauth': '../dist/jquery.oauth', 9 | 'store': 'store-js/store' 10 | } 11 | }); 12 | 13 | requirejs(['jquery-oauth'], function(jqOAuth){ 14 | var auth = new jqOAuth({ 15 | events: { 16 | 17 | } 18 | }); 19 | 20 | auth.login("1234", "1234"); 21 | 22 | $.get("assets/resource.json"); 23 | 24 | auth.logout(); 25 | 26 | $.get("assets/resource.json"); 27 | }); -------------------------------------------------------------------------------- /examples/assets/resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1 3 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"); 2 | var umd = require("gulp-umd"); 3 | var uglify = require("gulp-uglify"); 4 | var rename = require("gulp-rename"); 5 | var del = require("del"); 6 | 7 | gulp.task("umd", ["clean"], function() { 8 | return gulp.src("src/*") 9 | .pipe(umd({ 10 | dependencies: function(file) { 11 | return [ 12 | { 13 | name: "jquery", 14 | amd: "jquery", 15 | cjs: "jquery", 16 | global: "jQuery", 17 | param: "$" 18 | }, 19 | { 20 | name: "store", 21 | amd: "store", 22 | cjs: "store", 23 | global: "store", 24 | param: "storage" 25 | } 26 | ]; 27 | }, 28 | exports: function(file) { 29 | return 'jqOAuth'; 30 | }, 31 | namespace: function(file) { 32 | return 'jqOAuth'; 33 | } 34 | })) 35 | .pipe(gulp.dest("dist")); 36 | }); 37 | 38 | gulp.task("clean", function(callback){ 39 | return del(["dist"], callback); 40 | }); 41 | 42 | gulp.task("production", ["umd"], function(){ 43 | return gulp.src("dist/*.js") 44 | .pipe(rename({suffix: ".min"})) 45 | .pipe(uglify()) 46 | .pipe(gulp.dest("dist")) 47 | }); 48 | 49 | gulp.task("dev", function(){ 50 | return gulp.watch("src/**/*.js", ["umd"]); 51 | }); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-oauth", 3 | "version": "2.1.3", 4 | "description": "jQuery OAuth ============ A $.ajax wrapper for OAuth 2 access and refresh token management for use in a SPA.", 5 | "main": "gulpfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/esbenp/jquery-oauth.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/esbenp/jquery-oauth/issues" 17 | }, 18 | "homepage": "https://github.com/esbenp/jquery-oauth#readme", 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "del": "^2.1.0", 22 | "gulp": "^3.8.11", 23 | "gulp-rename": "^1.2.2", 24 | "gulp-uglify": "^1.5.1", 25 | "gulp-umd": "^0.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/jquery.oauth.js: -------------------------------------------------------------------------------- 1 | var jqOAuth = function jqOAuth(options) { 2 | this.options = {}; 3 | this.data = {}; 4 | this.intercept = false; 5 | this.refreshing = false; 6 | this.buffer = []; 7 | this.currentRequests = []; 8 | 9 | this._resetOptions(); 10 | this._setupInterceptor(); 11 | 12 | $.extend(this.options, options); 13 | 14 | if (this._hasStoredData()) { 15 | this._getStoredData(); 16 | 17 | if (this.hasAccessToken()) { 18 | this.login(this.data.accessToken); 19 | } 20 | } else { 21 | this._resetData(); 22 | this._updateStorage(); 23 | } 24 | 25 | if (options.csrfToken !== null) { 26 | this._setCsrfHeader(); 27 | } 28 | } 29 | 30 | // Public methods 31 | 32 | jqOAuth.prototype.getAccessToken = function getAccessToken() { 33 | return this.data.accessToken; 34 | }; 35 | 36 | jqOAuth.prototype.hasAccessToken = function hasAccessToken() { 37 | return this.data.accessToken !== null; 38 | }; 39 | 40 | jqOAuth.prototype.logout = function logout() { 41 | this._resetData(); 42 | this._updateStorage(); 43 | this._deactivateInterceptor(); 44 | this._removeAjaxHeader("Authorization"); 45 | this._fireEvent("logout"); 46 | }; 47 | 48 | jqOAuth.prototype.login = function login(accessToken) { 49 | this.setAccessToken(accessToken); 50 | 51 | this._activateInterceptor(); 52 | this._fireEvent("login"); 53 | }; 54 | 55 | jqOAuth.prototype.setAccessToken = function setAccessToken(accessToken) { 56 | this.data.accessToken = accessToken; 57 | 58 | this._setAuthorizationHeader(); 59 | this._updateStorage(); 60 | }; 61 | 62 | jqOAuth.prototype.setCsrfToken = function setCsrfToken(csrfToken) { 63 | this.options.csrfToken = csrfToken; 64 | 65 | this._setCsrfHeader(); 66 | }; 67 | 68 | // Private methods 69 | 70 | jqOAuth.prototype._activateInterceptor = function _activateInterceptor() { 71 | this.intercept = true; 72 | }; 73 | 74 | jqOAuth.prototype._addToBuffer = function _addToBuffer(settings, deferred) { 75 | this.buffer.push({ 76 | deferred: deferred, 77 | settings: settings 78 | }); 79 | }; 80 | 81 | jqOAuth.prototype._clearBuffer = function _clearBuffer() { 82 | this.buffer = []; 83 | }; 84 | 85 | jqOAuth.prototype._deactivateInterceptor = function _deactivateInterceptor() { 86 | this.intercept = false; 87 | }; 88 | 89 | jqOAuth.prototype._fireBuffer = function _fireBuffer() { 90 | var self = this; 91 | var deferred; 92 | var promises = []; 93 | 94 | for(var i in this.buffer) { 95 | deferred = this.buffer[i].deferred; 96 | 97 | this.buffer[i].settings.refreshRetry = true; 98 | this.buffer[i].settings.headers["Authorization"] = $.ajaxSettings.headers["Authorization"]; 99 | 100 | promises.push($.ajax(this.buffer[i].settings).then(deferred.resolve, deferred.reject)); 101 | } 102 | 103 | this._clearBuffer(); 104 | 105 | $.when.apply($, promises) 106 | .done(function() { 107 | self._setRefreshingFlag(false); 108 | }) 109 | .fail(function(){ 110 | self._setRefreshingFlag(false); 111 | self.logout(); 112 | }); 113 | }; 114 | 115 | jqOAuth.prototype._fireEvent = function _fireEvent(eventType) { 116 | if (this._hasEvent(eventType)) { 117 | return this.options.events[eventType](); 118 | } 119 | }; 120 | 121 | jqOAuth.prototype._getStoredData = function _getStoredData() { 122 | $.extend(this.data, storage.get(this.options.tokenName)); 123 | }; 124 | 125 | jqOAuth.prototype._hasEvent = function _hasEvent(eventType) { 126 | return this.options.events[eventType] !== undefined && typeof this.options.events[eventType] === "function"; 127 | }; 128 | 129 | jqOAuth.prototype._hasStoredData = function _hasStoredData() { 130 | return storage.get(this.options.tokenName) !== undefined; 131 | }; 132 | 133 | jqOAuth.prototype._isAjaxHeadersInitialized = function _isAjaxHeadersInitialized() { 134 | return $.ajaxSettings.headers !== undefined; 135 | }; 136 | 137 | jqOAuth.prototype._removeAjaxHeader = function _removeAjaxHeader(header) { 138 | if (!this._isAjaxHeadersInitialized()) { 139 | return true; 140 | } 141 | $.ajaxSettings.headers[header] = undefined; 142 | }; 143 | 144 | jqOAuth.prototype._removeAllAjaxHeaders = function _removeAllAjaxHeaders() { 145 | this._removeAjaxHeader("Authorization"); 146 | this._removeAjaxHeader("X-CSRF-Token"); 147 | }; 148 | 149 | jqOAuth.prototype._resetData = function _resetData() { 150 | this.data = { 151 | accessToken: null 152 | }; 153 | }; 154 | 155 | jqOAuth.prototype._resetOptions = function _resetOptions() { 156 | this.options = { 157 | bufferInterval: 25, 158 | bufferWaitLimit: 500, 159 | csrfToken: null, 160 | events: {}, 161 | tokenName: 'jquery.oauth' 162 | }; 163 | 164 | this._removeAllAjaxHeaders(); 165 | }; 166 | 167 | jqOAuth.prototype._setAjaxHeader = function _setAjaxHeader(header, value) { 168 | if (!this._isAjaxHeadersInitialized()) { 169 | $.ajaxSettings.headers = {}; 170 | } 171 | 172 | $.ajaxSettings.headers[header] = value; 173 | }; 174 | 175 | jqOAuth.prototype._setAuthorizationHeader = function _setAuthorizationHeader() { 176 | this._setAjaxHeader("Authorization", "Bearer " + this.data.accessToken); 177 | }; 178 | 179 | jqOAuth.prototype._setCsrfHeader = function _setCsrfHeader() { 180 | this._setAjaxHeader("X-CSRF-Token", this.options.csrfToken); 181 | }; 182 | 183 | jqOAuth.prototype._setRefreshingFlag = function _setRefreshingFlag(newFlag) { 184 | this.refreshing = newFlag; 185 | }; 186 | 187 | jqOAuth.prototype._setupInterceptor = function _setupInterceptor() { 188 | var self = this; 189 | 190 | // Credits to gnarf @ http://stackoverflow.com/a/12446363/602488 191 | $.ajaxPrefilter(function(options, originalOptions, jqxhr) { 192 | if (options.refreshRetry === true) { 193 | return; 194 | } 195 | 196 | var deferred = $.Deferred(); 197 | 198 | self.currentRequests.push(options.url); 199 | jqxhr.always(function(){ 200 | self.currentRequests.splice($.inArray(options.url, self.currentRequests), 1); 201 | }); 202 | jqxhr.done(deferred.resolve); 203 | jqxhr.fail(function() { 204 | var args = Array.prototype.slice.call(arguments); 205 | 206 | if (self.intercept && jqxhr.status === 401 && self._hasEvent("tokenExpiration")) { 207 | self._addToBuffer(options, deferred); 208 | 209 | if (!self.refreshing) { 210 | self._setRefreshingFlag(true); 211 | self._fireEvent("tokenExpiration") 212 | .success(function () { 213 | // Setup buffer interval that waits for all sent requests to return 214 | var waited = 0; 215 | self.interval = setInterval(function(){ 216 | waited += self.options.bufferInterval; 217 | 218 | // All requests have returned 401 and have been buffered 219 | if (self.currentRequests.length === 0 || waited >= self.options.bufferWaitLimit) { 220 | clearInterval(self.interval); 221 | self._fireBuffer(); 222 | } 223 | }, self.options.bufferInterval); 224 | }) 225 | .fail(function () { 226 | self.logout(); 227 | }); 228 | } 229 | } else { 230 | deferred.rejectWith(jqxhr, args); 231 | } 232 | }); 233 | 234 | return deferred.promise(jqxhr); 235 | }); 236 | }; 237 | 238 | jqOAuth.prototype._updateStorage = function _updateStorage() { 239 | storage.set(this.options.tokenName, this.data); 240 | }; 241 | --------------------------------------------------------------------------------