1 // Copyright (c) 2010 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.
6 * Constructor - no need to invoke directly, call initBackgroundPage instead.
8 * @param {String} url_request_token The OAuth request token URL.
9 * @param {String} url_auth_token The OAuth authorize token URL.
10 * @param {String} url_access_token The OAuth access token URL.
11 * @param {String} consumer_key The OAuth consumer key.
12 * @param {String} consumer_secret The OAuth consumer secret.
13 * @param {String} oauth_scope The OAuth scope parameter.
14 * @param {Object} opt_args Optional arguments. Recognized parameters:
15 * "app_name" {String} Name of the current application
16 * "callback_page" {String} If you renamed chrome_ex_oauth.html, the name
17 * this file was renamed to.
19 function ChromeExOAuth(url_request_token, url_auth_token, url_access_token,
20 consumer_key, consumer_secret, oauth_scope, opt_args) {
21 this.url_request_token = url_request_token;
22 this.url_auth_token = url_auth_token;
23 this.url_access_token = url_access_token;
24 this.consumer_key = consumer_key;
25 this.consumer_secret = consumer_secret;
26 this.oauth_scope = oauth_scope;
27 this.app_name = opt_args && opt_args['app_name'] ||
28 "ChromeExOAuth Library";
29 this.key_token = "oauth_token";
30 this.key_token_secret = "oauth_token_secret";
31 this.callback_page = opt_args && opt_args['callback_page'] ||
32 "chrome_ex_oauth.html";
33 this.auth_params = {};
34 if (opt_args && opt_args['auth_params']) {
35 for (key in opt_args['auth_params']) {
36 if (opt_args['auth_params'].hasOwnProperty(key)) {
37 this.auth_params[key] = opt_args['auth_params'][key];
43 /*******************************************************************************
45 * Call these from your background page.
46 ******************************************************************************/
49 * Initializes the OAuth helper from the background page. You must call this
50 * before attempting to make any OAuth calls.
51 * @param {Object} oauth_config Configuration parameters in a JavaScript object.
52 * The following parameters are recognized:
53 * "request_url" {String} OAuth request token URL.
54 * "authorize_url" {String} OAuth authorize token URL.
55 * "access_url" {String} OAuth access token URL.
56 * "consumer_key" {String} OAuth consumer key.
57 * "consumer_secret" {String} OAuth consumer secret.
58 * "scope" {String} OAuth access scope.
59 * "app_name" {String} Application name.
60 * "auth_params" {Object} Additional parameters to pass to the
61 * Authorization token URL. For an example, 'hd', 'hl', 'btmpl':
62 * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth
63 * @return {ChromeExOAuth} An initialized ChromeExOAuth object.
65 ChromeExOAuth.initBackgroundPage = function(oauth_config) {
66 window.chromeExOAuthConfig = oauth_config;
67 window.chromeExOAuth = ChromeExOAuth.fromConfig(oauth_config);
68 window.chromeExOAuthRedirectStarted = false;
69 window.chromeExOAuthRequestingAccess = false;
71 var url_match = chrome.extension.getURL(window.chromeExOAuth.callback_page);
73 chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
75 changeInfo.url.substr(0, url_match.length) === url_match &&
76 changeInfo.url != tabs[tabId] &&
77 window.chromeExOAuthRequestingAccess == false) {
78 chrome.tabs.create({ 'url' : changeInfo.url }, function(tab) {
79 tabs[tab.id] = tab.url;
80 chrome.tabs.remove(tabId);
85 return window.chromeExOAuth;
89 * Authorizes the current user with the configued API. You must call this
90 * before calling sendSignedRequest.
91 * @param {Function} callback A function to call once an access token has
92 * been obtained. This callback will be passed the following arguments:
93 * token {String} The OAuth access token.
94 * secret {String} The OAuth access token secret.
96 ChromeExOAuth.prototype.authorize = function(callback) {
97 if (this.hasToken()) {
98 callback(this.getToken(), this.getTokenSecret());
100 window.chromeExOAuthOnAuthorize = function(token, secret) {
101 callback(token, secret);
103 chrome.tabs.create({ 'url' :chrome.extension.getURL(this.callback_page) });
108 * Clears any OAuth tokens stored for this configuration. Effectively a
109 * "logout" of the configured OAuth API.
111 ChromeExOAuth.prototype.clearTokens = function() {
112 delete localStorage[this.key_token + encodeURI(this.oauth_scope)];
113 delete localStorage[this.key_token_secret + encodeURI(this.oauth_scope)];
117 * Returns whether a token is currently stored for this configuration.
118 * Effectively a check to see whether the current user is "logged in" to
119 * the configured OAuth API.
120 * @return {Boolean} True if an access token exists.
122 ChromeExOAuth.prototype.hasToken = function() {
123 return !!this.getToken();
127 * Makes an OAuth-signed HTTP request with the currently authorized tokens.
128 * @param {String} url The URL to send the request to. Querystring parameters
130 * @param {Function} callback A function to be called once the request is
131 * completed. This callback will be passed the following arguments:
132 * responseText {String} The text response.
133 * xhr {XMLHttpRequest} The XMLHttpRequest object which was used to
134 * send the request. Useful if you need to check response status
136 * @param {Object} opt_params Additional parameters to configure the request.
137 * The following parameters are accepted:
138 * "method" {String} The HTTP method to use. Defaults to "GET".
139 * "body" {String} A request body to send. Defaults to null.
140 * "parameters" {Object} Query parameters to include in the request.
141 * "headers" {Object} Additional headers to include in the request.
143 ChromeExOAuth.prototype.sendSignedRequest = function(url, callback,
145 var method = opt_params && opt_params['method'] || 'GET';
146 var body = opt_params && opt_params['body'] || null;
147 var params = opt_params && opt_params['parameters'] || {};
148 var headers = opt_params && opt_params['headers'] || {};
150 var signedUrl = this.signURL(url, method, params);
152 ChromeExOAuth.sendRequest(method, signedUrl, headers, body, function (xhr) {
153 if (xhr.readyState == 4) {
154 callback(xhr.responseText, xhr);
160 * Adds the required OAuth parameters to the given url and returns the
161 * result. Useful if you need a signed url but don't want to make an XHR
163 * @param {String} method The http method to use.
164 * @param {String} url The base url of the resource you are querying.
165 * @param {Object} opt_params Query parameters to include in the request.
166 * @return {String} The base url plus any query params plus any OAuth params.
168 ChromeExOAuth.prototype.signURL = function(url, method, opt_params) {
169 var token = this.getToken();
170 var secret = this.getTokenSecret();
171 if (!token || !secret) {
172 throw new Error("No oauth token or token secret");
175 var params = opt_params || {};
177 var result = OAuthSimple().sign({
182 consumer_key : this.consumer_key,
183 shared_secret : this.consumer_secret,
184 oauth_secret : secret,
189 return result.signed_url;
193 * Generates the Authorization header based on the oauth parameters.
194 * @param {String} url The base url of the resource you are querying.
195 * @param {Object} opt_params Query parameters to include in the request.
196 * @return {String} An Authorization header containing the oauth_* params.
198 ChromeExOAuth.prototype.getAuthorizationHeader = function(url, method,
200 var token = this.getToken();
201 var secret = this.getTokenSecret();
202 if (!token || !secret) {
203 throw new Error("No oauth token or token secret");
206 var params = opt_params || {};
208 return OAuthSimple().getHeaderString({
213 consumer_key : this.consumer_key,
214 shared_secret : this.consumer_secret,
215 oauth_secret : secret,
221 /*******************************************************************************
222 * PRIVATE API METHODS
223 * Used by the library. There should be no need to call these methods directly.
224 ******************************************************************************/
227 * Creates a new ChromeExOAuth object from the supplied configuration object.
228 * @param {Object} oauth_config Configuration parameters in a JavaScript object.
229 * The following parameters are recognized:
230 * "request_url" {String} OAuth request token URL.
231 * "authorize_url" {String} OAuth authorize token URL.
232 * "access_url" {String} OAuth access token URL.
233 * "consumer_key" {String} OAuth consumer key.
234 * "consumer_secret" {String} OAuth consumer secret.
235 * "scope" {String} OAuth access scope.
236 * "app_name" {String} Application name.
237 * "auth_params" {Object} Additional parameters to pass to the
238 * Authorization token URL. For an example, 'hd', 'hl', 'btmpl':
239 * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth
240 * @return {ChromeExOAuth} An initialized ChromeExOAuth object.
242 ChromeExOAuth.fromConfig = function(oauth_config) {
243 return new ChromeExOAuth(
244 oauth_config['request_url'],
245 oauth_config['authorize_url'],
246 oauth_config['access_url'],
247 oauth_config['consumer_key'],
248 oauth_config['consumer_secret'],
249 oauth_config['scope'],
251 'app_name' : oauth_config['app_name'],
252 'auth_params' : oauth_config['auth_params']
258 * Initializes chrome_ex_oauth.html and redirects the page if needed to start
259 * the OAuth flow. Once an access token is obtained, this function closes
260 * chrome_ex_oauth.html.
262 ChromeExOAuth.initCallbackPage = function() {
263 var background_page = chrome.extension.getBackgroundPage();
264 var oauth_config = background_page.chromeExOAuthConfig;
265 var oauth = ChromeExOAuth.fromConfig(oauth_config);
266 background_page.chromeExOAuthRedirectStarted = true;
267 oauth.initOAuthFlow(function (token, secret) {
268 background_page.chromeExOAuthOnAuthorize(token, secret);
269 background_page.chromeExOAuthRedirectStarted = false;
270 chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
271 chrome.tabs.remove(tabs[0].id);
277 * Sends an HTTP request. Convenience wrapper for XMLHttpRequest calls.
278 * @param {String} method The HTTP method to use.
279 * @param {String} url The URL to send the request to.
280 * @param {Object} headers Optional request headers in key/value format.
281 * @param {String} body Optional body content.
282 * @param {Function} callback Function to call when the XMLHttpRequest's
283 * ready state changes. See documentation for XMLHttpRequest's
284 * onreadystatechange handler for more information.
286 ChromeExOAuth.sendRequest = function(method, url, headers, body, callback) {
287 var xhr = new XMLHttpRequest();
288 xhr.onreadystatechange = function(data) {
291 xhr.open(method, url, true);
293 for (var header in headers) {
294 if (headers.hasOwnProperty(header)) {
295 xhr.setRequestHeader(header, headers[header]);
303 * Decodes a URL-encoded string into key/value pairs.
304 * @param {String} encoded An URL-encoded string.
305 * @return {Object} An object representing the decoded key/value pairs found
306 * in the encoded string.
308 ChromeExOAuth.formDecode = function(encoded) {
309 var params = encoded.split("&");
311 for (var i = 0, param; param = params[i]; i++) {
312 var keyval = param.split("=");
313 if (keyval.length == 2) {
314 var key = ChromeExOAuth.fromRfc3986(keyval[0]);
315 var val = ChromeExOAuth.fromRfc3986(keyval[1]);
323 * Returns the current window's querystring decoded into key/value pairs.
324 * @return {Object} A object representing any key/value pairs found in the
325 * current window's querystring.
327 ChromeExOAuth.getQueryStringParams = function() {
328 var urlparts = window.location.href.split("?");
329 if (urlparts.length >= 2) {
330 var querystring = urlparts.slice(1).join("?");
331 return ChromeExOAuth.formDecode(querystring);
337 * Binds a function call to a specific object. This function will also take
338 * a variable number of additional arguments which will be prepended to the
339 * arguments passed to the bound function when it is called.
340 * @param {Function} func The function to bind.
341 * @param {Object} obj The object to bind to the function's "this".
342 * @return {Function} A closure that will call the bound function.
344 ChromeExOAuth.bind = function(func, obj) {
345 var newargs = Array.prototype.slice.call(arguments).slice(2);
347 var combinedargs = newargs.concat(Array.prototype.slice.call(arguments));
348 func.apply(obj, combinedargs);
353 * Encodes a value according to the RFC3986 specification.
354 * @param {String} val The string to encode.
356 ChromeExOAuth.toRfc3986 = function(val){
357 return encodeURIComponent(val)
358 .replace(/\!/g, "%21")
359 .replace(/\*/g, "%2A")
360 .replace(/'/g, "%27")
361 .replace(/\(/g, "%28")
362 .replace(/\)/g, "%29");
366 * Decodes a string that has been encoded according to RFC3986.
367 * @param {String} val The string to decode.
369 ChromeExOAuth.fromRfc3986 = function(val){
371 .replace(/%21/g, "!")
372 .replace(/%2A/g, "*")
373 .replace(/%27/g, "'")
374 .replace(/%28/g, "(")
375 .replace(/%29/g, ")");
376 return decodeURIComponent(tmp);
380 * Adds a key/value parameter to the supplied URL.
381 * @param {String} url An URL which may or may not contain querystring values.
382 * @param {String} key A key
383 * @param {String} value A value
384 * @return {String} The URL with URL-encoded versions of the key and value
385 * appended, prefixing them with "&" or "?" as needed.
387 ChromeExOAuth.addURLParam = function(url, key, value) {
388 var sep = (url.indexOf('?') >= 0) ? "&" : "?";
390 ChromeExOAuth.toRfc3986(key) + "=" + ChromeExOAuth.toRfc3986(value);
394 * Stores an OAuth token for the configured scope.
395 * @param {String} token The token to store.
397 ChromeExOAuth.prototype.setToken = function(token) {
398 localStorage[this.key_token + encodeURI(this.oauth_scope)] = token;
402 * Retrieves any stored token for the configured scope.
403 * @return {String} The stored token.
405 ChromeExOAuth.prototype.getToken = function() {
406 return localStorage[this.key_token + encodeURI(this.oauth_scope)];
410 * Stores an OAuth token secret for the configured scope.
411 * @param {String} secret The secret to store.
413 ChromeExOAuth.prototype.setTokenSecret = function(secret) {
414 localStorage[this.key_token_secret + encodeURI(this.oauth_scope)] = secret;
418 * Retrieves any stored secret for the configured scope.
419 * @return {String} The stored secret.
421 ChromeExOAuth.prototype.getTokenSecret = function() {
422 return localStorage[this.key_token_secret + encodeURI(this.oauth_scope)];
426 * Starts an OAuth authorization flow for the current page. If a token exists,
427 * no redirect is needed and the supplied callback is called immediately.
428 * If this method detects that a redirect has finished, it grabs the
429 * appropriate OAuth parameters from the URL and attempts to retrieve an
430 * access token. If no token exists and no redirect has happened, then
431 * an access token is requested and the page is ultimately redirected.
432 * @param {Function} callback The function to call once the flow has finished.
433 * This callback will be passed the following arguments:
434 * token {String} The OAuth access token.
435 * secret {String} The OAuth access token secret.
437 ChromeExOAuth.prototype.initOAuthFlow = function(callback) {
438 if (!this.hasToken()) {
439 var params = ChromeExOAuth.getQueryStringParams();
440 if (params['chromeexoauthcallback'] == 'true') {
441 var oauth_token = params['oauth_token'];
442 var oauth_verifier = params['oauth_verifier']
443 this.getAccessToken(oauth_token, oauth_verifier, callback);
445 var request_params = {
446 'url_callback_param' : 'chromeexoauthcallback'
448 this.getRequestToken(function(url) {
449 window.location.href = url;
453 callback(this.getToken(), this.getTokenSecret());
458 * Requests an OAuth request token.
459 * @param {Function} callback Function to call once the authorize URL is
460 * calculated. This callback will be passed the following arguments:
461 * url {String} The URL the user must be redirected to in order to
463 * @param {Object} opt_args Optional arguments. The following parameters
465 * "url_callback" {String} The URL the OAuth provider will redirect to.
466 * "url_callback_param" {String} A parameter to include in the callback
467 * URL in order to indicate to this library that a redirect has
470 ChromeExOAuth.prototype.getRequestToken = function(callback, opt_args) {
471 if (typeof callback !== "function") {
472 throw new Error("Specified callback must be a function.");
474 var url = opt_args && opt_args['url_callback'] ||
475 window && window.top && window.top.location &&
476 window.top.location.href;
478 var url_param = opt_args && opt_args['url_callback_param'] ||
479 "chromeexoauthcallback";
480 var url_callback = ChromeExOAuth.addURLParam(url, url_param, "true");
482 var result = OAuthSimple().sign({
483 path : this.url_request_token,
485 "xoauth_displayname" : this.app_name,
486 "scope" : this.oauth_scope,
487 "oauth_callback" : url_callback
490 consumer_key : this.consumer_key,
491 shared_secret : this.consumer_secret
494 var onToken = ChromeExOAuth.bind(this.onRequestToken, this, callback);
495 ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken);
499 * Called when a request token has been returned. Stores the request token
500 * secret for later use and sends the authorization url to the supplied
501 * callback (for redirecting the user).
502 * @param {Function} callback Function to call once the authorize URL is
503 * calculated. This callback will be passed the following arguments:
504 * url {String} The URL the user must be redirected to in order to
506 * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the
509 ChromeExOAuth.prototype.onRequestToken = function(callback, xhr) {
510 if (xhr.readyState == 4) {
511 if (xhr.status == 200) {
512 var params = ChromeExOAuth.formDecode(xhr.responseText);
513 var token = params['oauth_token'];
514 this.setTokenSecret(params['oauth_token_secret']);
515 var url = ChromeExOAuth.addURLParam(this.url_auth_token,
516 "oauth_token", token);
517 for (var key in this.auth_params) {
518 if (this.auth_params.hasOwnProperty(key)) {
519 url = ChromeExOAuth.addURLParam(url, key, this.auth_params[key]);
524 throw new Error("Fetching request token failed. Status " + xhr.status);
530 * Requests an OAuth access token.
531 * @param {String} oauth_token The OAuth request token.
532 * @param {String} oauth_verifier The OAuth token verifier.
533 * @param {Function} callback The function to call once the token is obtained.
534 * This callback will be passed the following arguments:
535 * token {String} The OAuth access token.
536 * secret {String} The OAuth access token secret.
538 ChromeExOAuth.prototype.getAccessToken = function(oauth_token, oauth_verifier,
540 if (typeof callback !== "function") {
541 throw new Error("Specified callback must be a function.");
543 var bg = chrome.extension.getBackgroundPage();
544 if (bg.chromeExOAuthRequestingAccess == false) {
545 bg.chromeExOAuthRequestingAccess = true;
547 var result = OAuthSimple().sign({
548 path : this.url_access_token,
550 "oauth_token" : oauth_token,
551 "oauth_verifier" : oauth_verifier
554 consumer_key : this.consumer_key,
555 shared_secret : this.consumer_secret,
556 oauth_secret : this.getTokenSecret(this.oauth_scope)
560 var onToken = ChromeExOAuth.bind(this.onAccessToken, this, callback);
561 ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken);
566 * Called when an access token has been returned. Stores the access token and
567 * access token secret for later use and sends them to the supplied callback.
568 * @param {Function} callback The function to call once the token is obtained.
569 * This callback will be passed the following arguments:
570 * token {String} The OAuth access token.
571 * secret {String} The OAuth access token secret.
572 * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the
575 ChromeExOAuth.prototype.onAccessToken = function(callback, xhr) {
576 if (xhr.readyState == 4) {
577 var bg = chrome.extension.getBackgroundPage();
578 if (xhr.status == 200) {
579 var params = ChromeExOAuth.formDecode(xhr.responseText);
580 var token = params["oauth_token"];
581 var secret = params["oauth_token_secret"];
582 this.setToken(token);
583 this.setTokenSecret(secret);
584 bg.chromeExOAuthRequestingAccess = false;
585 callback(token, secret);
587 bg.chromeExOAuthRequestingAccess = false;
588 throw new Error("Fetching access token failed with status " + xhr.status);