(function () { 'use strict'; console.log('[DAPLIE oauth3.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 anchor; 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; }); } anchor = document.createElement('a'); anchor.href = window.location.href; parseParamsString(anchor.search); parseParamsString(anchor.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', '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 (window.opener||window.parent).completeLogin(null, null, incoming); } else { console.log('I would be closed by my parent now'); (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'); 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(); }());