394 lines
14 KiB
JavaScript
394 lines
14 KiB
JavaScript
$(function () {
|
|
'use strict';
|
|
|
|
var version = '1';
|
|
if (version !== window.localStorage.getItem('version')) {
|
|
window.localStorage.clear();
|
|
window.localStorage.setItem('version', version);
|
|
}
|
|
|
|
var OAUTH3 = window.OAUTH3;
|
|
var CONFIG = {
|
|
host: OAUTH3.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(loc.hash || loc.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 && clientParams.client_uri !== clientParams.client_id) {
|
|
console.warn("'client_id' should be used instead of 'client_uri'");
|
|
}
|
|
if (!(clientParams.client_id || clientParams.client_uri)) {
|
|
window.alert("'client_id' must exist as the uri identifying the client");
|
|
console.error("'client_id' must exist as the uri identifying the client");
|
|
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 + "'");
|
|
}
|
|
if (clientParams.subject) {
|
|
console.log('clientParams.subject: ', clientParams.subject);
|
|
}
|
|
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 clientLogo = OAUTH3.url.normalize(clientParams.client_uri) // optional relative logo ?
|
|
+ '/.well-known/oauth3/logo-128x128.png'
|
|
;
|
|
|
|
// TODO put in directives.json or similar
|
|
var grantDescriptions = {
|
|
// deprecated
|
|
'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)"
|
|
|
|
// new
|
|
, 'hello@example.com': "Hello World Example Access"
|
|
, 'authn@oauth3.org': "Basic secure authentication"
|
|
, 'wallet@oauth3.org': "Access to payments and subscriptions"
|
|
, 'bucket@oauth3.org': "Access to file storage"
|
|
, 'db@oauth3.org': "Access to app data"
|
|
, 'domains@oauth3.org': "Domain registration (and Glue and NS records)" // TODO make an alias
|
|
, 'domains:glue@oauth3.org': "Glue Record management (for vanity nameservers)"
|
|
, 'domains:ns@oauth3.org': "Name Server management"
|
|
, 'dns@oauth3.org': "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, clientParams).then(function (scopes) {
|
|
if (!scopes.pending.length) {
|
|
// looks like we've done all of this before
|
|
OAUTH3.authz.redirectWithToken(CONFIG.host, session, clientParams, 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()) {
|
|
location.href = clientParams.redirect_uri +'#'+ OAUTH3.query.stringify({
|
|
state: clientParams.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')
|
|
});
|
|
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');
|
|
});
|
|
}
|
|
|
|
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.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);
|
|
});
|
|
};
|
|
|
|
// Reference Implementation
|
|
util.submitLoginCode = function (ev) {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
|
|
// 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
|
|
, remember_device: $('.js-remember-label').find('.js-remember-checkbox').prop('checked')
|
|
, mock: true
|
|
}).then(function (session) {
|
|
$('.js-authn').removeClass('in').hide();
|
|
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)
|
|
});
|
|
});
|
|
}).then(function (session) {
|
|
return getGrants(session).catch(function (err) {
|
|
window.alert('grantResults: ' + err.message);
|
|
console.error('scope results', err);
|
|
});
|
|
}, function (error) {
|
|
console.error(error);
|
|
$('.error-msg').text('Incorrect code');
|
|
});
|
|
};
|
|
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) {
|
|
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 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: clientParams.state
|
|
, scope: clientParams.scope
|
|
};
|
|
|
|
window.location = clientParams.redirect_uri + '#' + OAUTH3.query.stringify(denyObj);
|
|
};
|
|
util.handleLogout = function () {
|
|
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);
|
|
};
|
|
util.editEmail = function () {
|
|
$('.js-authn').hide();
|
|
$('.js-userid-container').show();
|
|
};
|
|
|
|
|
|
//
|
|
// Page Setup
|
|
//
|
|
$('.js-authorization-dialog').hide();
|
|
$('.js-logout-container').hide();
|
|
$('.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-submit-code-btn', util.submitLoginCode);
|
|
$('body').on('click', '.js-login-allow', util.acceptScopesAndLogin);
|
|
$('body').on('click', '.js-login-deny', util.closeLoginDeny);
|
|
$('body').on('click', '.js-edit-email-button', util.editEmail);
|
|
$('body').on('keyup', 'form .js-oauth3-email', util.checkAuthEmail);
|
|
|
|
function handleAuthorizationDialog() {
|
|
return getSession(CONFIG.host).then(function (session) {
|
|
return getGrants(session);
|
|
}).catch(function () {
|
|
// 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()) {
|
|
location.href = clientParams.redirect_uri +'#'+ OAUTH3.query.stringify({
|
|
state: clientParams.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')
|
|
});
|
|
}
|
|
if (clientParams.subject) {
|
|
$('.js-oauth3-email').val(clientParams.subject);
|
|
$('.js-authn-show').prop('disabled', false);
|
|
}
|
|
$('.js-userid-container').show();
|
|
});
|
|
}
|
|
|
|
// Session initialization
|
|
return OAUTH3.discover(CONFIG.host, { client_uri: CONFIG.host }).then(function (directives) {
|
|
// TODO cache directives in memory (and storage)
|
|
CONFIG.directives = directives;
|
|
directives.issuer = directives.issuer || (window.location.host + window.location.pathname).replace(/\/$/, '');
|
|
|
|
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');
|
|
});
|
|
});
|