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_REFRESH_TOKEN_REVOKABLE_ =
35 'oauth2-refresh-token-revokable';
37 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token';
39 remoting.OAuth2.prototype.KEY_XSRF_TOKEN_ = 'oauth2-xsrf-token';
41 remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email';
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 * Removes all storage, and effectively unauthenticates the user.
90 * @return {void} Nothing.
92 remoting.OAuth2.prototype.clear = function() {
93 window.localStorage.removeItem(this.KEY_EMAIL_);
94 this.clearAccessToken_();
95 this.clearRefreshToken_();
99 * Sets the refresh token.
101 * This method also marks the token as revokable, so that this object will
102 * revoke the token when it no longer needs it.
104 * @param {string} token The new refresh token.
105 * @return {void} Nothing.
108 remoting.OAuth2.prototype.setRefreshToken_ = function(token) {
109 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token));
110 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_REVOKABLE_, true);
111 window.localStorage.removeItem(this.KEY_EMAIL_);
112 this.clearAccessToken_();
116 * Gets the refresh token.
118 * This method also marks the refresh token as not revokable, so that this
119 * object will not revoke the token when it no longer needs it. After this
120 * object has exported the token, it cannot know whether it is still in use
121 * when this object no longer needs it.
123 * @return {?string} The refresh token, if authenticated, or NULL.
125 remoting.OAuth2.prototype.exportRefreshToken = function() {
126 window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_REVOKABLE_);
127 return this.getRefreshToken_();
131 * @return {?string} The refresh token, if authenticated, or NULL.
134 remoting.OAuth2.prototype.getRefreshToken_ = function() {
135 var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_);
136 if (typeof value == 'string') {
137 return unescape(value);
143 * Clears the refresh token.
145 * @return {void} Nothing.
148 remoting.OAuth2.prototype.clearRefreshToken_ = function() {
149 if (window.localStorage.getItem(this.KEY_REFRESH_TOKEN_REVOKABLE_)) {
150 this.revokeToken_(this.getRefreshToken_());
152 window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_);
153 window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_REVOKABLE_);
157 * @param {string} token The new access token.
158 * @param {number} expiration Expiration time in milliseconds since epoch.
159 * @return {void} Nothing.
162 remoting.OAuth2.prototype.setAccessToken_ = function(token, expiration) {
163 // Offset expiration by 120 seconds so that we can guarantee that the token
164 // we return will be valid for at least 2 minutes.
165 // If the access token is to be useful, this object must make some
166 // guarantee as to how long the token will be valid for.
167 // The choice of 2 minutes is arbitrary, but that length of time
168 // is part of the contract satisfied by callWithToken().
169 // Offset by a further 30 seconds to account for RTT issues.
172 'expiration': (expiration - (120 + 30)) * 1000 + Date.now()
174 window.localStorage.setItem(this.KEY_ACCESS_TOKEN_,
175 JSON.stringify(access_token));
179 * Returns the current access token, setting it to a invalid value if none
183 * @return {{token: string, expiration: number}} The current access token, or
184 * an invalid token if not authenticated.
186 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() {
187 if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) {
188 // Always be able to return structured data.
189 this.setAccessToken_('', 0);
191 var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_);
192 if (typeof accessToken == 'string') {
193 var result = jsonParseSafe(accessToken);
194 if (result && 'token' in result && 'expiration' in result) {
195 return /** @type {{token: string, expiration: number}} */ result;
198 console.log('Invalid access token stored.');
199 return {'token': '', 'expiration': 0};
203 * Returns true if the access token is expired, or otherwise invalid.
205 * Will throw if !isAuthenticated().
207 * @return {boolean} True if a new access token is needed.
210 remoting.OAuth2.prototype.needsNewAccessToken_ = function() {
211 if (!this.isAuthenticated()) {
212 throw 'Not Authenticated.';
214 var access_token = this.getAccessTokenInternal_();
215 if (!access_token['token']) {
218 if (Date.now() > access_token['expiration']) {
225 * @return {void} Nothing.
228 remoting.OAuth2.prototype.clearAccessToken_ = function() {
229 window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_);
233 * Update state based on token response from the OAuth2 /token endpoint.
235 * @param {function(string):void} onOk Called with the new access token.
236 * @param {string} accessToken Access token.
237 * @param {number} expiresIn Expiration time for the access token.
238 * @return {void} Nothing.
241 remoting.OAuth2.prototype.onAccessToken_ =
242 function(onOk, accessToken, expiresIn) {
243 this.setAccessToken_(accessToken, expiresIn);
248 * Update state based on token response from the OAuth2 /token endpoint.
250 * @param {function():void} onOk Called after the new tokens are stored.
251 * @param {string} refreshToken Refresh token.
252 * @param {string} accessToken Access token.
253 * @param {number} expiresIn Expiration time for the access token.
254 * @return {void} Nothing.
257 remoting.OAuth2.prototype.onTokens_ =
258 function(onOk, refreshToken, accessToken, expiresIn) {
259 this.setAccessToken_(accessToken, expiresIn);
260 this.setRefreshToken_(refreshToken);
265 * Redirect page to get a new OAuth2 Refresh Token.
267 * @return {void} Nothing.
269 remoting.OAuth2.prototype.doAuthRedirect = function() {
270 /** @type {remoting.OAuth2} */
272 var xsrf_token = remoting.generateXsrfToken();
273 window.localStorage.setItem(this.KEY_XSRF_TOKEN_, xsrf_token);
274 var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' +
275 remoting.xhr.urlencodeParamHash({
276 'client_id': this.getClientId_(),
277 'redirect_uri': this.getRedirectUri_(),
278 'scope': this.SCOPE_,
280 'response_type': 'code',
281 'access_type': 'offline',
282 'approval_prompt': 'force'
286 * Processes the results of the oauth flow.
288 * @param {Object.<string, string>} message Dictionary containing the parsed
289 * OAuth redirect URL parameters.
291 function oauth2MessageListener(message) {
292 if ('code' in message && 'state' in message) {
293 var onDone = function() {
294 window.location.reload();
296 that.exchangeCodeForToken(
297 message['code'], message['state'], onDone);
299 if ('error' in message) {
301 'Could not obtain authorization code: ' + message['error']);
303 // We intentionally don't log the response - since we don't understand
304 // it, we can't tell if it has sensitive data.
305 console.error('Invalid oauth2 response.');
308 chrome.extension.onMessage.removeListener(oauth2MessageListener);
310 chrome.extension.onMessage.addListener(oauth2MessageListener);
311 window.open(GET_CODE_URL, '_blank', 'location=yes,toolbar=no,menubar=no');
315 * Asynchronously exchanges an authorization code for a refresh token.
317 * @param {string} code The OAuth2 authorization code.
318 * @param {string} state The state parameter received from the OAuth redirect.
319 * @param {function():void} onDone Callback to invoke on completion.
320 * @return {void} Nothing.
322 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, state, onDone) {
323 var xsrf_token = window.localStorage.getItem(this.KEY_XSRF_TOKEN_);
324 window.localStorage.removeItem(this.KEY_XSRF_TOKEN_);
325 if (xsrf_token == undefined || state != xsrf_token) {
326 // Invalid XSRF token, or unexpected OAuth2 redirect. Abort.
329 /** @param {remoting.Error} error */
330 var onError = function(error) {
331 console.error('Unable to exchange code for token: ', error);
334 remoting.OAuth2Api.exchangeCodeForTokens(
335 this.onTokens_.bind(this, onDone), onError,
336 this.getClientId_(), this.getClientSecret_(), code,
337 this.getRedirectUri_());
341 * Revokes a refresh or an access token.
343 * @param {string?} token An access or refresh token.
344 * @return {void} Nothing.
347 remoting.OAuth2.prototype.revokeToken_ = function(token) {
348 if (!token || (token.length == 0)) {
352 remoting.OAuth2Api.revokeToken(function() {}, function() {}, token);
356 * Call a function with an access token, refreshing it first if necessary.
357 * The access token will remain valid for at least 2 minutes.
359 * @param {function(string):void} onOk Function to invoke with access token if
360 * an access token was successfully retrieved.
361 * @param {function(remoting.Error):void} onError Function to invoke with an
362 * error code on failure.
363 * @return {void} Nothing.
365 remoting.OAuth2.prototype.callWithToken = function(onOk, onError) {
366 var refreshToken = this.getRefreshToken_();
368 if (this.needsNewAccessToken_()) {
369 remoting.OAuth2Api.refreshAccessToken(
370 this.onAccessToken_.bind(this, onOk), onError,
371 this.getClientId_(), this.getClientSecret_(),
374 onOk(this.getAccessTokenInternal_()['token']);
377 onError(remoting.Error.NOT_AUTHENTICATED);
382 * Get the user's email address.
384 * @param {function(string):void} onOk Callback invoked when the email
385 * address is available.
386 * @param {function(remoting.Error):void} onError Callback invoked if an
388 * @return {void} Nothing.
390 remoting.OAuth2.prototype.getEmail = function(onOk, onError) {
391 var cached = window.localStorage.getItem(this.KEY_EMAIL_);
392 if (typeof cached == 'string') {
396 /** @type {remoting.OAuth2} */
398 /** @param {string} email */
399 var onResponse = function(email) {
400 window.localStorage.setItem(that.KEY_EMAIL_, email);
405 remoting.OAuth2Api.getEmail.bind(null, onResponse, onError), onError);
409 * If the user's email address is cached, return it, otherwise return null.
411 * @return {?string} The email address, if it has been cached by a previous call
412 * to getEmail, otherwise null.
414 remoting.OAuth2.prototype.getCachedEmail = function() {
415 var value = window.localStorage.getItem(this.KEY_EMAIL_);
416 if (typeof value == 'string') {