From a449358dd6e0313eee8a0480f1a91512a699f7ae Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 13 Feb 2017 14:34:26 -0500 Subject: [PATCH] move .well-known/oauth3 to assets/org.oauth3/.well-known/oauth3 --- .well-known/oauth3/callback.html | 21 ++ .well-known/oauth3/callback.js | 66 ++++++ .well-known/oauth3/crossdomain.html | 0 .well-known/oauth3/crossdomain.js | 0 .well-known/oauth3/directives.js | 330 ++++++++++++++++++++++++++++ .well-known/oauth3/directives.json | 9 + .well-known/oauth3/index.html | 68 ++++++ .well-known/oauth3/logout.html | 0 .well-known/oauth3/logout.js | 0 9 files changed, 494 insertions(+) create mode 100644 .well-known/oauth3/callback.html create mode 100644 .well-known/oauth3/callback.js create mode 100644 .well-known/oauth3/crossdomain.html create mode 100644 .well-known/oauth3/crossdomain.js create mode 100644 .well-known/oauth3/directives.js create mode 100644 .well-known/oauth3/directives.json create mode 100644 .well-known/oauth3/index.html create mode 100644 .well-known/oauth3/logout.html create mode 100644 .well-known/oauth3/logout.js diff --git a/.well-known/oauth3/callback.html b/.well-known/oauth3/callback.html new file mode 100644 index 0000000..6cea1d3 --- /dev/null +++ b/.well-known/oauth3/callback.html @@ -0,0 +1,21 @@ + + + + + + Redirecting... + + + + Redirecting... + + + + + + + diff --git a/.well-known/oauth3/callback.js b/.well-known/oauth3/callback.js new file mode 100644 index 0000000..a9c7c88 --- /dev/null +++ b/.well-known/oauth3/callback.js @@ -0,0 +1,66 @@ +(function () { + "use strict"; + + var loc = window.location; + var loginWinObj = window.OAUTH3_CORE.queryparse(loc.hash || loc.search); + var prefix = "(" + window.location.hostname + ") [.well-known/oauth3/callback.html]"; + + if (loginWinObj.debug) { + console.warn(prefix, "DEBUG MODE ENABLED. Automatic redirects disabled."); + } + // '--oauth3-callback-' prefix exist for security so that an attacker can't social engineer execution an arbitrary function + // TODO finalize name of '--oauth3-callback-', this will be a defacto standard + // TODO maybe call it 'self-xss-' or 'hack-my-account-' to discourage people from doing dumb things? + var callbackName = '--oauth3-callback-' + loginWinObj.state; + + console.log(prefix, loc.href); + console.log('Parsed URL Obj: ', loginWinObj); + console.log('callbackName: ', callbackName); + + window.oauth3complete = function () { + // The hacks that used to be necessary for this on iOS should no longer be necessary in iOS 9+ + // see https://bugs.chromium.org/p/chromium/issues/detail?id=136610 and https://crbug.com/423444 + // TODO Should we still create an abstraction for older versions? + if (window.parent) { + // iframe + try { + window.parent[callbackName](loginWinObj); + return; + } catch(e) { + console.warn(e); + } + } + + if (window.opener) { + try { + window.opener[callbackName](loginWinObj); + return; + } catch(e) { + console.warn(e); + } + } + + console.error("neither window.parent nor window.opener existed to complete callback"); + + /* + // the caller should close (or signal to close) the window + try { + window.close(); + } catch (err) { + console.log('Error: ', err); + } + */ + }; + + if (!loginWinObj.debug) { + window.oauth3complete(); + } + else { + document.body.innerHTML = window.location.hostname + window.location.pathname + + '

You\'ve passed the \'debug\' parameter so we\'re pausing' + + ' to let you look at logs or whatever it is that you intended to do.' + + '

Continue with callback: javascript:window.oauth3complete()'; + return; + } + +}()); diff --git a/.well-known/oauth3/crossdomain.html b/.well-known/oauth3/crossdomain.html new file mode 100644 index 0000000..e69de29 diff --git a/.well-known/oauth3/crossdomain.js b/.well-known/oauth3/crossdomain.js new file mode 100644 index 0000000..e69de29 diff --git a/.well-known/oauth3/directives.js b/.well-known/oauth3/directives.js new file mode 100644 index 0000000..43e8efc --- /dev/null +++ b/.well-known/oauth3/directives.js @@ -0,0 +1,330 @@ +(function () { + 'use strict'; + + console.log('[DAPLIE oauth3 directives.js]'); + console.log(window.location); + + var iter = 0; + + function main() { + + var rpc = {}; + //var myself = location.protocol + '//' + location.host + location.pathname; + var incoming; + var forwarding = {}; + var err; + var browserState; + var browserCallback; + var action; + + function parseParams() { + var params = {}; + + function parseParamsString(str) { + str.substr(1).split('&').filter(function (el) { return el; }).forEach(function (pair) { + pair = pair.split('='); + var key = decodeURIComponent(pair[0]); + var val = decodeURIComponent(pair[1]); + + if (params[key]) { + console.warn("overwriting key '" + key + "' '" + params[key] + "'"); + } + params[key] = val; + }); + } + + parseParamsString(window.location.search); + // handle cases where hash is treated like it's own href + // TODO /#/?search=blah + parseParamsString(window.location.hash); + + return params; + } + + function querystringify(params) { + var arr = []; + + Object.keys(params).forEach(function (k) { + arr.push(encodeURIComponent(k) + '=' + encodeURIComponent(params[k])); + }); + + return arr.join('&'); + } + + function phoneAway(/*redirectURi, params*/) { + // TODO test for ? / # + window.location.href = incoming.redirect_uri + '#' + querystringify(forwarding); + } + + function lintAndSetRedirectable(browserState, params) { + if (!params.redirect_uri) { + window.alert('redirect_uri not defined'); + err = new Error('redirect_uri not defined'); + console.error(err.message); + console.warn(err.stack); + params.redirect_uri = document.referer; + return false; + } + + if (!browserState) { + forwarding.error = "E_NO_BROWSER_STATE"; + forwarding.error_description = "you must specify a state parameter"; + return false; + } + + localStorage.setItem('oauth3.states.' + browserState, JSON.stringify(params)); + return true; + } + + function redirectCallback() { + var redirect_uri = incoming.redirect_uri; + forwarding.callback = browserState; + forwarding.action = 'close'; + + var url = redirect_uri + '#' + querystringify(forwarding); + + console.log('[debug] redirect_uri + params:', url); + window.location.href = url; + setTimeout(function () { + if (iter >= 3) { + console.log("dancing way too much... stopping now"); + return; + } + iter += 1; + console.log("I'm dancing by myse-e-elf"); + // in case I'm redirecting to myself + main(); + }, 0); + } + + rpc = {}; + + // Act as a provider and log the user out + rpc.logout = function (browserState, incoming) { + var url; + if (!lintAndSetRedirectable(browserState, incoming)) { + // TODO fail + } + + localStorage.setItem('oauth3.states.' + browserState, JSON.stringify(incoming)); + url = '/#/logout/' + browserState; + + // TODO specify specific account or all? + window.location.href = url; + setTimeout(function () { + // in case I'm redirecting to myself + main(); + }, 0); + }; + + // Act as a provider and inform the consumer the logout is complete + rpc.logout_callback = function (browserState/*, incoming*/) { + // TODO pass redirect_uri and state through here so we can avoid localStorage + var forwarding = {}; + var originalRequest; + + if (!browserState) { + forwarding.error = "E_NO_BROWSER_STATE"; + forwarding.error_description = "you must specify a state parameter"; + if (incoming.redirect_uri) { + phoneAway(incoming.redirect_uri, forwarding); + } + return; + } + + originalRequest = JSON.parse(localStorage.getItem('oauth3.states.' + browserState)); + forwarding.action = 'close'; + forwarding.state = browserState; + //phoneAway(originalRequest.redirect_uri, forwarding); + window.location.href = originalRequest.redirect_uri + '#' + querystringify(forwarding); + }; + + rpc.directives = function (browserState, incoming) { + if (!lintAndSetRedirectable(browserState, incoming)) { + phoneAway(); + return; + } + + var updatedAt = new Date(localStorage.getItem('oauth3.directives.updated_at')).valueOf(); + var fresh = (Date.now() - updatedAt) < (24 * 60 * 60 * 1000); + var directives = localStorage.getItem('oauth3.directives'); + var redirected = false; + + function redirectIf() { + if (redirected) { + return; + } + + redirected = true; + redirectCallback(); + } + + if (directives) { + forwarding.directives = directives; + redirectIf(); + if (fresh) { + return; + } + } + + var req = new XMLHttpRequest(); + req.open('GET', '.well-known/oauth3.json', true); + req.addEventListener('readystatechange', function () { + if (4 !== req.readyState) { + return; + } + + if (200 !== req.status) { + forwarding.error = "E_STATUS_" + req.status; + forwarding.error_description = "expected 200 OK json or text response for oauth3.json but got '" + req.status + "'"; + redirectIf(); + return; + } + + try { + directives = btoa(JSON.stringify(JSON.parse(req.responseText))); + forwarding.directives = directives; + forwarding.callback = browserState; + localStorage.setItem('oauth3.directives', directives); + localStorage.setItem('oauth3.directives.updated_at', new Date().toISOString()); + } catch(e) { + forwarding.error = "E_PARSE_JSON"; + forwarding.error_description = e.message; + console.error(forwarding.error); + console.error(forwarding.error_description); + console.error(req.responseText); + } + + redirectIf(); + }); + req.send(); + }; + + // the provider is contacting me + rpc.close = function (browserState, incoming) { + incoming.callback = browserState; + catchAll(); + }; + // the provider is contacting me + rpc.redirect = function (/*browserState, incoming*/) { + catchAll(); + }; + + function catchAll() { + function phoneHome() { + if (browserCallback === 'completeLogin') { + // Deprecated + console.log('[deprecated] callback completeLogin'); + (window.opener||window.parent).completeLogin(null, null, incoming); + } else { + console.log('[DEBUG] I would be closed by my parent now'); + console.log('__oauth3_' + browserCallback); + console.log(window.opener && window.opener['__oauth3_' + browserCallback]); + console.log(window.parent && window.parent['__oauth3_' + browserCallback]); + console.log(incoming); + (window.opener||window.parent)['__oauth3_' + browserCallback](incoming); + } + } + + if (!(incoming.browser_state || incoming.state)) { + window.alert("callback URLs should include 'browser_state' (authorization code)" + + " or 'state' (implicit grant))"); + } + + setTimeout(function () { + // opener is for popup window, new tab + // parent is for iframe + phoneHome(); + }, 10); + + // iOS Webview (namely Chrome) workaround + setTimeout(function () { + console.log('I would close now'); + // XXX OAUTH3 DEBUG FRAME XXX // make this easy to find + window.open('', '_self', ''); + window.close(); + }, 50); + + setTimeout(function () { + var i; + var len = localStorage.length; + var key; + var json; + var fresh; + + for (i = 0; i < len; i += 1) { + key = localStorage.key(i); + // TODO check updatedAt + if (/^oauth3\./.test(key)) { + try { + json = localStorage.getItem(key); + if (json) { + json = JSON.parse(json); + } + } catch (e) { + // ignore + json = null; + } + + fresh = json && (Date.now() - json.updatedAt < (5 * 60 * 1000)); + + if (!fresh) { + localStorage.removeItem(key); + } + } + } + forwarding.updatedAt = Date.now(); + localStorage.setItem('oauth3.' + (forwarding.browser_state || forwarding.state), JSON.stringify(forwarding)); + }, 0); + + } + + function parseAction(params) { + if (params.action) { + return params.action; + } + + if (params.close) { + return 'close'; + } + if (params.logout_callback) { + return 'logout_callback'; + } + if (params.logout) { + return 'logout'; + } + if (params.callback) { + return 'close'; + } + if (params.directives) { + return 'directives'; + } + + return 'redirect'; + } + + incoming = parseParams(); + browserState = incoming.browser_state || incoming.state; + action = parseAction(incoming); + forwarding.url = window.location.href; + forwarding.browser_state = browserState; + forwarding.state = browserState; + + if (!incoming.provider_uri) { + browserCallback = incoming.callback || browserState; + } else { + // deprecated + browserCallback = 'completeLogin'; + } + + console.log('[debug]', action, incoming); + + if (rpc[action]) { + rpc[action](browserState, incoming); + } else { + window.alert('unsupported action'); + } + } + + main(); +}()); diff --git a/.well-known/oauth3/directives.json b/.well-known/oauth3/directives.json new file mode 100644 index 0000000..d482bc8 --- /dev/null +++ b/.well-known/oauth3/directives.json @@ -0,0 +1,9 @@ +{ "authorization_dialog": { "url": "#/authorization_dialog" } +, "access_token": { "method": "POST", "url": "api/org.oauth3.provider/access_token" } +, "otp": { "method": "POST" , "url": "api/org.oauth3.provider/otp" } +, "credential_otp": { "method": "POST" , "url": "api/org.oauth3.provider/otp" } +, "credential_meta": { "url": "api/org.oauth3.provider/logins/meta/:type/:id" } +, "credential_create": { "method": "POST" , "url": "api/org.oauth3.provider/logins" } +, "grants": { "method": "GET", "url": "api/org.oauth3.provider/grants/:azp/:sub" } +, "authorization_decision": { "method": "POST", "url": "api/org.oauth3.provider/authorization_decision" } +} diff --git a/.well-known/oauth3/index.html b/.well-known/oauth3/index.html new file mode 100644 index 0000000..81f4905 --- /dev/null +++ b/.well-known/oauth3/index.html @@ -0,0 +1,68 @@ + + + + + + + OAuth3 RPC + + + + + + diff --git a/.well-known/oauth3/logout.html b/.well-known/oauth3/logout.html new file mode 100644 index 0000000..e69de29 diff --git a/.well-known/oauth3/logout.js b/.well-known/oauth3/logout.js new file mode 100644 index 0000000..e69de29