OLD | NEW |
(Empty) | |
| 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.experimental && chrome.experimental.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 // Verify the host-supplied URL matches the domain's allowed URL patterns. |
| 64 for (var i = 0; i < this.tokenUrlPatterns_.length; i++) { |
| 65 if (this.tokenUrl_.match(this.tokenUrlPatterns_[i])) { |
| 66 var hostPermissions = new remoting.ThirdPartyHostPermissions( |
| 67 this.tokenUrl_); |
| 68 hostPermissions.getPermission( |
| 69 this.fetchTokenInternal_, |
| 70 this.failFetchToken_); |
| 71 |
| 72 return; |
| 73 } |
| 74 } |
| 75 // If the URL doesn't match any pattern in the list, refuse to access it. |
| 76 console.error('Token URL does not match the domain\'s allowed URL patterns.' + |
| 77 ' URL: ' + this.tokenUrl_ + ', patterns: ' + this.tokenUrlPatterns_); |
| 78 this.failFetchToken_(); |
| 79 }; |
| 80 |
| 81 /** |
| 82 * Parse the access token from the URL to which we were redirected. |
| 83 * |
| 84 * @param {string} responseUrl The URL to which we were redirected. |
| 85 * @private |
| 86 */ |
| 87 remoting.ThirdPartyTokenFetcher.prototype.parseRedirectUrl_ = |
| 88 function(responseUrl) { |
| 89 var token = ''; |
| 90 var sharedSecret = ''; |
| 91 if (responseUrl && |
| 92 responseUrl.search(this.redirectUri_ + '#') == 0) { |
| 93 var query = responseUrl.substring(this.redirectUri_.length + 1); |
| 94 var parts = query.split('&'); |
| 95 /** @type {Object.<string>} */ |
| 96 var queryArgs = {}; |
| 97 for (var i = 0; i < parts.length; i++) { |
| 98 var pair = parts[i].split('='); |
| 99 queryArgs[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); |
| 100 } |
| 101 |
| 102 // Check that 'state' contains the same XSRF token we sent in the request. |
| 103 var xsrfToken = queryArgs['state']; |
| 104 if (xsrfToken == this.xsrfToken_ && |
| 105 'code' in queryArgs && 'access_token' in queryArgs) { |
| 106 // Terminology note: |
| 107 // In the OAuth code/token exchange semantics, 'code' refers to the value |
| 108 // obtained when the *user* authenticates itself, while 'access_token' is |
| 109 // the value obtained when the *application* authenticates itself to the |
| 110 // server ("implicitly", by receiving it directly in the URL fragment, or |
| 111 // explicitly, by sending the 'code' and a 'client_secret' to the server). |
| 112 // Internally, the piece of data obtained when the user authenticates |
| 113 // itself is called the 'token', and the one obtained when the host |
| 114 // authenticates itself (using the 'token' received from the client and |
| 115 // its private key) is called the 'shared secret'. |
| 116 // The client implicitly authenticates itself, and directly obtains the |
| 117 // 'shared secret', along with the 'token' from the redirect URL fragment. |
| 118 token = queryArgs['code']; |
| 119 sharedSecret = queryArgs['access_token']; |
| 120 } |
| 121 } |
| 122 this.onThirdPartyTokenFetched_(token, sharedSecret); |
| 123 }; |
| 124 |
| 125 /** |
| 126 * Build a full token request URL from the parameters in this object. |
| 127 * |
| 128 * @return {string} Full URL to request a token. |
| 129 * @private |
| 130 */ |
| 131 remoting.ThirdPartyTokenFetcher.prototype.getFullTokenUrl_ = function() { |
| 132 return this.tokenUrl_ + '?' + remoting.xhr.urlencodeParamHash({ |
| 133 'redirect_uri': this.redirectUri_, |
| 134 'scope': this.tokenScope_, |
| 135 'client_id': this.hostPublicKey_, |
| 136 // The webapp uses an "implicit" OAuth flow with multiple response types to |
| 137 // obtain both the code and the shared secret in a single request. |
| 138 'response_type': 'code token', |
| 139 'state': this.xsrfToken_ |
| 140 }); |
| 141 }; |
| 142 |
| 143 /** |
| 144 * Fetch a token by opening a new window and redirecting to a content script. |
| 145 * @private |
| 146 */ |
| 147 remoting.ThirdPartyTokenFetcher.prototype.fetchTokenWindowOpen_ = function() { |
| 148 /** @type {remoting.ThirdPartyTokenFetcher} */ |
| 149 var that = this; |
| 150 var fullTokenUrl = this.getFullTokenUrl_(); |
| 151 // The function below can't be anonymous, since it needs to reference itself. |
| 152 /** @param {string} message Message received from the content script. */ |
| 153 function tokenMessageListener(message) { |
| 154 that.parseRedirectUrl_(message); |
| 155 chrome.extension.onMessage.removeListener(tokenMessageListener); |
| 156 } |
| 157 chrome.extension.onMessage.addListener(tokenMessageListener); |
| 158 window.open(fullTokenUrl, '_blank', 'location=yes,toolbar=no,menubar=no'); |
| 159 }; |
| 160 |
| 161 /** |
| 162 * Fetch a token from a token server using the identity.launchWebAuthFlow API. |
| 163 * @private |
| 164 */ |
| 165 remoting.ThirdPartyTokenFetcher.prototype.fetchTokenIdentityApi_ = function() { |
| 166 var fullTokenUrl = this.getFullTokenUrl_(); |
| 167 // TODO(rmsousa): chrome.identity.launchWebAuthFlow is experimental. |
| 168 chrome.experimental.identity.launchWebAuthFlow( |
| 169 {'url': fullTokenUrl, 'interactive': true}, |
| 170 this.parseRedirectUrl_.bind(this)); |
| 171 }; |
OLD | NEW |