Chromium Code Reviews| Index: remoting/webapp/third_party_auth.js |
| diff --git a/remoting/webapp/third_party_auth.js b/remoting/webapp/third_party_auth.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..c57c3d3b23d3df143805a796911c2b5255c39b7e |
| --- /dev/null |
| +++ b/remoting/webapp/third_party_auth.js |
| @@ -0,0 +1,257 @@ |
| +// Copyright (c) 2012 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} 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 {string} hostPublicKey Host public key (DER and Base64 encoded). |
| + * @param {function(string, string):void} onThirdPartyTokenFetched Callback. |
| + */ |
| +remoting.ThirdPartyTokenFetcher = function( |
| + tokenUrl, scope, tokenUrlPatterns, hostPublicKey, |
| + 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_); |
| + |
|
Jamie
2013/04/16 20:57:20
Missing return here?
rmsousa
2013/04/17 03:48:46
Done.
|
| + } |
| + } |
| + // 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. " + |
|
Jamie
2013/04/16 20:57:20
Single quotes for JS string, please.
rmsousa
2013/04/17 03:48:46
Done.
|
| + '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[pair[0]] = pair[1]; |
|
Jamie
2013/04/16 20:57:20
I think it would be better to use decodeURICompone
rmsousa
2013/04/17 03:48:46
Done.
|
| + } |
| + |
| + // Check that 'state' contains the same XSRF token we sent in the request. |
| + var xsrfToken = decodeURIComponent(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 = decodeURIComponent(queryArgs['code']); |
| + sharedSecret = decodeURIComponent(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}, |
|
Jamie
2013/04/16 20:57:20
For the non-third-party flow, ISTR that you have t
rmsousa
2013/04/17 03:48:46
Is that related to trying non-interactive first, o
Jamie
2013/04/17 19:16:52
You may be right. It's been a while since I played
|
| + this.parseRedirectUrl_.bind(this)); |
| +}; |
| + |
| +/** |
| + * @constructor |
| + * Encapsulates the UI to check/request permissions to a new host. |
| + * |
| + * @param {string} url The URL to request permission for. |
| + */ |
| +remoting.ThirdPartyHostPermissions = function(url) { |
|
Jamie
2013/04/16 20:57:20
I think this class is big enough to warrant a sepa
rmsousa
2013/04/17 03:48:46
Done.
But it's quite closely related to the third
|
| + this.url_ = url; |
| + this.permissions_ = {'origins': [url]}; |
| +}; |
| + |
| +/** |
| + * Get permissions to the URL, asking interactively if necessary. |
| + * |
| + * @param {function(): void} onOk Called if the permission is granted. |
| + * @param {function(): void} onError Called if the permission is denied. |
| + */ |
| +remoting.ThirdPartyHostPermissions.prototype.getPermission = function( |
| + onOk, onError) { |
| + /** @type {remoting.ThirdPartyHostPermissions} */ |
| + var that = this; |
| + chrome.permissions.contains(this.permissions_, |
| + /** @param {boolean} allowed Whether this extension has this permission. */ |
| + function(allowed) { |
| + if (allowed) { |
| + onOk(); |
| + } else { |
| + // Optional permissions must be requested in a user action context. This |
| + // is called from an asynchronous plugin callback, so we have to open a |
| + // confirmation dialog to perform the request on an interactive event. |
| + // In any case, we can use this dialog to explain to the user why we are |
| + // asking for the additional permission. |
| + that.showPermissionConfirmation_(onOk, onError); |
| + } |
| + }); |
| +}; |
| + |
| +/** |
| + * Show an interactive dialog informing the user of the new permissions. |
| + * |
| + * @param {function(): void} onOk Called if the permission is granted. |
| + * @param {function(): void} onError Called if the permission is denied. |
| + * @private |
| + */ |
| +remoting.ThirdPartyHostPermissions.prototype.showPermissionConfirmation_ = |
| + function(onOk, onError) { |
| + /** @type {HTMLElement} */ |
| + var button = document.getElementById('third-party-auth-button'); |
| + /** @type {HTMLElement} */ |
| + var url = document.getElementById('third-party-auth-url'); |
| + url.innerText = this.url_; |
| + |
| + /** @type {remoting.ThirdPartyHostPermissions} */ |
| + var that = this; |
| + |
| + var consentGranted = function(event) { |
| + remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); |
| + button.removeEventListener('click', consentGranted, false); |
| + that.requestPermission_(onOk, onError); |
| + }; |
| + |
| + button.addEventListener('click', consentGranted, false); |
| + remoting.setMode(remoting.AppMode.CLIENT_THIRD_PARTY_AUTH); |
| +}; |
| + |
| + |
| +/** |
| + * Request permission from the user to access the token-issue URL. |
| + * |
| + * @param {function(): void} onOk Called if the permission is granted. |
| + * @param {function(): void} onError Called if the permission is denied. |
| + * @private |
| + */ |
| +remoting.ThirdPartyHostPermissions.prototype.requestPermission_ = function( |
| + onOk, onError) { |
| + chrome.permissions.request( |
| + this.permissions_, |
| + /** @param {boolean} result Whether the permission was granted. */ |
| + function(result) { |
| + if (result) { |
| + onOk(); |
| + } else { |
| + onError(); |
| + } |
| + }); |
| +}; |