diff --git a/oauth3.implicit.js b/oauth3.implicit.js index 4494500..0d2acb0 100644 --- a/oauth3.implicit.js +++ b/oauth3.implicit.js @@ -22,6 +22,9 @@ } , uri: { normalize: function (uri) { + if ('string' !== typeof uri) { + console.error((new Error('stack')).stack); + } // tested with // example.com // example.com/ @@ -35,6 +38,9 @@ } , url: { normalize: function (url) { + if ('string' !== typeof url) { + console.error((new Error('stack')).stack); + } // tested with // example.com // example.com/ @@ -45,6 +51,15 @@ .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: { stringify: function (params) { @@ -115,7 +130,7 @@ var params = { action: 'directives' - , state: OAUTH3.utils.randomState() + , state: opts.state || OAUTH3.utils.randomState() , redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/') , response_type: 'rpc' , _method: 'GET' @@ -130,24 +145,95 @@ , 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; } } , hooks: { directives: { - get: function (providerUri) { + _get: function (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 = {}; } + 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) || '{}'); } - , set: function (providerUri, directives) { + , _set: function (providerUri, directives) { 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 = {}; } - window.localStorage.setItem('directives-' + providerUri, JSON.stringify(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; } } @@ -161,17 +247,13 @@ if (directives && directives.issuer) { 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); // OAUTH3.PromiseA.resolve() is taken care of because this is wrapped 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) { return OAUTH3._browser.request(preq); } @@ -179,10 +261,15 @@ var promise; 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); } 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) { @@ -199,9 +286,14 @@ , _discoverThenImplicitGrant: function(providerUri, opts) { opts.windowType = opts.windowType || 'popup'; 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) { + console.info('implicit grant complete', tokens); 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)) , windowType: opts.broker && opts.windowType || 'background' , broker: opts.broker + , state: opts._state || undefined , debug: opts.debug } ); @@ -242,7 +335,7 @@ // TODO allow postMessage from providerUri in addition to callback // TODO allow node to open a desktop browser window return OAUTH3._browser.frameRequest( - discReq.url + OAUTH3.utils.url.resolve(providerUri, discReq.url) , discReq.state , { windowType: opts.windowType , reuseWindow: opts.broker && '-broker' @@ -255,41 +348,48 @@ 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 - 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 + // 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 || undefined + , 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 } - ); + ).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) { - window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!"); - } + OAUTH3._browser.closeFrame(authReq.state, { debug: opts.debug || tokens.debug }); - 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; - }); + 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(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname); } - var discObj = OAUTH3.urls.discover( + var discReq = OAUTH3.urls.discover( 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 opts._windowType = opts.windowType; 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; - OAUTH3._browser.closeFrame(discObj.state, { debug: opts.debug || params.debug }); + OAUTH3._browser.closeFrame(discReq.state, { debug: opts.debug || params.debug }); 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))); return directives; }, 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); }); } , frameRequest: function (url, state, opts) { + console.log('frameRequest state', state); opts = opts || {}; var previousFrame = OAUTH3._browser._frames[state]; @@ -412,6 +524,8 @@ console.log('[oauth3.implicit.js] callbackName', '--oauth3-callback-' + state); 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); cleanup(); }; @@ -434,20 +548,26 @@ if ('background' === windowType) { if (previousFrame) { + console.log('previous frame in background'); previousFrame.location = url; //promise = previousFrame.promise; } else { + console.log('NO previous frame in background'); OAUTH3._browser._frames[state] = OAUTH3._browser.iframe(url, state, opts); } } else if ('popup' === windowType) { if (previousFrame) { + console.log('previous frame in pop'); + console.log(previousFrame); + console.log(url); previousFrame.location = url; if (opts.debug) { previousFrame.focus(); } } else { + console.log('NO previous frame in popup'); OAUTH3._browser._frames[state] = OAUTH3._browser.frame(url, state, opts); } } else if ('inline' === windowType) { @@ -463,27 +583,25 @@ } }).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); + console.log('frameRequest formatting params (weird that this place exists, but not weird to be here)'); + if (params.error) { + // TODO directives.issuer || directives.audience + return OAUTH3.PromiseA.reject(OAUTH3.utils._formatError('https://oauth3.org', params)); } - return params; }); } , closeFrame: function (state, opts) { function close() { + console.log("Attempting to close... ", OAUTH3._browser._frames[state]); try { OAUTH3._browser._frames[state].close(); } catch(e) { + console.error(e); try { OAUTH3._browser._frames[state].remove(); } catch(e) { + console.error(e); } }