// 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.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() { // If there is no list of patterns, this host cannot use a token URL. if (!this.tokenUrlPatterns_) { console.error('No token URLs are allowed for this host'); this.failFetchToken_(); } // 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('#') >= 0) { var query = responseUrl.substring(responseUrl.search('#') + 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. if ('state' in queryArgs && queryArgs['state'] == 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_(); chrome.identity.launchWebAuthFlow( {'url': fullTokenUrl, 'interactive': true}, this.parseRedirectUrl_.bind(this)); };