Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(585)

Side by Side Diff: remoting/webapp/third_party_auth.js

Issue 12905012: Webapp changes to support third party authentication (Closed) Base URL: http://git.chromium.org/chromium/src.git@master
Patch Set: Add token URL validation, refactor Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698