1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 /** 6 * @fileoverview 7 * Third party authentication support for the remoting web-app. 8 * 9 * When third party authentication is being used, the client must request both a 10 * token and a shared secret from a third-party server. The server can then 11 * present the user with an authentication page, or use any other method to 12 * authenticate the user via the browser. Once the user is authenticated, the 13 * server will redirect the browser to a URL containing the token and shared 14 * secret in its fragment. The client then sends only the token to the host. 15 * The host signs the token, then contacts the third-party server to exchange 16 * the token for the shared secret. Once both client and host have the shared 17 * secret, they use a zero-disclosure mutual authentication protocol to 18 * negotiate an authentication key, which is used to establish the connection. 19 */ 20 21 'use strict'; 22 23 /** @suppress {duplicate} */ 24 var remoting = remoting || {}; 25 26 /** 27 * @constructor 28 * Encapsulates the logic to fetch a third party authentication token. 29 * 30 * @param {string} tokenUrl Token-issue URL received from the host. 31 * @param {string} hostPublicKey Host public key (DER and Base64 encoded). 32 * @param {string} scope OAuth scope to request the token for. 33 * @param {Array.<string>} tokenUrlPatterns Token URL patterns allowed for the 34 * domain, received from the directory server. 35 * @param {function(string, string):void} onThirdPartyTokenFetched Callback. 36 */ 37 remoting.ThirdPartyTokenFetcher = function( 38 tokenUrl, hostPublicKey, scope, tokenUrlPatterns, 39 onThirdPartyTokenFetched) { 40 this.tokenUrl_ = tokenUrl; 41 this.tokenScope_ = scope; 42 this.onThirdPartyTokenFetched_ = onThirdPartyTokenFetched; 43 this.failFetchToken_ = function() { onThirdPartyTokenFetched('', ''); }; 44 this.xsrfToken_ = remoting.generateXsrfToken(); 45 this.tokenUrlPatterns_ = tokenUrlPatterns; 46 this.hostPublicKey_ = hostPublicKey; 47 if (chrome.identity) { 48 /** @type {function():void} 49 * @private */ 50 this.fetchTokenInternal_ = this.fetchTokenIdentityApi_.bind(this); 51 this.redirectUri_ = 'https://' + window.location.hostname + 52 '.chromiumapp.org/ThirdPartyAuth'; 53 } else { 54 this.fetchTokenInternal_ = this.fetchTokenWindowOpen_.bind(this); 55 this.redirectUri_ = remoting.settings.THIRD_PARTY_AUTH_REDIRECT_URI; 56 } 57 }; 58 59 /** 60 * Fetch a token with the parameters configured in this object. 61 */ 62 remoting.ThirdPartyTokenFetcher.prototype.fetchToken = function() { 63 // If there is no list of patterns, this host cannot use a token URL. 64 if (!this.tokenUrlPatterns_) { 65 console.error('No token URLs are allowed for this host'); 66 this.failFetchToken_(); 67 } 68 69 // Verify the host-supplied URL matches the domain's allowed URL patterns. 70 for (var i = 0; i < this.tokenUrlPatterns_.length; i++) { 71 if (this.tokenUrl_.match(this.tokenUrlPatterns_[i])) { 72 var hostPermissions = new remoting.ThirdPartyHostPermissions( 73 this.tokenUrl_); 74 hostPermissions.getPermission( 75 this.fetchTokenInternal_, 76 this.failFetchToken_); 77 return; 78 } 79 } 80 // If the URL doesn't match any pattern in the list, refuse to access it. 81 console.error('Token URL does not match the domain\'s allowed URL patterns.' + 82 ' URL: ' + this.tokenUrl_ + ', patterns: ' + this.tokenUrlPatterns_); 83 this.failFetchToken_(); 84 }; 85 86 /** 87 * Parse the access token from the URL to which we were redirected. 88 * 89 * @param {string} responseUrl The URL to which we were redirected. 90 * @private 91 */ 92 remoting.ThirdPartyTokenFetcher.prototype.parseRedirectUrl_ = 93 function(responseUrl) { 94 var token = ''; 95 var sharedSecret = ''; 96 97 if (responseUrl && responseUrl.search('#') >= 0) { 98 var query = responseUrl.substring(responseUrl.search('#') + 1); 99 var parts = query.split('&'); 100 /** @type {Object.<string>} */ 101 var queryArgs = {}; 102 for (var i = 0; i < parts.length; i++) { 103 var pair = parts[i].split('='); 104 queryArgs[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); 105 } 106 107 // Check that 'state' contains the same XSRF token we sent in the request. 108 if ('state' in queryArgs && queryArgs['state'] == this.xsrfToken_ && 109 'code' in queryArgs && 'access_token' in queryArgs) { 110 // Terminology note: 111 // In the OAuth code/token exchange semantics, 'code' refers to the value 112 // obtained when the *user* authenticates itself, while 'access_token' is 113 // the value obtained when the *application* authenticates itself to the 114 // server ("implicitly", by receiving it directly in the URL fragment, or 115 // explicitly, by sending the 'code' and a 'client_secret' to the server). 116 // Internally, the piece of data obtained when the user authenticates 117 // itself is called the 'token', and the one obtained when the host 118 // authenticates itself (using the 'token' received from the client and 119 // its private key) is called the 'shared secret'. 120 // The client implicitly authenticates itself, and directly obtains the 121 // 'shared secret', along with the 'token' from the redirect URL fragment. 122 token = queryArgs['code']; 123 sharedSecret = queryArgs['access_token']; 124 } 125 } 126 this.onThirdPartyTokenFetched_(token, sharedSecret); 127 }; 128 129 /** 130 * Build a full token request URL from the parameters in this object. 131 * 132 * @return {string} Full URL to request a token. 133 * @private 134 */ 135 remoting.ThirdPartyTokenFetcher.prototype.getFullTokenUrl_ = function() { 136 return this.tokenUrl_ + '?' + remoting.xhr.urlencodeParamHash({ 137 'redirect_uri': this.redirectUri_, 138 'scope': this.tokenScope_, 139 'client_id': this.hostPublicKey_, 140 // The webapp uses an "implicit" OAuth flow with multiple response types to 141 // obtain both the code and the shared secret in a single request. 142 'response_type': 'code token', 143 'state': this.xsrfToken_ 144 }); 145 }; 146 147 /** 148 * Fetch a token by opening a new window and redirecting to a content script. 149 * @private 150 */ 151 remoting.ThirdPartyTokenFetcher.prototype.fetchTokenWindowOpen_ = function() { 152 /** @type {remoting.ThirdPartyTokenFetcher} */ 153 var that = this; 154 var fullTokenUrl = this.getFullTokenUrl_(); 155 // The function below can't be anonymous, since it needs to reference itself. 156 /** @param {string} message Message received from the content script. */ 157 function tokenMessageListener(message) { 158 that.parseRedirectUrl_(message); 159 chrome.extension.onMessage.removeListener(tokenMessageListener); 160 } 161 chrome.extension.onMessage.addListener(tokenMessageListener); 162 window.open(fullTokenUrl, '_blank', 'location=yes,toolbar=no,menubar=no'); 163 }; 164 165 /** 166 * Fetch a token from a token server using the identity.launchWebAuthFlow API. 167 * @private 168 */ 169 remoting.ThirdPartyTokenFetcher.prototype.fetchTokenIdentityApi_ = function() { 170 var fullTokenUrl = this.getFullTokenUrl_(); 171 chrome.identity.launchWebAuthFlow( 172 {'url': fullTokenUrl, 'interactive': true}, 173 this.parseRedirectUrl_.bind(this)); 174 };