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_XSRF_TOKEN_
= 'oauth2-xsrf-token';
38 remoting
.OAuth2
.prototype.KEY_EMAIL_
= 'remoting-email';
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 * Removes all storage, and effectively unauthenticates the user.
87 * @return {void} Nothing.
89 remoting
.OAuth2
.prototype.clear = function() {
90 window
.localStorage
.removeItem(this.KEY_EMAIL_
);
91 this.clearAccessToken_();
92 this.clearRefreshToken_();
96 * Sets the refresh token.
98 * @param {string} token The new refresh token.
99 * @return {void} Nothing.
102 remoting
.OAuth2
.prototype.setRefreshToken_ = function(token
) {
103 window
.localStorage
.setItem(this.KEY_REFRESH_TOKEN_
, escape(token
));
104 window
.localStorage
.removeItem(this.KEY_EMAIL_
);
105 this.clearAccessToken_();
109 * @return {?string} The refresh token, if authenticated, or NULL.
111 remoting
.OAuth2
.prototype.getRefreshToken = function() {
112 var value
= window
.localStorage
.getItem(this.KEY_REFRESH_TOKEN_
);
113 if (typeof value
== 'string') {
114 return unescape(value
);
120 * Clears the refresh token.
122 * @return {void} Nothing.
125 remoting
.OAuth2
.prototype.clearRefreshToken_ = function() {
126 window
.localStorage
.removeItem(this.KEY_REFRESH_TOKEN_
);
130 * @param {string} token The new access token.
131 * @param {number} expiration Expiration time in milliseconds since epoch.
132 * @return {void} Nothing.
135 remoting
.OAuth2
.prototype.setAccessToken_ = function(token
, expiration
) {
136 // Offset expiration by 120 seconds so that we can guarantee that the token
137 // we return will be valid for at least 2 minutes.
138 // If the access token is to be useful, this object must make some
139 // guarantee as to how long the token will be valid for.
140 // The choice of 2 minutes is arbitrary, but that length of time
141 // is part of the contract satisfied by callWithToken().
142 // Offset by a further 30 seconds to account for RTT issues.
145 'expiration': (expiration
- (120 + 30)) * 1000 + Date
.now()
147 window
.localStorage
.setItem(this.KEY_ACCESS_TOKEN_
,
148 JSON
.stringify(access_token
));
152 * Returns the current access token, setting it to a invalid value if none
156 * @return {{token: string, expiration: number}} The current access token, or
157 * an invalid token if not authenticated.
159 remoting
.OAuth2
.prototype.getAccessTokenInternal_ = function() {
160 if (!window
.localStorage
.getItem(this.KEY_ACCESS_TOKEN_
)) {
161 // Always be able to return structured data.
162 this.setAccessToken_('', 0);
164 var accessToken
= window
.localStorage
.getItem(this.KEY_ACCESS_TOKEN_
);
165 if (typeof accessToken
== 'string') {
166 var result
= jsonParseSafe(accessToken
);
167 if (result
&& 'token' in result
&& 'expiration' in result
) {
168 return /** @type {{token: string, expiration: number}} */ result
;
171 console
.log('Invalid access token stored.');
172 return {'token': '', 'expiration': 0};
176 * Returns true if the access token is expired, or otherwise invalid.
178 * Will throw if !isAuthenticated().
180 * @return {boolean} True if a new access token is needed.
183 remoting
.OAuth2
.prototype.needsNewAccessToken_ = function() {
184 if (!this.isAuthenticated()) {
185 throw 'Not Authenticated.';
187 var access_token
= this.getAccessTokenInternal_();
188 if (!access_token
['token']) {
191 if (Date
.now() > access_token
['expiration']) {
198 * @return {void} Nothing.
201 remoting
.OAuth2
.prototype.clearAccessToken_ = function() {
202 window
.localStorage
.removeItem(this.KEY_ACCESS_TOKEN_
);
206 * Update state based on token response from the OAuth2 /token endpoint.
208 * @param {function(string):void} onOk Called with the new access token.
209 * @param {string} accessToken Access token.
210 * @param {number} expiresIn Expiration time for the access token.
211 * @return {void} Nothing.
214 remoting
.OAuth2
.prototype.onAccessToken_
=
215 function(onOk
, accessToken
, expiresIn
) {
216 this.setAccessToken_(accessToken
, expiresIn
);
221 * Update state based on token response from the OAuth2 /token endpoint.
223 * @param {function():void} onOk Called after the new tokens are stored.
224 * @param {string} refreshToken Refresh token.
225 * @param {string} accessToken Access token.
226 * @param {number} expiresIn Expiration time for the access token.
227 * @return {void} Nothing.
230 remoting
.OAuth2
.prototype.onTokens_
=
231 function(onOk
, refreshToken
, accessToken
, expiresIn
) {
232 this.setAccessToken_(accessToken
, expiresIn
);
233 this.setRefreshToken_(refreshToken
);
238 * Redirect page to get a new OAuth2 Refresh Token.
240 * @return {void} Nothing.
242 remoting
.OAuth2
.prototype.doAuthRedirect = function() {
243 /** @type {remoting.OAuth2} */
245 var xsrf_token
= remoting
.generateXsrfToken();
246 window
.localStorage
.setItem(this.KEY_XSRF_TOKEN_
, xsrf_token
);
247 var GET_CODE_URL
= this.getOAuth2AuthEndpoint_() + '?' +
248 remoting
.xhr
.urlencodeParamHash({
249 'client_id': this.getClientId_(),
250 'redirect_uri': this.getRedirectUri_(),
251 'scope': this.SCOPE_
,
253 'response_type': 'code',
254 'access_type': 'offline',
255 'approval_prompt': 'force'
259 * Processes the results of the oauth flow.
261 * @param {Object.<string, string>} message Dictionary containing the parsed
262 * OAuth redirect URL parameters.
264 function oauth2MessageListener(message
) {
265 if ('code' in message
&& 'state' in message
) {
266 var onDone = function() {
267 window
.location
.reload();
269 that
.exchangeCodeForToken(
270 message
['code'], message
['state'], onDone
);
272 if ('error' in message
) {
274 'Could not obtain authorization code: ' + message
['error']);
276 // We intentionally don't log the response - since we don't understand
277 // it, we can't tell if it has sensitive data.
278 console
.error('Invalid oauth2 response.');
281 chrome
.extension
.onMessage
.removeListener(oauth2MessageListener
);
283 chrome
.extension
.onMessage
.addListener(oauth2MessageListener
);
284 window
.open(GET_CODE_URL
, '_blank', 'location=yes,toolbar=no,menubar=no');
288 * Asynchronously exchanges an authorization code for a refresh token.
290 * @param {string} code The OAuth2 authorization code.
291 * @param {string} state The state parameter received from the OAuth redirect.
292 * @param {function():void} onDone Callback to invoke on completion.
293 * @return {void} Nothing.
295 remoting
.OAuth2
.prototype.exchangeCodeForToken = function(code
, state
, onDone
) {
296 var xsrf_token
= window
.localStorage
.getItem(this.KEY_XSRF_TOKEN_
);
297 window
.localStorage
.removeItem(this.KEY_XSRF_TOKEN_
);
298 if (xsrf_token
== undefined || state
!= xsrf_token
) {
299 // Invalid XSRF token, or unexpected OAuth2 redirect. Abort.
302 /** @param {remoting.Error} error */
303 var onError = function(error
) {
304 console
.error('Unable to exchange code for token: ', error
);
307 remoting
.OAuth2Api
.exchangeCodeForTokens(
308 this.onTokens_
.bind(this, onDone
), onError
,
309 this.getClientId_(), this.getClientSecret_(), code
,
310 this.getRedirectUri_());
314 * Call a function with an access token, refreshing it first if necessary.
315 * The access token will remain valid for at least 2 minutes.
317 * @param {function(string):void} onOk Function to invoke with access token if
318 * an access token was successfully retrieved.
319 * @param {function(remoting.Error):void} onError Function to invoke with an
320 * error code on failure.
321 * @return {void} Nothing.
323 remoting
.OAuth2
.prototype.callWithToken = function(onOk
, onError
) {
324 var refreshToken
= this.getRefreshToken();
326 if (this.needsNewAccessToken_()) {
327 remoting
.OAuth2Api
.refreshAccessToken(
328 this.onAccessToken_
.bind(this, onOk
), onError
,
329 this.getClientId_(), this.getClientSecret_(),
332 onOk(this.getAccessTokenInternal_()['token']);
335 onError(remoting
.Error
.NOT_AUTHENTICATED
);
340 * Get the user's email address.
342 * @param {function(string):void} onOk Callback invoked when the email
343 * address is available.
344 * @param {function(remoting.Error):void} onError Callback invoked if an
346 * @return {void} Nothing.
348 remoting
.OAuth2
.prototype.getEmail = function(onOk
, onError
) {
349 var cached
= window
.localStorage
.getItem(this.KEY_EMAIL_
);
350 if (typeof cached
== 'string') {
354 /** @type {remoting.OAuth2} */
356 /** @param {string} email */
357 var onResponse = function(email
) {
358 window
.localStorage
.setItem(that
.KEY_EMAIL_
, email
);
363 remoting
.OAuth2Api
.getEmail
.bind(null, onResponse
, onError
), onError
);
367 * If the user's email address is cached, return it, otherwise return null.
369 * @return {?string} The email address, if it has been cached by a previous call
370 * to getEmail, otherwise null.
372 remoting
.OAuth2
.prototype.getCachedEmail = function() {
373 var value
= window
.localStorage
.getItem(this.KEY_EMAIL_
);
374 if (typeof value
== 'string') {