15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Copyright (c) 2012 The Chromium Authors. All rights reserved. 25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// found in the LICENSE file. 45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @fileoverview 75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * OAuth2 class that handles retrieval/storage of an OAuth2 token. 85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Uses a content script to trampoline the OAuth redirect page back into the 105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * extension context. This works around the lack of native support for 115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * chrome-extensions in OAuth2. 125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// TODO(jamiewalch): Delete this code once Chromoting is a v2 app and uses the 152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// identity API (http://crbug.com/ 134213). 162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)'use strict'; 185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** @suppress {duplicate} */ 205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)var remoting = remoting || {}; 215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** @type {remoting.OAuth2} */ 235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.oauth2 = null; 245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** @constructor */ 275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2 = function() { 285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Constants representing keys used for storing persistent state. 315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** @private */ 325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token'; 335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** @private */ 345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_REVOKABLE_ = 355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 'oauth2-refresh-token-revokable'; 365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** @private */ 375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token'; 385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** @private */ 392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)remoting.OAuth2.prototype.KEY_XSRF_TOKEN_ = 'oauth2-xsrf-token'; 402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)/** @private */ 415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email'; 425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Constants for parameters used in retrieving the OAuth2 credentials. 445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** @private */ 455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.SCOPE_ = 465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 'https://www.googleapis.com/auth/chromoting ' + 475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 'https://www.googleapis.com/auth/googletalk ' + 485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 'https://www.googleapis.com/auth/userinfo#email'; 492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// Configurable URLs/strings. 512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)/** @private 522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * @return {string} OAuth2 redirect URI. 532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)remoting.OAuth2.prototype.getRedirectUri_ = function() { 552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return remoting.settings.OAUTH2_REDIRECT_URL; 562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)}; 572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)/** @private 592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * @return {string} API client ID. 602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)remoting.OAuth2.prototype.getClientId_ = function() { 622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return remoting.settings.OAUTH2_CLIENT_ID; 632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)}; 642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)/** @private 662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * @return {string} API client secret. 672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)remoting.OAuth2.prototype.getClientSecret_ = function() { 692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return remoting.settings.OAUTH2_CLIENT_SECRET; 702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)}; 712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)/** @private 732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * @return {string} OAuth2 authentication URL. 742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) */ 752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)remoting.OAuth2.prototype.getOAuth2AuthEndpoint_ = function() { 762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return remoting.settings.OAUTH2_BASE_URL + '/auth'; 772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)}; 782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** @return {boolean} True if the app is already authenticated. */ 805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.isAuthenticated = function() { 815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (this.getRefreshToken_()) { 825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return true; 835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return false; 855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Removes all storage, and effectively unauthenticates the user. 895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.clear = function() { 935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) window.localStorage.removeItem(this.KEY_EMAIL_); 945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) this.clearAccessToken_(); 955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) this.clearRefreshToken_(); 965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Sets the refresh token. 1005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 1015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * This method also marks the token as revokable, so that this object will 1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * revoke the token when it no longer needs it. 1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 1045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @param {string} token The new refresh token. 1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 106a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @private 1075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 108a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles)remoting.OAuth2.prototype.setRefreshToken_ = function(token) { 1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); 1105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) window.localStorage.setItem(this.KEY_REFRESH_TOKEN_REVOKABLE_, true); 1117d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles) window.localStorage.removeItem(this.KEY_EMAIL_); 1125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) this.clearAccessToken_(); 1135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 1145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 1165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Gets the refresh token. 1175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 1185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * This method also marks the refresh token as not revokable, so that this 1195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * object will not revoke the token when it no longer needs it. After this 1205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * object has exported the token, it cannot know whether it is still in use 1215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * when this object no longer needs it. 1225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 1235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {?string} The refresh token, if authenticated, or NULL. 1245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 1255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.exportRefreshToken = function() { 1265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_REVOKABLE_); 1275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return this.getRefreshToken_(); 1285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 1315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {?string} The refresh token, if authenticated, or NULL. 1325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @private 1335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.getRefreshToken_ = function() { 1355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_); 1365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (typeof value == 'string') { 1375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return unescape(value); 1385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 1395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return null; 1405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 1435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Clears the refresh token. 1445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 1455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 1465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @private 1475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 1485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.clearRefreshToken_ = function() { 1495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (window.localStorage.getItem(this.KEY_REFRESH_TOKEN_REVOKABLE_)) { 1505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) this.revokeToken_(this.getRefreshToken_()); 1515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 1525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_); 1535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_REVOKABLE_); 1545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 1555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 1575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @param {string} token The new access token. 1585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @param {number} expiration Expiration time in milliseconds since epoch. 1595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 160a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @private 1615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 162a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles)remoting.OAuth2.prototype.setAccessToken_ = function(token, expiration) { 163a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) // Offset expiration by 120 seconds so that we can guarantee that the token 164a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) // we return will be valid for at least 2 minutes. 165a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) // If the access token is to be useful, this object must make some 166a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) // guarantee as to how long the token will be valid for. 167a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) // The choice of 2 minutes is arbitrary, but that length of time 168a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) // is part of the contract satisfied by callWithToken(). 169a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) // Offset by a further 30 seconds to account for RTT issues. 170a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) var access_token = { 171a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) 'token': token, 172a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) 'expiration': (expiration - (120 + 30)) * 1000 + Date.now() 173a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) }; 1745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) window.localStorage.setItem(this.KEY_ACCESS_TOKEN_, 1755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) JSON.stringify(access_token)); 1765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 1775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 1795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Returns the current access token, setting it to a invalid value if none 1805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * existed before. 1815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 1825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @private 1835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {{token: string, expiration: number}} The current access token, or 1845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * an invalid token if not authenticated. 1855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 1865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.getAccessTokenInternal_ = function() { 1875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) { 1885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // Always be able to return structured data. 189a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.setAccessToken_('', 0); 1905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 1915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_); 1925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (typeof accessToken == 'string') { 1935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) var result = jsonParseSafe(accessToken); 1945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (result && 'token' in result && 'expiration' in result) { 1955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return /** @type {{token: string, expiration: number}} */ result; 1965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 1975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 1985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) console.log('Invalid access token stored.'); 1995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return {'token': '', 'expiration': 0}; 2005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 2015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 2035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Returns true if the access token is expired, or otherwise invalid. 2045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 2055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Will throw if !isAuthenticated(). 2065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 2075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {boolean} True if a new access token is needed. 2085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @private 2095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 2105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.needsNewAccessToken_ = function() { 2115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (!this.isAuthenticated()) { 2125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) throw 'Not Authenticated.'; 2135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 2145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) var access_token = this.getAccessTokenInternal_(); 2155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (!access_token['token']) { 2165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return true; 2175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 2185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (Date.now() > access_token['expiration']) { 2195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return true; 2205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 2215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return false; 2225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 2235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 2255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 2265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @private 2275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 2285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.clearAccessToken_ = function() { 2295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); 2305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 2315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 2335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Update state based on token response from the OAuth2 /token endpoint. 2345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 235a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @param {function(string):void} onOk Called with the new access token. 236a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @param {string} accessToken Access token. 237a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @param {number} expiresIn Expiration time for the access token. 2385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 239a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @private 2405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 241a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles)remoting.OAuth2.prototype.onAccessToken_ = 242a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) function(onOk, accessToken, expiresIn) { 243a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.setAccessToken_(accessToken, expiresIn); 244a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) onOk(accessToken); 2455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 2465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 248a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * Update state based on token response from the OAuth2 /token endpoint. 2495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 250a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @param {function():void} onOk Called after the new tokens are stored. 251a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @param {string} refreshToken Refresh token. 252a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @param {string} accessToken Access token. 253a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @param {number} expiresIn Expiration time for the access token. 2545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 2555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @private 2565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 257a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles)remoting.OAuth2.prototype.onTokens_ = 258a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) function(onOk, refreshToken, accessToken, expiresIn) { 259a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.setAccessToken_(accessToken, expiresIn); 260a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.setRefreshToken_(refreshToken); 261a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) onOk(); 2625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 2635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 2655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Redirect page to get a new OAuth2 Refresh Token. 2665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 2675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 2685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 2695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.doAuthRedirect = function() { 270762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) /** @type {remoting.OAuth2} */ 271762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) var that = this; 272c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) var xsrf_token = remoting.generateXsrfToken(); 2732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) window.localStorage.setItem(this.KEY_XSRF_TOKEN_, xsrf_token); 2742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' + 2755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) remoting.xhr.urlencodeParamHash({ 2762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 'client_id': this.getClientId_(), 2772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 'redirect_uri': this.getRedirectUri_(), 2785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 'scope': this.SCOPE_, 2792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 'state': xsrf_token, 2805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 'response_type': 'code', 2815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 'access_type': 'offline', 2825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 'approval_prompt': 'force' 2835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) }); 284762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) 285762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) /** 286762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) * Processes the results of the oauth flow. 287762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) * 288762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) * @param {Object.<string, string>} message Dictionary containing the parsed 289762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) * OAuth redirect URL parameters. 290762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) */ 291762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) function oauth2MessageListener(message) { 292762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) if ('code' in message && 'state' in message) { 293762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) var onDone = function() { 294762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) window.location.reload(); 295762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) }; 296762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) that.exchangeCodeForToken( 297762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) message['code'], message['state'], onDone); 298762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) } else { 299762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) if ('error' in message) { 300762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) console.error( 301762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) 'Could not obtain authorization code: ' + message['error']); 302762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) } else { 303762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) // We intentionally don't log the response - since we don't understand 304762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) // it, we can't tell if it has sensitive data. 305762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) console.error('Invalid oauth2 response.'); 306762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) } 307762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) } 308762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) chrome.extension.onMessage.removeListener(oauth2MessageListener); 309762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) } 310762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) chrome.extension.onMessage.addListener(oauth2MessageListener); 311762b2f2129a215bc851e73887244c3a82a892731Torne (Richard Coles) window.open(GET_CODE_URL, '_blank', 'location=yes,toolbar=no,menubar=no'); 3125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 3135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 3145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 3155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Asynchronously exchanges an authorization code for a refresh token. 3165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 317a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @param {string} code The OAuth2 authorization code. 3182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) * @param {string} state The state parameter received from the OAuth redirect. 319a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) * @param {function():void} onDone Callback to invoke on completion. 3205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 3215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 3222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)remoting.OAuth2.prototype.exchangeCodeForToken = function(code, state, onDone) { 3232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) var xsrf_token = window.localStorage.getItem(this.KEY_XSRF_TOKEN_); 3242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) window.localStorage.removeItem(this.KEY_XSRF_TOKEN_); 3252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (xsrf_token == undefined || state != xsrf_token) { 3262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Invalid XSRF token, or unexpected OAuth2 redirect. Abort. 327a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) onDone(); 3282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 329a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) /** @param {remoting.Error} error */ 330a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) var onError = function(error) { 331a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) console.error('Unable to exchange code for token: ', error); 3325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) }; 3335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 334a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) remoting.OAuth2Api.exchangeCodeForTokens( 335a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.onTokens_.bind(this, onDone), onError, 336a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.getClientId_(), this.getClientSecret_(), code, 337a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.getRedirectUri_()); 3385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 3395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 3405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 3415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Revokes a refresh or an access token. 3425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 3435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @param {string?} token An access or refresh token. 3445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 3455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @private 3465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 3475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.revokeToken_ = function(token) { 3485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (!token || (token.length == 0)) { 3495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return; 3505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 3515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 352a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) remoting.OAuth2Api.revokeToken(function() {}, function() {}, token); 3535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 3545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 3555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 3565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Call a function with an access token, refreshing it first if necessary. 3575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * The access token will remain valid for at least 2 minutes. 3585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 3595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @param {function(string):void} onOk Function to invoke with access token if 3605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * an access token was successfully retrieved. 3615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @param {function(remoting.Error):void} onError Function to invoke with an 3625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * error code on failure. 3635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 3645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 3655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.callWithToken = function(onOk, onError) { 366a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) var refreshToken = this.getRefreshToken_(); 367a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) if (refreshToken) { 3685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (this.needsNewAccessToken_()) { 369a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) remoting.OAuth2Api.refreshAccessToken( 370a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.onAccessToken_.bind(this, onOk), onError, 371a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.getClientId_(), this.getClientSecret_(), 372a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) refreshToken); 3735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } else { 3745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) onOk(this.getAccessTokenInternal_()['token']); 3755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 3765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } else { 3775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) onError(remoting.Error.NOT_AUTHENTICATED); 3785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 3795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 3805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 3815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 3825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Get the user's email address. 3835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 3845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @param {function(string):void} onOk Callback invoked when the email 3855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * address is available. 3865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @param {function(remoting.Error):void} onError Callback invoked if an 3875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * error occurs. 3885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {void} Nothing. 3895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 3905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.getEmail = function(onOk, onError) { 3915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) var cached = window.localStorage.getItem(this.KEY_EMAIL_); 3925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (typeof cached == 'string') { 3935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) onOk(cached); 3945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return; 3955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 3965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) /** @type {remoting.OAuth2} */ 3975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) var that = this; 398a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) /** @param {string} email */ 399a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) var onResponse = function(email) { 400a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) window.localStorage.setItem(that.KEY_EMAIL_, email); 401a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) onOk(email); 4025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) }; 4035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 404a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) this.callWithToken( 405a36e5920737c6adbddd3e43b760e5de8431db6e0Torne (Richard Coles) remoting.OAuth2Api.getEmail.bind(null, onResponse, onError), onError); 4065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 4075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 4085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/** 4095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * If the user's email address is cached, return it, otherwise return null. 4105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * 4115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * @return {?string} The email address, if it has been cached by a previous call 4125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * to getEmail, otherwise null. 4135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */ 4145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)remoting.OAuth2.prototype.getCachedEmail = function() { 4155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) var value = window.localStorage.getItem(this.KEY_EMAIL_); 4165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (typeof value == 'string') { 4175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return value; 4185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 4195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return null; 4205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}; 421