1/** 2 * Copyright (c) 2010 The Chromium Authors. All rights reserved. Use of this 3 * source code is governed by a BSD-style license that can be found in the 4 * LICENSE file. 5 */ 6 7/** 8 * Constructor - no need to invoke directly, call initBackgroundPage instead. 9 * @constructor 10 * @param {String} url_request_token The OAuth request token URL. 11 * @param {String} url_auth_token The OAuth authorize token URL. 12 * @param {String} url_access_token The OAuth access token URL. 13 * @param {String} consumer_key The OAuth consumer key. 14 * @param {String} consumer_secret The OAuth consumer secret. 15 * @param {String} oauth_scope The OAuth scope parameter. 16 * @param {Object} opt_args Optional arguments. Recognized parameters: 17 * "app_name" {String} Name of the current application 18 * "callback_page" {String} If you renamed chrome_ex_oauth.html, the name 19 * this file was renamed to. 20 */ 21function ChromeExOAuth(url_request_token, url_auth_token, url_access_token, 22 consumer_key, consumer_secret, oauth_scope, opt_args) { 23 this.url_request_token = url_request_token; 24 this.url_auth_token = url_auth_token; 25 this.url_access_token = url_access_token; 26 this.consumer_key = consumer_key; 27 this.consumer_secret = consumer_secret; 28 this.oauth_scope = oauth_scope; 29 this.app_name = opt_args && opt_args['app_name'] || 30 "ChromeExOAuth Library"; 31 this.key_token = "oauth_token"; 32 this.key_token_secret = "oauth_token_secret"; 33 this.callback_page = opt_args && opt_args['callback_page'] || 34 "chrome_ex_oauth.html"; 35 this.auth_params = {}; 36 if (opt_args && opt_args['auth_params']) { 37 for (key in opt_args['auth_params']) { 38 if (opt_args['auth_params'].hasOwnProperty(key)) { 39 this.auth_params[key] = opt_args['auth_params'][key]; 40 } 41 } 42 } 43}; 44 45/******************************************************************************* 46 * PUBLIC API METHODS 47 * Call these from your background page. 48 ******************************************************************************/ 49 50/** 51 * Initializes the OAuth helper from the background page. You must call this 52 * before attempting to make any OAuth calls. 53 * @param {Object} oauth_config Configuration parameters in a JavaScript object. 54 * The following parameters are recognized: 55 * "request_url" {String} OAuth request token URL. 56 * "authorize_url" {String} OAuth authorize token URL. 57 * "access_url" {String} OAuth access token URL. 58 * "consumer_key" {String} OAuth consumer key. 59 * "consumer_secret" {String} OAuth consumer secret. 60 * "scope" {String} OAuth access scope. 61 * "app_name" {String} Application name. 62 * "auth_params" {Object} Additional parameters to pass to the 63 * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': 64 * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth 65 * @return {ChromeExOAuth} An initialized ChromeExOAuth object. 66 */ 67ChromeExOAuth.initBackgroundPage = function(oauth_config) { 68 window.chromeExOAuthConfig = oauth_config; 69 window.chromeExOAuth = ChromeExOAuth.fromConfig(oauth_config); 70 window.chromeExOAuthRedirectStarted = false; 71 window.chromeExOAuthRequestingAccess = false; 72 73 var url_match = chrome.extension.getURL(window.chromeExOAuth.callback_page); 74 var tabs = {}; 75 chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { 76 if (changeInfo.url && 77 changeInfo.url.substr(0, url_match.length) === url_match && 78 changeInfo.url != tabs[tabId] && 79 window.chromeExOAuthRequestingAccess == false) { 80 chrome.tabs.create({ 'url' : changeInfo.url }, function(tab) { 81 tabs[tab.id] = tab.url; 82 chrome.tabs.remove(tabId); 83 }); 84 } 85 }); 86 87 return window.chromeExOAuth; 88}; 89 90/** 91 * Authorizes the current user with the configued API. You must call this 92 * before calling sendSignedRequest. 93 * @param {Function} callback A function to call once an access token has 94 * been obtained. This callback will be passed the following arguments: 95 * token {String} The OAuth access token. 96 * secret {String} The OAuth access token secret. 97 */ 98ChromeExOAuth.prototype.authorize = function(callback) { 99 if (this.hasToken()) { 100 callback(this.getToken(), this.getTokenSecret()); 101 } else { 102 window.chromeExOAuthOnAuthorize = function(token, secret) { 103 callback(token, secret); 104 }; 105 chrome.tabs.create({ 'url' :chrome.extension.getURL(this.callback_page) }); 106 } 107}; 108 109/** 110 * Clears any OAuth tokens stored for this configuration. Effectively a 111 * "logout" of the configured OAuth API. 112 */ 113ChromeExOAuth.prototype.clearTokens = function() { 114 delete localStorage[this.key_token + encodeURI(this.oauth_scope)]; 115 delete localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; 116}; 117 118/** 119 * Returns whether a token is currently stored for this configuration. 120 * Effectively a check to see whether the current user is "logged in" to 121 * the configured OAuth API. 122 * @return {Boolean} True if an access token exists. 123 */ 124ChromeExOAuth.prototype.hasToken = function() { 125 return !!this.getToken(); 126}; 127 128/** 129 * Makes an OAuth-signed HTTP request with the currently authorized tokens. 130 * @param {String} url The URL to send the request to. Querystring parameters 131 * should be omitted. 132 * @param {Function} callback A function to be called once the request is 133 * completed. This callback will be passed the following arguments: 134 * responseText {String} The text response. 135 * xhr {XMLHttpRequest} The XMLHttpRequest object which was used to 136 * send the request. Useful if you need to check response status 137 * code, etc. 138 * @param {Object} opt_params Additional parameters to configure the request. 139 * The following parameters are accepted: 140 * "method" {String} The HTTP method to use. Defaults to "GET". 141 * "body" {String} A request body to send. Defaults to null. 142 * "parameters" {Object} Query parameters to include in the request. 143 * "headers" {Object} Additional headers to include in the request. 144 */ 145ChromeExOAuth.prototype.sendSignedRequest = function(url, callback, 146 opt_params) { 147 var method = opt_params && opt_params['method'] || 'GET'; 148 var body = opt_params && opt_params['body'] || null; 149 var params = opt_params && opt_params['parameters'] || {}; 150 var headers = opt_params && opt_params['headers'] || {}; 151 152 var signedUrl = this.signURL(url, method, params); 153 154 ChromeExOAuth.sendRequest(method, signedUrl, headers, body, function (xhr) { 155 if (xhr.readyState == 4) { 156 callback(xhr.responseText, xhr); 157 } 158 }); 159}; 160 161/** 162 * Adds the required OAuth parameters to the given url and returns the 163 * result. Useful if you need a signed url but don't want to make an XHR 164 * request. 165 * @param {String} method The http method to use. 166 * @param {String} url The base url of the resource you are querying. 167 * @param {Object} opt_params Query parameters to include in the request. 168 * @return {String} The base url plus any query params plus any OAuth params. 169 */ 170ChromeExOAuth.prototype.signURL = function(url, method, opt_params) { 171 var token = this.getToken(); 172 var secret = this.getTokenSecret(); 173 if (!token || !secret) { 174 throw new Error("No oauth token or token secret"); 175 } 176 177 var params = opt_params || {}; 178 179 var result = OAuthSimple().sign({ 180 action : method, 181 path : url, 182 parameters : params, 183 signatures: { 184 consumer_key : this.consumer_key, 185 shared_secret : this.consumer_secret, 186 oauth_secret : secret, 187 oauth_token: token 188 } 189 }); 190 191 return result.signed_url; 192}; 193 194/** 195 * Generates the Authorization header based on the oauth parameters. 196 * @param {String} url The base url of the resource you are querying. 197 * @param {Object} opt_params Query parameters to include in the request. 198 * @return {String} An Authorization header containing the oauth_* params. 199 */ 200ChromeExOAuth.prototype.getAuthorizationHeader = function(url, method, 201 opt_params) { 202 var token = this.getToken(); 203 var secret = this.getTokenSecret(); 204 if (!token || !secret) { 205 throw new Error("No oauth token or token secret"); 206 } 207 208 var params = opt_params || {}; 209 210 return OAuthSimple().getHeaderString({ 211 action: method, 212 path : url, 213 parameters : params, 214 signatures: { 215 consumer_key : this.consumer_key, 216 shared_secret : this.consumer_secret, 217 oauth_secret : secret, 218 oauth_token: token 219 } 220 }); 221}; 222 223/******************************************************************************* 224 * PRIVATE API METHODS 225 * Used by the library. There should be no need to call these methods directly. 226 ******************************************************************************/ 227 228/** 229 * Creates a new ChromeExOAuth object from the supplied configuration object. 230 * @param {Object} oauth_config Configuration parameters in a JavaScript object. 231 * The following parameters are recognized: 232 * "request_url" {String} OAuth request token URL. 233 * "authorize_url" {String} OAuth authorize token URL. 234 * "access_url" {String} OAuth access token URL. 235 * "consumer_key" {String} OAuth consumer key. 236 * "consumer_secret" {String} OAuth consumer secret. 237 * "scope" {String} OAuth access scope. 238 * "app_name" {String} Application name. 239 * "auth_params" {Object} Additional parameters to pass to the 240 * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': 241 * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth 242 * @return {ChromeExOAuth} An initialized ChromeExOAuth object. 243 */ 244ChromeExOAuth.fromConfig = function(oauth_config) { 245 return new ChromeExOAuth( 246 oauth_config['request_url'], 247 oauth_config['authorize_url'], 248 oauth_config['access_url'], 249 oauth_config['consumer_key'], 250 oauth_config['consumer_secret'], 251 oauth_config['scope'], 252 { 253 'app_name' : oauth_config['app_name'], 254 'auth_params' : oauth_config['auth_params'] 255 } 256 ); 257}; 258 259/** 260 * Initializes chrome_ex_oauth.html and redirects the page if needed to start 261 * the OAuth flow. Once an access token is obtained, this function closes 262 * chrome_ex_oauth.html. 263 */ 264ChromeExOAuth.initCallbackPage = function() { 265 var background_page = chrome.extension.getBackgroundPage(); 266 var oauth_config = background_page.chromeExOAuthConfig; 267 var oauth = ChromeExOAuth.fromConfig(oauth_config); 268 background_page.chromeExOAuthRedirectStarted = true; 269 oauth.initOAuthFlow(function (token, secret) { 270 background_page.chromeExOAuthOnAuthorize(token, secret); 271 background_page.chromeExOAuthRedirectStarted = false; 272 chrome.tabs.getSelected(null, function (tab) { 273 chrome.tabs.remove(tab.id); 274 }); 275 }); 276}; 277 278/** 279 * Sends an HTTP request. Convenience wrapper for XMLHttpRequest calls. 280 * @param {String} method The HTTP method to use. 281 * @param {String} url The URL to send the request to. 282 * @param {Object} headers Optional request headers in key/value format. 283 * @param {String} body Optional body content. 284 * @param {Function} callback Function to call when the XMLHttpRequest's 285 * ready state changes. See documentation for XMLHttpRequest's 286 * onreadystatechange handler for more information. 287 */ 288ChromeExOAuth.sendRequest = function(method, url, headers, body, callback) { 289 var xhr = new XMLHttpRequest(); 290 xhr.onreadystatechange = function(data) { 291 callback(xhr, data); 292 } 293 xhr.open(method, url, true); 294 if (headers) { 295 for (var header in headers) { 296 if (headers.hasOwnProperty(header)) { 297 xhr.setRequestHeader(header, headers[header]); 298 } 299 } 300 } 301 xhr.send(body); 302}; 303 304/** 305 * Decodes a URL-encoded string into key/value pairs. 306 * @param {String} encoded An URL-encoded string. 307 * @return {Object} An object representing the decoded key/value pairs found 308 * in the encoded string. 309 */ 310ChromeExOAuth.formDecode = function(encoded) { 311 var params = encoded.split("&"); 312 var decoded = {}; 313 for (var i = 0, param; param = params[i]; i++) { 314 var keyval = param.split("="); 315 if (keyval.length == 2) { 316 var key = ChromeExOAuth.fromRfc3986(keyval[0]); 317 var val = ChromeExOAuth.fromRfc3986(keyval[1]); 318 decoded[key] = val; 319 } 320 } 321 return decoded; 322}; 323 324/** 325 * Returns the current window's querystring decoded into key/value pairs. 326 * @return {Object} A object representing any key/value pairs found in the 327 * current window's querystring. 328 */ 329ChromeExOAuth.getQueryStringParams = function() { 330 var urlparts = window.location.href.split("?"); 331 if (urlparts.length >= 2) { 332 var querystring = urlparts.slice(1).join("?"); 333 return ChromeExOAuth.formDecode(querystring); 334 } 335 return {}; 336}; 337 338/** 339 * Binds a function call to a specific object. This function will also take 340 * a variable number of additional arguments which will be prepended to the 341 * arguments passed to the bound function when it is called. 342 * @param {Function} func The function to bind. 343 * @param {Object} obj The object to bind to the function's "this". 344 * @return {Function} A closure that will call the bound function. 345 */ 346ChromeExOAuth.bind = function(func, obj) { 347 var newargs = Array.prototype.slice.call(arguments).slice(2); 348 return function() { 349 var combinedargs = newargs.concat(Array.prototype.slice.call(arguments)); 350 func.apply(obj, combinedargs); 351 }; 352}; 353 354/** 355 * Encodes a value according to the RFC3986 specification. 356 * @param {String} val The string to encode. 357 */ 358ChromeExOAuth.toRfc3986 = function(val){ 359 return encodeURIComponent(val) 360 .replace(/\!/g, "%21") 361 .replace(/\*/g, "%2A") 362 .replace(/'/g, "%27") 363 .replace(/\(/g, "%28") 364 .replace(/\)/g, "%29"); 365}; 366 367/** 368 * Decodes a string that has been encoded according to RFC3986. 369 * @param {String} val The string to decode. 370 */ 371ChromeExOAuth.fromRfc3986 = function(val){ 372 var tmp = val 373 .replace(/%21/g, "!") 374 .replace(/%2A/g, "*") 375 .replace(/%27/g, "'") 376 .replace(/%28/g, "(") 377 .replace(/%29/g, ")"); 378 return decodeURIComponent(tmp); 379}; 380 381/** 382 * Adds a key/value parameter to the supplied URL. 383 * @param {String} url An URL which may or may not contain querystring values. 384 * @param {String} key A key 385 * @param {String} value A value 386 * @return {String} The URL with URL-encoded versions of the key and value 387 * appended, prefixing them with "&" or "?" as needed. 388 */ 389ChromeExOAuth.addURLParam = function(url, key, value) { 390 var sep = (url.indexOf('?') >= 0) ? "&" : "?"; 391 return url + sep + 392 ChromeExOAuth.toRfc3986(key) + "=" + ChromeExOAuth.toRfc3986(value); 393}; 394 395/** 396 * Stores an OAuth token for the configured scope. 397 * @param {String} token The token to store. 398 */ 399ChromeExOAuth.prototype.setToken = function(token) { 400 localStorage[this.key_token + encodeURI(this.oauth_scope)] = token; 401}; 402 403/** 404 * Retrieves any stored token for the configured scope. 405 * @return {String} The stored token. 406 */ 407ChromeExOAuth.prototype.getToken = function() { 408 return localStorage[this.key_token + encodeURI(this.oauth_scope)]; 409}; 410 411/** 412 * Stores an OAuth token secret for the configured scope. 413 * @param {String} secret The secret to store. 414 */ 415ChromeExOAuth.prototype.setTokenSecret = function(secret) { 416 localStorage[this.key_token_secret + encodeURI(this.oauth_scope)] = secret; 417}; 418 419/** 420 * Retrieves any stored secret for the configured scope. 421 * @return {String} The stored secret. 422 */ 423ChromeExOAuth.prototype.getTokenSecret = function() { 424 return localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; 425}; 426 427/** 428 * Starts an OAuth authorization flow for the current page. If a token exists, 429 * no redirect is needed and the supplied callback is called immediately. 430 * If this method detects that a redirect has finished, it grabs the 431 * appropriate OAuth parameters from the URL and attempts to retrieve an 432 * access token. If no token exists and no redirect has happened, then 433 * an access token is requested and the page is ultimately redirected. 434 * @param {Function} callback The function to call once the flow has finished. 435 * This callback will be passed the following arguments: 436 * token {String} The OAuth access token. 437 * secret {String} The OAuth access token secret. 438 */ 439ChromeExOAuth.prototype.initOAuthFlow = function(callback) { 440 if (!this.hasToken()) { 441 var params = ChromeExOAuth.getQueryStringParams(); 442 if (params['chromeexoauthcallback'] == 'true') { 443 var oauth_token = params['oauth_token']; 444 var oauth_verifier = params['oauth_verifier'] 445 this.getAccessToken(oauth_token, oauth_verifier, callback); 446 } else { 447 var request_params = { 448 'url_callback_param' : 'chromeexoauthcallback' 449 } 450 this.getRequestToken(function(url) { 451 window.location.href = url; 452 }, request_params); 453 } 454 } else { 455 callback(this.getToken(), this.getTokenSecret()); 456 } 457}; 458 459/** 460 * Requests an OAuth request token. 461 * @param {Function} callback Function to call once the authorize URL is 462 * calculated. This callback will be passed the following arguments: 463 * url {String} The URL the user must be redirected to in order to 464 * approve the token. 465 * @param {Object} opt_args Optional arguments. The following parameters 466 * are accepted: 467 * "url_callback" {String} The URL the OAuth provider will redirect to. 468 * "url_callback_param" {String} A parameter to include in the callback 469 * URL in order to indicate to this library that a redirect has 470 * taken place. 471 */ 472ChromeExOAuth.prototype.getRequestToken = function(callback, opt_args) { 473 if (typeof callback !== "function") { 474 throw new Error("Specified callback must be a function."); 475 } 476 var url = opt_args && opt_args['url_callback'] || 477 window && window.top && window.top.location && 478 window.top.location.href; 479 480 var url_param = opt_args && opt_args['url_callback_param'] || 481 "chromeexoauthcallback"; 482 var url_callback = ChromeExOAuth.addURLParam(url, url_param, "true"); 483 484 var result = OAuthSimple().sign({ 485 path : this.url_request_token, 486 parameters: { 487 "xoauth_displayname" : this.app_name, 488 "scope" : this.oauth_scope, 489 "oauth_callback" : url_callback 490 }, 491 signatures: { 492 consumer_key : this.consumer_key, 493 shared_secret : this.consumer_secret 494 } 495 }); 496 var onToken = ChromeExOAuth.bind(this.onRequestToken, this, callback); 497 ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); 498}; 499 500/** 501 * Called when a request token has been returned. Stores the request token 502 * secret for later use and sends the authorization url to the supplied 503 * callback (for redirecting the user). 504 * @param {Function} callback Function to call once the authorize URL is 505 * calculated. This callback will be passed the following arguments: 506 * url {String} The URL the user must be redirected to in order to 507 * approve the token. 508 * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the 509 * request token. 510 */ 511ChromeExOAuth.prototype.onRequestToken = function(callback, xhr) { 512 if (xhr.readyState == 4) { 513 if (xhr.status == 200) { 514 var params = ChromeExOAuth.formDecode(xhr.responseText); 515 var token = params['oauth_token']; 516 this.setTokenSecret(params['oauth_token_secret']); 517 var url = ChromeExOAuth.addURLParam(this.url_auth_token, 518 "oauth_token", token); 519 for (var key in this.auth_params) { 520 if (this.auth_params.hasOwnProperty(key)) { 521 url = ChromeExOAuth.addURLParam(url, key, this.auth_params[key]); 522 } 523 } 524 callback(url); 525 } else { 526 throw new Error("Fetching request token failed. Status " + xhr.status); 527 } 528 } 529}; 530 531/** 532 * Requests an OAuth access token. 533 * @param {String} oauth_token The OAuth request token. 534 * @param {String} oauth_verifier The OAuth token verifier. 535 * @param {Function} callback The function to call once the token is obtained. 536 * This callback will be passed the following arguments: 537 * token {String} The OAuth access token. 538 * secret {String} The OAuth access token secret. 539 */ 540ChromeExOAuth.prototype.getAccessToken = function(oauth_token, oauth_verifier, 541 callback) { 542 if (typeof callback !== "function") { 543 throw new Error("Specified callback must be a function."); 544 } 545 var bg = chrome.extension.getBackgroundPage(); 546 if (bg.chromeExOAuthRequestingAccess == false) { 547 bg.chromeExOAuthRequestingAccess = true; 548 549 var result = OAuthSimple().sign({ 550 path : this.url_access_token, 551 parameters: { 552 "oauth_token" : oauth_token, 553 "oauth_verifier" : oauth_verifier 554 }, 555 signatures: { 556 consumer_key : this.consumer_key, 557 shared_secret : this.consumer_secret, 558 oauth_secret : this.getTokenSecret(this.oauth_scope) 559 } 560 }); 561 562 var onToken = ChromeExOAuth.bind(this.onAccessToken, this, callback); 563 ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); 564 } 565}; 566 567/** 568 * Called when an access token has been returned. Stores the access token and 569 * access token secret for later use and sends them to the supplied callback. 570 * @param {Function} callback The function to call once the token is obtained. 571 * This callback will be passed the following arguments: 572 * token {String} The OAuth access token. 573 * secret {String} The OAuth access token secret. 574 * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the 575 * access token. 576 */ 577ChromeExOAuth.prototype.onAccessToken = function(callback, xhr) { 578 if (xhr.readyState == 4) { 579 var bg = chrome.extension.getBackgroundPage(); 580 if (xhr.status == 200) { 581 var params = ChromeExOAuth.formDecode(xhr.responseText); 582 var token = params["oauth_token"]; 583 var secret = params["oauth_token_secret"]; 584 this.setToken(token); 585 this.setTokenSecret(secret); 586 bg.chromeExOAuthRequestingAccess = false; 587 callback(token, secret); 588 } else { 589 bg.chromeExOAuthRequestingAccess = false; 590 throw new Error("Fetching access token failed with status " + xhr.status); 591 } 592 } 593};