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 = '';
+
+ 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]);
+
+ 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() {
- delete window['--oauth3-callback-' + state];
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 iframe request did not complete within 15 seconds");
+ var err = new Error("the windowed request did not complete within 3 minutes");
err.code = "E_TIMEOUT";
reject(err);
cleanup();
- }, opts.timeout || 15 * 1000);
+ }, opts.timeout || 3 * 60 * 1000);
- // TODO hidden / non-hidden (via directive even)
- var framesrc = '';
-
- OAUTH3._browser.iframe._frames[state] = window.document.createElement('div');
- OAUTH3._browser.iframe._frames[state].innerHTML = framesrc;
-
- window.document.body.appendChild(OAUTH3._browser.iframe._frames[state]);
+ 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];
- }
}
}
};