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