oidc: do not notify login of ghost users

This commit is contained in:
Girish Ramakrishnan
2025-06-11 23:38:32 +02:00
parent d24189e8aa
commit f48231e309
3 changed files with 62 additions and 79 deletions

View File

@@ -390,7 +390,6 @@ async function interactionLogin(req, res, next) {
}
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
const userAgent = req.headers['user-agent'] || '';
const clientId = details.params.client_id;
debug(`interactionLogin: for OpenID client ${clientId} from ${ip}`);
@@ -414,12 +413,7 @@ async function interactionLogin(req, res, next) {
const [interactionFinishError, redirectTo] = await safe(gOidcProvider.interactionResult(req, res, result));
if (interactionFinishError) return next(new HttpError(500, interactionFinishError));
const auditSource = AuditSource.fromOidcRequest(req);
await eventlog.add(user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: user.id, user: users.removePrivateFields(user), appId: clientId });
await safe(users.notifyLoginLocation(user, ip, userAgent, auditSource), { debug });
// clear token as it is one-time use
await tokens.delByAccessToken(req.body.autoLoginToken);
await tokens.delByAccessToken(req.body.autoLoginToken); // clear token as it is one-time use
return res.status(200).send({ redirectTo });
}
@@ -438,11 +432,12 @@ async function interactionLogin(req, res, next) {
if (verifyError) return next(new HttpError(500, verifyError));
if (!user) return next(new HttpError(401, 'Username and password does not match'));
// TODO we may have to check what else the Account class provides, in which case we have to map those things
// this is saved as part of interaction.lastSubmission
const result = {
login: {
accountId: user.id,
},
ghost: !!user.ghost
};
const [interactionFinishError, redirectTo] = await safe(gOidcProvider.interactionResult(req, res, result));
@@ -452,81 +447,68 @@ async function interactionLogin(req, res, next) {
}
async function interactionConfirm(req, res, next) {
async function raiseLoginEvent(user, clientId) {
try {
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
const userAgent = req.headers['user-agent'] || '';
const auditSource = AuditSource.fromOidcRequest(req);
const interactionDetails = await gOidcProvider.interactionDetails(req, res);
const { grantId, uid, prompt: { name, details }, params, session: { accountId }, lastSubmission } = interactionDetails;
await eventlog.add(user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: user.id, user: users.removePrivateFields(user), appId: clientId });
await users.notifyLoginLocation(user, ip, userAgent, auditSource);
} catch (e) {
console.error('oidc: Failed to raise login event.', e);
debug(`route interaction confirm post uid:${uid} prompt.name:${name} accountId:${accountId}`);
const client = await oidcClients.get(params.client_id);
if (!client) return next(new Error('Client not found'));
const user = await users.get(accountId);
if (!user) return next(new Error('User not found'));
user.ghost = lastSubmission.ghost; // restore ghost flag
// Check if user has access to the app if client refers to an app
if (client.appId) {
const app = await apps.get(client.appId);
if (!app) return next(new Error('App not found'));
if (!apps.canAccess(app, user)) {
const result = {
error: 'access_denied',
error_description: 'User has no access to this app',
};
return await gOidcProvider.interactionFinished(req, res, result, { mergeWithLastSubmission: false });
}
}
try {
const interactionDetails = await gOidcProvider.interactionDetails(req, res);
const { grantId, uid, prompt: { name, details }, params, session: { accountId } } = interactionDetails;
debug(`route interaction confirm post uid:${uid} prompt.name:${name} accountId:${accountId}`);
assert.equal(name, 'consent');
const client = await oidcClients.get(params.client_id);
const user = await users.get(accountId);
// Check if user has access to the app if client refers to an app
// In most cases the user interaction already ends in the consent screen (see above)
if (client.appId) {
const app = await apps.get(client.appId);
if (!apps.canAccess(app, user)) {
const result = {
error: 'access_denied',
error_description: 'User has no access to this app',
};
await raiseLoginEvent(user, client.appId);
return await gOidcProvider.interactionFinished(req, res, result, { mergeWithLastSubmission: false });
}
}
let grant;
if (grantId) {
grant = await gOidcProvider.Grant.find(grantId);
} else {
grant = new gOidcProvider.Grant({
accountId,
clientId: params.client_id,
});
}
if (details.missingOIDCScope) {
grant.addOIDCScope(details.missingOIDCScope.join(' '));
}
if (details.missingOIDCClaims) {
grant.addOIDCClaims(details.missingOIDCClaims);
}
if (details.missingResourceScopes) {
for (const [indicator, scopes] of Object.entries(details.missingResourceScopes)) {
grant.addResourceScope(indicator, scopes.join(' '));
}
}
const savedGrantId = await grant.save();
const consent = {};
if (!interactionDetails.grantId) consent.grantId = savedGrantId;
await raiseLoginEvent(user, params.client_id);
const result = { consent };
await gOidcProvider.interactionFinished(req, res, result, { mergeWithLastSubmission: true });
} catch (err) {
next(err);
let grant;
if (grantId) {
grant = await gOidcProvider.Grant.find(grantId);
} else {
grant = new gOidcProvider.Grant({
accountId,
clientId: params.client_id,
});
}
// just confirm everything
if (details.missingOIDCScope) grant.addOIDCScope(details.missingOIDCScope.join(' '));
if (details.missingOIDCClaims) grant.addOIDCClaims(details.missingOIDCClaims);
if (details.missingResourceScopes) {
for (const [indicator, scopes] of Object.entries(details.missingResourceScopes)) {
grant.addResourceScope(indicator, scopes.join(' '));
}
}
const savedGrantId = await grant.save();
const consent = {};
if (!interactionDetails.grantId) consent.grantId = savedGrantId;
// create login event
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
const userAgent = req.headers['user-agent'] || '';
const auditSource = AuditSource.fromOidcRequest(req);
await eventlog.add(user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: user.id, user: users.removePrivateFields(user), appId: params.client_id });
await users.notifyLoginLocation(user, ip, userAgent, auditSource);
const result = { consent };
await gOidcProvider.interactionFinished(req, res, result, { mergeWithLastSubmission: true });
}
async function interactionAbort(req, res, next) {