From 4acad44297d044cfb9af63a2bfb815e3b5faa592 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 13 Feb 2017 17:53:54 -0700 Subject: [PATCH] WIP framing refactor --- oauth3.implicit.js | 370 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 324 insertions(+), 46 deletions(-) diff --git a/oauth3.implicit.js b/oauth3.implicit.js index 3f35c5c..fbcbc89 100644 --- a/oauth3.implicit.js +++ b/oauth3.implicit.js @@ -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,81 +355,195 @@ }); } - 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); }); } + , 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; + } + + return new OAUTH3.PromiseA(function (resolve, reject) { + var tok; + + function cleanup() { + delete window['--oauth3-callback-' + state]; + clearTimeout(tok); + tok = null; + } + + window['--oauth3-callback-' + state] = function (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 || 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: { - _frames: {} - , insert: function (url, state, opts) { - opts = opts || {}; + insert: function (url, state, opts) { + // TODO hidden / non-hidden (via directive even) + var framesrc = '