WIP window/iframe reuse

This commit is contained in:
AJ ONeal 2017-02-14 12:30:35 -07:00
parent 5a73cb1413
commit 4dd07c9f80
1 changed files with 178 additions and 60 deletions

View File

@ -22,6 +22,9 @@
} }
, uri: { , uri: {
normalize: function (uri) { normalize: function (uri) {
if ('string' !== typeof uri) {
console.error((new Error('stack')).stack);
}
// tested with // tested with
// example.com // example.com
// example.com/ // example.com/
@ -35,6 +38,9 @@
} }
, url: { , url: {
normalize: function (url) { normalize: function (url) {
if ('string' !== typeof url) {
console.error((new Error('stack')).stack);
}
// tested with // tested with
// example.com // example.com
// example.com/ // example.com/
@ -45,6 +51,15 @@
.replace(/\/?$/, '') .replace(/\/?$/, '')
; ;
} }
, resolve: function (base, next) {
if (/^https:\/\//i.test(next)) {
return next;
}
return this.normalize(base) + '/' + this._normalizePath(next);
}
, _normalizePath: function (path) {
return path.replace(/^\//, '').replace(/\/$/, '');
}
} }
, query: { , query: {
stringify: function (params) { stringify: function (params) {
@ -115,7 +130,7 @@
var params = { var params = {
action: 'directives' action: 'directives'
, state: OAUTH3.utils.randomState() , state: opts.state || OAUTH3.utils.randomState()
, redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/') , redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/')
, response_type: 'rpc' , response_type: 'rpc'
, _method: 'GET' , _method: 'GET'
@ -130,24 +145,95 @@
, query: params , query: params
}; };
return result;
}
, implicitGrant: function (directive, 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=`cryptoutil.random().toString('hex')`
// &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';
var scope = opts.scope || directive.authn_scope;
var args = directive[type];
var uri = args.url;
var state = opts.state || OAUTH3.utils.randomState();
console.log('implicit grant opts.state', opts.state);
var params = {
debug: opts.debug || undefined
, client_uri: opts.client_uri || opts.clientUri || undefined
, client_id: opts.client_id || opts.client_uri || undefined
};
var result;
params.state = state;
params.response_type = responseType;
if (scope) {
params.scope = OAUTH3.utils.scope.stringify(scope);
}
if (!opts.redirect_uri) {
// TODO consider making this optional
console.warn("auto-generating redirect_uri from hard-coded callback.html"
+ " (should be configurable... but then redirect_uri could just be manually-generated)");
opts.redirect_uri = OAUTH3.utils.url.resolve(
OAUTH3.utils.url.normalize(params.client_uri)
, '.well-known/oauth3/callback.html'
);
}
params.redirect_uri = opts.redirect_uri;
uri += '?' + OAUTH3.utils.query.stringify(params);
result = {
url: uri
, state: state
, method: args.method
, query: params
};
return result; return result;
} }
} }
, hooks: { , hooks: {
directives: { directives: {
get: function (providerUri) { _get: function (providerUri) {
providerUri = OAUTH3.utils.uri.normalize(providerUri); providerUri = OAUTH3.utils.uri.normalize(providerUri);
console.warn('[Warn] You should implement: OAUTH3.hooks.directives.get = function (providerUri) { return directives; }');
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._cache[providerUri] || this.get(providerUri))
.then(function (directives) {
// or do .then(this._set) to keep DRY?
OAUTH3.hooks.directives._cache[providerUri] = directives;
});
}
, _getCached: function (providerUri) {
return OAUTH3.hooks.directives._cache[providerUri];
}
, get: function (providerUri) {
console.warn('[Warn] You should implement: OAUTH3.hooks.directives.get = function (providerUri) { return directives; }');
return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}'); return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}');
} }
, set: function (providerUri, directives) { , _set: function (providerUri, directives) {
providerUri = OAUTH3.utils.uri.normalize(providerUri); providerUri = OAUTH3.utils.uri.normalize(providerUri);
console.warn('[Warn] You should implement: OAUTH3.hooks.directives.set = function (providerUri, directives) { return directives; }');
console.warn(directives);
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives));
OAUTH3.hooks.directives._cache[providerUri] = directives; OAUTH3.hooks.directives._cache[providerUri] = directives;
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives.set(providerUri, directives));
}
, set: function (providerUri, directives) {
console.warn('[Warn] You should implement: OAUTH3.hooks.directives.set = function (providerUri, directives) { return directives; }');
window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives));
return directives; return directives;
} }
} }
@ -161,17 +247,13 @@
if (directives && directives.issuer) { if (directives && directives.issuer) {
return directives; return directives;
} }
return OAUTH3._discoverHelper(providerUri, opts).then(function (directives) { return OAUTH3._discover(providerUri, opts).then(function (directives) {
directives.issuer = directives.issuer || OAUTH3.utils.url.normalize(providerUri); directives.issuer = directives.issuer || OAUTH3.utils.url.normalize(providerUri);
// OAUTH3.PromiseA.resolve() is taken care of because this is wrapped // OAUTH3.PromiseA.resolve() is taken care of because this is wrapped
return OAUTH3.hooks.directives.set(providerUri, directives); return OAUTH3.hooks.directives.set(providerUri, directives);
}); });
}); });
} }
// this is the browser version
, _discoverHelper: function (providerUri, opts) {
return OAUTH3._browser.discover(providerUri, opts);
}
, request: function (preq) { , request: function (preq) {
return OAUTH3._browser.request(preq); return OAUTH3._browser.request(preq);
} }
@ -179,10 +261,15 @@
var promise; var promise;
if (opts.broker) { if (opts.broker) {
// Discovery can happen in-flow because we know that this is
// a valid oauth3 provider
console.info("broker implicit grant");
promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts); promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts);
} }
else { else {
promise = OAUTH3._implicitGrant(providerUri, opts); // Discovery must take place before calling implicitGrant
console.info("direct implicit grant");
promise = OAUTH3._implicitGrant(OAUTH3.hooks.directives._getCached(providerUri), opts);
} }
return promise.then(function (tokens) { return promise.then(function (tokens) {
@ -199,9 +286,14 @@
, _discoverThenImplicitGrant: function(providerUri, opts) { , _discoverThenImplicitGrant: function(providerUri, opts) {
opts.windowType = opts.windowType || 'popup'; opts.windowType = opts.windowType || 'popup';
return OAUTH3._discover(providerUri, opts).then(function (directives) { return OAUTH3._discover(providerUri, opts).then(function (directives) {
console.info('discover complete');
console.log(directives);
console.log('DISCOVER COMPLETE opts._state', opts._state);
return OAUTH3._implicitGrant(directives, opts).then(function (tokens) { return OAUTH3._implicitGrant(directives, opts).then(function (tokens) {
console.info('implicit grant complete', tokens);
OAUTH3._browser.closeFrame(tokens.state || opts._state); OAUTH3._browser.closeFrame(tokens.state || opts._state);
opts._state = undefined; //opts._state = undefined;
return tokens;
}); });
}); });
} }
@ -229,6 +321,7 @@
, { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)) , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location))
, windowType: opts.broker && opts.windowType || 'background' , windowType: opts.broker && opts.windowType || 'background'
, broker: opts.broker , broker: opts.broker
, state: opts._state || undefined
, debug: opts.debug , debug: opts.debug
} }
); );
@ -242,7 +335,7 @@
// TODO allow postMessage from providerUri in addition to callback // TODO allow postMessage from providerUri in addition to callback
// TODO allow node to open a desktop browser window // TODO allow node to open a desktop browser window
return OAUTH3._browser.frameRequest( return OAUTH3._browser.frameRequest(
discReq.url OAUTH3.utils.url.resolve(providerUri, discReq.url)
, discReq.state , discReq.state
, { windowType: opts.windowType , { windowType: opts.windowType
, reuseWindow: opts.broker && '-broker' , reuseWindow: opts.broker && '-broker'
@ -255,41 +348,48 @@
return OAUTH3.hooks.directives.set(providerUri, directives); return OAUTH3.hooks.directives.set(providerUri, directives);
}); });
} }
, _implicitGrant: function(providerUri, opts) { , _implicitGrant: function(directives, opts) {
// TODO this may need to be synchronous for browser security policy // 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
// Do some stuff var authReq = OAUTH3.urls.implicitGrant(
var authReq = OAUTH3.urls.implicitGrant( directives
directives , { redirect_uri: opts.redirect_uri
, { redirect_uri: opts.redirect_uri , client_id: opts.client_id || opts.client_uri
, client_id: opts.client_id || opts.client_uri , client_uri: opts.client_uri || opts.client_id
, client_uri: opts.client_uri || opts.client_id , state: opts._state || undefined
, 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!");
}
if (opts._state) {
console.log('equal states authReq?', authReq.state === opts._state);
console.log(opts._state);
console.log(authReq.state);
}
return new OAUTH3.PromiseA(function (resolve, reject) {
console.log("framing request for implicit grant");
return OAUTH3._browser.frameRequest(
OAUTH3.utils.url.resolve(directives.issuer, authReq.url)
, authReq.state // state should recycle params
, { windowType: opts.windowType
, reuseWindow: opts.broker && '-broker'
, debug: opts.debug , debug: opts.debug
} }
); ).then(function (tokens) {
console.log("completed implicit grant");
if (tokens.error) {
// TODO directives.audience
return reject(OAUTH3.utils._formatError(directives.issuer /*providerUri*/, tokens));
}
if (opts.debug) { OAUTH3._browser.closeFrame(authReq.state, { debug: opts.debug || tokens.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 tokens;
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;
});
}); });
}); });
} }
@ -358,28 +458,40 @@
console.warn("It looks like your client_id doesn't match your current window... this probably won't end well"); 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); console.warn(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname);
} }
var discObj = OAUTH3.urls.discover( var discReq = OAUTH3.urls.discover(
providerUri providerUri
, { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)), debug: opts.debug } , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location))
, state: opts._state || undefined
, debug: opts.debug }
); );
if (opts._state) {
console.log('equal states discObj?', discReq.state === opts._state);
}
// TODO ability to reuse iframe instead of closing // TODO ability to reuse iframe instead of closing
opts._windowType = opts.windowType; opts._windowType = opts.windowType;
opts.windowType = opts.windowType || 'background'; opts.windowType = opts.windowType || 'background';
return OAUTH3._browser.frameRequest(discObj.url, discObj.state, opts).then(function (params) { return OAUTH3._browser.frameRequest(
OAUTH3.utils.url.resolve(providerUri, discReq.url)
, discReq.state
, opts
).then(function (params) {
opts.windowType = opts._windowType; opts.windowType = opts._windowType;
OAUTH3._browser.closeFrame(discObj.state, { debug: opts.debug || params.debug }); OAUTH3._browser.closeFrame(discReq.state, { debug: opts.debug || params.debug });
if (params.error) { if (params.error) {
return OAUTH3.utils._formatError(providerUri, params.error); // TODO directives.issuer || directives.audience
return OAUTH3.PromiseA.reject(OAUTH3.utils._formatError(providerUri, params));
} }
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; return directives;
}, function (err) { }, function (err) {
OAUTH3._browser.closeFrame(discObj.state, { debug: opts.debug || err.debug }); OAUTH3._browser.closeFrame(discReq.state, { debug: opts.debug || err.debug });
return OAUTH3.PromiseA.reject(err); return OAUTH3.PromiseA.reject(err);
}); });
} }
, frameRequest: function (url, state, opts) { , frameRequest: function (url, state, opts) {
console.log('frameRequest state', state);
opts = opts || {}; opts = opts || {};
var previousFrame = OAUTH3._browser._frames[state]; var previousFrame = OAUTH3._browser._frames[state];
@ -412,6 +524,8 @@
console.log('[oauth3.implicit.js] callbackName', '--oauth3-callback-' + state); console.log('[oauth3.implicit.js] callbackName', '--oauth3-callback-' + state);
window['--oauth3-callback-' + state] = function (params) { window['--oauth3-callback-' + state] = function (params) {
console.log("YO HO YO HO, A Pirate's life for me!", state);
console.error(new Error("Pirate's Life").stack);
resolve(params); resolve(params);
cleanup(); cleanup();
}; };
@ -434,20 +548,26 @@
if ('background' === windowType) { if ('background' === windowType) {
if (previousFrame) { if (previousFrame) {
console.log('previous frame in background');
previousFrame.location = url; previousFrame.location = url;
//promise = previousFrame.promise; //promise = previousFrame.promise;
} }
else { else {
console.log('NO previous frame in background');
OAUTH3._browser._frames[state] = OAUTH3._browser.iframe(url, state, opts); OAUTH3._browser._frames[state] = OAUTH3._browser.iframe(url, state, opts);
} }
} else if ('popup' === windowType) { } else if ('popup' === windowType) {
if (previousFrame) { if (previousFrame) {
console.log('previous frame in pop');
console.log(previousFrame);
console.log(url);
previousFrame.location = url; previousFrame.location = url;
if (opts.debug) { if (opts.debug) {
previousFrame.focus(); previousFrame.focus();
} }
} }
else { else {
console.log('NO previous frame in popup');
OAUTH3._browser._frames[state] = OAUTH3._browser.frame(url, state, opts); OAUTH3._browser._frames[state] = OAUTH3._browser.frame(url, state, opts);
} }
} else if ('inline' === windowType) { } else if ('inline' === windowType) {
@ -463,27 +583,25 @@
} }
}).then(function (params) { }).then(function (params) {
var err; console.log('frameRequest formatting params (weird that this place exists, but not weird to be here)');
if (params.error) {
if (params.error || params.error_description) { // TODO directives.issuer || directives.audience
err = new Error(params.error_description || "Unknown response error"); return OAUTH3.PromiseA.reject(OAUTH3.utils._formatError('https://oauth3.org', params));
err.code = params.error || "E_UKNOWN_ERROR";
err.params = params;
//_formatError
return OAUTH3.PromiseA.reject(err);
} }
return params; return params;
}); });
} }
, closeFrame: function (state, opts) { , closeFrame: function (state, opts) {
function close() { function close() {
console.log("Attempting to close... ", OAUTH3._browser._frames[state]);
try { try {
OAUTH3._browser._frames[state].close(); OAUTH3._browser._frames[state].close();
} catch(e) { } catch(e) {
console.error(e);
try { try {
OAUTH3._browser._frames[state].remove(); OAUTH3._browser._frames[state].remove();
} catch(e) { } catch(e) {
console.error(e);
} }
} }