fix(auth): Add-credential-Challenges pro Versuch und single-use
Speichert Challenges nach challenge statt userId für parallele Flows und invalidiert sie vor der Verifikation, damit fehlgeschlagene Versuche keine Leaks hinterlassen. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -613,7 +613,7 @@ export async function addPasskey(): Promise<void> {
|
|||||||
|
|
||||||
await apiJson(`${API_BASE}/add-credential-verify`, {
|
await apiJson(`${API_BASE}/add-credential-verify`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ credentialResponse })
|
body: JSON.stringify({ credentialResponse, challenge: options.challenge })
|
||||||
})
|
})
|
||||||
|
|
||||||
const masterKey = getActiveMasterKey()
|
const masterKey = getActiveMasterKey()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const rpID = process.env.RP_ID || 'localhost'
|
|||||||
const origin = process.env.ORIGIN || 'http://localhost:5173'
|
const origin = process.env.ORIGIN || 'http://localhost:5173'
|
||||||
|
|
||||||
const registrationChallenges = new Map<string, string>()
|
const registrationChallenges = new Map<string, string>()
|
||||||
|
/** WebAuthn registration challenges for add-credential flow: challenge -> userId */
|
||||||
const addCredentialChallenges = new Map<string, string>()
|
const addCredentialChallenges = new Map<string, string>()
|
||||||
const activeChallenges = new Set<string>()
|
const activeChallenges = new Set<string>()
|
||||||
|
|
||||||
@@ -462,7 +463,7 @@ router.post('/add-credential-options', requireReauth, async (req: any, res) => {
|
|||||||
excludeCredentials
|
excludeCredentials
|
||||||
})
|
})
|
||||||
|
|
||||||
addCredentialChallenges.set(req.userId, options.challenge)
|
addCredentialChallenges.set(options.challenge, req.userId)
|
||||||
|
|
||||||
return res.json(options)
|
return res.json(options)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -473,16 +474,23 @@ router.post('/add-credential-options', requireReauth, async (req: any, res) => {
|
|||||||
|
|
||||||
router.post('/add-credential-verify', requireReauth, async (req: any, res) => {
|
router.post('/add-credential-verify', requireReauth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { credentialResponse } = req.body
|
const { credentialResponse, challenge } = req.body
|
||||||
if (!credentialResponse) {
|
if (!credentialResponse || !challenge) {
|
||||||
return res.status(400).json({ error: 'credentialResponse is required' })
|
return res.status(400).json({ error: 'credentialResponse and challenge are required' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedChallenge = addCredentialChallenges.get(req.userId)
|
const challengeUserId = addCredentialChallenges.get(challenge)
|
||||||
if (!expectedChallenge) {
|
if (!challengeUserId) {
|
||||||
return res.status(400).json({ error: 'Challenge not found or expired' })
|
return res.status(400).json({ error: 'Challenge not found or expired' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (challengeUserId !== req.userId) {
|
||||||
|
return res.status(403).json({ error: 'Challenge does not belong to this account' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-use: invalidate before verification so failed attempts cannot be retried
|
||||||
|
addCredentialChallenges.delete(challenge)
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: req.userId }
|
where: { id: req.userId }
|
||||||
})
|
})
|
||||||
@@ -493,7 +501,7 @@ router.post('/add-credential-verify', requireReauth, async (req: any, res) => {
|
|||||||
|
|
||||||
const verification = await verifyRegistrationResponse({
|
const verification = await verifyRegistrationResponse({
|
||||||
response: credentialResponse,
|
response: credentialResponse,
|
||||||
expectedChallenge,
|
expectedChallenge: challenge,
|
||||||
expectedOrigin: origin,
|
expectedOrigin: origin,
|
||||||
expectedRPID: rpID
|
expectedRPID: rpID
|
||||||
})
|
})
|
||||||
@@ -522,8 +530,6 @@ router.post('/add-credential-verify', requireReauth, async (req: any, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
addCredentialChallenges.delete(req.userId)
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
verified: true,
|
verified: true,
|
||||||
credential: {
|
credential: {
|
||||||
|
|||||||
Reference in New Issue
Block a user