1/* OAuthSimple 2 * A simpler version of OAuth 3 * 4 * author: jr conlin 5 * mail: src@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 */ 35var OAuthSimple; 36 37if (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