diff --git a/oauth3.core.js b/oauth3.core.js new file mode 100644 index 0000000..bc4ad35 --- /dev/null +++ b/oauth3.core.js @@ -0,0 +1,1008 @@ +/* global Promise */ +;(function (exports) { + 'use strict'; + + var OAUTH3 = exports.OAUTH3 = { + clientUri: function (location) { + return OAUTH3.uri.normalize(location.host + location.pathname); + } + , error: { + parse: function (providerUri, params) { + var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'"); + err.uri = params.error_uri || params.error.uri; + err.code = params.error.code || params.error; + return err; + } + } + , _base64: { + atob: function (base64) { + // atob must be called from the global context + // http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome + return (exports.atob || require('atob'))(base64); + } + , decodeUrlSafe: function (b64) { + // URL-safe Base64 to Base64 + // https://en.wikipedia.org/wiki/Base64 + // https://gist.github.com/catwell/3046205 + var mod = b64.length % 4; + if (2 === mod) { b64 += '=='; } + if (3 === mod) { b64 += '='; } + b64 = b64.replace(/-/g, '+').replace(/_/g, '/'); + return OAUTH3._base64.atob(b64); + } + } + , uri: { + normalize: function (uri) { + if ('string' !== typeof uri) { + console.error((new Error('stack')).stack); + } + // tested with + // example.com + // example.com/ + // http://example.com + // https://example.com/ + return uri + .replace(/^(https?:\/\/)?/i, '') + .replace(/\/?$/, '') + ; + } + } + , url: { + normalize: function (url) { + if ('string' !== typeof url) { + console.error((new Error('stack')).stack); + } + // tested with + // example.com + // example.com/ + // http://example.com + // https://example.com/ + return url + .replace(/^(https?:\/\/)?/i, 'https://') + .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) { + var qs = []; + + Object.keys(params).forEach(function (key) { + // TODO nullify instead? + if ('undefined' === typeof params[key]) { + return; + } + + if ('scope' === key) { + params[key] = OAUTH3.scope.stringify(params[key]); + } + + qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); + }); + + return qs.join('&'); + } + } + , scope: { + stringify: function (scope) { + if (Array.isArray(scope)) { + scope = scope.join(' '); + } + return scope; + } + } + , randomState: function () { + // TODO put in different file for browser vs node + try { + return Array.prototype.slice.call( + window.crypto.getRandomValues(new Uint8Array(16)) + ).map(function (ch) { return (ch).toString(16); }).join(''); + } catch(e) { + return OAUTH3.utils._insecureRandomState(); + } + } + , _insecureRandomState: function () { + var i; + var ch; + var str; + // TODO use fisher-yates on 0..255 and select [0] 16 times + // [security] https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.5qx0bf95a + // https://github.com/v8/v8/blob/b0e4dce6091a8777bda80d962df76525dc6c5ea9/src/js/math.js#L135-L144 + // Note: newer versions of v8 do not have this bug, but other engines may still + console.warn('[security] crypto.getRandomValues() failed, falling back to Math.random()'); + str = ''; + for (i = 0; i < 32; i += 1) { + ch = Math.round(Math.random() * 255).toString(16); + if (ch.length < 2) { ch = '0' + ch; } + str += ch; + } + return str; + } + , jwt: { + // decode only (no verification) + decode: function (str) { + + // 'abc.qrs.xyz' + // [ 'abc', 'qrs', 'xyz' ] + // [ {}, {}, 'foo' ] + // { header: {}, payload: {}, signature: '' } + var parts = str.split(/\./g); + var jsons = parts.slice(0, 2).map(function (urlsafe64) { + var b64 = OAUTH3._base64.decodeUrlSafe(urlsafe64); + return b64; + }); + + return { + header: JSON.parse(jsons[0]) + , payload: JSON.parse(jsons[1]) + , signature: parts[2] // should remain url-safe base64 + }; + } + , freshness: function (tokenMeta, staletime, _now) { + staletime = staletime || (15 * 60); + var now = _now || Date.now(); + var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000)); + + if (fresh >= staletime) { + return 'fresh'; + } + + if (fresh <= 0) { + return 'expired'; + } + + return 'stale'; + } + } + , urls: { + discover: function (providerUri, opts) { + if (!providerUri) { + throw new Error("cannot discover without providerUri"); + } + if (!opts.client_id) { + throw new Error("cannot discover without options.client_id"); + } + var clientId = OAUTH3.url.normalize(opts.client_id || opts.client_uri); + providerUri = OAUTH3.url.normalize(providerUri); + + var params = { + action: 'directives' + , state: opts.state || OAUTH3.utils.randomState() + , redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/') + , response_type: 'rpc' + , _method: 'GET' + , _pathname: '.well-known/oauth3/directives.json' + , debug: opts.debug || undefined + }; + + var result = { + url: providerUri + '/.well-known/oauth3/#/?' + OAUTH3.query.stringify(params) + , state: params.state + , method: 'GET' + , query: params + }; + + return result; + } + , implicitGrant: function (directive, opts) { + // + // 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(); + var params = { + debug: opts.debug || undefined + , client_uri: opts.client_uri || opts.clientUri || undefined + , client_id: opts.client_id || opts.client_uri || undefined + , state: state + }; + var result; + + params.response_type = responseType; + if (scope) { + params.scope = OAUTH3.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.url.resolve( + OAUTH3.url.normalize(params.client_uri) + , '.well-known/oauth3/callback.html' + ); + } + params.redirect_uri = opts.redirect_uri; + + uri += '?' + OAUTH3.query.stringify(params); + + result = { + url: uri + , state: state + , method: args.method + , query: params + }; + + return result; + } + , refreshToken: function (directive, opts) { + // grant_type=refresh_token + + // Example Refresh Token Request + // (generally for 1st or 3rd party server-side, mobile, and desktop apps) + // + // POST https://example.com/api/oauth3/access_token + // { "grant_type": "refresh_token", "client_id": "<>", "scope": "<>" + // , "username": "<>", "password": "password" } + // + opts = opts || {}; + var type = 'access_token'; + var grantType = 'refresh_token'; + + var scope = opts.scope || directive.authn_scope; + var clientSecret = opts.client_secret; + var args = directive[type]; + var params = { + "grant_type": grantType + , "refresh_token": opts.refresh_token || (opts.session && opts.session.refresh_token) + , "response_type": 'token' + , "client_id": opts.client_id || opts.client_uri + , "client_uri": opts.client_uri + //, "scope": undefined + //, "client_secret": undefined + , debug: opts.debug || undefined + }; + var uri = args.url; + var body; + + if (clientSecret) { + // TODO not allowed in the browser + console.warn("if this is a browser, you must not use client_secret"); + params.client_secret = clientSecret; + } + + if (scope) { + params.scope = OAUTH3.scope.stringify(scope); + } + + if ('GET' === args.method.toUpperCase()) { + uri += '?' + OAUTH3.query.stringify(params); + } else { + body = params; + } + + return { + url: uri + , method: args.method + , data: body + }; + } + , logout: function (directive, opts) { + // action=logout + + // Example Logout Request + // (generally for 1st or 3rd party server-side, mobile, and desktop apps) + // + // GET https://example.com/#/logout/ + // ?client_id=<> + // &access_token=<> + // &sub=<> + // + // Note that the use of # keeps certain parameters from traveling across + // the network at all (and we use https anyway) + // + opts = opts || {}; + var action = 'logout'; + var args = directive[action]; + var state = opts.state || OAUTH3.utils.randomState(); + var params = { + action: action + //, response_type: 'confirmation' + , client_id: opts.client_id || opts.client_uri + , client_uri: opts.client_uri || opts.client_id + , state: state + , redirect_uri: opts.redirect_uri = OAUTH3.url.resolve( + OAUTH3.url.normalize(opts.client_uri || opts.client_id) + , '.well-known/oauth3/callback.html' + ) + , debug: opts.debug + }; + var uri = args.url; + var body; + + if ('GET' === args.method.toUpperCase()) { + uri += '?' + OAUTH3.query.stringify(params); + } else { + body = params; + } + + return { + url: OAUTH3.url.resolve(directive.issuer, uri) + , method: args.method + , state: state + , data: body + }; + } + } + , hooks: { + directives: { + _get: function (providerUri) { + providerUri = OAUTH3.uri.normalize(providerUri); + if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } + return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._cache[providerUri] + || OAUTH3.hooks.directives.get(providerUri)) + .then(function (directives) { + // or do .then(this._set) to keep DRY? + OAUTH3.hooks.directives._cache[providerUri] = directives; + return directives; + }); + } + , _getCached: function (providerUri) { + providerUri = OAUTH3.uri.normalize(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) { + providerUri = OAUTH3.uri.normalize(providerUri); + if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } + 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; + } + } + , session: { + refresh: function (oldSession, newSession) { + var providerUri = oldSession.provider_uri; + var clientUri = oldSession.client_uri; + + Object.keys(oldSession).forEach(function (key) { + oldSession[key] = undefined; + }); + Object.keys(newSession).forEach(function (key) { + oldSession[key] = newSession[key]; + }); + + // info about the session of this API call + oldSession.provider_uri = providerUri; // aud + oldSession.client_uri = clientUri; // azp + + // info about the newly-discovered token + oldSession.token = OAUTH3.jwt.decode(oldSession.access_token).payload; + + oldSession.token.sub = oldSession.token.sub || oldSession.token.acx.id; + oldSession.token.client_uri = clientUri; + oldSession.token.provider_uri = providerUri; + + if (oldSession.refresh_token) { + oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token).payload; + oldSession.refresh.sub = oldSession.refresh.sub || oldSession.refresh.acx.id; + oldSession.refresh.provider_uri = providerUri; + } + + // set for a set of audiences + return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.set(providerUri, oldSession)); + } + , check: function (preq, opts) { + if (!preq.session) { + return OAUTH3.PromiseA.resolve(null); + } + var freshness = OAUTH3.jwt.freshness(preq.session.token, opts.staletime); + + switch (freshness) { + case 'stale': + return OAUTH3.hooks.session.stale(preq.session); + case 'expired': + return OAUTH3.hooks.session.expired(preq.session).then(function (newSession) { + preq.session = newSession; + return newSession; + }); + //case 'fresh': + default: + return OAUTH3.PromiseA.resolve(preq.session); + } + } + , stale: function (staleSession) { + if (OAUTH3.hooks.session._stalePromise) { + return OAUTH3.PromiseA.resolve(staleSession); + } + + OAUTH3.hooks.session._stalePromise = OAUTH3._refreshToken( + staleSession.provider_uri + , { client_uri: staleSession.client_uri + , session: staleSession + , debug: staleSession.debug + } + ).then(function (newSession) { + OAUTH3.hooks.session._stalePromise = null; + return newSession; // oauth3.hooks.refreshSession(staleSession, newSession); + }, function () { + OAUTH3.hooks.session._stalePromise = null; + }); + + return OAUTH3.PromiseA.resolve(staleSession); + } + , expired: function (expiredSession) { + return OAUTH3._refreshToken( + expiredSession.provider_uri + , { client_uri: expiredSession.client_uri + , session: expiredSession + , debug: expiredSession.debug + } + ).then(function (newSession) { + return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession); + }); + } + , set: function (providerUri, newSession) { + if (!providerUri) { + console.error(new Error('no providerUri').stack); + throw new Error("providerUri is not set"); + } + providerUri = OAUTH3.uri.normalize(providerUri); + console.warn('[Warn] Please implement OAUTH3.hooks.session.set = function (providerUri, newSession) { return PromiseA; }'); + if (!OAUTH3.hooks.session._sessions) { OAUTH3.hooks.session._sessions = {}; } + OAUTH3.hooks.session._sessions[providerUri] = newSession; + return OAUTH3.PromiseA.resolve(newSession); + } + , _getCached: function (providerUri) { + providerUri = OAUTH3.uri.normalize(providerUri); + return OAUTH3.hooks.session._sessions[providerUri]; + } + , get: function (providerUri) { + providerUri = OAUTH3.uri.normalize(providerUri); + if (!providerUri) { + throw new Error("providerUri is not set"); + } + console.warn('[Warn] Please implement OAUTH3.hooks.session.get = function (providerUri) { return PromiseA; }'); + if (!OAUTH3.hooks.session._sessions) { OAUTH3.hooks.session._sessions = {}; } + return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session._sessions[providerUri]); + } + } + } + , discover: function (providerUri, opts) { + if (!providerUri) { + throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri); + } + + return OAUTH3.hooks.directives._get(providerUri).then(function (directives) { + if (directives && directives.issuer) { + return directives; + } + return OAUTH3._discoverHelper(providerUri, opts).then(function (directives) { + directives.azp = directives.azp || OAUTH3.url.normalize(providerUri); + directives.issuer = directives.issuer || OAUTH3.url.normalize(providerUri); + // OAUTH3.PromiseA.resolve() is taken care of because this is wrapped + return OAUTH3.hooks.directives._set(providerUri, directives); + }); + }); + } + , _discoverHelper: function(providerUri, opts) { + return OAUTH3._browser.discover(providerUri, opts); + } + , request: function (preq, opts) { + function fetch() { + if (preq.session) { + // TODO check session.token.aud against preq.url to make sure they match + console.warn("[security] session audience checking has not been implemented yet (it's up to you to check)"); + preq.headers = preq.headers || {}; + preq.headers.Authorization = 'Bearer ' + (preq.session.access_token || preq.session.accessToken); + } + + return OAUTH3._requestHelper(preq, opts); + } + + OAUTH3.url.resolve(preq.providerUri || preq.provider_uri || preq.directives && preq.directives.issuer, preq.url); + + if (!preq.session) { + return fetch(); + } + + return OAUTH3.hooks.session.check(preq, opts).then(fetch); + } + , _requestHelper: function (preq, opts) { + return OAUTH3._browser.request(preq, opts); + } + , implicitGrant: function(directives, opts) { + var promise; + var providerUri = directives.azp || directives.issuer || directives; + + if (opts.broker) { + // Discovery can happen in-flow because we know that this is + // a valid oauth3 provider + promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts); + } + else { + // Discovery must take place before calling implicitGrant + promise = OAUTH3._implicitGrant(OAUTH3.hooks.directives._getCached(providerUri), opts); + } + + return promise.then(function (tokens) { + // TODO abstract browser bits away + try { + OAUTH3._browser.closeFrame(tokens.state || opts._state, opts); + } catch(e) { + console.warn("[implicitGrant] TODO abstract browser bits away"); + } + opts._state = undefined; + return OAUTH3.hooks.session.refresh( + 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) { + return tokens; + }); + }); + } + , _implicitGrant: function(directives, opts) { + // TODO this may need to be synchronous for browser security policy + // 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!"); + } + + return OAUTH3._browser.frameRequest( + OAUTH3.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) { + if (tokens.error) { + // TODO directives.audience + return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, tokens)); + } + + return tokens; + }); + } + , _refreshToken: function (providerUri, opts) { + return OAUTH3.discover(providerUri, opts).then(function (directive) { + var prequest = OAUTH3.urls.refreshToken(directive, opts); + + return OAUTH3.request(prequest).then(function (req) { + var data = req.data; + data.provider_uri = providerUri; + if (data.error) { + return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data)); + } + return OAUTH3.hooks.session.refresh(opts, data); + }); + }); + } + , logout: function(providerUri, opts) { + return OAUTH3._logoutHelper(OAUTH3.hooks.directives._getCached(providerUri), opts); + } + , _logoutHelper: function(directives, opts) { + var logoutReq = OAUTH3.urls.logout( + directives + , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)) + , windowType: 'popup' // we'll figure out background later + , broker: opts.broker + //, state: opts._state + , debug: opts.debug + } + ); + + return OAUTH3._browser.frameRequest( + OAUTH3.url.resolve(directives.issuer, logoutReq.url) + , logoutReq.state // state should recycle params + , { windowType: 'popup' + , reuseWindow: opts.broker && '-broker' + , debug: opts.debug + } + ).then(function (params) { + OAUTH3._browser.closeFrame(params.state || opts._state, opts); + + if (params.error) { + // TODO directives.audience + return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params)); + } + + return params; + }); + } + + + // + // Let the Code Waste begin!! + // + , _browser: { + window: window + // TODO we don't need to include this if we're using jQuery or angular + , discover: function(providerUri, opts) { + opts = opts || {}; + providerUri = OAUTH3.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.url.normalize(providerUri) + '/.well-known/oauth3/directives.json' + }).then(function (resp) { + return resp.data; + }); + } + + 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 + , state: opts._state || undefined + , 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 + opts._windowType = opts.windowType; + opts.windowType = opts.windowType || 'background'; + return OAUTH3._browser.frameRequest( + OAUTH3.url.resolve(providerUri, discReq.url) + , discReq.state + // why not just pass opts whole? + , { windowType: opts.windowType + , reuseWindow: opts.broker && '-broker' + , debug: opts.debug + } + ).then(function (params) { + opts.windowType = opts._windowType; + + // caller will call OAUTH3._browser.closeFrame(discReq.state, { debug: opts.debug || params.debug }); + if (params.error) { + // TODO directives.issuer || directives.audience + return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, params)); + } + + // TODO params should have response_type indicating json, binary, etc + var directives = JSON.parse(OAUTH3._base64.decodeUrlSafe(params.result || params.directives)); + // caller will call OAUTH3.hooks.directives._set(providerUri, directives); + return directives; + }); + } + , 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 () { + 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(); + }); + } + , frameRequest: function (url, state, opts) { + opts = opts || {}; + var previousFrame = OAUTH3._browser._frames[state]; + + var windowType = opts.windowType; + if (!windowType) { + windowType = 'popup'; + } + + var timeout = opts.timeout; + if (opts.debug) { + timeout = timeout || 3 * 60 * 1000; + } + else { + timeout = timeout || ('background' === windowType ? 15 * 1000 : 3 * 60 * 1000); + } + + return new OAUTH3.PromiseA(function (resolve, reject) { + // TODO periodically garbage collect expired handlers from window object + var tok; + + function cleanup() { + delete window['--oauth3-callback-' + state]; + clearTimeout(tok); + tok = null; + // the actual close is done later (by the caller) so that the window/frame + // can be 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) { + resolve(params); + cleanup(); + }; + + tok = setTimeout(function () { + var err = new Error( + "the '" + windowType + "' request did not complete within " + Math.round(timeout / 1000) + "s" + ); + err.code = "E_TIMEOUT"; + reject(err); + cleanup(); + }, timeout); + + setTimeout(function () { + if (!OAUTH3._browser._frames[state]) { + reject(new Error("TODO: open the iframe first and discover oauth3 directives before popup")); + cleanup(); + } + }, 0); + + if ('background' === windowType) { + if (previousFrame) { + previousFrame.location = url; + //promise = previousFrame.promise; + } + else { + OAUTH3._browser._frames[state] = OAUTH3._browser.iframe(url, state, opts); + } + } else if ('popup' === windowType) { + if (previousFrame) { + previousFrame.location = url; + if (opts.debug) { + previousFrame.focus(); + } + } + else { + OAUTH3._browser._frames[state] = OAUTH3._browser.frame(url, state, opts); + } + } else if ('inline' === 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) { + if (params.error) { + // TODO directives.issuer || directives.audience + return OAUTH3.PromiseA.reject(OAUTH3.error.parse('https://oauth3.org', params)); + } + return params; + }); + } + , closeFrame: function (state, opts) { + opts = opts || {}; + function close() { + try { + OAUTH3._browser._frames[state].close(); + } catch(e) { + try { + OAUTH3._browser._frames[state].remove(); + } catch(e) { + console.error(new Error("Could not clase window/iframe. closeFrame may have been called twice.")); + } + } + + delete OAUTH3._browser._frames[state]; + } + + if (opts.debug) { + if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) { + close(); + } + } + else { + close(); + } + } + , _frames: {} + , iframe: function (url, state, opts) { + var framesrc = '