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.
7 * OAuth2 class that handles retrieval/storage of an OAuth2 token.
9 * Uses a content script to trampoline the OAuth redirect page back into the
10 * extension context. This works around the lack of native support for
11 * chrome-extensions in OAuth2.
14 // TODO(jamiewalch): Delete this code once Chromoting is a v2 app and uses the
15 // identity API (http://crbug.com/ 134213).
19 /** @suppress {duplicate} */
20 var remoting = remoting || {};
22 /** @type {remoting.OAuth2} */
23 remoting.oauth2 = null;
27 remoting.OAuth2 = function() {
30 // Constants representing keys used for storing persistent state.
32 remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token';
34 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token';
36 remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email';
38 remoting.OAuth2.prototype.KEY_FULLNAME_ = 'remoting-fullname';
40 // Constants for parameters used in retrieving the OAuth2 credentials.
42 remoting.OAuth2.prototype.SCOPE_ =
43 'https://www.googleapis.com/auth/chromoting ' +
44 'https://www.googleapis.com/auth/googletalk ' +
45 'https://www.googleapis.com/auth/userinfo#email';
47 // Configurable URLs/strings.
49 * @return {string} OAuth2 redirect URI.
51 remoting.OAuth2.prototype.getRedirectUri_ = function() {
52 return remoting.settings.OAUTH2_REDIRECT_URL();
56 * @return {string} API client ID.
58 remoting.OAuth2.prototype.getClientId_ = function() {
59 return remoting.settings.OAUTH2_CLIENT_ID;
63 * @return {string} API client secret.
65 remoting.OAuth2.prototype.getClientSecret_ = function() {
66 return remoting.settings.OAUTH2_CLIENT_SECRET;
70 * @return {string} OAuth2 authentication URL.
72 remoting.OAuth2.prototype.getOAuth2AuthEndpoint_ = function() {
73 return remoting.settings.OAUTH2_BASE_URL + '/auth';
76 /** @return {boolean} True if the app is already authenticated. */
77 remoting.OAuth2.prototype.isAuthenticated = function() {
78 if (this.getRefreshToken()) {
85 * Remove the cached auth token, if any.
87 * @return {!Promise<null>} A promise resolved with the operation completes.
89 remoting.OAuth2.prototype.removeCachedAuthToken = function() {
90 window.localStorage.removeItem(this.KEY_EMAIL_);
91 window.localStorage.removeItem(this.KEY_FULLNAME_);
92 this.clearAccessToken_();
93 this.clearRefreshToken_();
94 return Promise.resolve(null);
98 * Sets the refresh token.
100 * @param {string} token The new refresh token.
101 * @return {void} Nothing.
104 remoting.OAuth2.prototype.setRefreshToken_ = function(token) {
105 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token));
106 window.localStorage.removeItem(this.KEY_EMAIL_);
107 window.localStorage.removeItem(this.KEY_FULLNAME_);
108 this.clearAccessToken_();
112 * @return {?string} The refresh token, if authenticated, or NULL.
114 remoting.OAuth2.prototype.getRefreshToken = function() {
115 var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_);
116 if (typeof value == 'string') {
117 return unescape(value);
123 * Clears the refresh token.
125 * @return {void} Nothing.
128 remoting.OAuth2.prototype.clearRefreshToken_ = function() {
129 window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_);
133 * @param {string} token The new access token.
134 * @param {number} expiration Expiration time in milliseconds since epoch.
135 * @return {void} Nothing.
138 remoting.OAuth2.prototype.setAccessToken_ = function(token, expiration) {
139 // Offset expiration by 120 seconds so that we can guarantee that the token
140 // we return will be valid for at least 2 minutes.
141 // If the access token is to be useful, this object must make some
142 // guarantee as to how long the token will be valid for.
143 // The choice of 2 minutes is arbitrary, but that length of time
144 // is part of the contract satisfied by callWithToken().
145 // Offset by a further 30 seconds to account for RTT issues.
148 'expiration': (expiration - (120 + 30)) * 1000 + Date.now()
150 window.localStorage.setItem(this.KEY_ACCESS_TOKEN_,
151 JSON.stringify(access_token));
155 * Returns the current access token, setting it to a invalid value if none
159 * @return {{token: string, expiration: number}} The current access token, or
160 * an invalid token if not authenticated.
162 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() {
163 if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) {
164 // Always be able to return structured data.
165 this.setAccessToken_('', 0);
167 var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_);
168 if (typeof accessToken == 'string') {
169 var result = base.jsonParseSafe(accessToken);
170 if (result && 'token' in result && 'expiration' in result) {
171 return /** @type {{token: string, expiration: number}} */(result);
174 console.log('Invalid access token stored.');
175 return {'token': '', 'expiration': 0};
179 * Returns true if the access token is expired, or otherwise invalid.
181 * Will throw if !isAuthenticated().
183 * @return {boolean} True if a new access token is needed.
186 remoting.OAuth2.prototype.needsNewAccessToken_ = function() {
187 if (!this.isAuthenticated()) {
188 throw 'Not Authenticated.';
190 var access_token = this.getAccessTokenInternal_();
191 if (!access_token['token']) {
194 if (Date.now() > access_token['expiration']) {
201 * @return {void} Nothing.
204 remoting.OAuth2.prototype.clearAccessToken_ = function() {
205 window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_);
209 * Update state based on token response from the OAuth2 /token endpoint.
211 * @param {function(string):void} onOk Called with the new access token.
212 * @param {string} accessToken Access token.
213 * @param {number} expiresIn Expiration time for the access token.
214 * @return {void} Nothing.
217 remoting.OAuth2.prototype.onAccessToken_ =
218 function(onOk, accessToken, expiresIn) {
219 this.setAccessToken_(accessToken, expiresIn);
224 * Update state based on token response from the OAuth2 /token endpoint.
226 * @param {function():void} onOk Called after the new tokens are stored.
227 * @param {string} refreshToken Refresh token.
228 * @param {string} accessToken Access token.
229 * @param {number} expiresIn Expiration time for the access token.
230 * @return {void} Nothing.
233 remoting.OAuth2.prototype.onTokens_ =
234 function(onOk, refreshToken, accessToken, expiresIn) {
235 this.setAccessToken_(accessToken, expiresIn);
236 this.setRefreshToken_(refreshToken);
241 * Redirect page to get a new OAuth2 authorization code
243 * @param {function(?string):void} onDone Completion callback to receive
244 * the authorization code, or null on error.
245 * @return {void} Nothing.
247 remoting.OAuth2.prototype.getAuthorizationCode = function(onDone) {
248 var xsrf_token = base.generateXsrfToken();
249 var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' +
250 remoting.xhr.urlencodeParamHash({
251 'client_id': this.getClientId_(),
252 'redirect_uri': this.getRedirectUri_(),
253 'scope': this.SCOPE_,
255 'response_type': 'code',
256 'access_type': 'offline',
257 'approval_prompt': 'force'
261 * Processes the results of the oauth flow.
263 * @param {Object<string, string>} message Dictionary containing the parsed
264 * OAuth redirect URL parameters.
265 * @param {function(*)} sendResponse Function to send response.
267 function oauth2MessageListener(message, sender, sendResponse) {
268 if ('code' in message && 'state' in message) {
269 if (message['state'] == xsrf_token) {
270 onDone(message['code']);
272 console.error('Invalid XSRF token.');
276 if ('error' in message) {
278 'Could not obtain authorization code: ' + message['error']);
280 // We intentionally don't log the response - since we don't understand
281 // it, we can't tell if it has sensitive data.
282 console.error('Invalid oauth2 response.');
286 chrome.extension.onMessage.removeListener(oauth2MessageListener);
289 chrome.extension.onMessage.addListener(oauth2MessageListener);
290 window.open(GET_CODE_URL, '_blank', 'location=yes,toolbar=no,menubar=no');
294 * Redirect page to get a new OAuth Refresh Token.
296 * @param {function():void} onDone Completion callback.
297 * @return {void} Nothing.
299 remoting.OAuth2.prototype.doAuthRedirect = function(onDone) {
300 /** @type {remoting.OAuth2} */
302 /** @param {?string} code */
303 var onAuthorizationCode = function(code) {
305 that.exchangeCodeForToken(code, onDone);
310 this.getAuthorizationCode(onAuthorizationCode);
314 * Asynchronously exchanges an authorization code for a refresh token.
316 * @param {string} code The OAuth2 authorization code.
317 * @param {function():void} onDone Callback to invoke on completion.
318 * @return {void} Nothing.
320 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) {
321 /** @param {remoting.Error} error */
322 var onError = function(error) {
323 console.error('Unable to exchange code for token: ', error);
326 remoting.oauth2Api.exchangeCodeForTokens(
327 this.onTokens_.bind(this, onDone), onError,
328 this.getClientId_(), this.getClientSecret_(), code,
329 this.getRedirectUri_());
333 * Print a command-line that can be used to register a host on Linux platforms.
335 remoting.OAuth2.prototype.printStartHostCommandLine = function() {
336 /** @type {string} */
337 var redirectUri = this.getRedirectUri_();
338 /** @param {?string} code */
339 var onAuthorizationCode = function(code) {
341 console.log('Run the following command to register a host:');
343 '%c/opt/google/chrome-remote-desktop/start-host' +
345 ' --redirect-url=' + redirectUri +
346 ' --name=$HOSTNAME', 'font-weight: bold;');
349 this.getAuthorizationCode(onAuthorizationCode);
353 * Get an access token, refreshing it first if necessary. The access
354 * token will remain valid for at least 2 minutes.
356 * @return {!Promise<string>} A promise resolved the an access token or
357 * rejected with a remoting.Error.
359 remoting.OAuth2.prototype.getToken = function() {
363 return new Promise(function(resolve, reject) {
364 var refreshToken = that.getRefreshToken();
366 if (that.needsNewAccessToken_()) {
367 remoting.oauth2Api.refreshAccessToken(
368 that.onAccessToken_.bind(that, resolve), reject,
369 that.getClientId_(), that.getClientSecret_(),
372 resolve(that.getAccessTokenInternal_()['token']);
375 reject(remoting.Error.NOT_AUTHENTICATED);
381 * Get the user's email address.
383 * @return {!Promise<string>} Promise resolved with the user's email
384 * address or rejected with a remoting.Error.
386 remoting.OAuth2.prototype.getEmail = function() {
387 var cached = window.localStorage.getItem(this.KEY_EMAIL_);
388 if (typeof cached == 'string') {
389 return Promise.resolve(cached);
391 /** @type {remoting.OAuth2} */
394 return new Promise(function(resolve, reject) {
395 /** @param {string} email */
396 var onResponse = function(email) {
397 window.localStorage.setItem(that.KEY_EMAIL_, email);
398 window.localStorage.setItem(that.KEY_FULLNAME_, '');
402 that.getToken().then(
403 remoting.oauth2Api.getEmail.bind(
404 remoting.oauth2Api, onResponse, reject),
410 * Get the user's email address and full name.
412 * @return {!Promise<{email: string, name: string}>} Promise
413 * resolved with the user's email address and full name, or rejected
414 * with a remoting.Error.
416 remoting.OAuth2.prototype.getUserInfo = function() {
417 var cachedEmail = window.localStorage.getItem(this.KEY_EMAIL_);
418 var cachedName = window.localStorage.getItem(this.KEY_FULLNAME_);
419 if (typeof cachedEmail == 'string' && typeof cachedName == 'string') {
421 * The temp variable is needed to work around a compiler bug.
422 * @type {{email: string, name: string}}
424 var result = {email: cachedEmail, name: cachedName};
425 return Promise.resolve(result);
428 /** @type {remoting.OAuth2} */
431 return new Promise(function(resolve, reject) {
433 * @param {string} email
434 * @param {string} name
436 var onResponse = function(email, name) {
437 window.localStorage.setItem(that.KEY_EMAIL_, email);
438 window.localStorage.setItem(that.KEY_FULLNAME_, name);
439 resolve({email: email, name: name});
442 that.getToken().then(
443 remoting.oauth2Api.getUserInfo.bind(
444 remoting.oauth2Api, onResponse, reject),
450 * If the user's email address is cached, return it, otherwise return null.
452 * @return {?string} The email address, if it has been cached by a previous call
453 * to getEmail or getUserInfo, otherwise null.
455 remoting.OAuth2.prototype.getCachedEmail = function() {
456 var value = window.localStorage.getItem(this.KEY_EMAIL_);
457 if (typeof value == 'string') {
464 * If the user's full name is cached, return it, otherwise return null.
466 * @return {?string} The user's full name, if it has been cached by a previous
467 * call to getUserInfo, otherwise null.
469 remoting.OAuth2.prototype.getCachedUserFullName = function() {
470 var value = window.localStorage.getItem(this.KEY_FULLNAME_);
471 if (typeof value == 'string') {