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