2FA-Login: Prüfung schlägt nicht offen fehl, Fehlversuche vor dem Aufruf zählen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
acc86c267b
commit
d7c2027d3f
6 files changed
+48
-5
No files matched your search
@@ -10,7 +10,7 @@ import {
|
||||
updateDocument,
|
||||
Query,
|
||||
} from '../services/appwriteAdmin.js'
|
||||
import { loginWithAppwrite, getLoginCooldownRemainingSec } from '../services/appwriteClient.js'
|
||||
import { loginWithAppwrite, getLoginCooldownRemainingSec, sessionNeedsSecondFactor } from '../services/appwriteClient.js'
|
||||
import {
|
||||
SESSION_REVOKED_MESSAGE,
|
||||
clearPortalSession,
|
||||
@@ -169,7 +169,8 @@ router.post('/login', async (req, res) => {
|
||||
const staff = await resolveStaffAccount(user.$id, email.trim(), account)
|
||||
if (staff) {
|
||||
// Mitarbeiter mit Appwrite-2FA: erst der Code aus der Mail macht daraus eine Portal-Sitzung
|
||||
if (account?.mfa === true) {
|
||||
// mfa-Flag ODER Appwrite verlangt einen zweiten Faktor: scheitert der Konto-Abruf, entscheidet Appwrite
|
||||
if (account?.mfa === true || (await sessionNeedsSecondFactor(user.sessionCookie))) {
|
||||
const { key, maskedEmail } = await startStaffMfa({ userId: user.$id, email: staff.email, sessionCookie: user.sessionCookie }).catch((err) => {
|
||||
if (err.type === 'user_email_not_verified') {
|
||||
err.status = 403
|
||||
|
||||
@@ -64,6 +64,21 @@ export function appwriteSessionFetch(path, { method = 'GET', body, sessionCookie
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Braucht die Sitzung noch einen zweiten Faktor (Appwrite-2FA)? Fragt Appwrite selbst statt des
|
||||
* mfa-Flags aus dem Admin-Abruf, damit ein gestoerter Abruf nicht ohne Code durchlaesst. Andere
|
||||
* Fehler werden geworfen (fail closed).
|
||||
*/
|
||||
export async function sessionNeedsSecondFactor(sessionCookie) {
|
||||
try {
|
||||
await appwriteSessionFetch('/account', { sessionCookie })
|
||||
return false
|
||||
} catch (err) {
|
||||
if (err.type === 'user_more_factors_required') return true
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login via Appwrite Auth REST. userId kommt aus der Session – kein users.read nötig.
|
||||
*/
|
||||
|
||||
@@ -51,7 +51,9 @@ export async function startStaffMfa({ userId, email, sessionCookie }) {
|
||||
/** @returns {Promise<{ok: true, userId: string, email: string} | {ok: false, reason: 'invalid' | 'gone'}>} */
|
||||
export async function verifyStaffMfa(key, otp) {
|
||||
const entry = liveEntry(key)
|
||||
if (!entry) return { ok: false, reason: 'gone' }
|
||||
if (!entry || entry.attempts >= MAX_ATTEMPTS) return { ok: false, reason: 'gone' }
|
||||
// Versuch vor dem Appwrite-Aufruf reservieren, sonst zaehlen parallele Anfragen alle als erster Versuch
|
||||
entry.attempts += 1
|
||||
try {
|
||||
await appwriteSessionFetch('/account/mfa/challenges', {
|
||||
method: 'PUT',
|
||||
@@ -59,8 +61,10 @@ export async function verifyStaffMfa(key, otp) {
|
||||
sessionCookie: entry.sessionCookie,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err.type !== 'user_invalid_token') throw err
|
||||
entry.attempts += 1
|
||||
if (err.type !== 'user_invalid_token') {
|
||||
entry.attempts -= 1
|
||||
throw err
|
||||
}
|
||||
if (entry.attempts >= MAX_ATTEMPTS) pending.delete(key)
|
||||
return { ok: false, reason: 'invalid' }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user