walnut.js/lib/com.daplie.walnut/scripts/controllers/authorization-dialog.js

348 lines
11 KiB
JavaScript
Raw Normal View History

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