Home | History | Annotate | Download | only in gdocs
      1 /* OAuthSimple
      2   * A simpler version of OAuth
      3   *
      4   * author:     jr conlin
      5   * mail:       src (at) anticipatr.com
      6   * copyright:  unitedHeroes.net
      7   * version:    1.0
      8   * url:        http://unitedHeroes.net/OAuthSimple
      9   *
     10   * Copyright (c) 2009, unitedHeroes.net
     11   * All rights reserved.
     12   *
     13   * Redistribution and use in source and binary forms, with or without
     14   * modification, are permitted provided that the following conditions are met:
     15   *     * Redistributions of source code must retain the above copyright
     16   *       notice, this list of conditions and the following disclaimer.
     17   *     * Redistributions in binary form must reproduce the above copyright
     18   *       notice, this list of conditions and the following disclaimer in the
     19   *       documentation and/or other materials provided with the distribution.
     20   *     * Neither the name of the unitedHeroes.net nor the
     21   *       names of its contributors may be used to endorse or promote products
     22   *       derived from this software without specific prior written permission.
     23   *
     24   * THIS SOFTWARE IS PROVIDED BY UNITEDHEROES.NET ''AS IS'' AND ANY
     25   * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     26   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     27   * DISCLAIMED. IN NO EVENT SHALL UNITEDHEROES.NET BE LIABLE FOR ANY
     28   * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     29   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     30   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     31   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     32   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     33   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     34  */
     35 var OAuthSimple;
     36 
     37 if (OAuthSimple === undefined)
     38 {
     39     /* Simple OAuth
     40      *
     41      * This class only builds the OAuth elements, it does not do the actual
     42      * transmission or reception of the tokens. It does not validate elements
     43      * of the token. It is for client use only.
     44      *
     45      * api_key is the API key, also known as the OAuth consumer key
     46      * shared_secret is the shared secret (duh).
     47      *
     48      * Both the api_key and shared_secret are generally provided by the site
     49      * offering OAuth services. You need to specify them at object creation
     50      * because nobody <explative>ing uses OAuth without that minimal set of
     51      * signatures.
     52      *
     53      * If you want to use the higher order security that comes from the
     54      * OAuth token (sorry, I don't provide the functions to fetch that because
     55      * sites aren't horribly consistent about how they offer that), you need to
     56      * pass those in either with .setTokensAndSecrets() or as an argument to the
     57      * .sign() or .getHeaderString() functions.
     58      *
     59      * Example:
     60        <code>
     61         var oauthObject = OAuthSimple().sign({path:'http://example.com/rest/',
     62                                               parameters: 'foo=bar&gorp=banana',
     63                                               signatures:{
     64                                                 api_key:'12345abcd',
     65                                                 shared_secret:'xyz-5309'
     66                                              }});
     67         document.getElementById('someLink').href=oauthObject.signed_url;
     68        </code>
     69      *
     70      * that will sign as a "GET" using "SHA1-MAC" the url. If you need more than
     71      * that, read on, McDuff.
     72      */
     73 
     74     /** OAuthSimple creator
     75      *
     76      * Create an instance of OAuthSimple
     77      *
     78      * @param api_key {string}       The API Key (sometimes referred to as the consumer key) This value is usually supplied by the site you wish to use.
     79      * @param shared_secret (string) The shared secret. This value is also usually provided by the site you wish to use.
     80      */
     81     OAuthSimple = function (consumer_key,shared_secret)
     82     {
     83 /*        if (api_key == undefined)
     84             throw("Missing argument: api_key (oauth_consumer_key) for OAuthSimple. This is usually provided by the hosting site.");
     85         if (shared_secret == undefined)
     86             throw("Missing argument: shared_secret (shared secret) for OAuthSimple. This is usually provided by the hosting site.");
     87 */      this._secrets={};
     88         this._parameters={};
     89 
     90         // General configuration options.
     91         if (consumer_key !== undefined) {
     92             this._secrets['consumer_key'] = consumer_key;
     93             }
     94         if (shared_secret !== undefined) {
     95             this._secrets['shared_secret'] = shared_secret;
     96             }
     97         this._default_signature_method= "HMAC-SHA1";
     98         this._action = "GET";
     99         this._nonce_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    100 
    101 
    102         this.reset = function() {
    103             this._parameters={};
    104             this._path=undefined;
    105             return this;
    106         };
    107 
    108         /** set the parameters either from a hash or a string
    109          *
    110          * @param {string,object} List of parameters for the call, this can either be a URI string (e.g. "foo=bar&gorp=banana" or an object/hash)
    111          */
    112         this.setParameters = function (parameters) {
    113             if (parameters === undefined) {
    114                 parameters = {};
    115                 }
    116             if (typeof(parameters) == 'string') {
    117                 parameters=this._parseParameterString(parameters);
    118                 }
    119             this._parameters = parameters;
    120             if (this._parameters['oauth_nonce'] === undefined) {
    121                 this._getNonce();
    122                 }
    123             if (this._parameters['oauth_timestamp'] === undefined) {
    124                 this._getTimestamp();
    125                 }
    126             if (this._parameters['oauth_method'] === undefined) {
    127                 this.setSignatureMethod();
    128                 }
    129             if (this._parameters['oauth_consumer_key'] === undefined) {
    130                 this._getApiKey();
    131                 }
    132             if(this._parameters['oauth_token'] === undefined) {
    133                 this._getAccessToken();
    134                 }
    135 
    136             return this;
    137         };
    138 
    139         /** convienence method for setParameters
    140          *
    141          * @param parameters {string,object} See .setParameters
    142          */
    143         this.setQueryString = function (parameters) {
    144             return this.setParameters(parameters);
    145         };
    146 
    147         /** Set the target URL (does not include the parameters)
    148          *
    149          * @param path {string} the fully qualified URI (excluding query arguments) (e.g "http://example.org/foo")
    150          */
    151         this.setURL = function (path) {
    152             if (path == '') {
    153                 throw ('No path specified for OAuthSimple.setURL');
    154                 }
    155             this._path = path;
    156             return this;
    157         };
    158 
    159         /** convienence method for setURL
    160          *
    161          * @param path {string} see .setURL
    162          */
    163         this.setPath = function(path){
    164             return this.setURL(path);
    165         };
    166 
    167         /** set the "action" for the url, (e.g. GET,POST, DELETE, etc.)
    168          *
    169          * @param action {string} HTTP Action word.
    170          */
    171         this.setAction = function(action) {
    172             if (action === undefined) {
    173                 action="GET";
    174                 }
    175             action = action.toUpperCase();
    176             if (action.match('[^A-Z]')) {
    177                 throw ('Invalid action specified for OAuthSimple.setAction');
    178                 }
    179             this._action = action;
    180             return this;
    181         };
    182 
    183         /** set the signatures (as well as validate the ones you have)
    184          *
    185          * @param signatures {object} object/hash of the token/signature pairs {api_key:, shared_secret:, oauth_token: oauth_secret:}
    186          */
    187         this.setTokensAndSecrets = function(signatures) {
    188             if (signatures)
    189             {
    190                 for (var i in signatures) {
    191                     this._secrets[i] = signatures[i];
    192                     }
    193             }
    194             // Aliases
    195             if (this._secrets['api_key']) {
    196                 this._secrets.consumer_key = this._secrets.api_key;
    197                 }
    198             if (this._secrets['access_token']) {
    199                 this._secrets.oauth_token = this._secrets.access_token;
    200                 }
    201             if (this._secrets['access_secret']) {
    202                 this._secrets.oauth_secret = this._secrets.access_secret;
    203                 }
    204             // Gauntlet
    205             if (this._secrets.consumer_key === undefined) {
    206                 throw('Missing required consumer_key in OAuthSimple.setTokensAndSecrets');
    207                 }
    208             if (this._secrets.shared_secret === undefined) {
    209                 throw('Missing required shared_secret in OAuthSimple.setTokensAndSecrets');
    210                 }
    211             if ((this._secrets.oauth_token !== undefined) && (this._secrets.oauth_secret === undefined)) {
    212                 throw('Missing oauth_secret for supplied oauth_token in OAuthSimple.setTokensAndSecrets');
    213                 }
    214             return this;
    215         };
    216 
    217         /** set the signature method (currently only Plaintext or SHA-MAC1)
    218          *
    219          * @param method {string} Method of signing the transaction (only PLAINTEXT and SHA-MAC1 allowed for now)
    220          */
    221         this.setSignatureMethod = function(method) {
    222             if (method === undefined) {
    223                 method = this._default_signature_method;
    224                 }
    225             //TODO: accept things other than PlainText or SHA-MAC1
    226             if (method.toUpperCase().match(/(PLAINTEXT|HMAC-SHA1)/) === undefined) {
    227                 throw ('Unknown signing method specified for OAuthSimple.setSignatureMethod');
    228                 }
    229             this._parameters['oauth_signature_method']= method.toUpperCase();
    230             return this;
    231         };
    232 
    233         /** sign the request
    234          *
    235          * note: all arguments are optional, provided you've set them using the
    236          * other helper functions.
    237          *
    238          * @param args {object} hash of arguments for the call
    239          *                   {action:, path:, parameters:, method:, signatures:}
    240          *                   all arguments are optional.
    241          */
    242         this.sign = function (args) {
    243             if (args === undefined) {
    244                 args = {};
    245                 }
    246             // Set any given parameters
    247             if(args['action'] !== undefined) {
    248                 this.setAction(args['action']);
    249                 }
    250             if (args['path'] !== undefined) {
    251                 this.setPath(args['path']);
    252                 }
    253             if (args['method'] !== undefined) {
    254                 this.setSignatureMethod(args['method']);
    255                 }
    256             this.setTokensAndSecrets(args['signatures']);
    257             if (args['parameters'] !== undefined){
    258             this.setParameters(args['parameters']);
    259             }
    260             // check the parameters
    261             var normParams = this._normalizedParameters();
    262             this._parameters['oauth_signature']=this._generateSignature(normParams);
    263             return {
    264                 parameters: this._parameters,
    265                 signature: this._oauthEscape(this._parameters['oauth_signature']),
    266                 signed_url: this._path + '?' + this._normalizedParameters(),
    267                 header: this.getHeaderString()
    268             };
    269         };
    270 
    271         /** Return a formatted "header" string
    272          *
    273          * NOTE: This doesn't set the "Authorization: " prefix, which is required.
    274          * I don't set it because various set header functions prefer different
    275          * ways to do that.
    276          *
    277          * @param args {object} see .sign
    278          */
    279         this.getHeaderString = function(args) {
    280             if (this._parameters['oauth_signature'] === undefined) {
    281                 this.sign(args);
    282                 }
    283 
    284             var result = 'OAuth ';
    285             for (var pName in this._parameters)
    286             {
    287                 if (!pName.match(/^oauth/)) {
    288                     continue;
    289                     }
    290                 if ((this._parameters[pName]) instanceof Array)
    291                 {
    292                     var pLength = this._parameters[pName].length;
    293                     for (var j=0;j<pLength;j++)
    294                     {
    295                         result += pName +'="'+this._oauthEscape(this._parameters[pName][j])+'" ';
    296                     }
    297                 }
    298                 else
    299                 {
    300                     result += pName + '="'+this._oauthEscape(this._parameters[pName])+'" ';
    301                 }
    302             }
    303             return result;
    304         };
    305 
    306         // Start Private Methods.
    307 
    308         /** convert the parameter string into a hash of objects.
    309          *
    310          */
    311         this._parseParameterString = function(paramString){
    312             var elements = paramString.split('&');
    313             var result={};
    314             for(var element=elements.shift();element;element=elements.shift())
    315             {
    316                 var keyToken=element.split('=');
    317                 var value='';
    318                 if (keyToken[1]) {
    319                     value=decodeURIComponent(keyToken[1]);
    320                     }
    321                 if(result[keyToken[0]]){
    322                     if (!(result[keyToken[0]] instanceof Array))
    323                     {
    324                         result[keyToken[0]] = Array(result[keyToken[0]],value);
    325                     }
    326                     else
    327                     {
    328                         result[keyToken[0]].push(value);
    329                     }
    330                 }
    331                 else
    332                 {
    333                     result[keyToken[0]]=value;
    334                 }
    335             }
    336             return result;
    337         };
    338 
    339         this._oauthEscape = function(string) {
    340             if (string === undefined) {
    341                 return "";
    342                 }
    343             if (string instanceof Array)
    344             {
    345                 throw('Array passed to _oauthEscape');
    346             }
    347             return encodeURIComponent(string).replace(/\!/g, "%21").
    348             replace(/\*/g, "%2A").
    349             replace(/'/g, "%27").
    350             replace(/\(/g, "%28").
    351             replace(/\)/g, "%29");
    352         };
    353 
    354         this._getNonce = function (length) {
    355             if (length === undefined) {
    356                 length=5;
    357                 }
    358             var result = "";
    359             var cLength = this._nonce_chars.length;
    360             for (var i = 0; i < length;i++) {
    361                 var rnum = Math.floor(Math.random() *cLength);
    362                 result += this._nonce_chars.substring(rnum,rnum+1);
    363             }
    364             this._parameters['oauth_nonce']=result;
    365             return result;
    366         };
    367 
    368         this._getApiKey = function() {
    369             if (this._secrets.consumer_key === undefined) {
    370                 throw('No consumer_key set for OAuthSimple.');
    371                 }
    372             this._parameters['oauth_consumer_key']=this._secrets.consumer_key;
    373             return this._parameters.oauth_consumer_key;
    374         };
    375 
    376         this._getAccessToken = function() {
    377             if (this._secrets['oauth_secret'] === undefined) {
    378                 return '';
    379                 }
    380             if (this._secrets['oauth_token'] === undefined) {
    381                 throw('No oauth_token (access_token) set for OAuthSimple.');
    382                 }
    383             this._parameters['oauth_token'] = this._secrets.oauth_token;
    384             return this._parameters.oauth_token;
    385         };
    386 
    387         this._getTimestamp = function() {
    388             var d = new Date();
    389             var ts = Math.floor(d.getTime()/1000);
    390             this._parameters['oauth_timestamp'] = ts;
    391             return ts;
    392         };
    393 
    394         this.b64_hmac_sha1 = function(k,d,_p,_z){
    395         // heavily optimized and compressed version of http://pajhome.org.uk/crypt/md5/sha1.js
    396         // _p = b64pad, _z = character size; not used here but I left them available just in case
    397         if(!_p){_p='=';}if(!_z){_z=8;}function _f(t,b,c,d){if(t<20){return(b&c)|((~b)&d);}if(t<40){return b^c^d;}if(t<60){return(b&c)|(b&d)|(c&d);}return b^c^d;}function _k(t){return(t<20)?1518500249:(t<40)?1859775393:(t<60)?-1894007588:-899497514;}function _s(x,y){var l=(x&0xFFFF)+(y&0xFFFF),m=(x>>16)+(y>>16)+(l>>16);return(m<<16)|(l&0xFFFF);}function _r(n,c){return(n<<c)|(n>>>(32-c));}function _c(x,l){x[l>>5]|=0x80<<(24-l%32);x[((l+64>>9)<<4)+15]=l;var w=[80],a=1732584193,b=-271733879,c=-1732584194,d=271733878,e=-1009589776;for(var i=0;i<x.length;i+=16){var o=a,p=b,q=c,r=d,s=e;for(var j=0;j<80;j++){if(j<16){w[j]=x[i+j];}else{w[j]=_r(w[j-3]^w[j-8]^w[j-14]^w[j-16],1);}var t=_s(_s(_r(a,5),_f(j,b,c,d)),_s(_s(e,w[j]),_k(j)));e=d;d=c;c=_r(b,30);b=a;a=t;}a=_s(a,o);b=_s(b,p);c=_s(c,q);d=_s(d,r);e=_s(e,s);}return[a,b,c,d,e];}function _b(s){var b=[],m=(1<<_z)-1;for(var i=0;i<s.length*_z;i+=_z){b[i>>5]|=(s.charCodeAt(i/8)&m)<<(32-_z-i%32);}return b;}function _h(k,d){var b=_b(k);if(b.length>16){b=_c(b,k.length*_z);}var p=[16],o=[16];for(var i=0;i<16;i++){p[i]=b[i]^0x36363636;o[i]=b[i]^0x5C5C5C5C;}var h=_c(p.concat(_b(d)),512+d.length*_z);return _c(o.concat(h),512+160);}function _n(b){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s='';for(var i=0;i<b.length*4;i+=3){var r=(((b[i>>2]>>8*(3-i%4))&0xFF)<<16)|(((b[i+1>>2]>>8*(3-(i+1)%4))&0xFF)<<8)|((b[i+2>>2]>>8*(3-(i+2)%4))&0xFF);for(var j=0;j<4;j++){if(i*8+j*6>b.length*32){s+=_p;}else{s+=t.charAt((r>>6*(3-j))&0x3F);}}}return s;}function _x(k,d){return _n(_h(k,d));}return _x(k,d);
    398         }
    399 
    400 
    401         this._normalizedParameters = function() {
    402             var elements = new Array();
    403             var paramNames = [];
    404             var ra =0;
    405             for (var paramName in this._parameters)
    406             {
    407                 if (ra++ > 1000) {
    408                     throw('runaway 1');
    409                     }
    410                 paramNames.unshift(paramName);
    411             }
    412             paramNames = paramNames.sort();
    413             pLen = paramNames.length;
    414             for (var i=0;i<pLen; i++)
    415             {
    416                 paramName=paramNames[i];
    417                 //skip secrets.
    418                 if (paramName.match(/\w+_secret/)) {
    419                     continue;
    420                     }
    421                 if (this._parameters[paramName] instanceof Array)
    422                 {
    423                     var sorted = this._parameters[paramName].sort();
    424                     var spLen = sorted.length;
    425                     for (var j = 0;j<spLen;j++){
    426                         if (ra++ > 1000) {
    427                             throw('runaway 1');
    428                             }
    429                         elements.push(this._oauthEscape(paramName) + '=' +
    430                                   this._oauthEscape(sorted[j]));
    431                     }
    432                     continue;
    433                 }
    434                 elements.push(this._oauthEscape(paramName) + '=' +
    435                               this._oauthEscape(this._parameters[paramName]));
    436             }
    437             return elements.join('&');
    438         };
    439 
    440         this._generateSignature = function() {
    441 
    442             var secretKey = this._oauthEscape(this._secrets.shared_secret)+'&'+
    443                 this._oauthEscape(this._secrets.oauth_secret);
    444             if (this._parameters['oauth_signature_method'] == 'PLAINTEXT')
    445             {
    446                 return secretKey;
    447             }
    448             if (this._parameters['oauth_signature_method'] == 'HMAC-SHA1')
    449             {
    450                 var sigString = this._oauthEscape(this._action)+'&'+this._oauthEscape(this._path)+'&'+this._oauthEscape(this._normalizedParameters());
    451                 return this.b64_hmac_sha1(secretKey,sigString);
    452             }
    453             return null;
    454         };
    455 
    456     return this;
    457     };
    458 }
    459