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 });
 | |
|     };
 | |
|   }]);
 |