feat(auth): implement Android refresh token handling and session management
- Added support for generating Android access tokens and managing refresh sessions in the auth endpoints. - Implemented new tests for login, logout, and refresh functionalities specific to Android clients. - Enhanced password reset logging with normalization and masking of email addresses. - Created a new diagnostics endpoint for password reset attempts, including filtering and summarizing logs. - Introduced a new utility for managing password reset logs with retention policies. - Added tests for password reset log utilities to ensure proper functionality and privacy compliance. - Updated WebAuthn configuration tests to validate origin handling for production and allowed origins.
This commit is contained in:
@@ -5,6 +5,7 @@ vi.mock('../server/utils/auth.js', () => ({
|
||||
getUserFromToken: vi.fn(),
|
||||
readUsers: vi.fn(),
|
||||
writeUsers: vi.fn(),
|
||||
revokeRefreshSessionsForUser: vi.fn(),
|
||||
hasRole: vi.fn((user, role) => {
|
||||
if (!user) return false
|
||||
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
|
||||
@@ -37,14 +38,23 @@ vi.mock('nodemailer', () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../server/utils/password-reset-log.js', () => ({
|
||||
fingerprintResetEmail: vi.fn(email => `fingerprint:${String(email || '').trim().toLowerCase()}`),
|
||||
normalizeResetEmail: vi.fn(email => String(email || '').trim().toLowerCase()),
|
||||
PASSWORD_RESET_LOG_RETENTION_HOURS: 72,
|
||||
readPasswordResetLogs: vi.fn()
|
||||
}))
|
||||
|
||||
const authUtils = await import('../server/utils/auth.js')
|
||||
const nodemailer = await import('nodemailer')
|
||||
const passwordResetLog = await import('../server/utils/password-reset-log.js')
|
||||
|
||||
import usersListHandler from '../server/api/cms/users/list.get.js'
|
||||
import usersApproveHandler from '../server/api/cms/users/approve.post.js'
|
||||
import usersDeactivateHandler from '../server/api/cms/users/deactivate.post.js'
|
||||
import usersRejectHandler from '../server/api/cms/users/reject.post.js'
|
||||
import usersUpdateRoleHandler from '../server/api/cms/users/update-role.post.js'
|
||||
import passwordResetDiagnosticsHandler from '../server/api/cms/password-reset-diagnostics.get.js'
|
||||
|
||||
describe('CMS User Management Endpoints', () => {
|
||||
beforeEach(() => {
|
||||
@@ -138,6 +148,7 @@ describe('CMS User Management Endpoints', () => {
|
||||
const response = await usersDeactivateHandler(event)
|
||||
expect(response.success).toBe(true)
|
||||
expect(authUtils.writeUsers).toHaveBeenCalled()
|
||||
expect(authUtils.revokeRefreshSessionsForUser).toHaveBeenCalledWith('1', 'account_deactivated')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -184,4 +195,49 @@ describe('CMS User Management Endpoints', () => {
|
||||
expect(authUtils.writeUsers).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/cms/password-reset-diagnostics', () => {
|
||||
it('verweigert Zugriff für Vorstand ohne Admin-Rolle', async () => {
|
||||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||
authUtils.getUserFromToken.mockResolvedValue({ id: 'vorstand', roles: ['vorstand'] })
|
||||
authUtils.hasRole.mockReturnValue(false)
|
||||
|
||||
await expect(passwordResetDiagnosticsHandler(event)).rejects.toMatchObject({ statusCode: 403 })
|
||||
})
|
||||
|
||||
it('findet Benutzer und gefilterte fehlgeschlagene Reset-Abläufe', async () => {
|
||||
const event = adminEvent()
|
||||
event.__query = { email: ' User@Example.com ', failedOnly: 'true' }
|
||||
authUtils.hasRole.mockReturnValue(true)
|
||||
authUtils.readUsers.mockResolvedValue([
|
||||
{ id: '1', email: 'user@example.com', name: 'User Beispiel', active: true }
|
||||
])
|
||||
passwordResetLog.readPasswordResetLogs.mockResolvedValue([
|
||||
{
|
||||
requestId: 'r1',
|
||||
ts: '2026-05-27T10:00:01.000Z',
|
||||
emailMasked: 'us***@ex***.com',
|
||||
emailFingerprint: 'fingerprint:user@example.com',
|
||||
ip: '127.0.0.1',
|
||||
step: 'request_completed',
|
||||
status: 'no_account'
|
||||
},
|
||||
{
|
||||
requestId: 'r2',
|
||||
ts: '2026-05-27T10:00:02.000Z',
|
||||
emailMasked: 'ot***@ex***.com',
|
||||
emailFingerprint: 'fingerprint:other@example.com',
|
||||
ip: '127.0.0.1',
|
||||
step: 'request_completed',
|
||||
status: 'failed'
|
||||
}
|
||||
])
|
||||
|
||||
const response = await passwordResetDiagnosticsHandler(event)
|
||||
|
||||
expect(response.matchingUsers).toHaveLength(1)
|
||||
expect(response.attempts).toHaveLength(1)
|
||||
expect(response.attempts[0]).toMatchObject({ requestId: 'r1', failed: true })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user