forked from coolaj86/walnut.js
		
	
		
			
				
	
	
		
			348 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			348 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'use strict';
 | 
						|
 | 
						|
/**
 | 
						|
 * @ngdoc function
 | 
						|
 * @name yololiumApp.controller:OauthCtrl
 | 
						|
 * @description
 | 
						|
 * # OauthCtrl
 | 
						|
 * Controller of the yololiumApp
 | 
						|
 */
 | 
						|
angular.module('yololiumApp')
 | 
						|
  .controller('AuthorizationDialogController', [
 | 
						|
    '$window'
 | 
						|
  , '$location'
 | 
						|
  , '$stateParams'
 | 
						|
  , '$q'
 | 
						|
  , '$timeout'
 | 
						|
  , '$scope'
 | 
						|
  , '$http'
 | 
						|
  , 'DaplieApiConfig'
 | 
						|
  , 'DaplieApiSession'
 | 
						|
  , 'DaplieApiRequest'
 | 
						|
  , function (
 | 
						|
      $window
 | 
						|
    , $location
 | 
						|
    , $stateParams
 | 
						|
    , $q
 | 
						|
    , $timeout
 | 
						|
    , $scope
 | 
						|
    , $http
 | 
						|
    , LdsApiConfig
 | 
						|
    , LdsApiSession
 | 
						|
    , LdsApiRequest
 | 
						|
    ) {
 | 
						|
 | 
						|
    var scope = this;
 | 
						|
 | 
						|
    function isIframe () {
 | 
						|
      try {
 | 
						|
        return window.self !== window.top;
 | 
						|
      } catch (e) {
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // TODO move into config
 | 
						|
    var scopeMessages = {
 | 
						|
      directories: "View directories"
 | 
						|
    , me: "View your own Account"
 | 
						|
    , '*': "Use the Full Developer API"
 | 
						|
    };
 | 
						|
 | 
						|
    function updateAccepted() {
 | 
						|
      scope.acceptedString = scope.pendingScope.filter(function (obj) {
 | 
						|
        return obj.acceptable && obj.accepted;
 | 
						|
      }).map(function (obj) {
 | 
						|
        return obj.value;
 | 
						|
      }).join(' ');
 | 
						|
 | 
						|
      return scope.acceptedString;
 | 
						|
    }
 | 
						|
 | 
						|
    function scopeStrToObj(value, accepted) {
 | 
						|
      // TODO parse subresource (dns:example.com:cname)
 | 
						|
      return {
 | 
						|
        accepted: accepted
 | 
						|
      , acceptable: !!scopeMessages[value]
 | 
						|
      , name: scopeMessages[value] || 'Invalid Scope \'' + value + '\''
 | 
						|
      , value: value
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    function requestSelectedAccount(account, query, origin) {
 | 
						|
      // TODO Desired Process
 | 
						|
      // * check locally
 | 
						|
      // * if permissions pass, sign a jwt and post to server
 | 
						|
      // * if permissions fail, get from server (posting public key), then sign jwt
 | 
						|
      // * redirect to authorization_code_callback?code= or oauth3.html#token=
 | 
						|
      return $http.get(
 | 
						|
        LdsApiConfig.providerUri + '/api/org.oauth3.accounts/:account_id/grants/:client_id'
 | 
						|
          .replace(/:account_id/g, account.accountId)
 | 
						|
          .replace(/:client_id/g, query.client_id)
 | 
						|
      , { headers: { Authorization: "Bearer " + account.token } }
 | 
						|
      ).then(function (resp) {
 | 
						|
        var err;
 | 
						|
 | 
						|
        if (!resp.data) {
 | 
						|
          err = new Error("[Uknown Error] got no response (not even an error)");
 | 
						|
          console.error(err.stack);
 | 
						|
          throw err;
 | 
						|
        }
 | 
						|
 | 
						|
        if (resp.data.error) {
 | 
						|
          console.error('[authorization-dialog] resp.data');
 | 
						|
          err = new Error(resp.data.error.message || resp.data.error_description);
 | 
						|
          console.error(err.stack);
 | 
						|
          scope.error = resp.data.error;
 | 
						|
          scope.rawResponse = resp.data;
 | 
						|
          return $q.reject(err);
 | 
						|
        }
 | 
						|
 | 
						|
        return resp.data;
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    scope.chooseAccount = function (/*profile*/) {
 | 
						|
      $window.alert("user switching not yet implemented");
 | 
						|
    };
 | 
						|
    scope.updateScope = function () {
 | 
						|
      updateAccepted();
 | 
						|
    };
 | 
						|
 | 
						|
    function parseScope(scope) {
 | 
						|
      return (scope||'').split(/[\s,]/g)
 | 
						|
    }
 | 
						|
    function getNewPermissions(grant, query) {
 | 
						|
      var grantedArr = parseScope(grant.scope);
 | 
						|
      var requestedArr = parseScope(query.scope||'');
 | 
						|
 | 
						|
      return requestedArr.filter(function (scope) {
 | 
						|
        return -1 === grantedArr.indexOf(scope);
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    function generateToken(account, grant, query) {
 | 
						|
      var err = new Error("generateToken not yet implemented");
 | 
						|
      throw err;
 | 
						|
    }
 | 
						|
 | 
						|
    function generateCode(account, grant, query) {
 | 
						|
      var err = new Error("generateCode not yet implemented");
 | 
						|
      throw err;
 | 
						|
    }
 | 
						|
 | 
						|
    function getAccountPermissions(account, query, origin) {
 | 
						|
      return requestSelectedAccount(account, query, origin).then(function (grants) {
 | 
						|
        var grant = grants[query.client_id] || grants;
 | 
						|
        var grantedArr = parseScope(grant.scope);
 | 
						|
        var pendingArr = getNewPermissions(grant, query);
 | 
						|
 | 
						|
        var grantedObj = grantedArr.map(scopeStrToObj);
 | 
						|
        // '!' is a debug scope that ensures the permission dialog will be activated
 | 
						|
        // also could be used for switch user
 | 
						|
        var pendingObj = pendingArr.filter(function (v) { return '!' !== v; }).map(scopeStrToObj);
 | 
						|
 | 
						|
        scope.client = grant.client;
 | 
						|
 | 
						|
        if (!scope.client.title) {
 | 
						|
          scope.client.title = scope.client.name || 'Missing App Title';
 | 
						|
        }
 | 
						|
 | 
						|
        scope.selectedAccountId = account.accountId;
 | 
						|
 | 
						|
        if (!checkRedirect(grant, query)) {
 | 
						|
          location.href = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK';
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        // key generation in browser
 | 
						|
        // possible iframe vulns?
 | 
						|
        if (pendingArr.length) {
 | 
						|
          if (scope.iframe) {
 | 
						|
            location.href = query.redirect_uri + '#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;
 | 
						|
          }
 | 
						|
 | 
						|
          updateAccepted();
 | 
						|
          return grant;
 | 
						|
        }
 | 
						|
        else if ('token' === query.response_type) {
 | 
						|
          generateToken(account, grant, query).then(function (token) {
 | 
						|
            location.href = query.redirect_uri + '#token=' + token;
 | 
						|
          });
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        else if ('code' === query.response_type) {
 | 
						|
          // NOTE
 | 
						|
          // A client secret may never be exposed in a client
 | 
						|
          // A code always requires a secret
 | 
						|
          // Therefore this redirect_uri will always be to a server, not a local page
 | 
						|
          generateCode(account, grant, query).then(function () {
 | 
						|
            location.href = query.redirect_uri + '?code=' + code;
 | 
						|
          });
 | 
						|
          return;
 | 
						|
        } else {
 | 
						|
          location.href = query.redirect_uri + '#error=E_UNKNOWN_RESPONSE_TYPE&error_description='
 | 
						|
            + encodeURIComponent("The '?response_type=' parameter must be set to either 'token' or 'code'.")
 | 
						|
            + '&error_uri=' + encodeURIComponent('https://oauth3.org/docs/errors/#E_UNKNOWN_RESPONSE_TYPE');
 | 
						|
          return;
 | 
						|
        }
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    function redirectToFailure() {
 | 
						|
      var redirectUri = $location.search().redirect_uri;
 | 
						|
 | 
						|
      var parser = document.createElement('a');
 | 
						|
      parser.href = redirectUri;
 | 
						|
      if (parser.search) {
 | 
						|
        parser.search += '&';
 | 
						|
      } else {
 | 
						|
        parser.search += '?';
 | 
						|
      }
 | 
						|
      parser.search += 'error=E_NO_SESSION';
 | 
						|
      redirectUri = parser.href;
 | 
						|
 | 
						|
      window.location.href = redirectUri;
 | 
						|
    }
 | 
						|
 | 
						|
    function initAccount(session, query, origin) {
 | 
						|
      return LdsApiRequest.getAccountSummaries(session).then(function (accounts) {
 | 
						|
        var account = LdsApiSession.selectAccount(session);
 | 
						|
        var profile;
 | 
						|
 | 
						|
        scope.accounts = accounts.map(function (account) {
 | 
						|
          return account.profile.me;
 | 
						|
        });
 | 
						|
        accounts.some(function (a) {
 | 
						|
          if (LdsApiSession.getId(a) === LdsApiSession.getId(account)) {
 | 
						|
            profile = a.profile;
 | 
						|
            a.selected = true;
 | 
						|
            return true;
 | 
						|
          }
 | 
						|
        });
 | 
						|
 | 
						|
        if (profile.me.photos[0]) {
 | 
						|
          if (!profile.me.photos[0].appScopedId) {
 | 
						|
            // TODO fix API to ensure corrent id
 | 
						|
            profile.me.photos[0].appScopedId = profile.me.appScopedId || profile.me.app_scoped_id;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        profile.me.photo = profile.me.photos[0] && LdsApiRequest.photoUrl(account, profile.me.photos[0], 'medium');
 | 
						|
        scope.account = profile.me;
 | 
						|
 | 
						|
        scope.token = $stateParams.token;
 | 
						|
 | 
						|
        /*
 | 
						|
        scope.accounts.push({
 | 
						|
          displayName: 'Login as a different user'
 | 
						|
        , new: true
 | 
						|
        });
 | 
						|
        */
 | 
						|
 | 
						|
        //return determinePermissions(session, account);
 | 
						|
        return getAccountPermissions(account, query, origin).then(function () {
 | 
						|
          // do nothing?
 | 
						|
          scope.selectedAccount = session; //.account;
 | 
						|
          scope.previousAccount = session; //.account;
 | 
						|
          scope.updateScope();
 | 
						|
        }, function (err) {
 | 
						|
          if (/logged in/.test(err.message)) {
 | 
						|
            return LdsApiSession.destroy().then(function () {
 | 
						|
              init();
 | 
						|
            });
 | 
						|
          }
 | 
						|
 | 
						|
          if ('E_INVALID_TRANSACTION' === err.code) {
 | 
						|
            window.alert(err.message);
 | 
						|
            return;
 | 
						|
          }
 | 
						|
 | 
						|
          console.warn("[ldsconnect.org] [authorization-dialog] ERROR somewhere in oauth process");
 | 
						|
          console.warn(err);
 | 
						|
          window.alert(err.message);
 | 
						|
        });
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    function init() {
 | 
						|
      scope.iframe = isIframe();
 | 
						|
      var query = $location.search();
 | 
						|
      var referrer = $window.document.referer || $window.document.origin;
 | 
						|
      // TODO XXX this should be drawn from site-specific config
 | 
						|
      var apiHost = 'https://oauth3.org';
 | 
						|
 | 
						|
      // if the client didn't specify an id the client is the referrer
 | 
						|
      if (!query.client_id) {
 | 
						|
        // if we were redirect here by our own apiHost we can trust the host as the client_id
 | 
						|
        // (and it will be checked against allowed urls anyway)
 | 
						|
        if (referrer === apiHost) {
 | 
						|
          query.client_id = ('https://' + query.host);
 | 
						|
        } else {
 | 
						|
          query.client_id = referrer;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      // TODO XXX to allow or to disallow mounted apps, that is the question
 | 
						|
      // https://example.com/blah/ -> example.com/blah
 | 
						|
      query.client_id = query.client_id.replace(/^https?:\/\//i, '').replace(/\/$/, '');
 | 
						|
 | 
						|
      if (scope.iframe) {
 | 
						|
        return LdsApiSession.checkSession().then(function (session) {
 | 
						|
          if (session.accounts.length) {
 | 
						|
            // TODO make sure this fails / notifies
 | 
						|
            return initAccount(session, query, origin);
 | 
						|
          } else {
 | 
						|
            // TODO also notify to bring to front
 | 
						|
            redirectToFailure();
 | 
						|
          }
 | 
						|
        });
 | 
						|
      }
 | 
						|
 | 
						|
      // session means both login(s) and account(s)
 | 
						|
      return LdsApiSession.requireSession(
 | 
						|
        // role
 | 
						|
        null
 | 
						|
        // TODO login opts (these are hypothetical)
 | 
						|
      , { close: false
 | 
						|
        , options: ['login', 'create']
 | 
						|
        , default: 'login'
 | 
						|
        }
 | 
						|
        // TODO account opts
 | 
						|
      , { verify: ['email', 'phone']
 | 
						|
        }
 | 
						|
      , { clientId: query.clientId
 | 
						|
        }
 | 
						|
      ).then(function (session) {
 | 
						|
        initAccount(session, query, origin)
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    init();
 | 
						|
 | 
						|
    // I couldn't figure out how to get angular to bubble the event
 | 
						|
    // and the oauth2orize framework didn't seem to work with json form uploads
 | 
						|
    // so I dropped down to quick'n'dirty jQuery to get it all to work
 | 
						|
    scope.hackFormSubmit = function (opts) {
 | 
						|
      scope.submitting = true;
 | 
						|
      scope.cancelHack = !opts.allow;
 | 
						|
      scope.authorizationDecisionUri = LdsApiConfig.providerUri + '/api/oauth3/authorization_decision';
 | 
						|
      scope.updateScope();
 | 
						|
 | 
						|
      $window.jQuery('form.js-hack-hidden-form').attr('action', scope.authorizationDecisionUri);
 | 
						|
 | 
						|
      // give time for the apply to take place
 | 
						|
      $timeout(function () {
 | 
						|
        $window.jQuery('form.js-hack-hidden-form').submit();
 | 
						|
      }, 50);
 | 
						|
    };
 | 
						|
    scope.allowHack = function () {
 | 
						|
      scope.hackFormSubmit({ allow: true });
 | 
						|
    };
 | 
						|
    scope.rejectHack = function () {
 | 
						|
      scope.hackFormSubmit({ allow: false });
 | 
						|
    };
 | 
						|
  }]);
 |