458 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			458 lines
		
	
	
		
			16 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.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('');
 | |
| 
 | |
|   /*
 | |
|   OAUTH3._hooks.sessions.all = function (providerUri) {
 | |
|   };
 | |
|   */
 | |
|   OAUTH3._hooks = { sessions: {} };
 | |
|   OAUTH3._hooks.sessions.get = function (providerUri, id) {
 | |
|     return JSON.parse(window.localStorage.getItem('session-' + providerUri + (id || '')) || 'null');
 | |
|   };
 | |
|   OAUTH3._hooks.sessions.set = function (providerUri, newSession, id) {
 | |
|     window.localStorage.setItem('session-' + providerUri, JSON.stringify(newSession));
 | |
|     window.localStorage.setItem('session-' + providerUri + (id || newSession.id || newSession.token.id || ''), JSON.stringify(newSession));
 | |
|     return newSession;
 | |
|   };
 | |
| 
 | |
|   // 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 + "'");
 | |
|     }
 | |
|     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 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 = {
 | |
|       // 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, 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);
 | |
|       });
 | |
|     }, function (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) {
 | |
|       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);
 | |
|   };
 | |
|   util.editEmail = function () {
 | |
|     $('.js-authn').hide();
 | |
|     $('.js-userid-container').show();
 | |
|     debugger;
 | |
|   };
 | |
| 
 | |
| 
 | |
|   //
 | |
|   // 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('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);
 | |
|     }, 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;
 | |
|       }
 | |
|       if (clientParams.subject) {
 | |
|         $('.js-oauth3-email').val(clientParams.subject);
 | |
|         $('.js-authn-show').prop('disabled', false);
 | |
|       }
 | |
|       $('.js-userid-container').show();
 | |
|     }).then(function () {
 | |
|       //$('body').addClass('in');
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   // Session initialization
 | |
|   return OAUTH3.discover(
 | |
|     OAUTH3.clientUri(window.location)
 | |
|   , { client_uri: OAUTH3.clientUri(window.location) }
 | |
|   ).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');
 | |
| 
 | |
|   });
 | |
| 
 | |
| });
 |