WIP framing refactor
This commit is contained in:
		
							parent
							
								
									bbc557c349
								
							
						
					
					
						commit
						4acad44297
					
				| @ -4,10 +4,13 @@ | ||||
| 
 | ||||
|   var OAUTH3 = exports.OAUTH3 = { | ||||
|     utils: { | ||||
|       atob: function (base64) { | ||||
|       clientUri: function (location) { | ||||
|         return OAUTH3.utils.uri.normalize(location.host + location.pathname); | ||||
|       } | ||||
|     , atob: function (base64) { | ||||
|         return (exports.atob || require('atob'))(base64); | ||||
|       } | ||||
|     , urlSafeBase64ToBase64: function (b64) { | ||||
|     , _urlSafeBase64ToBase64: function (b64) { | ||||
|         // URL-safe Base64 to Base64
 | ||||
|         // https://en.wikipedia.org/wiki/Base64
 | ||||
|         // https://gist.github.com/catwell/3046205
 | ||||
| @ -43,13 +46,6 @@ | ||||
|             ; | ||||
|         } | ||||
|       } | ||||
|       , getDefaultAppUrl: function () { | ||||
|         console.warn('[deprecated] using window.location.{protocol, host, pathname} when opts.client_id should be used'); | ||||
|         return window.location.protocol | ||||
|           + '//' + window.location.host | ||||
|           + (window.location.pathname).replace(/\/?$/, '') | ||||
|           ; | ||||
|       } | ||||
|     , query: { | ||||
|         stringify: function (params) { | ||||
|           var qs = []; | ||||
| @ -163,11 +159,10 @@ | ||||
| 
 | ||||
|       return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives.get(providerUri)).then(function (directives) { | ||||
|         if (directives && directives.issuer) { | ||||
|           return OAUTH3.PromiseA.resolve(directives); | ||||
|           return directives; | ||||
|         } | ||||
|         return OAUTH3._discoverHelper(providerUri, opts).then(function (directives) { | ||||
|           directives.issuer = directives.issuer || OAUTH3.utils.url.normalize(providerUri); | ||||
|           console.log('discoverHelper', directives); | ||||
|           // OAUTH3.PromiseA.resolve() is taken care of because this is wrapped
 | ||||
|           return OAUTH3.hooks.directives.set(providerUri, directives); | ||||
|         }); | ||||
| @ -177,12 +172,181 @@ | ||||
|   , _discoverHelper: function (providerUri, opts) { | ||||
|       return OAUTH3._browser.discover(providerUri, opts); | ||||
|     } | ||||
|   , request: function (preq) { | ||||
|       return OAUTH3._browser.request(preq); | ||||
|     } | ||||
|   , implicitGrant: function(providerUri, opts) { | ||||
|       var promise; | ||||
| 
 | ||||
|       if (opts.broker) { | ||||
|         promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts); | ||||
|       } | ||||
|       else { | ||||
|         promise = OAUTH3._implicitGrant(providerUri, opts); | ||||
|       } | ||||
| 
 | ||||
|       return promise.then(function (tokens) { | ||||
|         return OAUTH3.hooks.refreshSession( | ||||
|           opts.session || { | ||||
|             provider_uri: providerUri | ||||
|           , client_id: opts.client_id | ||||
|           , client_uri: opts.client_uri || opts.clientUri | ||||
|           } | ||||
|         , tokens | ||||
|         ); | ||||
|       }); | ||||
|     } | ||||
|   , _discoverThenImplicitGrant: function(providerUri, opts) { | ||||
|       opts.windowType = opts.windowType || 'popup'; | ||||
|       return OAUTH3._discover(providerUri, opts).then(function (directives) { | ||||
|         return OAUTH3._implicitGrant(directives, opts).then(function (tokens) { | ||||
|           OAUTH3._browser.closeFrame(tokens.state || opts._state); | ||||
|           opts._state = undefined; | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   , _discover: function(providerUri, opts) { | ||||
|       providerUri = OAUTH3.utils.url.normalize(providerUri); | ||||
| 
 | ||||
|       if (providerUri.match(OAUTH3._browser.window.location.hostname)) { | ||||
|         console.warn("It looks like you're a provider checking for your own directive," | ||||
|           + " so we we're just gonna use" | ||||
|           + " OAUTH3.request({ method: 'GET', url: '.well-known/oauth3/directive.json' })"); | ||||
|         return OAUTH3.request({ | ||||
|           method: 'GET' | ||||
|         , url: OAUTH3.utils.url.normalize(providerUri) + '/.well-known/oauth3/directives.json' | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       if (!(opts.client_id || opts.client_uri).match(OAUTH3._browser.window.location.hostname)) { | ||||
|         console.warn("It looks like your client_id doesn't match your current window..." | ||||
|           + " this probably won't end well"); | ||||
|         console.warn(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname); | ||||
|       } | ||||
| 
 | ||||
|       var discReq = OAUTH3.urls.discover( | ||||
|         providerUri | ||||
|       , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)) | ||||
|         , windowType: opts.broker && opts.windowType || 'background' | ||||
|         , broker: opts.broker | ||||
|         , debug: opts.debug | ||||
|         } | ||||
|       ); | ||||
|       opts._state = discReq.state; | ||||
|       //var discReq = OAUTH3.urls.discover(providerUri, opts);
 | ||||
| 
 | ||||
|       // hmm... we're gonna need a broker for this since switching windows is distracting,
 | ||||
|       // popups are obnoxious, iframes are sometimes blocked, and most servers don't implement CORS
 | ||||
|       // eventually it should be the browser (and postMessage may be a viable option now), but whatever...
 | ||||
| 
 | ||||
|       // TODO allow postMessage from providerUri in addition to callback
 | ||||
|       // TODO allow node to open a desktop browser window
 | ||||
|       return OAUTH3._browser.frameRequest( | ||||
|         discReq.url | ||||
|       , discReq.state | ||||
|       , { windowType: opts.windowType | ||||
|         , reuseWindow: opts.broker && '-broker' | ||||
|         , debug: opts.debug | ||||
|         } | ||||
|       ).then(function (params) { | ||||
|         // discWin.child.close()
 | ||||
|         // TODO params should have response_type indicating json, binary, etc
 | ||||
|         var directives = JSON.parse(OAUTH3.utils.atob(OAUTH3.utils.urlSafeBase64ToBase64(params.result || params.directives))); | ||||
|         return OAUTH3.hooks.directives.set(providerUri, directives); | ||||
|       }); | ||||
|     } | ||||
|   , _implicitGrant: function(providerUri, opts) { | ||||
|       // TODO this may need to be synchronous for browser security policy
 | ||||
|       return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives.get(providerUri)).then(function (directives) { | ||||
|         // Do some stuff
 | ||||
|         var authReq = OAUTH3.urls.implicitGrant( | ||||
|           directives | ||||
|         , { redirect_uri: opts.redirect_uri | ||||
|           , client_id: opts.client_id || opts.client_uri | ||||
|           , client_uri: opts.client_uri || opts.client_id | ||||
|           , state: opts._state | ||||
|           , debug: opts.debug | ||||
|           } | ||||
|         ); | ||||
| 
 | ||||
|         if (opts.debug) { | ||||
|           window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!"); | ||||
|         } | ||||
| 
 | ||||
|         return new OAUTH3.PromiseA(function (resolve, reject) { | ||||
|           return OAUTH3._browser.frameRequest( | ||||
|             authReq.url | ||||
|           , authReq.state // state should recycle params
 | ||||
|           , { windowType: opts.windowType | ||||
|             , reuseWindow: opts.broker && '-broker' | ||||
|             , debug: opts.debug | ||||
|             } | ||||
|           ).then(function (tokens) { | ||||
|             if (tokens.error) { | ||||
|               return reject(OAUTH3.utils._formatError(tokens.error)); | ||||
|             } | ||||
| 
 | ||||
|             OAUTH3._browser.closeFrame(authReq.state, { debug: opts.debug || tokens.debug }); | ||||
| 
 | ||||
|             return tokens; | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     //
 | ||||
|     // Let the Code Waste begin!!
 | ||||
|     //
 | ||||
|   , _browser: { | ||||
|       discover: function (providerUri, opts) { | ||||
|       window: window | ||||
|       // TODO we don't need to include this if we're using jQuery or angular
 | ||||
|     , request: function (preq, _sys) { | ||||
|         return new OAUTH3.PromiseA(function (resolve, reject) { | ||||
|           var xhr; | ||||
|           try { | ||||
|             xhr = new XMLHttpRequest(_sys); | ||||
|           } catch(e) { | ||||
|             xhr = new XMLHttpRequest(); | ||||
|           } | ||||
|           xhr.onreadystatechange = function () { | ||||
|             console.error('state change'); | ||||
|             var data; | ||||
|             if (xhr.readyState !== XMLHttpRequest.DONE) { | ||||
|               // nothing to do here
 | ||||
|               return; | ||||
|             } | ||||
| 
 | ||||
|             if (xhr.status !== 200) { | ||||
|               reject(new Error('bad status code: ' + xhr.status)); | ||||
|               return; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|               data = JSON.parse(xhr.responseText); | ||||
|             } catch(e) { | ||||
|               data = xhr.responseText; | ||||
|             } | ||||
| 
 | ||||
|             resolve({ | ||||
|               request: xhr | ||||
|             , data: data | ||||
|             , status: xhr.status | ||||
|             }); | ||||
|           }; | ||||
|           xhr.open(preq.method, preq.url, true); | ||||
|           var headers = preq.headers || {}; | ||||
|           Object.keys(headers).forEach(function (key) { | ||||
|             xhr.setRequestHeader(key, headers[key]); | ||||
|           }); | ||||
|           xhr.send(); | ||||
|         }); | ||||
|       } | ||||
|     , discover: function (providerUri, opts) { | ||||
|         opts = opts || {}; | ||||
|         //opts.debug = true;
 | ||||
|         providerUri = OAUTH3.utils.url.normalize(providerUri); | ||||
|         if (window.location.hostname.match(providerUri)) { | ||||
|         if (providerUri.match(OAUTH3._browser.window.location.hostname)) { | ||||
|           console.warn("It looks like you're a provider checking for your own directive," | ||||
|             + " so we we're just gonna use OAUTH3.request({ method: 'GET', url: '.well-known/oauth3/directive.json' })"); | ||||
|           return OAUTH3.request({ | ||||
| @ -191,36 +355,41 @@ | ||||
|           }); | ||||
|         } | ||||
| 
 | ||||
|         if (!window.location.hostname.match(opts.client_id || opts.client_uri)) { | ||||
|         if (!(opts.client_id || opts.client_uri).match(OAUTH3._browser.window.location.hostname)) { | ||||
|           console.warn("It looks like your client_id doesn't match your current window... this probably won't end well"); | ||||
|           console.warn(opts.client_id || opts.client_uri, window.location.hostname); | ||||
|           console.warn(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname); | ||||
|         } | ||||
|         var discObj = OAUTH3.urls.discover( | ||||
|           providerUri | ||||
|         , { client_id: (opts.client_id || opts.client_uri || OAUTH3.utils.getDefaultAppUrl()), debug: opts.debug } | ||||
|         , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)), debug: opts.debug } | ||||
|         ); | ||||
| 
 | ||||
|         // TODO ability to reuse iframe instead of closing
 | ||||
|         return OAUTH3._browser.iframe.insert(discObj.url, discObj.state, opts).then(function (params) { | ||||
|           OAUTH3._browser.iframe.remove(discObj.state); | ||||
|         return OAUTH3._browser._iframe.insert(discObj.url, discObj.state, opts).then(function (params) { | ||||
|           OAUTH3._browser.closeFrame(discObj.state, { debug: opts.debug || params.debug }); | ||||
|           if (params.error) { | ||||
|             return OAUTH3.utils._formatError(providerUri, params.error); | ||||
|           } | ||||
|           var directives = JSON.parse(OAUTH3.utils.atob(OAUTH3.utils.urlSafeBase64ToBase64(params.result || params.directives))); | ||||
|           var directives = JSON.parse(OAUTH3.utils.atob(OAUTH3.utils._urlSafeBase64ToBase64(params.result || params.directives))); | ||||
|           return directives; | ||||
|         }, function (err) { | ||||
|           OAUTH3._browser.iframe.remove(discObj.state); | ||||
|           OAUTH3._browser.closeFrame(discObj.state, { debug: opts.debug || err.debug }); | ||||
|           return OAUTH3.PromiseA.reject(err); | ||||
|         }); | ||||
|       } | ||||
|     , iframe: { | ||||
|         _frames: {} | ||||
|       , insert: function (url, state, opts) { | ||||
|     , frameRequest: function (url, state, opts) { | ||||
|         var previousFrame = OAUTH3._browser._frames[state]; | ||||
| 
 | ||||
|         if (!opts.windowType) { | ||||
|           opts.windowType = 'popup'; | ||||
|         } | ||||
| 
 | ||||
|         opts = opts || {}; | ||||
|         if (opts.debug) { | ||||
|           opts.timeout = opts.timeout || 15 * 60 * 1000; | ||||
|         } | ||||
|           var promise = new OAUTH3.PromiseA(function (resolve, reject) { | ||||
| 
 | ||||
|         return new OAUTH3.PromiseA(function (resolve, reject) { | ||||
|           var tok; | ||||
| 
 | ||||
|           function cleanup() { | ||||
| @ -241,6 +410,76 @@ | ||||
|             cleanup(); | ||||
|           }, opts.timeout || 15 * 1000); | ||||
| 
 | ||||
|           if ('background' === opts.windowType) { | ||||
|             if (previousFrame) { | ||||
|               previousFrame.location = url; | ||||
|               //promise = previousFrame.promise;
 | ||||
|             } | ||||
|             else { | ||||
|               OAUTH3._browser._iframe.insert(url, state, opts); | ||||
|             } | ||||
|           } else if ('popup' === opts.windowType) { | ||||
|             if (previousFrame) { | ||||
|               previousFrame.location = url; | ||||
|               if (opts.debug) { | ||||
|                 previousFrame.focus(); | ||||
|               } | ||||
|             } | ||||
|             else { | ||||
|               OAUTH3._browser.frame.open(url, state, opts); | ||||
|             } | ||||
|           } else if ('inline' === opts.windowType) { | ||||
|             // callback function will never execute and would need to redirect back to current page
 | ||||
|             // rather than the callback.html
 | ||||
|             url += '&original_url=' + OAUTH3._browser.window.location.href; | ||||
|             OAUTH3._browser.window.location = url; | ||||
|             //promise = OAUTH3.PromiseA.resolve({ url: url });
 | ||||
|             return; | ||||
|           } else { | ||||
|             throw new Error("login framing method options.windowType=" | ||||
|               + opts.windowType + " not type yet implemented"); | ||||
|           } | ||||
| 
 | ||||
|         }).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; | ||||
|             //_formatError
 | ||||
|             return OAUTH3.PromiseA.reject(err); | ||||
|           } | ||||
| 
 | ||||
|           return params; | ||||
|         }); | ||||
|       } | ||||
|     , closeFrame: function (state, opts) { | ||||
|         function close() { | ||||
|           try { | ||||
|             OAUTH3._browser._frames[state].close(); | ||||
|           } catch(e) { | ||||
|             try { | ||||
|               OAUTH3._browser._frames[state].remove(); | ||||
|             } catch(e) { | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           delete OAUTH3._browser._frames[state]; | ||||
|         } | ||||
| 
 | ||||
|         if (opts.debug) { | ||||
|           if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) { | ||||
|             close(); | ||||
|           } | ||||
|         } | ||||
|         else { | ||||
|           close(); | ||||
|         } | ||||
|       } | ||||
|     , _frames: {} | ||||
|     , iframe: { | ||||
|         insert: function (url, state, opts) { | ||||
|           // TODO hidden / non-hidden (via directive even)
 | ||||
|           var framesrc = '<iframe class="js-oauth3-iframe" src="' + url + '" '; | ||||
|           if (opts.debug) { | ||||
| @ -251,21 +490,60 @@ | ||||
|           } | ||||
|           framesrc += '></iframe>'; | ||||
| 
 | ||||
|             OAUTH3._browser.iframe._frames[state] = window.document.createElement('div'); | ||||
|             OAUTH3._browser.iframe._frames[state].innerHTML = framesrc; | ||||
|           var frame = OAUTH3._browser._frames[state] = OAUTH3._browser.window.document.createElement('div'); | ||||
|           OAUTH3._browser._frames[state].innerHTML = framesrc; | ||||
|           OAUTH3._browser.window.document.body.appendChild(OAUTH3._browser._frames[state]); | ||||
| 
 | ||||
|             window.document.body.appendChild(OAUTH3._browser.iframe._frames[state]); | ||||
|           return frame; | ||||
|         } | ||||
|       } | ||||
|     , frame: { | ||||
|         open: function (url, state, opts) { | ||||
|           if (opts.debug) { | ||||
|             opts.timeout = opts.timeout || 15 * 60 * 1000; | ||||
|           } | ||||
| 
 | ||||
|           var promise = new OAUTH3.PromiseA(function (resolve, reject) { | ||||
|             var tok; | ||||
| 
 | ||||
|             function cleanup() { | ||||
|               clearTimeout(tok); | ||||
|               tok = null; | ||||
|               delete window['--oauth3-callback-' + state]; | ||||
|               // close is done later in case the window is reused or self-closes synchronously itself / by parent
 | ||||
|               // (probably won't ever happen, but that's a negotiable implementation detail)
 | ||||
|             } | ||||
| 
 | ||||
|             window['--oauth3-callback-' + state] = function (params) { | ||||
|               console.log('YOLO!!'); | ||||
|               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); | ||||
| 
 | ||||
|             setTimeout(function () { | ||||
|               if (!promise.child) { | ||||
|                 reject("TODO: open the iframe first and discover oauth3 directives before popup"); | ||||
|                 cleanup(); | ||||
|               } | ||||
|             }, 0); | ||||
|           }); | ||||
| 
 | ||||
|           // TODO allow size changes (via directive even)
 | ||||
|           OAUTH3._browser._frames[state] = window.open( | ||||
|             url | ||||
|           , 'oauth3-login-' + (opts.reuseWindow || state) | ||||
|           , 'height=' + (opts.height || 720) + ',width=' + (opts.width || 620) | ||||
|           ); | ||||
|           // TODO periodically garbage collect expired handlers from window object
 | ||||
|           return promise; | ||||
|         } | ||||
|       , remove: function (state) { | ||||
|           if (OAUTH3._browser.iframe._frames[state]) { | ||||
|             OAUTH3._browser.iframe._frames[state].remove(); | ||||
|           } | ||||
|           delete OAUTH3._browser.iframe._frames[state]; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user