$(function () { 'use strict'; var OAUTH3 = window.OAUTH3; var CONFIG = { host: OAUTH3.utils.clientUri(window.location) , directives: null // will be populated before the login button appears }; var loc = window.location; var util = {}; var email; var tpls = { scope: $('.js-scopes-container').html() }; $('.js-scopes-container').html(''); // TODO let query.parse do location.hash || location.search || location var clientParams = OAUTH3.query.parse(window.location.hash || window.location.search); if (/authorization_dialog/.test(window.location.href)) { // OAUTH3.lintClientParams(params, window) // OAUTH3.normalizeClientParams(params, window) if (clientParams.debug) { console.info("'debug' exists, debug mode enabled. :)"); } if (-1 === [ 'token', 'code' ].indexOf(clientParams.response_type)) { window.alert("'response_type' must exist and be either 'token' (implicit flow) or 'code' (authorization flow)"); return; } if (!clientParams.state || -1 !== [ 'undefined', 'null' ].indexOf(clientParams.state)) { // TODO check bits window.alert("'state' should exist as a crypto-random string with 128-bits of entropy (32 hex characters)"); return; } if (!clientParams.client_id || -1 !== [ 'undefined', 'null' ].indexOf(clientParams.client_id)) { console.warn( "'client_id' should exist as the uri identifying the client," + " such as example.com or example.com:8080/my-app or, well," + "'" + OAUTH3.url.normalize(window.document.referrer) + "'" ); } if (clientParams.client_uri) { console.warn("'client_id' should be used instead of 'client_uri'"); } if (!(clientParams.client_id || clientParams.client_uri)) { window.alert("'response_type' must exist and be either 'token' (implicit flow) or 'code' (authorization flow)"); console.error("'response_type' must exist and be either 'token' (implicit flow) or 'code' (authorization flow)"); clientParams.client_id = clientParams.client_uri = OAUTH3.url.normalize(window.document.referrer); } if (!clientParams.redirect_uri) { clientParams.redirect_uri = OAUTH3.url.normalize(clientParams.client_uri) + "/.well-known/oauth3/callback.html'"; window.alert("'redirect_uri' must exist and should point to '" + clientParams.redirect_uri + "'"); console.error("'redirect_uri' must exist and should point to '" + clientParams.redirect_uri + "'"); } clientParams.referrer = window.document.referrer; } function normalizeSession(session) { // TODO casing // TODO expiry calculation // TODO leave this up to OAUTH3 session.provider_uri = session.provider_rui || CONFIG.host; session.client_uri = session.client_uri || CONFIG.host; // same as provider in this case } function getSession(providerUri) { return OAUTH3.hooks.session.get(providerUri).then(function (session) { if (session && session.access_token) { normalizeSession(session); return OAUTH3.PromiseA.resolve(session); } else { return OAUTH3.PromiseA.reject(new Error("no access_token in session")); } }); } function getGrants(session) { var clientObj = OAUTH3.query.parse(loc.hash || loc.search); var clientLogo = OAUTH3.url.normalize(clientObj.client_uri) // optional relative logo ? + '/.well-known/oauth3/logo-128x128.png' ; var callbackUrl; // TODO put in directives.json or similar var grantDescriptions = { 'oauth3_authn': "Basic secure authentication" , 'wallet': "Access to payments and subscriptions" , 'bucket': "Access to file storage" , 'db': "Access to app data" , 'domains': "Domain registration (and Glue and NS records)" // TODO make an alias , 'domains:glue': "Glue Record management (for vanity nameservers)" , 'domains:ns': "Name Server management" , 'dns': "DNS records (A/AAAA, TXT, SRV, MX, etc)" , '*': "FULL ACCOUNT ACCESS" }; if ('oauth3_authn' === clientParams.scope) { // implicit ppid grant is automatic console.warn('[security] fix scope checking on backend so that we can do automatic grants'); // TODO check user preference if implicit ppid grant is allowed //return generateToken(session, clientObj); } $('.js-client-logo').attr('src', clientLogo); //$('.js-user-avatar').attr('src', userAvatar); return OAUTH3.authz.scopes(CONFIG.host, session, clientObj).then(function (scopes) { if (!scopes.pending.length) { // looks like we've done all of this before OAUTH3.authz.redirectWithToken(CONFIG.host, session, clientObj, scopes); return; } // This is to prevent click-jacking // TODO secure iFrame from click-jacking by requiring input? // ex: input.security-code[type="text"].val(Math.random()); input.js-verify-code[placeholder="Type what you see"] if (OAUTH3._browser.isIframe()) { callbackUrl = clientObj.redirect_uri + '#state=' + clientObj.state + '&error=access_denied&error_description=' + encodeURIComponent("You're requesting permission in an iframe, but the permissions have not yet been granted") + '&error_uri=' + encodeURIComponent('https://oauth3.org/docs/errors/#E_IFRAME_DENIED'); location.href = callbackUrl; return; } // TODO handle special scopes // ! always show permission dialog // ^ switch user // @n require actual login if not within n seconds // * account takeover scopes.pending.forEach(function (scope) { var $scope = $(tpls.scope); $scope.find('.js-scope-toggle').attr('name', scope); $scope.find('.js-scope-toggle').prop('checked', true); if (-1 !== scopes.granted.indexOf(scope)) { $scope.find('.js-scope-toggle').prop('disabled', true); } // the front-end recognizes the scope as valid // TODO list scopes in directive if (grantDescriptions[scope]) { $scope.find('.js-scope-desc').text(grantDescriptions[scope]); } else { $scope.find('.js-scope-toggle').prop('checked', false); $scope.find('.js-scope-toggle').prop('disabled', true); $scope.find('.js-scope-desc').text(scope); } $('.js-scopes-container').append($scope); }); $('.js-authz').show().addClass('in'); }, function (err) { window.alert('grantResults: ' + err.message); console.error('scope results', err); }); } util.checkAuthEmail = function (ev) { ev.preventDefault(); ev.stopPropagation(); var email = $(this).val(); // wizarding - email detection if (/gmail|yahoo|msn|live/.test(email)) { $('.js-provider-logo').attr('src', 'img/not-provider.png'); } else { $('.js-provider-logo').attr('src', 'img/daplie-provider.jpeg'); } // TODO debounce 150ms // TODO test email by mx record if (/.+@.+\..+/.test(email)) { $('.js-authn-show').removeAttr('disabled'); $('.js-oauth3-email').val(email); } else { $('.js-authn-show').prop('disabled', true); } }; util.submitAuthEmail = function (ev) { ev.preventDefault(); ev.stopPropagation(); $('.js-authn-show').prop('disabled', true); // TODO loading email = $('.js-oauth3-email').val(); return OAUTH3.authn.loginMeta(CONFIG.directives, {email: email, mock: true}).then(function (userResults) { if (!userResults.data.error) { console.log('User exists:', userResults); } if (userResults.data.error) { $('.js-authn-show').removeAttr('disabled'); console.warn('User does not exist:', email); console.warn('User Results:', userResults); //window.alert('userResults: ' + userResults.data.error_description || userResults.data.error.message); //return; } return OAUTH3.authn.otp(CONFIG.directives, {email: email, mock: true}).then(function (otpResults) { if (otpResults.data.error) { window.alert('otpResults: ' + otpResults.data.error_description || otpResults.data.error.message); return; } var ua = window.navigator.userAgent; $('.js-sniffed-device').text(ua); $('.js-userid-container').removeClass('in').hide(); $('.js-authn').show().addClass('in'); $('.js-authn-otp-uuid').val(otpResults.data.uuid); $('.js-user-email').text(email); }); }); }; util.rememberDevice = function (ev) { ev.preventDefault(); ev.stopPropagation(); util.submitLoginCode({ rememberDevice: true }); }; util.rememberDeviceNot = function (ev) { ev.preventDefault(); ev.stopPropagation(); util.submitLoginCode({ rememberDevice: false }); }; // Reference Implementation util.submitLoginCode = function (opts) { // TODO // perhaps we should check that the code is valid before continuing to login (so that we don't send the key) // TODO // we should be sending the public key for this device as a jwk along with the authentication // (and how long to remember this device) var uuid = $('.js-authn-otp-uuid').val(); var code = $('.js-authn-otp-code').val().trim(); return OAUTH3.authn.resourceOwnerPassword(CONFIG.directives, { // TODO replace with signed hosted file client_agree_tos: 'oauth3.org/tos/draft' , client_id: CONFIG.host , client_uri: CONFIG.host , username: email , password: undefined , otp_code: code // TODO should be otp_id (agnostic of uuid) , otp_uuid: uuid // add expiration to the refresh token and/or public key , expire: opts.rememberDevice || (1 * 60 * 60 * 1000) , mock: true }).then(function (session) { $('.js-authn').removeClass('in').hide(); function getAccount(session) { if (session.token.sub) { return OAUTH3.PromiseA.resolve(session); } return OAUTH3.requests.accounts.create(CONFIG.directives, session, { display_name: email.replace(/@.*/, '') , comment: "created for '" + email + "' by '" + CONFIG.host + "'" , priority: 1000 // default priority for first account , name: undefined // TODO we could ask in the UI }).then(function (resp) { var results = resp.data; return OAUTH3.hooks.session.refresh(session, { access_token: (results.access_token || results.accessToken) , refresh_token: (results.refresh_token || results.refreshToken) }); }); } return getAccount(session).then(function () { return getGrants(session); }); }); }; util.acceptScopesAndLogin = function (ev) { ev.preventDefault(); ev.stopPropagation(); // TODO choose from the selected accepted scopes var acceptedScopes = []; $('form.js-authorization-decision').find('input[type=checkbox]').each(function (i, el) { var $input = $(el); if ($input.prop('checked')/* && !$input.prop('disabled')*/) { acceptedScopes.push($input.attr('name')); } }); getSession(CONFIG.host).then(function (session) { var clientParams = OAUTH3.query.parse(loc.hash || loc.search); return OAUTH3.authz.scopes(CONFIG.host, session, clientParams).then(function (scopes) { scopes.new = acceptedScopes; return OAUTH3.authz.redirectWithToken(CONFIG.host, session, clientParams, scopes); }); }, function (err) { console.error("Accept Scopes and Login"); console.error(err); }); }; util.closeLoginDeny = function (ev) { ev.preventDefault(); ev.stopPropagation(); var loginWinObj = OAUTH3.query.parse(loc.hash || loc.search); var denyObj = { error: 'access_denied' , error_description: 'The user has denied access.' , error_uri: 'https://' + CONFIG.host + '/.well-known/oauth3/errors.html#/?error=access_denied' , state: loginWinObj.state , scope: loginWinObj.scope }; window.location = loginWinObj.redirect_uri + '#' + OAUTH3.query.stringify(denyObj); }; util.handleLogout = function () { var clientParams = OAUTH3.query.parse(loc.hash || loc.search); localStorage.clear(); clientParams.redirect_uri += '?' + OAUTH3.query.stringify({ state: clientParams.state , debug: clientParams.debug }); window.location = OAUTH3.url.resolve(clientParams.client_uri, clientParams.redirect_uri); }; // // Page Setup // $('.js-userid-container').hide(); $('.js-authn').hide(); $('.js-authz').hide(); $('body').on('click', '.js-logout', util.handleLogout); $('body').on('click', '.js-authn-show', util.submitAuthEmail); $('body').on('click', '.js-authz-remember-me', util.rememberDevice); $('body').on('click', '.js-authz-remember-me-not', util.rememberDeviceNot); $('body').on('click', '.js-login-allow', util.acceptScopesAndLogin); $('body').on('click', '.js-login-deny', util.closeLoginDeny); $('body').on('keyup', 'form .js-oauth3-email', util.checkAuthEmail); function handleAuthorizationDialog() { return getSession(CONFIG.host).then(function (session) { return getGrants(session); }, function (e) { var clientObj = OAUTH3.query.parse(loc.hash || loc.search); // TODO select the providers the client wants to show // providers=daplie.com,facebook.com,google.com // etc // TODO let the client specify switch_user // TODO let the client specify relogin if stale if (OAUTH3._browser.isIframe()) { var callbackUrl = clientObj.redirect_uri + '#state=' + clientObj.state + '&error=access_denied&error_description=' + encodeURIComponent("You're requesting permission in an iframe, but the user is not yet authenticated") + '&error_uri=' + encodeURIComponent('https://oauth3.org/docs/errors/#E_IFRAME_DENIED'); location.href = callbackUrl; } $('.js-userid-container').show(); }).then(function () { //$('body').addClass('in'); }); } // Session initialization return $.ajax({ url: '.well-known/oauth3/directives.json' }).then(function (directives) { // TODO cache directives in memory (and storage) CONFIG.directives = directives; directives.issuer = directives.issuer || (window.location.host + window.location.pathname).replace(/\/$/, ''); $('.js-authorization-dialog').hide(); $('.js-logout-container').hide(); if (/authorization_dialog/.test(window.location.href)) { $('.js-authorization-dialog').show(); handleAuthorizationDialog(); } else if (/logout/.test(window.location.href)) { $('.js-logout-container').show(); } $('body').addClass('in'); }); });