/** * Copyright (c) 2010 The Chromium Authors. All rights reserved. Use of this * source code is governed by a BSD-style license that can be found in the * LICENSE file. */ /** * Constructor - no need to invoke directly, call initBackgroundPage instead. * @constructor * @param {String} url_request_token The OAuth request token URL. * @param {String} url_auth_token The OAuth authorize token URL. * @param {String} url_access_token The OAuth access token URL. * @param {String} consumer_key The OAuth consumer key. * @param {String} consumer_secret The OAuth consumer secret. * @param {String} oauth_scope The OAuth scope parameter. * @param {Object} opt_args Optional arguments. Recognized parameters: * "app_name" {String} Name of the current application * "callback_page" {String} If you renamed chrome_ex_oauth.html, the name * this file was renamed to. */ function ChromeExOAuth(url_request_token, url_auth_token, url_access_token, consumer_key, consumer_secret, oauth_scope, opt_args) { this.url_request_token = url_request_token; this.url_auth_token = url_auth_token; this.url_access_token = url_access_token; this.consumer_key = consumer_key; this.consumer_secret = consumer_secret; this.oauth_scope = oauth_scope; this.app_name = opt_args && opt_args['app_name'] || "ChromeExOAuth Library"; this.key_token = "oauth_token"; this.key_token_secret = "oauth_token_secret"; this.callback_page = opt_args && opt_args['callback_page'] || "chrome_ex_oauth.html"; this.auth_params = {}; if (opt_args && opt_args['auth_params']) { for (key in opt_args['auth_params']) { if (opt_args['auth_params'].hasOwnProperty(key)) { this.auth_params[key] = opt_args['auth_params'][key]; } } } }; /******************************************************************************* * PUBLIC API METHODS * Call these from your background page. ******************************************************************************/ /** * Initializes the OAuth helper from the background page. You must call this * before attempting to make any OAuth calls. * @param {Object} oauth_config Configuration parameters in a JavaScript object. * The following parameters are recognized: * "request_url" {String} OAuth request token URL. * "authorize_url" {String} OAuth authorize token URL. * "access_url" {String} OAuth access token URL. * "consumer_key" {String} OAuth consumer key. * "consumer_secret" {String} OAuth consumer secret. * "scope" {String} OAuth access scope. * "app_name" {String} Application name. * "auth_params" {Object} Additional parameters to pass to the * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth * @return {ChromeExOAuth} An initialized ChromeExOAuth object. */ ChromeExOAuth.initBackgroundPage = function(oauth_config) { window.chromeExOAuthConfig = oauth_config; window.chromeExOAuth = ChromeExOAuth.fromConfig(oauth_config); window.chromeExOAuthRedirectStarted = false; window.chromeExOAuthRequestingAccess = false; var url_match = chrome.extension.getURL(window.chromeExOAuth.callback_page); var tabs = {}; chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { if (changeInfo.url && changeInfo.url.substr(0, url_match.length) === url_match && changeInfo.url != tabs[tabId] && window.chromeExOAuthRequestingAccess == false) { chrome.tabs.create({ 'url' : changeInfo.url }, function(tab) { tabs[tab.id] = tab.url; chrome.tabs.remove(tabId); }); } }); return window.chromeExOAuth; }; /** * Authorizes the current user with the configued API. You must call this * before calling sendSignedRequest. * @param {Function} callback A function to call once an access token has * been obtained. This callback will be passed the following arguments: * token {String} The OAuth access token. * secret {String} The OAuth access token secret. */ ChromeExOAuth.prototype.authorize = function(callback) { if (this.hasToken()) { callback(this.getToken(), this.getTokenSecret()); } else { window.chromeExOAuthOnAuthorize = function(token, secret) { callback(token, secret); }; chrome.tabs.create({ 'url' :chrome.extension.getURL(this.callback_page) }); } }; /** * Clears any OAuth tokens stored for this configuration. Effectively a * "logout" of the configured OAuth API. */ ChromeExOAuth.prototype.clearTokens = function() { delete localStorage[this.key_token + encodeURI(this.oauth_scope)]; delete localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; }; /** * Returns whether a token is currently stored for this configuration. * Effectively a check to see whether the current user is "logged in" to * the configured OAuth API. * @return {Boolean} True if an access token exists. */ ChromeExOAuth.prototype.hasToken = function() { return !!this.getToken(); }; /** * Makes an OAuth-signed HTTP request with the currently authorized tokens. * @param {String} url The URL to send the request to. Querystring parameters * should be omitted. * @param {Function} callback A function to be called once the request is * completed. This callback will be passed the following arguments: * responseText {String} The text response. * xhr {XMLHttpRequest} The XMLHttpRequest object which was used to * send the request. Useful if you need to check response status * code, etc. * @param {Object} opt_params Additional parameters to configure the request. * The following parameters are accepted: * "method" {String} The HTTP method to use. Defaults to "GET". * "body" {String} A request body to send. Defaults to null. * "parameters" {Object} Query parameters to include in the request. * "headers" {Object} Additional headers to include in the request. */ ChromeExOAuth.prototype.sendSignedRequest = function(url, callback, opt_params) { var method = opt_params && opt_params['method'] || 'GET'; var body = opt_params && opt_params['body'] || null; var params = opt_params && opt_params['parameters'] || {}; var headers = opt_params && opt_params['headers'] || {}; var signedUrl = this.signURL(url, method, params); ChromeExOAuth.sendRequest(method, signedUrl, headers, body, function (xhr) { if (xhr.readyState == 4) { callback(xhr.responseText, xhr); } }); }; /** * Adds the required OAuth parameters to the given url and returns the * result. Useful if you need a signed url but don't want to make an XHR * request. * @param {String} method The http method to use. * @param {String} url The base url of the resource you are querying. * @param {Object} opt_params Query parameters to include in the request. * @return {String} The base url plus any query params plus any OAuth params. */ ChromeExOAuth.prototype.signURL = function(url, method, opt_params) { var token = this.getToken(); var secret = this.getTokenSecret(); if (!token || !secret) { throw new Error("No oauth token or token secret"); } var params = opt_params || {}; var result = OAuthSimple().sign({ action : method, path : url, parameters : params, signatures: { consumer_key : this.consumer_key, shared_secret : this.consumer_secret, oauth_secret : secret, oauth_token: token } }); return result.signed_url; }; /** * Generates the Authorization header based on the oauth parameters. * @param {String} url The base url of the resource you are querying. * @param {Object} opt_params Query parameters to include in the request. * @return {String} An Authorization header containing the oauth_* params. */ ChromeExOAuth.prototype.getAuthorizationHeader = function(url, method, opt_params) { var token = this.getToken(); var secret = this.getTokenSecret(); if (!token || !secret) { throw new Error("No oauth token or token secret"); } var params = opt_params || {}; return OAuthSimple().getHeaderString({ action: method, path : url, parameters : params, signatures: { consumer_key : this.consumer_key, shared_secret : this.consumer_secret, oauth_secret : secret, oauth_token: token } }); }; /******************************************************************************* * PRIVATE API METHODS * Used by the library. There should be no need to call these methods directly. ******************************************************************************/ /** * Creates a new ChromeExOAuth object from the supplied configuration object. * @param {Object} oauth_config Configuration parameters in a JavaScript object. * The following parameters are recognized: * "request_url" {String} OAuth request token URL. * "authorize_url" {String} OAuth authorize token URL. * "access_url" {String} OAuth access token URL. * "consumer_key" {String} OAuth consumer key. * "consumer_secret" {String} OAuth consumer secret. * "scope" {String} OAuth access scope. * "app_name" {String} Application name. * "auth_params" {Object} Additional parameters to pass to the * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth * @return {ChromeExOAuth} An initialized ChromeExOAuth object. */ ChromeExOAuth.fromConfig = function(oauth_config) { return new ChromeExOAuth( oauth_config['request_url'], oauth_config['authorize_url'], oauth_config['access_url'], oauth_config['consumer_key'], oauth_config['consumer_secret'], oauth_config['scope'], { 'app_name' : oauth_config['app_name'], 'auth_params' : oauth_config['auth_params'] } ); }; /** * Initializes chrome_ex_oauth.html and redirects the page if needed to start * the OAuth flow. Once an access token is obtained, this function closes * chrome_ex_oauth.html. */ ChromeExOAuth.initCallbackPage = function() { var background_page = chrome.extension.getBackgroundPage(); var oauth_config = background_page.chromeExOAuthConfig; var oauth = ChromeExOAuth.fromConfig(oauth_config); background_page.chromeExOAuthRedirectStarted = true; oauth.initOAuthFlow(function (token, secret) { background_page.chromeExOAuthOnAuthorize(token, secret); background_page.chromeExOAuthRedirectStarted = false; chrome.tabs.getSelected(null, function (tab) { chrome.tabs.remove(tab.id); }); }); }; /** * Sends an HTTP request. Convenience wrapper for XMLHttpRequest calls. * @param {String} method The HTTP method to use. * @param {String} url The URL to send the request to. * @param {Object} headers Optional request headers in key/value format. * @param {String} body Optional body content. * @param {Function} callback Function to call when the XMLHttpRequest's * ready state changes. See documentation for XMLHttpRequest's * onreadystatechange handler for more information. */ ChromeExOAuth.sendRequest = function(method, url, headers, body, callback) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(data) { callback(xhr, data); } xhr.open(method, url, true); if (headers) { for (var header in headers) { if (headers.hasOwnProperty(header)) { xhr.setRequestHeader(header, headers[header]); } } } xhr.send(body); }; /** * Decodes a URL-encoded string into key/value pairs. * @param {String} encoded An URL-encoded string. * @return {Object} An object representing the decoded key/value pairs found * in the encoded string. */ ChromeExOAuth.formDecode = function(encoded) { var params = encoded.split("&"); var decoded = {}; for (var i = 0, param; param = params[i]; i++) { var keyval = param.split("="); if (keyval.length == 2) { var key = ChromeExOAuth.fromRfc3986(keyval[0]); var val = ChromeExOAuth.fromRfc3986(keyval[1]); decoded[key] = val; } } return decoded; }; /** * Returns the current window's querystring decoded into key/value pairs. * @return {Object} A object representing any key/value pairs found in the * current window's querystring. */ ChromeExOAuth.getQueryStringParams = function() { var urlparts = window.location.href.split("?"); if (urlparts.length >= 2) { var querystring = urlparts.slice(1).join("?"); return ChromeExOAuth.formDecode(querystring); } return {}; }; /** * Binds a function call to a specific object. This function will also take * a variable number of additional arguments which will be prepended to the * arguments passed to the bound function when it is called. * @param {Function} func The function to bind. * @param {Object} obj The object to bind to the function's "this". * @return {Function} A closure that will call the bound function. */ ChromeExOAuth.bind = function(func, obj) { var newargs = Array.prototype.slice.call(arguments).slice(2); return function() { var combinedargs = newargs.concat(Array.prototype.slice.call(arguments)); func.apply(obj, combinedargs); }; }; /** * Encodes a value according to the RFC3986 specification. * @param {String} val The string to encode. */ ChromeExOAuth.toRfc3986 = function(val){ return encodeURIComponent(val) .replace(/\!/g, "%21") .replace(/\*/g, "%2A") .replace(/'/g, "%27") .replace(/\(/g, "%28") .replace(/\)/g, "%29"); }; /** * Decodes a string that has been encoded according to RFC3986. * @param {String} val The string to decode. */ ChromeExOAuth.fromRfc3986 = function(val){ var tmp = val .replace(/%21/g, "!") .replace(/%2A/g, "*") .replace(/%27/g, "'") .replace(/%28/g, "(") .replace(/%29/g, ")"); return decodeURIComponent(tmp); }; /** * Adds a key/value parameter to the supplied URL. * @param {String} url An URL which may or may not contain querystring values. * @param {String} key A key * @param {String} value A value * @return {String} The URL with URL-encoded versions of the key and value * appended, prefixing them with "&" or "?" as needed. */ ChromeExOAuth.addURLParam = function(url, key, value) { var sep = (url.indexOf('?') >= 0) ? "&" : "?"; return url + sep + ChromeExOAuth.toRfc3986(key) + "=" + ChromeExOAuth.toRfc3986(value); }; /** * Stores an OAuth token for the configured scope. * @param {String} token The token to store. */ ChromeExOAuth.prototype.setToken = function(token) { localStorage[this.key_token + encodeURI(this.oauth_scope)] = token; }; /** * Retrieves any stored token for the configured scope. * @return {String} The stored token. */ ChromeExOAuth.prototype.getToken = function() { return localStorage[this.key_token + encodeURI(this.oauth_scope)]; }; /** * Stores an OAuth token secret for the configured scope. * @param {String} secret The secret to store. */ ChromeExOAuth.prototype.setTokenSecret = function(secret) { localStorage[this.key_token_secret + encodeURI(this.oauth_scope)] = secret; }; /** * Retrieves any stored secret for the configured scope. * @return {String} The stored secret. */ ChromeExOAuth.prototype.getTokenSecret = function() { return localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; }; /** * Starts an OAuth authorization flow for the current page. If a token exists, * no redirect is needed and the supplied callback is called immediately. * If this method detects that a redirect has finished, it grabs the * appropriate OAuth parameters from the URL and attempts to retrieve an * access token. If no token exists and no redirect has happened, then * an access token is requested and the page is ultimately redirected. * @param {Function} callback The function to call once the flow has finished. * This callback will be passed the following arguments: * token {String} The OAuth access token. * secret {String} The OAuth access token secret. */ ChromeExOAuth.prototype.initOAuthFlow = function(callback) { if (!this.hasToken()) { var params = ChromeExOAuth.getQueryStringParams(); if (params['chromeexoauthcallback'] == 'true') { var oauth_token = params['oauth_token']; var oauth_verifier = params['oauth_verifier'] this.getAccessToken(oauth_token, oauth_verifier, callback); } else { var request_params = { 'url_callback_param' : 'chromeexoauthcallback' } this.getRequestToken(function(url) { window.location.href = url; }, request_params); } } else { callback(this.getToken(), this.getTokenSecret()); } }; /** * Requests an OAuth request token. * @param {Function} callback Function to call once the authorize URL is * calculated. This callback will be passed the following arguments: * url {String} The URL the user must be redirected to in order to * approve the token. * @param {Object} opt_args Optional arguments. The following parameters * are accepted: * "url_callback" {String} The URL the OAuth provider will redirect to. * "url_callback_param" {String} A parameter to include in the callback * URL in order to indicate to this library that a redirect has * taken place. */ ChromeExOAuth.prototype.getRequestToken = function(callback, opt_args) { if (typeof callback !== "function") { throw new Error("Specified callback must be a function."); } var url = opt_args && opt_args['url_callback'] || window && window.top && window.top.location && window.top.location.href; var url_param = opt_args && opt_args['url_callback_param'] || "chromeexoauthcallback"; var url_callback = ChromeExOAuth.addURLParam(url, url_param, "true"); var result = OAuthSimple().sign({ path : this.url_request_token, parameters: { "xoauth_displayname" : this.app_name, "scope" : this.oauth_scope, "oauth_callback" : url_callback }, signatures: { consumer_key : this.consumer_key, shared_secret : this.consumer_secret } }); var onToken = ChromeExOAuth.bind(this.onRequestToken, this, callback); ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); }; /** * Called when a request token has been returned. Stores the request token * secret for later use and sends the authorization url to the supplied * callback (for redirecting the user). * @param {Function} callback Function to call once the authorize URL is * calculated. This callback will be passed the following arguments: * url {String} The URL the user must be redirected to in order to * approve the token. * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the * request token. */ ChromeExOAuth.prototype.onRequestToken = function(callback, xhr) { if (xhr.readyState == 4) { if (xhr.status == 200) { var params = ChromeExOAuth.formDecode(xhr.responseText); var token = params['oauth_token']; this.setTokenSecret(params['oauth_token_secret']); var url = ChromeExOAuth.addURLParam(this.url_auth_token, "oauth_token", token); for (var key in this.auth_params) { if (this.auth_params.hasOwnProperty(key)) { url = ChromeExOAuth.addURLParam(url, key, this.auth_params[key]); } } callback(url); } else { throw new Error("Fetching request token failed. Status " + xhr.status); } } }; /** * Requests an OAuth access token. * @param {String} oauth_token The OAuth request token. * @param {String} oauth_verifier The OAuth token verifier. * @param {Function} callback The function to call once the token is obtained. * This callback will be passed the following arguments: * token {String} The OAuth access token. * secret {String} The OAuth access token secret. */ ChromeExOAuth.prototype.getAccessToken = function(oauth_token, oauth_verifier, callback) { if (typeof callback !== "function") { throw new Error("Specified callback must be a function."); } var bg = chrome.extension.getBackgroundPage(); if (bg.chromeExOAuthRequestingAccess == false) { bg.chromeExOAuthRequestingAccess = true; var result = OAuthSimple().sign({ path : this.url_access_token, parameters: { "oauth_token" : oauth_token, "oauth_verifier" : oauth_verifier }, signatures: { consumer_key : this.consumer_key, shared_secret : this.consumer_secret, oauth_secret : this.getTokenSecret(this.oauth_scope) } }); var onToken = ChromeExOAuth.bind(this.onAccessToken, this, callback); ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); } }; /** * Called when an access token has been returned. Stores the access token and * access token secret for later use and sends them to the supplied callback. * @param {Function} callback The function to call once the token is obtained. * This callback will be passed the following arguments: * token {String} The OAuth access token. * secret {String} The OAuth access token secret. * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the * access token. */ ChromeExOAuth.prototype.onAccessToken = function(callback, xhr) { if (xhr.readyState == 4) { var bg = chrome.extension.getBackgroundPage(); if (xhr.status == 200) { var params = ChromeExOAuth.formDecode(xhr.responseText); var token = params["oauth_token"]; var secret = params["oauth_token_secret"]; this.setToken(token); this.setTokenSecret(secret); bg.chromeExOAuthRequestingAccess = false; callback(token, secret); } else { bg.chromeExOAuthRequestingAccess = false; throw new Error("Fetching access token failed with status " + xhr.status); } } };