Home | History | Annotate | Download | only in oauth_contacts
      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  */
     21 function 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  */
     67 ChromeExOAuth.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  */
     98 ChromeExOAuth.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  */
    113 ChromeExOAuth.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  */
    124 ChromeExOAuth.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  */
    145 ChromeExOAuth.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  */
    170 ChromeExOAuth.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  */
    200 ChromeExOAuth.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  */
    244 ChromeExOAuth.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  */
    264 ChromeExOAuth.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  */
    288 ChromeExOAuth.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  */
    310 ChromeExOAuth.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  */
    329 ChromeExOAuth.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  */
    346 ChromeExOAuth.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  */
    358 ChromeExOAuth.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  */
    371 ChromeExOAuth.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  */
    389 ChromeExOAuth.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  */
    399 ChromeExOAuth.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  */
    407 ChromeExOAuth.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  */
    415 ChromeExOAuth.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  */
    423 ChromeExOAuth.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  */
    439 ChromeExOAuth.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  */
    472 ChromeExOAuth.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  */
    511 ChromeExOAuth.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  */
    540 ChromeExOAuth.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  */
    577 ChromeExOAuth.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 };