forked from coolaj86/walnut.js
		
	
		
			
	
	
		
			739 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			739 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | (function (exports) { | ||
|  |   'use strict'; | ||
|  | 
 | ||
|  |   var oauth3 = {}; | ||
|  |   var logins = {}; | ||
|  |   oauth3.requests = logins; | ||
|  | 
 | ||
|  |   if ('undefined' !== typeof Promise) { | ||
|  |     oauth3.PromiseA = Promise; | ||
|  |   } else { | ||
|  |     console.warn("[oauth3.js] Remember to call oauth3.providePromise(Promise) with a proper Promise implementation"); | ||
|  |   } | ||
|  | 
 | ||
|  |   // TODO move to a test / lint suite?
 | ||
|  |   oauth3._testPromise = function (PromiseA) { | ||
|  |     var promise; | ||
|  |     var x = 1; | ||
|  | 
 | ||
|  |     // tests that this promise has all of the necessary api
 | ||
|  |     promise = new PromiseA(function (resolve, reject) { | ||
|  |       if (x === 1) { | ||
|  |         throw new Error("bad promise, create not asynchronous"); | ||
|  |       } | ||
|  | 
 | ||
|  |       PromiseA.resolve().then(function () { | ||
|  |         var promise2; | ||
|  | 
 | ||
|  |         if (x === 1 || x === 2) { | ||
|  |           throw new Error("bad promise, resolve not asynchronous"); | ||
|  |         } | ||
|  | 
 | ||
|  |         promise2 = PromiseA.reject().then(reject, function () { | ||
|  |           if (x === 1 || x === 2 || x === 3) { | ||
|  |             throw new Error("bad promise, reject not asynchronous"); | ||
|  |           } | ||
|  | 
 | ||
|  |           if ('undefined' === typeof angular) { | ||
|  |             throw new Error("[NOT AN ERROR] Dear angular users: ignore this error-handling test"); | ||
|  |           } else { | ||
|  |             return PromiseA.reject(new Error("[NOT AN ERROR] ignore this error-handling test")); | ||
|  |           } | ||
|  |         }); | ||
|  | 
 | ||
|  |         x = 4; | ||
|  | 
 | ||
|  |         return promise2; | ||
|  |       }).catch(function (e) { | ||
|  |         if (e.message.match('NOT AN ERROR')) { | ||
|  |           resolve({ success: true }); | ||
|  |         } else { | ||
|  |           reject(e); | ||
|  |         } | ||
|  |       }); | ||
|  | 
 | ||
|  |       x = 3; | ||
|  |     }); | ||
|  | 
 | ||
|  |     x = 2; | ||
|  |     return promise; | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.providePromise = function (PromiseA) { | ||
|  |     oauth3.PromiseA = PromiseA; | ||
|  |     return oauth3._testPromise(PromiseA).then(function () { | ||
|  |       oauth3.PromiseA = PromiseA; | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.provideRequest = function (request, opts) { | ||
|  |     opts = opts || {}; | ||
|  |     var Recase = exports.Recase || require('recase'); | ||
|  |     // TODO make insensitive to providing exceptions
 | ||
|  |     var recase = Recase.create({ exceptions: {} }); | ||
|  | 
 | ||
|  |     if (opts.rawCase) { | ||
|  |       oauth3.request = request; | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     // Wrap oauth3 api calls in snake_case / camelCase conversion
 | ||
|  |     oauth3.request = function (req, opts) { | ||
|  |       //console.log('[D] [oauth3 req.url]', req.url);
 | ||
|  |       opts = opts || {}; | ||
|  | 
 | ||
|  |       if (opts.rawCase) { | ||
|  |         return request(req); | ||
|  |       } | ||
|  | 
 | ||
|  |       // convert JavaScript camelCase to oauth3 snake_case
 | ||
|  |       if (req.data && 'object' === typeof req.data) { | ||
|  |         req.originalData = req.data; | ||
|  |         req.data = recase.snakeCopy(req.data); | ||
|  |       } | ||
|  | 
 | ||
|  |       //console.log('[F] [oauth3 req.url]', req.url);
 | ||
|  |       return request(req).then(function (resp) { | ||
|  |         // convert oauth3 snake_case to JavaScript camelCase
 | ||
|  |         if (resp.data && 'object' === typeof resp.data) { | ||
|  |           resp.originalData = resp.data; | ||
|  |           resp.data = recase.camelCopy(resp.data); | ||
|  |         } | ||
|  |         return resp; | ||
|  |       }); | ||
|  |     }; | ||
|  | 
 | ||
|  |     /* | ||
|  |     return oauth3._testRequest(request).then(function () { | ||
|  |       oauth3.request = request; | ||
|  |     }); | ||
|  |     */ | ||
|  |   }; | ||
|  | 
 | ||
|  |   logins.authorizationRedirect = function (providerUri, opts) { | ||
|  |     // TODO get own directives
 | ||
|  |     return oauth3.authorizationRedirect( | ||
|  |       providerUri | ||
|  |     , opts.authorizationRedirect | ||
|  |     , opts | ||
|  |     ).then(function (prequest) { | ||
|  |       if (!prequest.state) { | ||
|  |         throw new Error("[Devolper Error] [authorization redirect] prequest.state is empty"); | ||
|  |       } | ||
|  | 
 | ||
|  |       return oauth3.frameRequest(prequest.url, prequest.state, opts); | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   logins.implicitGrant = function (providerUri, opts) { | ||
|  |     // TODO OAuth3 provider should use the redirect URI as the appId?
 | ||
|  |     return oauth3.implicitGrant( | ||
|  |       providerUri | ||
|  |       // TODO OAuth3 provider should referer / origin as the appId?
 | ||
|  |     , opts | ||
|  |     ).then(function (prequest) { | ||
|  |       // console.log('[debug] prequest', prequest);
 | ||
|  |       if (!prequest.state) { | ||
|  |         throw new Error("[Devolper Error] [implicit grant] prequest.state is empty"); | ||
|  |       } | ||
|  | 
 | ||
|  |       return oauth3.frameRequest(prequest.url, prequest.state, opts); | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   logins.resourceOwnerPassword = function (providerUri, username, passphrase, opts) { | ||
|  |     console.log('DEBUG logins.resourceOwnerPassword opts', opts); | ||
|  |     //var scope = opts.scope;
 | ||
|  |     //var appId = opts.appId;
 | ||
|  |     return oauth3.resourceOwnerPassword( | ||
|  |       providerUri | ||
|  |     , username | ||
|  |     , passphrase | ||
|  |     , opts | ||
|  |     //, scope
 | ||
|  |     //, appId
 | ||
|  |     ).then(function (request) { | ||
|  |       console.log('DEBUG oauth3.resourceOwnerPassword', request); | ||
|  |       return oauth3.request({ | ||
|  |         url: request.url | ||
|  |       , method: request.method | ||
|  |       , data: request.data | ||
|  |       }); | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.frameRequest = function (url, state, opts) { | ||
|  |     var promise; | ||
|  | 
 | ||
|  |     if ('background' === opts.type) { | ||
|  |       promise = oauth3.insertIframe(url, state, opts); | ||
|  |     } else if ('popup' === opts.type) { | ||
|  |       promise = oauth3.openWindow(url, state, opts); | ||
|  |     } else { | ||
|  |       throw new Error("login framing method not specified or not type yet implemented"); | ||
|  |     } | ||
|  | 
 | ||
|  |     return promise.then(function (params) { | ||
|  |       var err; | ||
|  | 
 | ||
|  |       if (params.error || params.error_description) { | ||
|  |         err = new Error(params.error_description || "Unknown response error"); | ||
|  |         err.code = params.error || "E_UKNOWN_ERROR"; | ||
|  |         err.params = params; | ||
|  |         return oauth3.PromiseA.reject(err); | ||
|  |       } | ||
|  | 
 | ||
|  |       return params; | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.login = function (providerUri, opts) { | ||
|  |     console.log('##### DEBUG oauth3.login providerUri, opts'); | ||
|  |     console.log(providerUri); | ||
|  |     console.log(opts); | ||
|  |     // Four styles of login:
 | ||
|  |     //   * background (hidden iframe)
 | ||
|  |     //   * iframe (visible iframe, needs border color and width x height params)
 | ||
|  |     //   * popup (needs width x height and positioning? params)
 | ||
|  |     //   * window (params?)
 | ||
|  | 
 | ||
|  |     // Two strategies
 | ||
|  |     //  * authorization_redirect (to server authorization code)
 | ||
|  |     //  * implicit_grant (default, browser-only)
 | ||
|  |     // If both are selected, implicit happens first and then the other happens in background
 | ||
|  | 
 | ||
|  |     var promise; | ||
|  | 
 | ||
|  |     if (opts.username || opts.password) { | ||
|  |       /* jshint ignore:start */ | ||
|  |       // ingore "confusing use of !"
 | ||
|  |       if (!opts.username !== !opts.password) { | ||
|  |         throw new Error("you did not specify both username and password"); | ||
|  |       } | ||
|  |       /* jshint ignore:end */ | ||
|  | 
 | ||
|  |       var username = opts.username; | ||
|  |       var password = opts.password; | ||
|  |       delete opts.username; | ||
|  |       delete opts.password; | ||
|  | 
 | ||
|  |       return logins.resourceOwnerPassword(providerUri, username, password, opts).then(function (resp) { | ||
|  |         if (!resp || !resp.data) { | ||
|  |           var err = new Error("bad response"); | ||
|  |           err.response = resp; | ||
|  |           err.data = resp && resp.data || undefined; | ||
|  |           return oauth3.PromiseA.reject(err); | ||
|  |         } | ||
|  |         return resp.data; | ||
|  |       }); | ||
|  |     } | ||
|  | 
 | ||
|  |     // TODO support dual-strategy login
 | ||
|  |     // by default, always get implicitGrant (for client)
 | ||
|  |     // and optionally do authorizationCode (for server session)
 | ||
|  |     if ('background' === opts.type || opts.background) { | ||
|  |       opts.type = 'background'; | ||
|  |       opts.background = true; | ||
|  |     } | ||
|  |     else { | ||
|  |       opts.type = 'popup'; | ||
|  |       opts.popup = true; | ||
|  |     } | ||
|  |     if (opts.authorizationRedirect) { | ||
|  |       promise = logins.authorizationRedirect(providerUri, opts); | ||
|  |     } | ||
|  |     else { | ||
|  |       promise = logins.implicitGrant(providerUri, opts); | ||
|  |     } | ||
|  | 
 | ||
|  |     return promise; | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.backgroundLogin = function (providerUri, opts) { | ||
|  |     opts = opts || {}; | ||
|  |     opts.type = 'background'; | ||
|  |     return oauth3.login(providerUri, opts); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.insertIframe = function (url, state, opts) { | ||
|  |     opts = opts || {}; | ||
|  |     var promise = new oauth3.PromiseA(function (resolve, reject) { | ||
|  |       var tok; | ||
|  |       var $iframe; | ||
|  | 
 | ||
|  |       function cleanup() { | ||
|  |         delete window['__oauth3_' + state]; | ||
|  |         $iframe.remove(); | ||
|  |         clearTimeout(tok); | ||
|  |         tok = null; | ||
|  |       } | ||
|  | 
 | ||
|  |       window['__oauth3_' + state] = function (params) { | ||
|  |         //console.info('[iframe] complete', params);
 | ||
|  |         resolve(params); | ||
|  |         cleanup(); | ||
|  |       }; | ||
|  | 
 | ||
|  |       tok = setTimeout(function () { | ||
|  |         var err = new Error("the iframe request did not complete within 15 seconds"); | ||
|  |         err.code = "E_TIMEOUT"; | ||
|  |         reject(err); | ||
|  |         cleanup(); | ||
|  |       }, opts.timeout || 15000); | ||
|  | 
 | ||
|  |       // TODO hidden / non-hidden (via directive even)
 | ||
|  |       $iframe = $( | ||
|  |         '<iframe src="' + url | ||
|  |       //+ '" width="800px" height="800px" style="opacity: 0.8;" frameborder="1"></iframe>'
 | ||
|  |       + '" width="1px" height="1px" frameborder="0"></iframe>' | ||
|  |       ); | ||
|  | 
 | ||
|  |       $('body').append($iframe); | ||
|  |     }); | ||
|  | 
 | ||
|  |     // TODO periodically garbage collect expired handlers from window object
 | ||
|  |     return promise; | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.openWindow = function (url, state, opts) { | ||
|  |     var promise = new oauth3.PromiseA(function (resolve, reject) { | ||
|  |       var winref; | ||
|  |       var tok; | ||
|  | 
 | ||
|  |       function cleanup() { | ||
|  |         delete window['__oauth3_' + state]; | ||
|  |         clearTimeout(tok); | ||
|  |         tok = null; | ||
|  |         // this is last in case the window self-closes synchronously
 | ||
|  |         // (should never happen, but that's a negotiable implementation detail)
 | ||
|  |         //winref.close();
 | ||
|  |       } | ||
|  | 
 | ||
|  |       window['__oauth3_' + state] = function (params) { | ||
|  |         //console.info('[popup] (or window) complete', params);
 | ||
|  |         resolve(params); | ||
|  |         cleanup(); | ||
|  |       }; | ||
|  | 
 | ||
|  |       tok = setTimeout(function () { | ||
|  |         var err = new Error("the windowed request did not complete within 3 minutes"); | ||
|  |         err.code = "E_TIMEOUT"; | ||
|  |         reject(err); | ||
|  |         cleanup(); | ||
|  |       }, opts.timeout || 3 * 60 * 1000); | ||
|  | 
 | ||
|  |       // TODO allow size changes (via directive even)
 | ||
|  |       winref = window.open(url, 'oauth3-login-' + state, 'height=720,width=620'); | ||
|  |       if (!winref) { | ||
|  |         reject("TODO: open the iframe first and discover oauth3 directives before popup"); | ||
|  |         cleanup(); | ||
|  |       } | ||
|  |     }); | ||
|  | 
 | ||
|  |     // TODO periodically garbage collect expired handlers from window object
 | ||
|  |     return promise; | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.logout = function (providerUri, opts) { | ||
|  |     opts = opts || {}; | ||
|  | 
 | ||
|  |     // Oauth3.init({ logout: function () {} });
 | ||
|  |     //return Oauth3.logout();
 | ||
|  | 
 | ||
|  |     var state = parseInt(Math.random().toString().replace('0.', ''), 10).toString('36'); | ||
|  |     var url = providerUri.replace(/\/$/, '') + (opts.providerOauth3Html || '/oauth3.html'); | ||
|  |     var redirectUri = opts.redirectUri | ||
|  |       || (window.location.protocol + '//' + (window.location.host + window.location.pathname) + 'oauth3.html') | ||
|  |       ; | ||
|  |     var params = { | ||
|  |       // logout=true for all logins/accounts
 | ||
|  |       // logout=app-scoped-login-id for a single login
 | ||
|  |       action: 'logout' | ||
|  |       // TODO specify specific accounts / logins to delete from session
 | ||
|  |     , accounts: true | ||
|  |     , logins: true | ||
|  |     , redirect_uri: redirectUri | ||
|  |     , state: state | ||
|  |     }; | ||
|  | 
 | ||
|  |     //console.log('DEBUG oauth3.logout NIX insertIframe');
 | ||
|  |     //console.log(url, params.redirect_uri);
 | ||
|  |     //console.log(state);
 | ||
|  |     //console.log(params); // redirect_uri
 | ||
|  |     //console.log(opts);
 | ||
|  | 
 | ||
|  |     if (url === params.redirect_uri) { | ||
|  |       return oauth3.PromiseA.resolve(); | ||
|  |     } | ||
|  | 
 | ||
|  |     url += '#' + oauth3.querystringify(params); | ||
|  | 
 | ||
|  |     return oauth3.insertIframe(url, state, opts); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.stringifyscope = function (scope) { | ||
|  |     if (Array.isArray(scope)) { | ||
|  |       scope = scope.join(' '); | ||
|  |     } | ||
|  |     return scope; | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.querystringify = function (params) { | ||
|  |     var qs = []; | ||
|  | 
 | ||
|  |     Object.keys(params).forEach(function (key) { | ||
|  |       if ('scope' === key) { | ||
|  |         params[key] = oauth3.stringifyscope(params[key]); | ||
|  |       } | ||
|  |       qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); | ||
|  |     }); | ||
|  | 
 | ||
|  |     return qs.join('&'); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.createState = function () { | ||
|  |     // TODO mo' betta' random function
 | ||
|  |     // maybe gather some entropy from mouse / keyboard events?
 | ||
|  |     // (probably not, just use webCrypto or be sucky)
 | ||
|  |     return parseInt(Math.random().toString().replace('0.', ''), 10).toString('36'); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.normalizeProviderUri = function (providerUri) { | ||
|  |     // tested with
 | ||
|  |     //   example.com
 | ||
|  |     //   example.com/
 | ||
|  |     //   http://example.com
 | ||
|  |     //   https://example.com/
 | ||
|  |     providerUri = providerUri | ||
|  |       .replace(/^(https?:\/\/)?/, 'https://') | ||
|  |       .replace(/\/?$/, '') | ||
|  |       ; | ||
|  | 
 | ||
|  |     return providerUri; | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3._discoverHelper = function (providerUri, opts) { | ||
|  |     opts = opts || {}; | ||
|  |     var state = oauth3.createState(); | ||
|  |     var params; | ||
|  |     var url; | ||
|  | 
 | ||
|  |     params = { | ||
|  |       action: 'directives' | ||
|  |     , state: state | ||
|  |       // TODO this should be configurable (i.e. I want a dev vs production oauth3.html)
 | ||
|  |     , redirect_uri: window.location.protocol + '//' + window.location.host | ||
|  |         + window.location.pathname + 'oauth3.html' | ||
|  |     }; | ||
|  | 
 | ||
|  |     url = providerUri + '/oauth3.html#' + oauth3.querystringify(params); | ||
|  | 
 | ||
|  |     return oauth3.insertIframe(url, state, opts).then(function (directives) { | ||
|  |       return directives; | ||
|  |     }, function (err) { | ||
|  |       return oauth3.PromiseA.reject(err); | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.discover = function (providerUri, opts) { | ||
|  |     opts = opts || {}; | ||
|  | 
 | ||
|  |     console.log('DEBUG oauth3.discover', providerUri); | ||
|  |     console.log(opts); | ||
|  |     if (opts.directives) { | ||
|  |       return oauth3.PromiseA.resolve(opts.directives); | ||
|  |     } | ||
|  | 
 | ||
|  |     var promise; | ||
|  |     var promise2; | ||
|  |     var directives; | ||
|  |     var updatedAt; | ||
|  |     var fresh; | ||
|  | 
 | ||
|  |     providerUri = oauth3.normalizeProviderUri(providerUri); | ||
|  |     try { | ||
|  |       directives = JSON.parse(localStorage.getItem('oauth3.' + providerUri + '.directives')); | ||
|  |       console.log('DEBUG oauth3.discover cache', directives); | ||
|  |       updatedAt = localStorage.getItem('oauth3.' + providerUri + '.directives.updated_at'); | ||
|  |       console.log('DEBUG oauth3.discover updatedAt', updatedAt); | ||
|  |       updatedAt = new Date(updatedAt).valueOf(); | ||
|  |       console.log('DEBUG oauth3.discover updatedAt', updatedAt); | ||
|  |     } catch(e) { | ||
|  |       // ignore
 | ||
|  |     } | ||
|  | 
 | ||
|  |     fresh = (Date.now() - updatedAt) < (24 * 60 * 60 * 1000); | ||
|  | 
 | ||
|  |     if (directives) { | ||
|  |       promise = oauth3.PromiseA.resolve(directives); | ||
|  | 
 | ||
|  |       if (fresh) { | ||
|  |         //console.log('[local] [fresh directives]', directives);
 | ||
|  |         return promise; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     promise2 = oauth3._discoverHelper(providerUri, opts).then(function (params) { | ||
|  |       console.log('DEBUG oauth3._discoverHelper', params); | ||
|  |       var err; | ||
|  | 
 | ||
|  |       if (!params.directives) { | ||
|  |         err = new Error(params.error_description || "Unknown error when discoving provider '" + providerUri + "'"); | ||
|  |         err.code = params.error || "E_UNKNOWN_ERROR"; | ||
|  |         return oauth3.PromiseA.reject(err); | ||
|  |       } | ||
|  | 
 | ||
|  |       try { | ||
|  |         directives = JSON.parse(atob(params.directives)); | ||
|  |         console.log('DEBUG oauth3._discoverHelper directives', directives); | ||
|  |       } catch(e) { | ||
|  |         err = new Error(params.error_description || "could not parse directives for provider '" + providerUri + "'"); | ||
|  |         err.code = params.error || "E_PARSE_DIRECTIVE"; | ||
|  |         return oauth3.PromiseA.reject(err); | ||
|  |       } | ||
|  | 
 | ||
|  |       if ( | ||
|  |           (directives.authorization_dialog && directives.authorization_dialog.url) | ||
|  |         || (directives.access_token && directives.access_token.url) | ||
|  |       ) { | ||
|  |         // TODO lint directives
 | ||
|  |         localStorage.setItem('oauth3.' + providerUri + '.directives', JSON.stringify(directives)); | ||
|  |         localStorage.setItem('oauth3.' + providerUri + '.directives.updated_at', new Date().toISOString()); | ||
|  | 
 | ||
|  |         return oauth3.PromiseA.resolve(directives); | ||
|  |       } else { | ||
|  |         // ignore
 | ||
|  |         console.error("the directives provided by '" + providerUri + "' were invalid."); | ||
|  |         params.error = params.error || "E_INVALID_DIRECTIVE"; | ||
|  |         params.error_description = params.error_description | ||
|  |           || "directives did not include authorization_dialog.url"; | ||
|  |         err = new Error(params.error_description || "Unknown error when discoving provider '" + providerUri + "'"); | ||
|  |         err.code = params.error; | ||
|  |         return oauth3.PromiseA.reject(err); | ||
|  |       } | ||
|  |     }); | ||
|  | 
 | ||
|  |     return promise || promise2; | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.authorizationRedirect = function (providerUri, authorizationRedirect, opts) { | ||
|  |     //console.log('[authorizationRedirect]');
 | ||
|  |     //
 | ||
|  |     // Example Authorization Redirect - from Browser to Consumer API
 | ||
|  |     // (for generating a session securely on your own server)
 | ||
|  |     //
 | ||
|  |     // i.e. GET https://<<CONSUMER>>.com/api/org.oauth3.consumer/authorization_redirect/<<PROVIDER>>.com
 | ||
|  |     //
 | ||
|  |     // GET https://myapp.com/api/org.oauth3.consumer/authorization_redirect/`encodeURIComponent('example.com')`
 | ||
|  |     //  &scope=`encodeURIComponent('profile.login profile.email')`
 | ||
|  |     //
 | ||
|  |     // (optional)
 | ||
|  |     //  &state=`Math.random()`
 | ||
|  |     //  &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')`
 | ||
|  |     //
 | ||
|  |     // NOTE: This is not a request sent to the provider, but rather a request sent to the
 | ||
|  |     // consumer (your own API) which then sets some state and redirects.
 | ||
|  |     // This will initiate the `authorization_code` request on your server
 | ||
|  |     //
 | ||
|  |     opts = opts || {}; | ||
|  | 
 | ||
|  |     return oauth3.discover(providerUri, opts).then(function (directive) { | ||
|  |       if (!directive) { | ||
|  |         throw new Error("Developer Error: directive should exist when discovery is successful"); | ||
|  |       } | ||
|  | 
 | ||
|  |       var scope = opts.scope || directive.authn_scope; | ||
|  | 
 | ||
|  |       var state = Math.random().toString().replace(/^0\./, ''); | ||
|  |       var params = {}; | ||
|  |       var slimProviderUri = encodeURIComponent(providerUri.replace(/^(https?|spdy):\/\//, '')); | ||
|  | 
 | ||
|  |       params.state = state; | ||
|  |       if (scope) { | ||
|  |         params.scope = scope; | ||
|  |       } | ||
|  |       if (opts.redirectUri) { | ||
|  |         // this is really only for debugging
 | ||
|  |         params.redirect_uri = opts.redirectUri; | ||
|  |       } | ||
|  |       // Note: the type check is necessary because we allow 'true'
 | ||
|  |       // as an automatic mechanism when it isn't necessary to specify
 | ||
|  |       if ('string' !== typeof authorizationRedirect) { | ||
|  |         // TODO oauth3.json for self?
 | ||
|  |         authorizationRedirect = 'https://' + window.location.host | ||
|  |           + '/api/org.oauth3.consumer/authorization_redirect/:provider_uri'; | ||
|  |       } | ||
|  |       authorizationRedirect = authorizationRedirect | ||
|  |         .replace(/!(provider_uri)/, slimProviderUri) | ||
|  |         .replace(/:provider_uri/, slimProviderUri) | ||
|  |         .replace(/#{provider_uri}/, slimProviderUri) | ||
|  |         .replace(/{{provider_uri}}/, slimProviderUri) | ||
|  |         ; | ||
|  | 
 | ||
|  |       return oauth3.PromiseA.resolve({ | ||
|  |         url: authorizationRedirect + '?' + oauth3.querystringify(params) | ||
|  |       , method: 'GET' | ||
|  |       , state: state    // this becomes browser_state
 | ||
|  |       , params: params  // includes scope, final redirect_uri?
 | ||
|  |       }); | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.authorizationCode = function (/*providerUri, scope, redirectUri, clientId*/) { | ||
|  |     //
 | ||
|  |     // Example Authorization Code Request
 | ||
|  |     // (not for use in the browser)
 | ||
|  |     //
 | ||
|  |     // GET https://example.com/api/org.oauth3.provider/authorization_dialog
 | ||
|  |     //  ?response_type=code
 | ||
|  |     //  &scope=`encodeURIComponent('profile.login profile.email')`
 | ||
|  |     //  &state=`Math.random()`
 | ||
|  |     //  &client_id=xxxxxxxxxxx
 | ||
|  |     //  &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')`
 | ||
|  |     //
 | ||
|  |     // NOTE: `redirect_uri` itself may also contain URI-encoded components
 | ||
|  |     //
 | ||
|  |     // NOTE: This probably shouldn't be done in the browser because the server
 | ||
|  |     //   needs to initiate the state. If it is done in a browser, the browser
 | ||
|  |     //   should probably request 'state' from the server beforehand
 | ||
|  |     //
 | ||
|  | 
 | ||
|  |     throw new Error("not implemented"); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.implicitGrant = function (providerUri, opts) { | ||
|  |     //console.log('[implicitGrant]');
 | ||
|  |     //
 | ||
|  |     // Example Implicit Grant Request
 | ||
|  |     // (for generating a browser-only session, not a session on your server)
 | ||
|  |     //
 | ||
|  |     // GET https://example.com/api/org.oauth3.provider/authorization_dialog
 | ||
|  |     //  ?response_type=token
 | ||
|  |     //  &scope=`encodeURIComponent('profile.login profile.email')`
 | ||
|  |     //  &state=`Math.random()`
 | ||
|  |     //  &client_id=xxxxxxxxxxx
 | ||
|  |     //  &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')`
 | ||
|  |     //
 | ||
|  |     // NOTE: `redirect_uri` itself may also contain URI-encoded components
 | ||
|  |     //
 | ||
|  | 
 | ||
|  |     opts = opts || {}; | ||
|  |     var type = 'authorization_dialog'; | ||
|  |     var responseType = 'token'; | ||
|  | 
 | ||
|  |     return oauth3.discover(providerUri, opts).then(function (directive) { | ||
|  |       var redirectUri = opts.redirectUri; | ||
|  |       var scope = opts.scope || directive.authn_scope; | ||
|  |       var clientId = opts.appId; | ||
|  |       var args = directive[type]; | ||
|  |       var uri = args.url; | ||
|  |       var state = Math.random().toString().replace(/^0\./, ''); | ||
|  |       var params = {}; | ||
|  |       var loc; | ||
|  |       var result; | ||
|  | 
 | ||
|  |       params.state = state; | ||
|  |       params.response_type = responseType; | ||
|  |       if (scope) { | ||
|  |         if (Array.isArray(scope)) { | ||
|  |           scope = scope.join(' '); | ||
|  |         } | ||
|  |         params.scope = scope; | ||
|  |       } | ||
|  |       if (clientId) { | ||
|  |         // In OAuth3 client_id is optional for implicit grant
 | ||
|  |         params.client_id = clientId; | ||
|  |       } | ||
|  |       if (!redirectUri) { | ||
|  |         loc = window.location; | ||
|  |         redirectUri = loc.protocol + '//' + loc.host + loc.pathname; | ||
|  |         if ('/' !== redirectUri[redirectUri.length - 1]) { | ||
|  |           redirectUri += '/'; | ||
|  |         } | ||
|  |         redirectUri += 'oauth3.html'; | ||
|  |       } | ||
|  |       params.redirect_uri = redirectUri; | ||
|  | 
 | ||
|  |       uri += '?' + oauth3.querystringify(params); | ||
|  | 
 | ||
|  |       result = { | ||
|  |         url: uri | ||
|  |       , state: state | ||
|  |       , method: args.method | ||
|  |       , query: params | ||
|  |       }; | ||
|  |       return oauth3.PromiseA.resolve(result); | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   oauth3.resourceOwnerPassword = function (providerUri, username, passphrase, opts) { | ||
|  |     //
 | ||
|  |     // Example Resource Owner Password Request
 | ||
|  |     // (generally for 1st party and direct-partner mobile apps, and webapps)
 | ||
|  |     //
 | ||
|  |     // POST https://example.com/api/org.oauth3.provider/access_token
 | ||
|  |     //    { "grant_type": "password", "client_id": "<<id>>", "scope": "<<scope>>"
 | ||
|  |     //    , "username": "<<username>>", "password": "password" }
 | ||
|  |     //
 | ||
|  |     opts = opts || {}; | ||
|  |     var type = 'access_token'; | ||
|  |     var grantType = 'password'; | ||
|  | 
 | ||
|  |     return oauth3.discover(providerUri, opts).then(function (directive) { | ||
|  |       var scope = opts.scope || directive.authn_scope; | ||
|  |       var clientId = opts.appId; | ||
|  |       var clientAgreeTos = opts.clientAgreeTos; | ||
|  |       var clientUri = opts.clientUri; | ||
|  |       var args = directive[type]; | ||
|  |       var params = { | ||
|  |         "grant_type": grantType | ||
|  |       , "username": username | ||
|  |       , "password": passphrase | ||
|  |       //, "totp": opts.totp
 | ||
|  |       }; | ||
|  |       var uri = args.url; | ||
|  |       var body; | ||
|  |       if (opts.totp) { | ||
|  |         params.totp = opts.totp; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (clientId) { | ||
|  |         params.clientId = clientId; | ||
|  |       } | ||
|  |       if (clientUri) { | ||
|  |         params.clientUri = clientUri; | ||
|  |         params.clientAgreeTos = clientAgreeTos; | ||
|  |         if (!clientAgreeTos) { | ||
|  |           throw new Error('Developer Error: missing clientAgreeTos uri'); | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       if (scope) { | ||
|  |         if (Array.isArray(scope)) { | ||
|  |           scope = scope.join(' '); | ||
|  |         } | ||
|  |         params.scope = scope; | ||
|  |       } | ||
|  | 
 | ||
|  |       if ('GET' === args.method.toUpperCase()) { | ||
|  |         uri += '?' + oauth3.querystringify(params); | ||
|  |       } else { | ||
|  |         body = params; | ||
|  |       } | ||
|  | 
 | ||
|  |       return { | ||
|  |         url: uri | ||
|  |       , method: args.method | ||
|  |       , data: body | ||
|  |       }; | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   exports.OAUTH3 = oauth3.oauth3 = oauth3.OAUTH3 = oauth3; | ||
|  |   exports.oauth3 = exports.OAUTH3; | ||
|  | 
 | ||
|  |   if ('undefined' !== typeof module) { | ||
|  |     module.exports = oauth3; | ||
|  |   } | ||
|  | }('undefined' !== typeof exports ? exports : window)); |