OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012 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} scope OAuth scope to request the token for. | |
32 * @param {Array.<string>} tokenUrlPatterns Token URL patterns allowed for the | |
33 * domain, received from the directory server. | |
34 * @param {string} hostPublicKey Host public key (DER and Base64 encoded). | |
35 * @param {function(string, string):void} onThirdPartyTokenFetched Callback. | |
36 */ | |
37 remoting.ThirdPartyTokenFetcher = function( | |
38 tokenUrl, scope, tokenUrlPatterns, hostPublicKey, | |
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 | |
Jamie
2013/04/16 20:57:20
Missing return here?
rmsousa
2013/04/17 03:48:46
Done.
| |
72 } | |
73 } | |
74 // If the URL doesn't match any pattern in the list, refuse to access it. | |
75 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.
| |
76 'URL: ' + this.tokenUrl_ + ', patterns: ' + this.tokenUrlPatterns_); | |
77 this.failFetchToken_(); | |
78 }; | |
79 | |
80 /** | |
81 * Parse the access token from the URL to which we were redirected. | |
82 * | |
83 * @param {string} responseUrl The URL to which we were redirected. | |
84 * @private | |
85 */ | |
86 remoting.ThirdPartyTokenFetcher.prototype.parseRedirectUrl_ = | |
87 function(responseUrl) { | |
88 var token = ''; | |
89 var sharedSecret = ''; | |
90 if (responseUrl && | |
91 responseUrl.search(this.redirectUri_ + '#') == 0) { | |
92 var query = responseUrl.substring(this.redirectUri_.length + 1); | |
93 var parts = query.split('&'); | |
94 /** @type {Object.<string>} */ | |
95 var queryArgs = {}; | |
96 for (var i = 0; i < parts.length; i++) { | |
97 var pair = parts[i].split('='); | |
98 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.
| |
99 } | |
100 | |
101 // Check that 'state' contains the same XSRF token we sent in the request. | |
102 var xsrfToken = decodeURIComponent(queryArgs['state']); | |
103 if (xsrfToken == this.xsrfToken_ && | |
104 'code' in queryArgs && 'access_token' in queryArgs) { | |
105 // Terminology note: | |
106 // In the OAuth code/token exchange semantics, 'code' refers to the value | |
107 // obtained when the *user* authenticates itself, while 'access_token' is | |
108 // the value obtained when the *application* authenticates itself to the | |
109 // server ("implicitly", by receiving it directly in the URL fragment, or | |
110 // explicitly, by sending the 'code' and a 'client_secret' to the server). | |
111 // Internally, the piece of data obtained when the user authenticates | |
112 // itself is called the 'token', and the one obtained when the host | |
113 // authenticates itself (using the 'token' received from the client and | |
114 // its private key) is called the 'shared secret'. | |
115 // The client implicitly authenticates itself, and directly obtains the | |
116 // 'shared secret', along with the 'token' from the redirect URL fragment. | |
117 token = decodeURIComponent(queryArgs['code']); | |
118 sharedSecret = decodeURIComponent(queryArgs['access_token']); | |
119 } | |
120 } | |
121 this.onThirdPartyTokenFetched_(token, sharedSecret); | |
122 }; | |
123 | |
124 /** | |
125 * Build a full token request URL from the parameters in this object. | |
126 * | |
127 * @return {string} Full URL to request a token. | |
128 * @private | |
129 */ | |
130 remoting.ThirdPartyTokenFetcher.prototype.getFullTokenUrl_ = function() { | |
131 return this.tokenUrl_ + '?' + remoting.xhr.urlencodeParamHash({ | |
132 'redirect_uri': this.redirectUri_, | |
133 'scope': this.tokenScope_, | |
134 'client_id': this.hostPublicKey_, | |
135 // The webapp uses an "implicit" OAuth flow with multiple response types to | |
136 // obtain both the code and the shared secret in a single request. | |
137 'response_type': 'code token', | |
138 'state': this.xsrfToken_ | |
139 }); | |
140 }; | |
141 | |
142 /** | |
143 * Fetch a token by opening a new window and redirecting to a content script. | |
144 * @private | |
145 */ | |
146 remoting.ThirdPartyTokenFetcher.prototype.fetchTokenWindowOpen_ = function() { | |
147 /** @type {remoting.ThirdPartyTokenFetcher} */ | |
148 var that = this; | |
149 var fullTokenUrl = this.getFullTokenUrl_(); | |
150 // The function below can't be anonymous, since it needs to reference itself. | |
151 /** @param {string} message Message received from the content script. */ | |
152 function tokenMessageListener(message) { | |
153 that.parseRedirectUrl_(message); | |
154 chrome.extension.onMessage.removeListener(tokenMessageListener); | |
155 } | |
156 chrome.extension.onMessage.addListener(tokenMessageListener); | |
157 window.open(fullTokenUrl, '_blank', 'location=yes,toolbar=no,menubar=no'); | |
158 }; | |
159 | |
160 /** | |
161 * Fetch a token from a token server using the identity.launchWebAuthFlow API. | |
162 * @private | |
163 */ | |
164 remoting.ThirdPartyTokenFetcher.prototype.fetchTokenIdentityApi_ = function() { | |
165 var fullTokenUrl = this.getFullTokenUrl_(); | |
166 // TODO(rmsousa): chrome.identity.launchWebAuthFlow is experimental. | |
167 chrome.experimental.identity.launchWebAuthFlow( | |
168 {'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
| |
169 this.parseRedirectUrl_.bind(this)); | |
170 }; | |
171 | |
172 /** | |
173 * @constructor | |
174 * Encapsulates the UI to check/request permissions to a new host. | |
175 * | |
176 * @param {string} url The URL to request permission for. | |
177 */ | |
178 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
| |
179 this.url_ = url; | |
180 this.permissions_ = {'origins': [url]}; | |
181 }; | |
182 | |
183 /** | |
184 * Get permissions to the URL, asking interactively if necessary. | |
185 * | |
186 * @param {function(): void} onOk Called if the permission is granted. | |
187 * @param {function(): void} onError Called if the permission is denied. | |
188 */ | |
189 remoting.ThirdPartyHostPermissions.prototype.getPermission = function( | |
190 onOk, onError) { | |
191 /** @type {remoting.ThirdPartyHostPermissions} */ | |
192 var that = this; | |
193 chrome.permissions.contains(this.permissions_, | |
194 /** @param {boolean} allowed Whether this extension has this permission. */ | |
195 function(allowed) { | |
196 if (allowed) { | |
197 onOk(); | |
198 } else { | |
199 // Optional permissions must be requested in a user action context. This | |
200 // is called from an asynchronous plugin callback, so we have to open a | |
201 // confirmation dialog to perform the request on an interactive event. | |
202 // In any case, we can use this dialog to explain to the user why we are | |
203 // asking for the additional permission. | |
204 that.showPermissionConfirmation_(onOk, onError); | |
205 } | |
206 }); | |
207 }; | |
208 | |
209 /** | |
210 * Show an interactive dialog informing the user of the new permissions. | |
211 * | |
212 * @param {function(): void} onOk Called if the permission is granted. | |
213 * @param {function(): void} onError Called if the permission is denied. | |
214 * @private | |
215 */ | |
216 remoting.ThirdPartyHostPermissions.prototype.showPermissionConfirmation_ = | |
217 function(onOk, onError) { | |
218 /** @type {HTMLElement} */ | |
219 var button = document.getElementById('third-party-auth-button'); | |
220 /** @type {HTMLElement} */ | |
221 var url = document.getElementById('third-party-auth-url'); | |
222 url.innerText = this.url_; | |
223 | |
224 /** @type {remoting.ThirdPartyHostPermissions} */ | |
225 var that = this; | |
226 | |
227 var consentGranted = function(event) { | |
228 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); | |
229 button.removeEventListener('click', consentGranted, false); | |
230 that.requestPermission_(onOk, onError); | |
231 }; | |
232 | |
233 button.addEventListener('click', consentGranted, false); | |
234 remoting.setMode(remoting.AppMode.CLIENT_THIRD_PARTY_AUTH); | |
235 }; | |
236 | |
237 | |
238 /** | |
239 * Request permission from the user to access the token-issue URL. | |
240 * | |
241 * @param {function(): void} onOk Called if the permission is granted. | |
242 * @param {function(): void} onError Called if the permission is denied. | |
243 * @private | |
244 */ | |
245 remoting.ThirdPartyHostPermissions.prototype.requestPermission_ = function( | |
246 onOk, onError) { | |
247 chrome.permissions.request( | |
248 this.permissions_, | |
249 /** @param {boolean} result Whether the permission was granted. */ | |
250 function(result) { | |
251 if (result) { | |
252 onOk(); | |
253 } else { | |
254 onError(); | |
255 } | |
256 }); | |
257 }; | |
OLD | NEW |