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;
28 * @extends {remoting.Identity}
30 remoting
.OAuth2 = function() {
33 // Constants representing keys used for storing persistent state.
35 remoting
.OAuth2
.prototype.KEY_REFRESH_TOKEN_
= 'oauth2-refresh-token';
37 remoting
.OAuth2
.prototype.KEY_ACCESS_TOKEN_
= 'oauth2-access-token';
39 remoting
.OAuth2
.prototype.KEY_EMAIL_
= 'remoting-email';
41 remoting
.OAuth2
.prototype.KEY_FULLNAME_
= 'remoting-fullname';
43 // Constants for parameters used in retrieving the OAuth2 credentials.
45 remoting
.OAuth2
.prototype.SCOPE_
=
46 'https://www.googleapis.com/auth/chromoting ' +
47 'https://www.googleapis.com/auth/googletalk ' +
48 'https://www.googleapis.com/auth/userinfo#email';
50 // Configurable URLs/strings.
52 * @return {string} OAuth2 redirect URI.
54 remoting
.OAuth2
.prototype.getRedirectUri_ = function() {
55 return remoting
.settings
.OAUTH2_REDIRECT_URL();
59 * @return {string} API client ID.
61 remoting
.OAuth2
.prototype.getClientId_ = function() {
62 return remoting
.settings
.OAUTH2_CLIENT_ID
;
66 * @return {string} API client secret.
68 remoting
.OAuth2
.prototype.getClientSecret_ = function() {
69 return remoting
.settings
.OAUTH2_CLIENT_SECRET
;
73 * @return {string} OAuth2 authentication URL.
75 remoting
.OAuth2
.prototype.getOAuth2AuthEndpoint_ = function() {
76 return remoting
.settings
.OAUTH2_BASE_URL
+ '/auth';
79 /** @return {boolean} True if the app is already authenticated. */
80 remoting
.OAuth2
.prototype.isAuthenticated = function() {
81 if (this.getRefreshToken()) {
88 * Remove the cached auth token, if any.
90 * @return {!Promise<null>} A promise resolved with the operation completes.
92 remoting
.OAuth2
.prototype.removeCachedAuthToken = function() {
93 window
.localStorage
.removeItem(this.KEY_EMAIL_
);
94 window
.localStorage
.removeItem(this.KEY_FULLNAME_
);
95 this.clearAccessToken_();
96 this.clearRefreshToken_();
97 return Promise
.resolve(null);
101 * Sets the refresh token.
103 * @param {string} token The new refresh token.
104 * @return {void} Nothing.
107 remoting
.OAuth2
.prototype.setRefreshToken_ = function(token
) {
108 window
.localStorage
.setItem(this.KEY_REFRESH_TOKEN_
, escape(token
));
109 window
.localStorage
.removeItem(this.KEY_EMAIL_
);
110 window
.localStorage
.removeItem(this.KEY_FULLNAME_
);
111 this.clearAccessToken_();
115 * @return {?string} The refresh token, if authenticated, or NULL.
117 remoting
.OAuth2
.prototype.getRefreshToken = function() {
118 var value
= window
.localStorage
.getItem(this.KEY_REFRESH_TOKEN_
);
119 if (typeof value
== 'string') {
120 return unescape(value
);
126 * Clears the refresh token.
128 * @return {void} Nothing.
131 remoting
.OAuth2
.prototype.clearRefreshToken_ = function() {
132 window
.localStorage
.removeItem(this.KEY_REFRESH_TOKEN_
);
136 * @param {string} token The new access token.
137 * @param {number} expiration Expiration time in milliseconds since epoch.
138 * @return {void} Nothing.
141 remoting
.OAuth2
.prototype.setAccessToken_ = function(token
, expiration
) {
142 // Offset expiration by 120 seconds so that we can guarantee that the token
143 // we return will be valid for at least 2 minutes.
144 // If the access token is to be useful, this object must make some
145 // guarantee as to how long the token will be valid for.
146 // The choice of 2 minutes is arbitrary, but that length of time
147 // is part of the contract satisfied by callWithToken().
148 // Offset by a further 30 seconds to account for RTT issues.
151 'expiration': (expiration
- (120 + 30)) * 1000 + Date
.now()
153 window
.localStorage
.setItem(this.KEY_ACCESS_TOKEN_
,
154 JSON
.stringify(access_token
));
158 * Returns the current access token, setting it to a invalid value if none
162 * @return {{token: string, expiration: number}} The current access token, or
163 * an invalid token if not authenticated.
165 remoting
.OAuth2
.prototype.getAccessTokenInternal_ = function() {
166 if (!window
.localStorage
.getItem(this.KEY_ACCESS_TOKEN_
)) {
167 // Always be able to return structured data.
168 this.setAccessToken_('', 0);
170 var accessToken
= window
.localStorage
.getItem(this.KEY_ACCESS_TOKEN_
);
171 if (typeof accessToken
== 'string') {
172 var result
= base
.jsonParseSafe(accessToken
);
173 if (result
&& 'token' in result
&& 'expiration' in result
) {
174 return /** @type {{token: string, expiration: number}} */(result
);
177 console
.log('Invalid access token stored.');
178 return {'token': '', 'expiration': 0};
182 * Returns true if the access token is expired, or otherwise invalid.
184 * Will throw if !isAuthenticated().
186 * @return {boolean} True if a new access token is needed.
189 remoting
.OAuth2
.prototype.needsNewAccessToken_ = function() {
190 if (!this.isAuthenticated()) {
191 throw 'Not Authenticated.';
193 var access_token
= this.getAccessTokenInternal_();
194 if (!access_token
['token']) {
197 if (Date
.now() > access_token
['expiration']) {
204 * @return {void} Nothing.
207 remoting
.OAuth2
.prototype.clearAccessToken_ = function() {
208 window
.localStorage
.removeItem(this.KEY_ACCESS_TOKEN_
);
212 * Update state based on token response from the OAuth2 /token endpoint.
214 * @param {function(string):void} onOk Called with the new access token.
215 * @param {string} accessToken Access token.
216 * @param {number} expiresIn Expiration time for the access token.
217 * @return {void} Nothing.
220 remoting
.OAuth2
.prototype.onAccessToken_
=
221 function(onOk
, accessToken
, expiresIn
) {
222 this.setAccessToken_(accessToken
, expiresIn
);
227 * Update state based on token response from the OAuth2 /token endpoint.
229 * @param {function():void} onOk Called after the new tokens are stored.
230 * @param {string} refreshToken Refresh token.
231 * @param {string} accessToken Access token.
232 * @param {number} expiresIn Expiration time for the access token.
233 * @return {void} Nothing.
236 remoting
.OAuth2
.prototype.onTokens_
=
237 function(onOk
, refreshToken
, accessToken
, expiresIn
) {
238 this.setAccessToken_(accessToken
, expiresIn
);
239 this.setRefreshToken_(refreshToken
);
244 * Redirect page to get a new OAuth2 authorization code
246 * @param {function(?string):void} onDone Completion callback to receive
247 * the authorization code, or null on error.
248 * @return {void} Nothing.
250 remoting
.OAuth2
.prototype.getAuthorizationCode = function(onDone
) {
251 var xsrf_token
= base
.generateXsrfToken();
252 var GET_CODE_URL
= this.getOAuth2AuthEndpoint_() + '?' +
253 remoting
.Xhr
.urlencodeParamHash({
254 'client_id': this.getClientId_(),
255 'redirect_uri': this.getRedirectUri_(),
256 'scope': this.SCOPE_
,
258 'response_type': 'code',
259 'access_type': 'offline',
260 'approval_prompt': 'force'
264 * Processes the results of the oauth flow.
266 * @param {Object<string>} message Dictionary containing the parsed OAuth
267 * redirect URL parameters.
268 * @param {function(*)} sendResponse Function to send response.
270 function oauth2MessageListener(message
, sender
, sendResponse
) {
271 if ('code' in message
&& 'state' in message
) {
272 if (message
['state'] == xsrf_token
) {
273 onDone(message
['code']);
275 console
.error('Invalid XSRF token.');
279 if ('error' in message
) {
281 'Could not obtain authorization code: ' + message
['error']);
283 // We intentionally don't log the response - since we don't understand
284 // it, we can't tell if it has sensitive data.
285 console
.error('Invalid oauth2 response.');
289 chrome
.extension
.onMessage
.removeListener(oauth2MessageListener
);
292 chrome
.extension
.onMessage
.addListener(oauth2MessageListener
);
293 window
.open(GET_CODE_URL
, '_blank', 'location=yes,toolbar=no,menubar=no');
297 * Redirect page to get a new OAuth Refresh Token.
299 * @param {function():void} onDone Completion callback.
300 * @return {void} Nothing.
302 remoting
.OAuth2
.prototype.doAuthRedirect = function(onDone
) {
303 /** @type {remoting.OAuth2} */
305 /** @param {?string} code */
306 var onAuthorizationCode = function(code
) {
308 that
.exchangeCodeForToken(code
, onDone
);
313 this.getAuthorizationCode(onAuthorizationCode
);
317 * Asynchronously exchanges an authorization code for a refresh token.
319 * @param {string} code The OAuth2 authorization code.
320 * @param {function():void} onDone Callback to invoke on completion.
321 * @return {void} Nothing.
323 remoting
.OAuth2
.prototype.exchangeCodeForToken = function(code
, onDone
) {
324 /** @param {!remoting.Error} error */
325 var onError = function(error
) {
326 console
.error('Unable to exchange code for token: ' + error
.toString());
329 remoting
.oauth2Api
.exchangeCodeForTokens(
330 this.onTokens_
.bind(this, onDone
), onError
,
331 this.getClientId_(), this.getClientSecret_(), code
,
332 this.getRedirectUri_());
336 * Print a command-line that can be used to register a host on Linux platforms.
338 remoting
.OAuth2
.prototype.printStartHostCommandLine = function() {
339 /** @type {string} */
340 var redirectUri
= this.getRedirectUri_();
341 /** @param {?string} code */
342 var onAuthorizationCode = function(code
) {
344 console
.log('Run the following command to register a host:');
346 '%c/opt/google/chrome-remote-desktop/start-host' +
348 ' --redirect-url=' + redirectUri
+
349 ' --name=$HOSTNAME', 'font-weight: bold;');
352 this.getAuthorizationCode(onAuthorizationCode
);
356 * Get an access token, refreshing it first if necessary. The access
357 * token will remain valid for at least 2 minutes.
359 * @return {!Promise<string>} A promise resolved the an access token or
360 * rejected with a remoting.Error.
362 remoting
.OAuth2
.prototype.getToken = function() {
366 return new Promise(function(resolve
, reject
) {
367 var refreshToken
= that
.getRefreshToken();
369 if (that
.needsNewAccessToken_()) {
370 remoting
.oauth2Api
.refreshAccessToken(
371 that
.onAccessToken_
.bind(that
, resolve
), reject
,
372 that
.getClientId_(), that
.getClientSecret_(),
375 resolve(that
.getAccessTokenInternal_()['token']);
378 reject(new remoting
.Error(remoting
.Error
.Tag
.NOT_AUTHENTICATED
));
384 * Get the user's email address.
386 * @return {!Promise<string>} Promise resolved with the user's email
387 * address or rejected with a remoting.Error.
389 remoting
.OAuth2
.prototype.getEmail = function() {
390 var cached
= window
.localStorage
.getItem(this.KEY_EMAIL_
);
391 if (typeof cached
== 'string') {
392 return Promise
.resolve(cached
);
394 /** @type {remoting.OAuth2} */
397 return new Promise(function(resolve
, reject
) {
398 /** @param {string} email */
399 var onResponse = function(email
) {
400 window
.localStorage
.setItem(that
.KEY_EMAIL_
, email
);
401 window
.localStorage
.setItem(that
.KEY_FULLNAME_
, '');
405 that
.getToken().then(
406 remoting
.oauth2Api
.getEmail
.bind(
407 remoting
.oauth2Api
, onResponse
, reject
),
413 * Get the user's email address and full name.
415 * @return {!Promise<{email: string, name: string}>} Promise
416 * resolved with the user's email address and full name, or rejected
417 * with a remoting.Error.
419 remoting
.OAuth2
.prototype.getUserInfo = function() {
420 var cachedEmail
= window
.localStorage
.getItem(this.KEY_EMAIL_
);
421 var cachedName
= window
.localStorage
.getItem(this.KEY_FULLNAME_
);
422 if (typeof cachedEmail
== 'string' && typeof cachedName
== 'string') {
424 * The temp variable is needed to work around a compiler bug.
425 * @type {{email: string, name: string}}
427 var result
= {email
: cachedEmail
, name
: cachedName
};
428 return Promise
.resolve(result
);
431 /** @type {remoting.OAuth2} */
434 return new Promise(function(resolve
, reject
) {
436 * @param {string} email
437 * @param {string} name
439 var onResponse = function(email
, name
) {
440 window
.localStorage
.setItem(that
.KEY_EMAIL_
, email
);
441 window
.localStorage
.setItem(that
.KEY_FULLNAME_
, name
);
442 resolve({email
: email
, name
: name
});
445 that
.getToken().then(
446 remoting
.oauth2Api
.getUserInfo
.bind(
447 remoting
.oauth2Api
, onResponse
, reject
),