diff options
author | rmsousa@chromium.org <rmsousa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-04-26 02:35:53 +0000 |
---|---|---|
committer | rmsousa@chromium.org <rmsousa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-04-26 02:35:53 +0000 |
commit | 6e7bc186f6b0194d2cad9631be0f3093245d47ab (patch) | |
tree | aee7a808883b3532d03a3dd99c91ab8d92847860 | |
parent | 3d828ba01876bf20e19161a7f1fbc384f353ba1d (diff) | |
download | chromium_src-6e7bc186f6b0194d2cad9631be0f3093245d47ab.zip chromium_src-6e7bc186f6b0194d2cad9631be0f3093245d47ab.tar.gz chromium_src-6e7bc186f6b0194d2cad9631be0f3093245d47ab.tar.bz2 |
Webapp changes to support third party authentication
This uses an OAuth flow on the server to fetch the token and shared secret. There are two implementations for this:
* The current one manually opens a tab and asks for a redirect to a blank page in talkgadget, which we content-script to sendmessage the token/secret back to the extension (fairly similar to our OAuth trampoline)
* Once we're running on appsv2, and identity is out of experimental, we can use launchWebAuthFlow to do this.
This includes an interstitial to ask for an optional permission to the given host. The window.open method doesn't actually need this, but the identity API one does, so I thought I'd leave it in to make its behavior match closely the one of the identity API, which is the one we'll use in the future.
Most of the code is shared between these two versions, the only different pieces are the mechanics to open the window/launchWebFlow, and to send the redirectedUrl back to the webapp for parsing.
BUG=115899
Review URL: https://chromiumcodereview.appspot.com/12905012
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@196580 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | remoting/remoting.gyp | 3 | ||||
-rw-r--r-- | remoting/resources/remoting_strings.grd | 6 | ||||
-rw-r--r-- | remoting/webapp/appsv2.patch | 35 | ||||
-rwxr-xr-x | remoting/webapp/build-webapp.py | 8 | ||||
-rw-r--r-- | remoting/webapp/client_plugin.js | 10 | ||||
-rw-r--r-- | remoting/webapp/client_plugin_async.js | 32 | ||||
-rw-r--r-- | remoting/webapp/client_screen.js | 15 | ||||
-rw-r--r-- | remoting/webapp/client_session.js | 26 | ||||
-rw-r--r-- | remoting/webapp/cs_third_party_auth_trampoline.js | 10 | ||||
-rw-r--r-- | remoting/webapp/host.js | 2 | ||||
-rw-r--r-- | remoting/webapp/jscompiler_hacks.js | 30 | ||||
-rw-r--r-- | remoting/webapp/main.html | 17 | ||||
-rw-r--r-- | remoting/webapp/manifest.json | 11 | ||||
-rw-r--r-- | remoting/webapp/oauth2.js | 12 | ||||
-rw-r--r-- | remoting/webapp/plugin_settings.js | 5 | ||||
-rw-r--r-- | remoting/webapp/remoting.js | 11 | ||||
-rw-r--r-- | remoting/webapp/session_connector.js | 26 | ||||
-rw-r--r-- | remoting/webapp/third_party_host_permissions.js | 107 | ||||
-rw-r--r-- | remoting/webapp/third_party_token_fetcher.js | 171 | ||||
-rw-r--r-- | remoting/webapp/ui_mode.js | 1 |
20 files changed, 495 insertions, 43 deletions
diff --git a/remoting/remoting.gyp b/remoting/remoting.gyp index a09377d..b21c52d 100644 --- a/remoting/remoting.gyp +++ b/remoting/remoting.gyp @@ -182,6 +182,7 @@ 'webapp/connection_history.js', 'webapp/connection_stats.js', 'webapp/cs_oauth2_trampoline.js', + 'webapp/cs_third_party_auth_trampoline.js', 'webapp/error.js', 'webapp/event_handlers.js', 'webapp/format_iq.js', @@ -208,6 +209,8 @@ 'webapp/stats_accumulator.js', 'webapp/storage.js', 'webapp/survey.js', + 'webapp/third_party_host_permissions.js', + 'webapp/third_party_token_fetcher.js', 'webapp/toolbar.js', 'webapp/ui_mode.js', 'webapp/wcs.js', diff --git a/remoting/resources/remoting_strings.grd b/remoting/resources/remoting_strings.grd index 808e2e9..8c7f17b 100644 --- a/remoting/resources/remoting_strings.grd +++ b/remoting/resources/remoting_strings.grd @@ -142,6 +142,9 @@ <message desc="Text shown when the app first starts, or if the access token is invalidated, explaining the need to authorize the Chrome Remote Desktop app before use." name="IDR_DESCRIPTION_AUTHORIZE"> To use Chrome Remote Desktop you must grant extended access permissions to your computer. You only have to do this once. </message> + <message desc="Confirmation prompt shown when a user needs to authenticate against a third party, explaining the need to authorize the Chrome Remote Desktop app to access the authentication URL." name="IDR_DESCRIPTION_THIRD_PARTY_AUTH"> + The remote host requires you to authenticate to a third-party website. To continue, you must grant Chrome Remote Desktop additional permissions to access this address: + </message> <message desc="Description for the home screen. This is shown when the app starts up, above buttons to share or connect." name="IDR_DESCRIPTION_HOME"> Chrome Remote Desktop allows you to securely share your computer over the Web. Both users must be running the Chrome Remote Desktop app, which can be found at <ph name="URL">$1<ex><a href=http://chrome.google.com/remotedesktop>chrome.google.com/remotedesktop</a></ex></ph>. </message> @@ -205,6 +208,9 @@ <message desc="Text shown when the app first starts, or if the access token is invalidated, explaining the need to authorize the Chromoting app before use." name="IDR_DESCRIPTION_AUTHORIZE"> To use Chromoting you must grant extended access permissions to your computer. You only have to do this once. </message> + <message desc="Confirmation prompt shown when a user needs to authenticate against a third party, explaining the need to authorize the Chromoting app to access the authentication URL." name="IDR_DESCRIPTION_THIRD_PARTY_AUTH"> + The remote host requires you to authenticate to a third-party website. To continue, you must grant Chromoting additional permissions to access this address: + </message> <message desc="Description for the home screen. This is shown when the app starts up, above buttons to share or connect." name="IDR_DESCRIPTION_HOME"> Chromoting allows you to securely share your computer over the Web. Both users must be running the Chromoting app, which can be found at <ph name="URL">$1<ex><a href=http://chrome.google.com/remotedesktop>chrome.google.com/remotedesktop</a></ex></ph>. </message> diff --git a/remoting/webapp/appsv2.patch b/remoting/webapp/appsv2.patch index 6ff3063..3d72941 100644 --- a/remoting/webapp/appsv2.patch +++ b/remoting/webapp/appsv2.patch @@ -43,19 +43,21 @@ index 061caeb..f61e532 100644 <script src="log_to_server.js"></script> <script src="menu_button.js"></script> diff --git a/remoting/webapp/manifest.json b/remoting/webapp/manifest.json -index 5be9243..39052b9 100644 +index d1f8d1f..67bf660 100644 --- a/manifest.json +++ b/manifest.json -@@ -5,24 +5,16 @@ +@@ -5,30 +5,16 @@ "manifest_version": 2, "default_locale": "en", "app": { - "launch": { - "local_path": "main.html" +- } +- }, + "background": { + "scripts": ["background.js"] - } - }, ++ } ++ }, + "key": "chromotingappsv2", "icons": { "128": "chromoting128.webp", @@ -68,27 +70,30 @@ index 5be9243..39052b9 100644 - "OAUTH2_REDIRECT_URL" - ], - "js": [ "cs_oauth2_trampoline.js" ] +- }, +- { +- "matches": [ +- "THIRD_PARTY_AUTH_REDIRECT_URL" +- ], +- "js": [ "cs_third_party_auth_trampoline.js" ] - } - ], - "content_security_policy": "default-src 'self'; script-src 'self' TALK_GADGET_HOST; style-src 'self' https://fonts.googleapis.com; img-src 'self' TALK_GADGET_HOST; font-src *; connect-src 'self' OAUTH2_ACCOUNTS_HOST GOOGLE_API_HOSTS TALK_GADGET_HOST https://relay.google.com", - "permissions": [ - "OAUTH2_ACCOUNTS_HOST/*", - "OAUTH2_API_BASE_URL/*", -@@ -30,18 +22,22 @@ - "TALK_GADGET_HOST/talkgadget/*", - "https://relay.google.com/*", + "optional_permissions": [ + "<all_urls>" + ], +@@ -41,16 +27,20 @@ "storage", "clipboardRead", -- "clipboardWrite" -- ], + "clipboardWrite", ++ "experimental" + ], - "plugins": [ - { "path": "remoting_host_plugin.dll", "public": false }, - { "path": "libremoting_host_plugin.ia32.so", "public": false }, - { "path": "libremoting_host_plugin.x64.so", "public": false }, - { "path": "remoting_host_plugin.plugin", "public": false } -+ "clipboardWrite", -+ "experimental" - ], +- ], + "oauth2": { + "client_id": "45833509441.apps.googleusercontent.com", + "scopes": [ diff --git a/remoting/webapp/build-webapp.py b/remoting/webapp/build-webapp.py index 8da8843..c55ea50 100755 --- a/remoting/webapp/build-webapp.py +++ b/remoting/webapp/build-webapp.py @@ -242,6 +242,8 @@ def buildWebApp(buildtype, version, mimetype, destination, zip_path, plugin, else: oauth2RedirectUrlJs = "'" + oauth2RedirectBaseUrlJs + "/dev'" oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/dev*' + thirdPartyAuthUrlJs = "'" + oauth2RedirectBaseUrlJs + "/thirdpartyauth'" + thirdPartyAuthUrlJson = oauth2RedirectBaseUrlJson + '/thirdpartyauth*' findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'TALK_GADGET_URL'", "'" + talkGadgetBaseUrl + "'") findAndReplace(os.path.join(destination, 'plugin_settings.js'), @@ -264,6 +266,12 @@ def buildWebApp(buildtype, version, mimetype, destination, zip_path, plugin, "Boolean('XMPP_SERVER_USE_TLS')", xmppServerUseTls) findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'DIRECTORY_BOT_JID'", "'" + directoryBotJid + "'") + findAndReplace(os.path.join(destination, 'plugin_settings.js'), + "'THIRD_PARTY_AUTH_REDIRECT_URL'", + thirdPartyAuthUrlJs) + findAndReplace(os.path.join(destination, 'manifest.json'), + "THIRD_PARTY_AUTH_REDIRECT_URL", + thirdPartyAuthUrlJson) # Set the correct API keys. # For overriding the client ID/secret via env vars, see google_api_keys.py. diff --git a/remoting/webapp/client_plugin.js b/remoting/webapp/client_plugin.js index fe3a9cd..6c5eff4 100644 --- a/remoting/webapp/client_plugin.js +++ b/remoting/webapp/client_plugin.js @@ -65,6 +65,7 @@ remoting.ClientPlugin.Feature = { PAUSE_AUDIO: 'pauseAudio', REMAP_KEY: 'remapKey', SEND_CLIPBOARD_ITEM: 'sendClipboardItem', + THIRD_PARTY_AUTH: 'thirdPartyAuth', TRAP_KEY: 'trapKey' }; @@ -189,3 +190,12 @@ remoting.ClientPlugin.prototype.onPinFetched = function(pin) {}; * Tells the plugin to ask for the PIN asynchronously. */ remoting.ClientPlugin.prototype.useAsyncPinDialog = function() {}; + +/** + * Sets the third party authentication token and shared secret. + * + * @param {string} token The token received from the token URL. + * @param {string} sharedSecret Shared secret received from the token URL. + */ +remoting.ClientPlugin.prototype.onThirdPartyTokenFetched = + function(token, sharedSecret) {}; diff --git a/remoting/webapp/client_plugin_async.js b/remoting/webapp/client_plugin_async.js index 71ec21f..788b49a 100644 --- a/remoting/webapp/client_plugin_async.js +++ b/remoting/webapp/client_plugin_async.js @@ -40,6 +40,13 @@ remoting.ClientPluginAsync = function(plugin) { this.onConnectionStatusUpdateHandler = function(state, error) {}; /** @param {boolean} ready Connection ready state. */ this.onConnectionReadyHandler = function(ready) {}; + /** + * @param {string} tokenUrl Token-request URL, received from the host. + * @param {string} hostPublicKey Public key for the host. + * @param {string} scope OAuth scope to request the token for. + */ + this.fetchThirdPartyTokenHandler = function( + tokenUrl, hostPublicKey, scope) {}; this.onDesktopSizeUpdateHandler = function () {}; /** @param {!Array.<string>} capabilities The negotiated capabilities. */ this.onSetCapabilitiesHandler = function (capabilities) {}; @@ -266,6 +273,18 @@ remoting.ClientPluginAsync.prototype.handleMessage_ = function(messageStr) { /** @type {!Array.<string>} */ var capabilities = tokenize(message.data['capabilities']); this.onSetCapabilitiesHandler(capabilities); + } else if (message.method == 'fetchThirdPartyToken') { + if (typeof message.data['tokenUrl'] != 'string' || + typeof message.data['hostPublicKey'] != 'string' || + typeof message.data['scope'] != 'string') { + console.error('Received incorrect fetchThirdPartyToken message.'); + return; + } + var tokenUrl = /** @type {string} */ message.data['tokenUrl']; + var hostPublicKey = + /** @type {string} */ message.data['hostPublicKey']; + var scope = /** @type {string} */ message.data['scope']; + this.fetchThirdPartyTokenHandler(tokenUrl, hostPublicKey, scope); } }; @@ -523,6 +542,19 @@ remoting.ClientPluginAsync.prototype.useAsyncPinDialog = }; /** + * Sets the third party authentication token and shared secret. + * + * @param {string} token The token received from the token URL. + * @param {string} sharedSecret Shared secret received from the token URL. + */ +remoting.ClientPluginAsync.prototype.onThirdPartyTokenFetched = function( + token, sharedSecret) { + this.plugin.postMessage(JSON.stringify( + { method: 'onThirdPartyTokenFetched', + data: { token: token, sharedSecret: sharedSecret}})); +}; + +/** * If we haven't yet received a "hello" message from the plugin, change its * size so that the user can confirm it if click-to-play is enabled, or can * see the "this plugin is disabled" message if it is actually disabled. diff --git a/remoting/webapp/client_screen.js b/remoting/webapp/client_screen.js index 77ff7ec..04a308e 100644 --- a/remoting/webapp/client_screen.js +++ b/remoting/webapp/client_screen.js @@ -251,6 +251,19 @@ remoting.connectMe2MeHostVersionAcknowledged_ = function(host) { } remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); +/** + * @param {string} tokenUrl Token-issue URL received from the host. + * @param {string} scope OAuth scope to request the token for. + * @param {function(string, string):void} onThirdPartyTokenFetched Callback. + */ + var fetchThirdPartyToken = function( + tokenUrl, hostPublicKey, scope, onThirdPartyTokenFetched) { + var thirdPartyTokenFetcher = new remoting.ThirdPartyTokenFetcher( + tokenUrl, hostPublicKey, scope, host.tokenUrlPatterns, + onThirdPartyTokenFetched); + thirdPartyTokenFetcher.fetchToken(); + }; + /** @param {function(string):void} onPinFetched */ var requestPin = function(onPinFetched) { /** @type {Element} */ @@ -286,7 +299,7 @@ remoting.connectMe2MeHostVersionAcknowledged_ = function(host) { l10n.localizeElement(message, host.hostName); remoting.setMode(remoting.AppMode.CLIENT_PIN_PROMPT); }; - remoting.connector.connectMe2Me(host, requestPin); + remoting.connector.connectMe2Me(host, requestPin, fetchThirdPartyToken); }; /** @param {remoting.ClientSession} clientSession */ diff --git a/remoting/webapp/client_session.js b/remoting/webapp/client_session.js index a05e2e1..5ba7f74 100644 --- a/remoting/webapp/client_session.js +++ b/remoting/webapp/client_session.js @@ -30,6 +30,9 @@ var remoting = remoting || {}; * @param {string} accessCode The IT2Me access code. Blank for Me2Me. * @param {function(function(string): void): void} fetchPin Called by Me2Me * connections when a PIN needs to be obtained interactively. + * @param {function(string, string, function(string, string): void): void} + * fetchThirdPartyToken Called by Me2Me connections when a third party + * authentication token must be obtained. * @param {string} authenticationMethods Comma-separated list of * authentication methods the client should attempt to use. * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me. @@ -39,7 +42,8 @@ var remoting = remoting || {}; * @constructor */ remoting.ClientSession = function(hostJid, clientJid, hostPublicKey, accessCode, - fetchPin, authenticationMethods, hostId, + fetchPin, fetchThirdPartyToken, + authenticationMethods, hostId, mode, hostDisplayName) { this.state = remoting.ClientSession.State.CREATED; @@ -50,6 +54,8 @@ remoting.ClientSession = function(hostJid, clientJid, hostPublicKey, accessCode, this.accessCode_ = accessCode; /** @private */ this.fetchPin_ = fetchPin; + /** @private */ + this.fetchThirdPartyToken_ = fetchThirdPartyToken; this.authenticationMethods = authenticationMethods; this.hostId = hostId; /** @type {string} */ @@ -404,7 +410,6 @@ remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) { this.onDesktopSizeChanged_.bind(this); this.plugin.onSetCapabilitiesHandler = this.onSetCapabilities_.bind(this); - this.connectPluginToWcs_(); }; @@ -721,22 +726,29 @@ remoting.ClientSession.prototype.connectPluginToWcs_ = function() { }; remoting.wcsSandbox.setOnIq(onIncomingIq); + /** @type remoting.ClientSession */ + var that = this; + if (plugin.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) { + /** @type{function(string, string, string): void} */ + var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) { + that.fetchThirdPartyToken_( + tokenUrl, hostPublicKey, scope, + plugin.onThirdPartyTokenFetched.bind(plugin)); + }; + plugin.fetchThirdPartyTokenHandler = fetchThirdPartyToken; + } if (this.accessCode_) { // Shared secret was already supplied before connecting (It2Me case). this.connectToHost_(this.accessCode_); - } else if (plugin.hasFeature( remoting.ClientPlugin.Feature.ASYNC_PIN)) { // Plugin supports asynchronously asking for the PIN. plugin.useAsyncPinDialog(); - /** @type remoting.ClientSession */ - var that = this; var fetchPin = function() { that.fetchPin_(plugin.onPinFetched.bind(plugin)); }; plugin.fetchPinHandler = fetchPin; this.connectToHost_(''); - } else { // Plugin doesn't support asynchronously asking for the PIN, ask now. this.fetchPin_(this.connectToHost_.bind(this)); @@ -793,7 +805,7 @@ remoting.ClientSession.prototype.onConnectionReady_ = function(ready) { } else { this.plugin.element().classList.remove("session-client-inactive"); } -} +}; /** * Called when the client-host capabilities negotiation is complete. diff --git a/remoting/webapp/cs_third_party_auth_trampoline.js b/remoting/webapp/cs_third_party_auth_trampoline.js new file mode 100644 index 0000000..f3c65c2 --- /dev/null +++ b/remoting/webapp/cs_third_party_auth_trampoline.js @@ -0,0 +1,10 @@ +// 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. + +var thirdPartyPath = '/talkgadget/oauth/chrome-remote-desktop/thirdpartyauth'; + +if (window.location.pathname == thirdPartyPath) { + chrome.extension.sendMessage(window.location.href); + window.close(); +} diff --git a/remoting/webapp/host.js b/remoting/webapp/host.js index 90b7cdd..24634b6 100644 --- a/remoting/webapp/host.js +++ b/remoting/webapp/host.js @@ -30,6 +30,8 @@ remoting.Host = function() { this.publicKey = ''; /** @type {string} */ this.hostVersion = ''; + /** @type {Array.<string>} */ + this.tokenUrlPatterns = []; }; /** diff --git a/remoting/webapp/jscompiler_hacks.js b/remoting/webapp/jscompiler_hacks.js index 79f2750..c5df2fc 100644 --- a/remoting/webapp/jscompiler_hacks.js +++ b/remoting/webapp/jscompiler_hacks.js @@ -104,7 +104,12 @@ chrome.experimental.identity = { * @param {Object.<string>} parameters * @param {function(string):void} callback */ - getAuthToken: function(parameters, callback) {} + getAuthToken: function(parameters, callback) {}, + /** + * @param {Object.<string>} parameters + * @param {function(string):void} callback + */ + launchWebAuthFlow: function(parameters, callback) {} }; /** @constructor */ @@ -113,6 +118,9 @@ chrome.Event = function() {}; /** @param {function():void} callback */ chrome.Event.prototype.addListener = function(callback) {}; +/** @param {function():void} callback */ +chrome.Event.prototype.removeListener = function(callback) {}; + /** @constructor */ chrome.extension.Port = function() {}; @@ -172,3 +180,23 @@ chrome.Window = function() { /** @type {string} */ this.type = ''; }; + +/** @param {string} message*/ +chrome.extension.sendMessage = function(message) {} + +/** @type {chrome.Event} */ +chrome.extension.onMessage; + +/** @type {Object} */ +chrome.permissions = { + /** + * @param {Object.<string>} permissions + * @param {function(boolean):void} callback + */ + contains: function(permissions, callback) {}, +/** + * @param {Object.<string>} permissions + * @param {function(boolean):void} callback + */ + request: function(permissions, callback) {} +}; diff --git a/remoting/webapp/main.html b/remoting/webapp/main.html index 94d7c9b..7d633765 100644 --- a/remoting/webapp/main.html +++ b/remoting/webapp/main.html @@ -46,6 +46,8 @@ found in the LICENSE file. <script src="stats_accumulator.js"></script> <script src="storage.js"></script> <script src="survey.js"></script> + <script src="third_party_host_permissions.js"></script> + <script src="third_party_token_fetcher.js"></script> <script src="toolbar.js"></script> <script src="ui_mode.js"></script> <script src="xhr.js"></script> @@ -552,6 +554,21 @@ found in the LICENSE file. </div> </div> <!-- client.pin-prompt --> + <div data-ui-mode="home.client.third-party-auth" class="centered"> + <div id="third-party-auth-message" + i18n-content="DESCRIPTION_THIRD_PARTY_AUTH" + class="message"></div> + <div id="third-party-auth-url" + class="message"></div> + <div> + <button id="third-party-auth-button" + type="button" + autofocus="autofocus" + i18n-content="CONTINUE_BUTTON"> + </button> + </div> + </div> <!-- third-party-auth-dialog --> + <div data-ui-mode="home.client.connect-failed" class="message centered"> <span id="connect-error-message" class="error-state"></span> diff --git a/remoting/webapp/manifest.json b/remoting/webapp/manifest.json index 13f9346..d1f8d1f 100644 --- a/remoting/webapp/manifest.json +++ b/remoting/webapp/manifest.json @@ -20,9 +20,18 @@ "OAUTH2_REDIRECT_URL" ], "js": [ "cs_oauth2_trampoline.js" ] + }, + { + "matches": [ + "THIRD_PARTY_AUTH_REDIRECT_URL" + ], + "js": [ "cs_third_party_auth_trampoline.js" ] } ], "content_security_policy": "default-src 'self'; script-src 'self' TALK_GADGET_HOST; style-src 'self' https://fonts.googleapis.com; img-src 'self' TALK_GADGET_HOST; font-src *; connect-src 'self' OAUTH2_ACCOUNTS_HOST GOOGLE_API_HOSTS TALK_GADGET_HOST https://relay.google.com", + "optional_permissions": [ + "<all_urls>" + ], "permissions": [ "OAUTH2_ACCOUNTS_HOST/*", "OAUTH2_API_BASE_URL/*", @@ -31,7 +40,7 @@ "https://relay.google.com/*", "storage", "clipboardRead", - "clipboardWrite" + "clipboardWrite", ], "plugins": [ { "path": "remoting_host_plugin.dll", "public": false }, diff --git a/remoting/webapp/oauth2.js b/remoting/webapp/oauth2.js index 752c880..69c6d2d 100644 --- a/remoting/webapp/oauth2.js +++ b/remoting/webapp/oauth2.js @@ -308,22 +308,12 @@ remoting.OAuth2.prototype.refreshAccessToken_ = function(onDone) { }; /** - * @private - * @return {string} A URL-Safe Base64-encoded 128-bit random value. */ -remoting.OAuth2.prototype.generateXsrfToken_ = function() { - var random = new Uint8Array(16); - window.crypto.getRandomValues(random); - var base64Token = window.btoa(String.fromCharCode.apply(null, random)); - return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -}; - -/** * Redirect page to get a new OAuth2 Refresh Token. * * @return {void} Nothing. */ remoting.OAuth2.prototype.doAuthRedirect = function() { - var xsrf_token = this.generateXsrfToken_(); + var xsrf_token = remoting.generateXsrfToken(); window.localStorage.setItem(this.KEY_XSRF_TOKEN_, xsrf_token); var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' + remoting.xhr.urlencodeParamHash({ diff --git a/remoting/webapp/plugin_settings.js b/remoting/webapp/plugin_settings.js index 8ca53be..81e95bd 100644 --- a/remoting/webapp/plugin_settings.js +++ b/remoting/webapp/plugin_settings.js @@ -46,3 +46,8 @@ remoting.Settings.prototype.XMPP_SERVER_ADDRESS = 'XMPP_SERVER_ADDRESS'; /** @type {boolean} Whether to use TLS on connections to the XMPP server. */ remoting.Settings.prototype.XMPP_SERVER_USE_TLS = Boolean('XMPP_SERVER_USE_TLS'); + +// Third party authentication settings. +/** @type {string} The third party auth redirect URI. */ +remoting.Settings.prototype.THIRD_PARTY_AUTH_REDIRECT_URI = + 'THIRD_PARTY_AUTH_REDIRECT_URL'; diff --git a/remoting/webapp/remoting.js b/remoting/webapp/remoting.js index 616732a..0989777 100644 --- a/remoting/webapp/remoting.js +++ b/remoting/webapp/remoting.js @@ -382,3 +382,14 @@ function migrateLocalToChromeStorage_() { } } } + +/** + * Generate a nonce, to be used as an xsrf protection token. + * + * @return {string} A URL-Safe Base64-encoded 128-bit random value. */ +remoting.generateXsrfToken = function() { + var random = new Uint8Array(16); + window.crypto.getRandomValues(random); + var base64Token = window.btoa(String.fromCharCode.apply(null, random)); + return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +}; diff --git a/remoting/webapp/session_connector.js b/remoting/webapp/session_connector.js index 649d129..935cfc8 100644 --- a/remoting/webapp/session_connector.js +++ b/remoting/webapp/session_connector.js @@ -118,13 +118,19 @@ remoting.SessionConnector.prototype.reset = function() { this.pendingXhr_ = null; /** - * Function to interactively obtain the PIN from the user. - * @type {function(function(string):void):void} + * @type {function(function(string):void): void} * @private */ this.fetchPin_ = function(onPinFetched) {}; /** + * @type {function(string, string, function(string, string):void): void} + * @private + */ + this.fetchThirdPartyToken_ = function( + tokenUrl, scope, onThirdPartyTokenFetched) {}; + + /** * Host 'name', as displayed in the client tool-bar. For a Me2Me connection, * this is the name of the host; for an IT2Me connection, it is the email * address of the person sharing their computer. @@ -141,15 +147,21 @@ remoting.SessionConnector.prototype.reset = function() { * @param {remoting.Host} host The Me2Me host to which to connect. * @param {function(function(string):void):void} fetchPin Function to * interactively obtain the PIN from the user. + * @param {function(string, string, function(string, string): void): void} + * fetchThirdPartyToken Function to obtain a token from a third party + * authenticaiton server. * @return {void} Nothing. */ -remoting.SessionConnector.prototype.connectMe2Me = function(host, fetchPin) { +remoting.SessionConnector.prototype.connectMe2Me = function( + host, fetchPin, fetchThirdPartyToken) { // Cancel any existing connect operation. this.cancel(); this.hostId_ = host.hostId; this.hostJid_ = host.jabberId; + this.hostPublicKey_ = host.publicKey; this.fetchPin_ = fetchPin; + this.fetchThirdPartyToken_ = fetchThirdPartyToken; this.hostDisplayName_ = host.hostName; this.connectionMode_ = remoting.ClientSession.Mode.ME2ME; this.createSessionIfReady_(); @@ -299,11 +311,11 @@ remoting.SessionConnector.prototype.createSessionIfReady_ = function() { return; } - var securityTypes = 'spake2_hmac,spake2_plain'; + var securityTypes = 'third_party,spake2_hmac,spake2_plain'; this.clientSession_ = new remoting.ClientSession( this.hostJid_, this.clientJid_, this.hostPublicKey_, this.passPhrase_, - this.fetchPin_, securityTypes, this.hostId_, this.connectionMode_, - this.hostDisplayName_); + this.fetchPin_, this.fetchThirdPartyToken_, securityTypes, this.hostId_, + this.connectionMode_, this.hostDisplayName_); this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_); this.clientSession_.setOnStateChange(this.onStateChange_.bind(this)); this.clientSession_.createPluginAndConnect(this.pluginParent_); @@ -393,7 +405,7 @@ remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) { if (success) { var host = remoting.hostList.getHostForId(this.hostId_); if (host) { - this.connectMe2Me(host, this.fetchPin_); + this.connectMe2Me(host, this.fetchPin_, this.fetchThirdPartyToken_); return; } } diff --git a/remoting/webapp/third_party_host_permissions.js b/remoting/webapp/third_party_host_permissions.js new file mode 100644 index 0000000..1fd3edf --- /dev/null +++ b/remoting/webapp/third_party_host_permissions.js @@ -0,0 +1,107 @@ +// 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 + * Obtains additional host permissions, showing a consent dialog if needed. + * + * When third party authentication is being used, the client must talk to a + * third-party server. For that, once the URL is received from the host the + * webapp must use Chrome's optional permissions API to check if it has the + * "host" permission needed to access that URL. If the webapp hasn't already + * been granted that permission, it shows a dialog explaining why it is being + * requested, then uses the Chrome API ask the user for the new permission. + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** + * @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) { + 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(); + } + }); +}; diff --git a/remoting/webapp/third_party_token_fetcher.js b/remoting/webapp/third_party_token_fetcher.js new file mode 100644 index 0000000..a16246c --- /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)); +};
\ No newline at end of file diff --git a/remoting/webapp/ui_mode.js b/remoting/webapp/ui_mode.js index 6b1f172..e3b92a9 100644 --- a/remoting/webapp/ui_mode.js +++ b/remoting/webapp/ui_mode.js @@ -31,6 +31,7 @@ remoting.AppMode = { CLIENT: 'home.client', CLIENT_UNCONNECTED: 'home.client.unconnected', CLIENT_PIN_PROMPT: 'home.client.pin-prompt', + CLIENT_THIRD_PARTY_AUTH: 'home.client.third-party-auth', CLIENT_CONNECTING: 'home.client.connecting', CLIENT_CONNECT_FAILED_IT2ME: 'home.client.connect-failed.it2me', CLIENT_CONNECT_FAILED_ME2ME: 'home.client.connect-failed.me2me', |