diff options
author | New Tobin Paradigm <email@mattatobin.com> | 2019-12-31 20:17:35 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-12-31 20:17:35 -0500 |
commit | 26b297510a11758727438df4669357a2a2bc42ce (patch) | |
tree | 038c36c0214be1e19d11c8bdb58bf82ac4b3a994 /mailnews/base/util | |
parent | 357405f6356e28e5fa94cecc078b65c20433d236 (diff) | |
parent | 12eb1554f9ff0c0d8dc49da44b6bd0081b1231a1 (diff) | |
download | UXP-26b297510a11758727438df4669357a2a2bc42ce.tar UXP-26b297510a11758727438df4669357a2a2bc42ce.tar.gz UXP-26b297510a11758727438df4669357a2a2bc42ce.tar.lz UXP-26b297510a11758727438df4669357a2a2bc42ce.tar.xz UXP-26b297510a11758727438df4669357a2a2bc42ce.zip |
Merge pull request #1340 from g4jc/mailnews_enhance
OAuth2 updates
Diffstat (limited to 'mailnews/base/util')
-rw-r--r-- | mailnews/base/util/OAuth2.jsm | 173 |
1 files changed, 102 insertions, 71 deletions
diff --git a/mailnews/base/util/OAuth2.jsm b/mailnews/base/util/OAuth2.jsm index 94f850e0b..8c9282d02 100644 --- a/mailnews/base/util/OAuth2.jsm +++ b/mailnews/base/util/OAuth2.jsm @@ -3,30 +3,37 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ /** - * Provides OAuth 2.0 authentication + * Provides OAuth 2.0 authentication. + * @see RFC 6749 */ var EXPORTED_SYMBOLS = ["OAuth2"]; var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; -Cu.import("resource://gre/modules/Http.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource:///modules/gloda/log4moz.js"); -function parseURLData(aData) { - let result = {}; - aData.split(/[?#]/, 2)[1].split("&").forEach(function (aParam) { - let [key, value] = aParam.split("="); - result[key] = decodeURIComponent(value); - }); - return result; -} +Cu.importGlobalProperties(["fetch"]); // Only allow one connecting window per endpoint. var gConnecting = {}; -function OAuth2(aBaseURI, aScope, aAppKey, aAppSecret) { +/** + * Constructor for the OAuth2 object. + * + * @constructor + * @param {string} aBaseURI - The base URI for authentication and token + * requests, oauth2/auth or oauth2/token will be added for the actual + * requests. + * @param {?string} aScope - The scope as specified by RFC 6749 Section 3.3. + * Will not be included in the requests if falsy. + * @param {string} aAppKey - The client_id as specified by RFC 6749 Section + * 2.3.1. + * @param {string} [aAppSecret=null] - The client_secret as specified in + * RFC 6749 section 2.3.1. Will not be included in the requests if null. + */ +function OAuth2(aBaseURI, aScope, aAppKey, aAppSecret = null) { this.authURI = aBaseURI + "oauth2/auth"; this.tokenURI = aBaseURI + "oauth2/token"; this.consumerKey = aAppKey; @@ -37,12 +44,7 @@ function OAuth2(aBaseURI, aScope, aAppKey, aAppSecret) { this.log = Log4Moz.getConfiguredLogger("TBOAuth"); } -OAuth2.CODE_AUTHORIZATION = "authorization_code"; -OAuth2.CODE_REFRESH = "refresh_token"; - OAuth2.prototype = { - - responseType: "code", consumerKey: null, consumerSecret: null, completionURI: "http://localhost", @@ -63,7 +65,7 @@ OAuth2.prototype = { if (!aRefresh && this.accessToken) { aSuccess(); } else if (this.refreshToken) { - this.requestAccessToken(this.refreshToken, OAuth2.CODE_REFRESH); + this.requestAccessToken(this.refreshToken, true); } else { if (!aWithUI) { aFailure('{ "error": "auth_noui" }'); @@ -78,25 +80,31 @@ OAuth2.prototype = { }, requestAuthorization: function requestAuthorization() { - let params = [ - ["response_type", this.responseType], - ["client_id", this.consumerKey], - ["redirect_uri", this.completionURI], - ]; - // The scope can be optional. + let params = new URLSearchParams({ + response_type: "code", + client_id: this.consumerKey, + redirect_uri: this.completionURI, + }); + + // The scope is optional. if (this.scope) { - params.push(["scope", this.scope]); + params.append("scope", this.scope); } - // Add extra parameters - params.push(...this.extraAuthParams); + for (let [name, value] of this.extraAuthParams) { + params.append(name, value); + } - // Now map the parameters to a string - params = params.map(([k,v]) => k + "=" + encodeURIComponent(v)).join("&"); + let authEndpointURI = this.authURI + "?" + params.toString(); + this.log.info( + "Interacting with the resource owner to obtain an authorization grant " + + "from the authorization endpoint: " + + authEndpointURI + ); this._browserRequest = { account: this, - url: this.authURI + "?" + params, + url: authEndpointURI, _active: true, iconURI: "", cancelled: function() { @@ -170,65 +178,88 @@ OAuth2.prototype = { delete this._browserRequest; }, - onAuthorizationReceived: function(aData) { - this.log.info("authorization received" + aData); - let results = parseURLData(aData); - if (this.responseType == "code" && results.code) { - this.requestAccessToken(results.code, OAuth2.CODE_AUTHORIZATION); - } else if (this.responseType == "token") { - this.onAccessTokenReceived(JSON.stringify(results)); + // @see RFC 6749 section 4.1.2: Authorization Response + onAuthorizationReceived(aURL) { + this.log.info("OAuth2 authorization received: url=" + aURL); + let params = new URLSearchParams(aURL.split("?", 2)[1]); + if (params.has("code")) { + this.requestAccessToken(params.get("code"), false); + } else { + this.onAuthorizationFailed(null, aURL); } - else - this.onAuthorizationFailed(null, aData); }, onAuthorizationFailed: function(aError, aData) { this.connectFailureCallback(aData); }, - requestAccessToken: function requestAccessToken(aCode, aType) { - let params = [ - ["client_id", this.consumerKey], - ["client_secret", this.consumerSecret], - ["grant_type", aType], - ]; - - if (aType == OAuth2.CODE_AUTHORIZATION) { - params.push(["code", aCode]); - params.push(["redirect_uri", this.completionURI]); - } else if (aType == OAuth2.CODE_REFRESH) { - params.push(["refresh_token", aCode]); + /** + * Request a new access token, or refresh an existing one. + * @param {string} aCode - The token issued to the client. + * @param {boolean} aRefresh - Whether it's a refresh of a token or not. + */ + requestAccessToken(aCode, aRefresh) { + // @see RFC 6749 section 4.1.3. Access Token Request + // @see RFC 6749 section 6. Refreshing an Access Token + + let data = new URLSearchParams(); + data.append("client_id", this.consumerKey); + if (this.consumerSecret !== null) { + // Section 2.3.1. of RFC 6749 states that empty secrets MAY be omitted + // by the client. This OAuth implementation delegates this decission to + // the caller: If the secret is null, it will be omitted. + data.append("client_secret", this.consumerSecret); + } + + if (aRefresh) { + this.log.info( + `Making a refresh request to the token endpoint: ${this.tokenURI}` + ); + data.append("grant_type", "refresh_token"); + data.append("refresh_token", aCode); + } else { + this.log.info( + `Making access token request to the token endpoint: ${this.tokenURI}` + ); + data.append("grant_type", "authorization_code"); + data.append("code", aCode); + data.append("redirect_uri", this.completionURI); } - let options = { - postData: params, - onLoad: this.onAccessTokenReceived.bind(this), - onError: this.onAccessTokenFailed.bind(this) + fetch(this.tokenURI, { + method: "POST", + cache: "no-cache", + body: data, + }) + .then(response => response.json()) + .then(result => { + if ("error" in result) { + // RFC 6749 section 5.2. Error Response + this.log.info( + `The authorization server returned an error response: ${JSON.stringify( + result + )}` + ); + this.connectFailureCallback(result); + return; } - httpRequest(this.tokenURI, options); - }, - - onAccessTokenFailed: function onAccessTokenFailed(aError, aData) { - if (aError != "offline") { - this.refreshToken = null; - } - this.connectFailureCallback(aData); - }, - - onAccessTokenReceived: function onRequestTokenReceived(aData) { - let result = JSON.parse(aData); + // RFC 6749 section 5.1. Successful Response + this.log.info("The authorization server issued an access token."); this.accessToken = result.access_token; if ("refresh_token" in result) { - this.refreshToken = result.refresh_token; + this.refreshToken = result.refresh_token; } if ("expires_in" in result) { - this.tokenExpires = (new Date()).getTime() + (result.expires_in * 1000); + this.tokenExpires = new Date().getTime() + result.expires_in * 1000; } else { - this.tokenExpires = Number.MAX_VALUE; + this.tokenExpires = Number.MAX_VALUE; } - this.tokenType = result.token_type; - this.connectSuccessCallback(); + }) + .catch(err => { + this.log.info(`Connection to authorization server failed: ${err}`); + this.connectFailureCallback(err); + }); } }; |