diff --git a/CHANGES b/CHANGES index 45098c951..abd565f66 100644 --- a/CHANGES +++ b/CHANGES @@ -2945,4 +2945,5 @@ * mail: rename delivered -> sent and received -> saved in event log * graphs: replace collectd with custom collector * profile: drop gravatar support +* login: suppress notification of impersonated users diff --git a/src/oidcserver.js b/src/oidcserver.js index a75ad581c..df4197321 100644 --- a/src/oidcserver.js +++ b/src/oidcserver.js @@ -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) { diff --git a/src/users.js b/src/users.js index 3dc58d91a..963176fda 100644 --- a/src/users.js +++ b/src/users.js @@ -747,7 +747,7 @@ async function notifyLoginLocation(user, ip, userAgent, auditSource) { assert.strictEqual(typeof userAgent, 'string'); assert.strictEqual(typeof auditSource, 'object'); - debug(`notifyLoginLocation: ${user.id} ${ip} ${userAgent}`); + debug(`notifyLoginLocation: ${user.id} ${ip} ${userAgent} ghost:${!!user.ghost}`); if (constants.DEMO) return; if (constants.TEST && ip === '127.0.0.1') return;