;(function (exports) { 'use strict'; var OAUTH3 = exports.OAUTH3; var OAUTH3_CORE = exports.OAUTH3_CORE; function getDefaultAppUrl() { 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(/\/?$/, '') ; } var browser = exports.OAUTH3_BROWSER = { window: window , clientUri: function (location) { return OAUTH3_CORE.normalizeUri(location.host + location.pathname); } , discover: function (providerUri, opts) { if (!providerUri) { throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri); } var directives = OAUTH3.hooks.getDirectives(providerUri); if (directives && directives.issuer) { return OAUTH3.PromiseA.resolve(directives); } return browser._discoverHelper(providerUri, opts).then(function (directives) { directives.issuer = directives.issuer || OAUTH3_CORE.normalizeUrl(providerUri); console.log('discoverHelper', directives); return OAUTH3.hooks.setDirectives(providerUri, directives); }); } , _discoverHelper: function (providerUri, opts) { opts = opts || {}; //opts.debug = true; providerUri = OAUTH3_CORE.normalizeUrl(providerUri); if (window.location.hostname.match(providerUri)) { 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.core.normalizeUrl(providerUri) + '/.well-known/oauth3/directives.json' }); } if (!window.location.hostname.match(opts.client_id || opts.client_uri)) { 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); } var discObj = OAUTH3_CORE.urls.discover(providerUri, { client_id: (opts.client_id || opts.client_uri || getDefaultAppUrl()), debug: opts.debug }); // TODO ability to reuse iframe instead of closing return browser.insertIframe(discObj.url, discObj.state, opts).then(function (params) { if (params.error) { return OAUTH3_CORE.formatError(providerUri, params.error); } var directives = JSON.parse(atob(OAUTH3_CORE.utils.urlSafeBase64ToBase64(params.result || params.directives))); return directives; }, function (err) { return OAUTH3.PromiseA.reject(err); }); } , discoverAuthorizationDialog: function(providerUri, opts) { var discObj = OAUTH3.core.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 var discWin = OAUTH3.openWindow(discObj.url, discObj.state, { reuseWindow: 'conquerer' }); return discWin.then(function (params) { console.log('discwin params'); console.log(params); // discWin.child // TODO params should have response_type indicating json, binary, etc var directives = JSON.parse(atob(OAUTH3.core.utils.urlSafeBase64ToBase64(params.result || params.directives))); console.log('directives'); console.log(directives); // Do some stuff var authObj = OAUTH3.core.implicitGrant( directives , { redirect_uri: opts.redirect_uri , debug: opts.debug , client_id: opts.client_id || opts.client_uri , client_uri: opts.client_uri || opts.client_id } ); if (params.debug) { window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!"); } return new OAUTH3.PromiseA(function (resolve, reject) { // TODO check if authObj.url is relative or full discWin.child.location = OAUTH3.core.urls.resolve(providerUri, authObj.url); if (params.debug) { discWin.child.focus(); } window['--oauth3-callback-' + authObj.state] = function (tokens) { if (tokens.error) { return reject(OAUTH3.core.formatError(tokens.error)); } if (params.debug || tokens.debug) { if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) { discWin.child.close(); } } else { discWin.child.close(); } resolve(tokens); }; }); }).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 ); }); } , frameRequest: function (url, state, opts) { var promise; if (!opts.windowType) { opts.windowType = 'popup'; } if ('background' === opts.windowType) { promise = browser.insertIframe(url, state, opts); } else if ('popup' === opts.windowType) { promise = browser.openWindow(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=' + browser.window.location.href; promise = browser.window.location = url; } else { throw new Error("login framing method options.windowType not specified or not type yet implemented"); } return promise.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; return OAUTH3.PromiseA.reject(err); } return params; }); } , insertIframe: function (url, state, opts) { opts = opts || {}; if (opts.debug) { opts.timeout = opts.timeout || 15 * 60 * 1000; } var promise = new OAUTH3.PromiseA(function (resolve, reject) { var tok; var iframeDiv; function cleanup() { delete window['--oauth3-callback-' + state]; iframeDiv.remove(); 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); // TODO hidden / non-hidden (via directive even) var framesrc = '<iframe class="js-oauth3-iframe" src="' + url + '" '; if (opts.debug) { framesrc += ' width="800px" height="800px" style="opacity: 0.8;" frameborder="1"'; } else { framesrc += ' width="1px" height="1px" frameborder="0"'; } framesrc += '></iframe>'; iframeDiv = window.document.createElement('div'); iframeDiv.innerHTML = framesrc; window.document.body.appendChild(iframeDiv); }); // TODO periodically garbage collect expired handlers from window object return promise; } , openWindow: 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() { clearTimeout(tok); tok = null; delete window['--oauth3-callback-' + state]; // this is last in case the window self-closes synchronously // (should never happen, but that's a negotiable implementation detail) if (!opts.reuseWindow) { promise.child.close(); } } window['--oauth3-callback-' + state] = function (params) { console.log('YOLO!!'); resolve(params); cleanup(); }; tok = setTimeout(function () { var err = new Error("the windowed request did not complete within 3 minutes"); err.code = "E_TIMEOUT"; reject(err); cleanup(); }, opts.timeout || 3 * 60 * 1000); 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) promise.child = 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; } // // Logins // , authn: { authorizationRedirect: function (providerUri, opts) { // TODO get own directives return OAUTH3.discover(providerUri, opts).then(function (directive) { var prequest = OAUTH3_CORE.urls.authorizationRedirect( directive , opts ); if (!prequest.state) { throw new Error("[Devolper Error] [authorization redirect] prequest.state is empty"); } return browser.frameRequest(prequest.url, prequest.state, opts); }).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 ); }); } , implicitGrant: function (providerUri, opts) { // TODO let broker=true change behavior to open discover inline with frameRequest // TODO OAuth3 provider should use the redirect URI as the appId? return OAUTH3.discover(providerUri, opts).then(function (directive) { var prequest = OAUTH3_CORE.urls.implicitGrant( directive // TODO OAuth3 provider should referrer / referer / origin as the appId? , opts ); if (!prequest.state) { throw new Error("[Devolper Error] [implicit grant] prequest.state is empty"); } return browser.frameRequest(prequest.url, prequest.state, opts); }).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 ); }); } , logout: function (providerUri, opts) { opts = opts || {}; return OAUTH3.discover(providerUri, opts).then(function (directive) { var prequest = OAUTH3_CORE.urls.logout( directive , opts ); // Oauth3.init({ logout: function () {} }); //return Oauth3.logout(); var redirectUri = opts.redirect_uri || opts.redirectUri || (window.location.protocol + '//' + (window.location.host + window.location.pathname) + 'oauth3.html') ; var params = { // logout=true for all logins/accounts // logout=app-scoped-login-id for a single login action: 'logout' // TODO specify specific accounts / logins to delete from session , accounts: true , logins: true , redirect_uri: redirectUri , state: prequest.state , debug: opts.debug }; if (prequest.url === params.redirect_uri) { return OAUTH3.PromiseA.resolve(); } prequest.url += '#' + OAUTH3_CORE.querystringify(params); return OAUTH3.insertIframe(prequest.url, prequest.state, opts); }); } } , isIframe: function isIframe () { try { return window.self !== window.top; } catch (e) { return true; } } , parseUrl: function (url) { var parser = document.createElement('a'); parser.href = url; return parser; } , isRedirectHostSafe: function (referrerUrl, redirectUrl) { var src = browser.parseUrl(referrerUrl); var dst = browser.parseUrl(redirectUrl); // TODO how should we handle subdomains? // It should be safe for api.example.com to redirect to example.com // But it may not be safe for to example.com to redirect to aj.example.com // It is also probably not safe for sally.example.com to redirect to john.example.com // The client should have a list of allowed URLs to choose from and perhaps a wildcard will do // // api.example.com.evil.com SHOULD NOT match example.com return dst.hostname === src.hostname; } , checkRedirect: function (client, query) { console.warn("[security] URL path checking not yet implemented"); var clientUrl = OAUTH3.core.normalizeUrl(client.url); var redirectUrl = OAUTH3.core.normalizeUrl(query.redirect_uri); // General rule: // I can callback to a shorter domain (fewer subs) or a shorter path (on the same domain) // but not a longer (more subs) or different domain or a longer path (on the same domain) // We can callback to an explicitly listed domain (TODO and path) if (browser.isRedirectHostSafe(clientUrl, redirectUrl)) { return true; } return false; } /* , redirect: function (redirect) { if (parser.search) { parser.search += '&'; } else { parser.search += '?'; } parser.search += 'error=E_NO_SESSION'; redirectUri = parser.href; window.location.href = redirectUri; } */ , hackFormSubmit: function (opts) { opts = opts || {}; scope.authorizationDecisionUri = DaplieApiConfig.providerUri + '/api/org.oauth3.provider/authorization_decision'; scope.updateScope(); var redirectUri = scope.appQuery.redirect_uri.replace(/^https?:\/\//i, 'https://'); var separator; // TODO check that we appropriately use '#' for implicit and '?' for code // (server-side) in an OAuth2 backwards-compatible way if ('token' === scope.appQuery.response_type) { separator = '#'; } else if ('code' === scope.appQuery.response_type) { separator = '?'; } else { separator = '#'; } if (scope.pendingScope.length && !opts.allow) { redirectUri += separator + Oauth3.querystringify({ error: 'access_denied' , error_description: 'None of the permissions were accepted' , error_uri: 'https://oauth3.org/docs/errors#access_denied' , state: scope.appQuery.state }); window.location.href = redirectUri; return; } // TODO move to Oauth3? or not? // this could be implementation-specific, // but it may still be nice to provide it as de-facto var url = DaplieApiConfig.apiBaseUri + '/api/org.oauth3.provider/grants/:client_id/:account_id' .replace(/:client_id/g, scope.appQuery.client_id || scope.appQuery.client_uri) .replace(/:account_id/g, scope.selectedAccountId) ; var account = scope.sessionAccount; var session = { accessToken: account.token, refreshToken: account.refreshToken }; var preq = { url: url , method: 'POST' , data: { scope: updateAccepted() , response_type: scope.appQuery.response_type , referrer: document.referrer || document.referer || '' , referer: document.referrer || document.referer || '' , tenant_id: scope.appQuery.tenant_id , client_id: scope.appQuery.client_id , client_uri: scope.appQuery.client_uri } , session: session }; preq.clientId = preq.appId = DaplieApiConfig.appId || DaplieApiConfig.clientId; preq.clientUri = preq.appUri = DaplieApiConfig.appUri || DaplieApiConfig.clientUri; // TODO need a way to have middleware in Oauth3.request for TherapySession et al return Oauth3.request(preq).then(function (resp) { var err; var data = resp.data || {}; if (data.error) { err = new Error(data.error.message || data.errorDescription); err.message = data.error.message || data.errorDescription; err.code = resp.data.error.code || resp.data.error; err.uri = 'https://oauth3.org/docs/errors#' + (resp.data.error.code || resp.data.error); return $q.reject(err); } if (!(data.code || data.accessToken)) { err = new Error("No grant code"); return $q.reject(err); } return data; }).then(function (data) { redirectUri += separator + Oauth3.querystringify({ state: scope.appQuery.state , code: data.code , access_token: data.access_token , expires_at: data.expires_at , expires_in: data.expires_in , scope: data.scope , refresh_token: data.refresh_token , refresh_expires_at: data.refresh_expires_at , refresh_expires_in: data.refresh_expires_in }); if ('token' === scope.appQuery.response_type) { window.location.href = redirectUri; return; } else if ('code' === scope.appQuery.response_type) { scope.hackFormSubmitHelper(redirectUri); return; } else { console.warn("Grant Code NOT IMPLEMENTED for '" + scope.appQuery.response_type + "'"); console.warn(redirectUri); throw new Error("Grant Code NOT IMPLEMENTED for '" + scope.appQuery.response_type + "'"); } }, function (err) { redirectUri += separator + Oauth3.querystringify({ error: err.code || 'server_error' , error_description: err.message || "Server Error: It's not your fault" , error_uri: err.uri || 'https://oauth3.org/docs/errors#server_error' , state: scope.appQuery.state }); console.error('Grant Code Error NOT IMPLEMENTED'); console.error(err); console.error(redirectUri); //window.location.href = redirectUri; }); } , hackFormSubmitHelper: function (uri) { // TODO de-jQuerify //window.location.href = redirectUri; //return; // the only way to do a POST that redirects the current window window.jQuery('form.js-hack-hidden-form').attr('action', uri); // give time for the apply to take place window.setTimeout(function () { window.jQuery('form.js-hack-hidden-form').submit(); }, 50); } }; browser.requests = browser.authn; Object.keys(browser).forEach(function (key) { if ('requests' === key) { Object.keys(browser.requests).forEach(function (key) { OAUTH3.requests[key] = browser.requests[key]; }); return; } OAUTH3[key] = browser[key]; }); }('undefined' !== typeof exports ? exports : window));