Index: remoting/webapp/third_party_token_fetcher.js |
diff --git a/remoting/webapp/third_party_token_fetcher.js b/remoting/webapp/third_party_token_fetcher.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a16246cd0109067ea9411bdb557c6b3a53fc30e5 |
--- /dev/null |
+++ b/remoting/webapp/third_party_token_fetcher.js |
@@ -0,0 +1,171 @@ |
+// Copyright 2013 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+/** |
+ * @fileoverview |
+ * Third party authentication support for the remoting web-app. |
+ * |
+ * When third party authentication is being used, the client must request both a |
+ * token and a shared secret from a third-party server. The server can then |
+ * present the user with an authentication page, or use any other method to |
+ * authenticate the user via the browser. Once the user is authenticated, the |
+ * server will redirect the browser to a URL containing the token and shared |
+ * secret in its fragment. The client then sends only the token to the host. |
+ * The host signs the token, then contacts the third-party server to exchange |
+ * the token for the shared secret. Once both client and host have the shared |
+ * secret, they use a zero-disclosure mutual authentication protocol to |
+ * negotiate an authentication key, which is used to establish the connection. |
+ */ |
+ |
+'use strict'; |
+ |
+/** @suppress {duplicate} */ |
+var remoting = remoting || {}; |
+ |
+/** |
+ * @constructor |
+ * Encapsulates the logic to fetch a third party authentication token. |
+ * |
+ * @param {string} tokenUrl Token-issue URL received from the host. |
+ * @param {string} hostPublicKey Host public key (DER and Base64 encoded). |
+ * @param {string} scope OAuth scope to request the token for. |
+ * @param {Array.<string>} tokenUrlPatterns Token URL patterns allowed for the |
+ * domain, received from the directory server. |
+ * @param {function(string, string):void} onThirdPartyTokenFetched Callback. |
+ */ |
+remoting.ThirdPartyTokenFetcher = function( |
+ tokenUrl, hostPublicKey, scope, tokenUrlPatterns, |
+ onThirdPartyTokenFetched) { |
+ this.tokenUrl_ = tokenUrl; |
+ this.tokenScope_ = scope; |
+ this.onThirdPartyTokenFetched_ = onThirdPartyTokenFetched; |
+ this.failFetchToken_ = function() { onThirdPartyTokenFetched('', ''); }; |
+ this.xsrfToken_ = remoting.generateXsrfToken(); |
+ this.tokenUrlPatterns_ = tokenUrlPatterns; |
+ this.hostPublicKey_ = hostPublicKey; |
+ if (chrome.experimental && chrome.experimental.identity) { |
+ /** @type {function():void} |
+ * @private */ |
+ this.fetchTokenInternal_ = this.fetchTokenIdentityApi_.bind(this); |
+ this.redirectUri_ = 'https://' + window.location.hostname + |
+ '.chromiumapp.org/ThirdPartyAuth'; |
+ } else { |
+ this.fetchTokenInternal_ = this.fetchTokenWindowOpen_.bind(this); |
+ this.redirectUri_ = remoting.settings.THIRD_PARTY_AUTH_REDIRECT_URI; |
+ } |
+}; |
+ |
+/** |
+ * Fetch a token with the parameters configured in this object. |
+ */ |
+remoting.ThirdPartyTokenFetcher.prototype.fetchToken = function() { |
+ // Verify the host-supplied URL matches the domain's allowed URL patterns. |
+ for (var i = 0; i < this.tokenUrlPatterns_.length; i++) { |
+ if (this.tokenUrl_.match(this.tokenUrlPatterns_[i])) { |
+ var hostPermissions = new remoting.ThirdPartyHostPermissions( |
+ this.tokenUrl_); |
+ hostPermissions.getPermission( |
+ this.fetchTokenInternal_, |
+ this.failFetchToken_); |
+ |
+ return; |
+ } |
+ } |
+ // If the URL doesn't match any pattern in the list, refuse to access it. |
+ console.error('Token URL does not match the domain\'s allowed URL patterns.' + |
+ ' URL: ' + this.tokenUrl_ + ', patterns: ' + this.tokenUrlPatterns_); |
+ this.failFetchToken_(); |
+}; |
+ |
+/** |
+ * Parse the access token from the URL to which we were redirected. |
+ * |
+ * @param {string} responseUrl The URL to which we were redirected. |
+ * @private |
+ */ |
+remoting.ThirdPartyTokenFetcher.prototype.parseRedirectUrl_ = |
+ function(responseUrl) { |
+ var token = ''; |
+ var sharedSecret = ''; |
+ if (responseUrl && |
+ responseUrl.search(this.redirectUri_ + '#') == 0) { |
+ var query = responseUrl.substring(this.redirectUri_.length + 1); |
+ var parts = query.split('&'); |
+ /** @type {Object.<string>} */ |
+ var queryArgs = {}; |
+ for (var i = 0; i < parts.length; i++) { |
+ var pair = parts[i].split('='); |
+ queryArgs[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); |
+ } |
+ |
+ // Check that 'state' contains the same XSRF token we sent in the request. |
+ var xsrfToken = queryArgs['state']; |
+ if (xsrfToken == this.xsrfToken_ && |
+ 'code' in queryArgs && 'access_token' in queryArgs) { |
+ // Terminology note: |
+ // In the OAuth code/token exchange semantics, 'code' refers to the value |
+ // obtained when the *user* authenticates itself, while 'access_token' is |
+ // the value obtained when the *application* authenticates itself to the |
+ // server ("implicitly", by receiving it directly in the URL fragment, or |
+ // explicitly, by sending the 'code' and a 'client_secret' to the server). |
+ // Internally, the piece of data obtained when the user authenticates |
+ // itself is called the 'token', and the one obtained when the host |
+ // authenticates itself (using the 'token' received from the client and |
+ // its private key) is called the 'shared secret'. |
+ // The client implicitly authenticates itself, and directly obtains the |
+ // 'shared secret', along with the 'token' from the redirect URL fragment. |
+ token = queryArgs['code']; |
+ sharedSecret = queryArgs['access_token']; |
+ } |
+ } |
+ this.onThirdPartyTokenFetched_(token, sharedSecret); |
+}; |
+ |
+/** |
+ * Build a full token request URL from the parameters in this object. |
+ * |
+ * @return {string} Full URL to request a token. |
+ * @private |
+ */ |
+remoting.ThirdPartyTokenFetcher.prototype.getFullTokenUrl_ = function() { |
+ return this.tokenUrl_ + '?' + remoting.xhr.urlencodeParamHash({ |
+ 'redirect_uri': this.redirectUri_, |
+ 'scope': this.tokenScope_, |
+ 'client_id': this.hostPublicKey_, |
+ // The webapp uses an "implicit" OAuth flow with multiple response types to |
+ // obtain both the code and the shared secret in a single request. |
+ 'response_type': 'code token', |
+ 'state': this.xsrfToken_ |
+ }); |
+}; |
+ |
+/** |
+ * Fetch a token by opening a new window and redirecting to a content script. |
+ * @private |
+ */ |
+remoting.ThirdPartyTokenFetcher.prototype.fetchTokenWindowOpen_ = function() { |
+ /** @type {remoting.ThirdPartyTokenFetcher} */ |
+ var that = this; |
+ var fullTokenUrl = this.getFullTokenUrl_(); |
+ // The function below can't be anonymous, since it needs to reference itself. |
+ /** @param {string} message Message received from the content script. */ |
+ function tokenMessageListener(message) { |
+ that.parseRedirectUrl_(message); |
+ chrome.extension.onMessage.removeListener(tokenMessageListener); |
+ } |
+ chrome.extension.onMessage.addListener(tokenMessageListener); |
+ window.open(fullTokenUrl, '_blank', 'location=yes,toolbar=no,menubar=no'); |
+}; |
+ |
+/** |
+ * Fetch a token from a token server using the identity.launchWebAuthFlow API. |
+ * @private |
+ */ |
+remoting.ThirdPartyTokenFetcher.prototype.fetchTokenIdentityApi_ = function() { |
+ var fullTokenUrl = this.getFullTokenUrl_(); |
+ // TODO(rmsousa): chrome.identity.launchWebAuthFlow is experimental. |
+ chrome.experimental.identity.launchWebAuthFlow( |
+ {'url': fullTokenUrl, 'interactive': true}, |
+ this.parseRedirectUrl_.bind(this)); |
+}; |