/* global Promise */ ;(function (exports) { 'use strict'; if ('undefined' !== typeof window && 'https:' !== window.location.protocol) { window.alert("You must use https. We suggest using caddy as your webserver (or serve-https if testing locally)"); } 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; } } , _binStr: { bufferToBinStr: function (buf) { return Array.prototype.map.call(new Uint8Array(buf), function(ch) { return String.fromCharCode(ch); }).join(''); } , binStrToBuffer: function (str) { var buf; if ('undefined' !== typeof Uint8Array) { buf = new Uint8Array(str.length); } else { buf = []; } Array.prototype.forEach.call(str, function (ch, ind) { buf[ind] = ch.charCodeAt(0); }); return buf; } } , _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); } , btoa: function (b64) { // for directive passing in .well-known/oauth3 // http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome return (exports.btoa || require('btoa'))(b64); } , 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); } , encodeUrlSafe: function (b64) { // for directive passing in .well-known/oauth3 // Base64 to URL-safe Base64 b64 = OAUTH3._base64.btoa(b64); b64 = b64.replace(/\+/g, '-').replace(/\//g, '_'); b64 = b64.replace(/=+/g, ''); return b64; } , urlSafeToBuffer: function (str) { return OAUTH3._binStr.binStrToBuffer(OAUTH3._base64.decodeUrlSafe(str)); } , bufferToUrlSafe: function (buf) { return OAUTH3._base64.encodeUrlSafe(OAUTH3._binStr.bufferToBinStr(buf)); } } , 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: { parse: function (search) { // needed for .well-known/oauth3 // parse a query or a hash if (-1 !== ['#', '?'].indexOf(search[0])) { search = search.substring(1); } // Solve for case of search within hash // example: #/authorization_dialog/?state=...&redirect_uri=... var queryIndex = search.indexOf('?'); if (-1 !== queryIndex) { search = search.substr(queryIndex + 1); } var args = search.split('&'); var argsParsed = {}; var i, arg, kvp, key, value; for (i = 0; i < args.length; i += 1) { arg = args[i]; if (-1 === arg.indexOf('=')) { argsParsed[decodeURIComponent(arg).trim()] = true; } else { kvp = arg.split('='); key = decodeURIComponent(kvp[0]).trim(); value = decodeURIComponent(kvp[1]).trim(); argsParsed[key] = value; } } return argsParsed; } , 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( OAUTH3._browser.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) { return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64)); }); return { header: jsons[0], payload: jsons[1] }; } , verify: function (jwk, token) { var parts = token.split(/\./g); var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.')); var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]); return OAUTH3.crypto.core.verify(jwk, data, signature); } , 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); return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._getCached(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); if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } return OAUTH3.hooks.directives._cache[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)); } , _get: function (providerUri) { if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.get) { console.warn('[Warn] Please implement OAUTH3._hooks.directives.get = function (providerUri) { return PromiseA; }'); return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}'); } return OAUTH3._hooks.directives.get(providerUri); } , _set: function (providerUri, directives) { if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.set) { console.warn('[Warn] Please implement OAUTH3._hooks.directives.set = function (providerUri, directives) { return PromiseA; }'); window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives)); return directives; } return OAUTH3._hooks.directives.set(providerUri, 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.axs||[])[0]||{}).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.axs||[])[0]||{}).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) { opts = 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); }); } , _getCached: function (providerUri, id) { providerUri = OAUTH3.uri.normalize(providerUri); if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; } if (id) { return OAUTH3.hooks.session._cache[providerUri + id]; } return OAUTH3.hooks.session._cache[providerUri]; } , set: function (providerUri, newSession, id) { if (!providerUri) { console.error(new Error('no providerUri').stack); throw new Error("providerUri is not set"); } providerUri = OAUTH3.uri.normalize(providerUri); if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; } OAUTH3.hooks.session._cache[providerUri + (id || newSession.id || newSession.token.id || '')] = newSession; if (!id) { OAUTH3.hooks.session._cache[providerUri] = newSession; } return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session._set(providerUri, newSession)); } , get: function (providerUri, id) { providerUri = OAUTH3.uri.normalize(providerUri); if (!providerUri) { throw new Error("providerUri is not set"); } return OAUTH3.PromiseA.resolve( OAUTH3.hooks.session._getCached(providerUri, id) || OAUTH3.hooks.session._get(providerUri, id) ).then(function (session) { var s = session || { token: {} }; OAUTH3.hooks.session._cache[providerUri + (id || s.id || s.token.id || '')] = session; if (!id) { OAUTH3.hooks.session._cache[providerUri] = session; } return session; }); } , _get: function (providerUri, id) { if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.all) { console.warn('[Warn] Please implement OAUTH3._hooks.sessions.all = function ([providerUri]) { return PromiseA; }'); } if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.get) { console.warn('[Warn] Please implement OAUTH3._hooks.sessions.get = function (providerUri[, id]) { return PromiseA; }'); return JSON.parse(window.sessionStorage.getItem('session-' + providerUri + (id || '')) || 'null'); } return OAUTH3._hooks.directives.get(providerUri, id); } , _set: function (providerUri, newSession, id) { if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.set) { console.warn('[Warn] Please implement OAUTH3._hooks.sessions.set = function (providerUri, newSession[, id]) { return PromiseA; }'); window.sessionStorage.setItem('session-' + providerUri, JSON.stringify(newSession)); window.sessionStorage.setItem('session-' + providerUri + (id || newSession.id || newSession.token.id || ''), JSON.stringify(newSession)); return newSession; } return OAUTH3._hooks.directives.set(providerUri, newSession, id); } } } , 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); directives.api = OAUTH3.url.normalize((directives.api||':hostname').replace(/:hostname/, OAUTH3.uri.normalize(directives.issuer) || OAUTH3.uri.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); } if (!preq.session) { return fetch(); } return OAUTH3.hooks.session.check(preq, opts).then(fetch); } , _requestHelper: function (preq, opts) { /* if (opts && opts.directives) { preq.url = OAUTH3.url.resolve(opts.directives.issuer, preq.url); } */ 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 , scope: opts.scope , 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); prequest.url = OAUTH3.url.resolve(providerUri/*directives.issuer*/, prequest.url); return OAUTH3.request(prequest/*, { directives: directive }*/).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.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } , 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)); } OAUTH3.hooks.session._cache = {}; return params; }); } // // Let the Code Waste begin!! // , _browser: { window: 'undefined' !== typeof window ? window : null // 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 , headers: null // TODO , data: data , status: xhr.status }); }; xhr.open(preq.method || 'GET', preq.url, true); var headers = preq.headers || {}; if (preq.data) { headers['Content-Type'] = 'application/json'; // TODO XXX TODO utf8 } Object.keys(headers).forEach(function (key) { xhr.setRequestHeader(key, headers[key]); }); xhr.send(JSON.stringify(preq.data)); }); } , 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 = '