From fd94b817abd8fa628586746eed3a1f61b4a2b3d8 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 3 Jul 2019 07:18:07 -0400 Subject: Implement Webauthn 🎉 (#5088) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement Webauthn :tada: * Share hexifyAB * Move hr inside template and add AttestationChallenges janitor daemon * Apply suggestions from code review Co-Authored-By: Acid Chicken (硫酸鶏) * Add newline at the end of file * Fix stray newline in promise chain * Ignore var in try{}catch(){} block Co-Authored-By: Acid Chicken (硫酸鶏) * Add missing comma * Add missing semicolon * Support more attestation formats * add support for more key types and linter pass * Refactor * Refactor * credentialId --> id * Fix * Improve readability * Add indexes * fixes for credentialId->id * Avoid changing store state * Fix syntax error and code style * Remove unused import * Refactor of getkey API * Create 1561706992953-webauthn.ts * Update ja-JP.yml * Add type annotations * Fix code style * Specify depedency version * Fix code style * Fix janitor daemon and login requesting 2FA regardless of status --- src/server/api/private/signin.ts | 137 ++++++++++++++++++++++++++++++--------- 1 file changed, 107 insertions(+), 30 deletions(-) (limited to 'src/server/api/private') diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 02361a139d..cd9fe5bb9d 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -4,10 +4,11 @@ import * as speakeasy from 'speakeasy'; import { publishMainStream } from '../../../services/stream'; import signin from '../common/signin'; import config from '../../../config'; -import { Users, Signins, UserProfiles } from '../../../models'; +import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '../../../models'; import { ILocalUser } from '../../../models/entities/user'; import { genId } from '../../../misc/gen-id'; import { ensure } from '../../../prelude/ensure'; +import { verifyLogin, hash } from '../2fa'; export default async (ctx: Koa.BaseContext) => { ctx.set('Access-Control-Allow-Origin', config.url); @@ -51,40 +52,116 @@ export default async (ctx: Koa.BaseContext) => { // Compare password const same = await bcrypt.compare(password, profile.password!); - if (same) { - if (profile.twoFactorEnabled) { - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorSecret, - encoding: 'base32', - token: token - }); + async function fail(status?: number, failure?: {error: string}) { + // Append signin history + const record = await Signins.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + ip: ctx.ip, + headers: ctx.headers, + success: !!(status || failure) + }); - if (verified) { - signin(ctx, user); - } else { - ctx.throw(403, { - error: 'invalid token' - }); - } - } else { + // Publish signin event + publishMainStream(user.id, 'signin', await Signins.pack(record)); + + if (status && failure) { + ctx.throw(status, failure); + } + } + + if (!same) { + await fail(403, { + error: 'incorrect password' + }); + return; + } + + if (!profile.twoFactorEnabled) { + signin(ctx, user); + return; + } + + if (token) { + const verified = (speakeasy as any).totp.verify({ + secret: profile.twoFactorSecret, + encoding: 'base32', + token: token + }); + + if (verified) { signin(ctx, user); + return; + } else { + await fail(403, { + error: 'invalid token' + }); + return; } } else { - ctx.throw(403, { - error: 'incorrect password' + const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); + const clientData = JSON.parse(clientDataJSON.toString('utf-8')); + const challenge = await AttestationChallenges.findOne({ + userId: user.id, + id: body.challengeId, + registrationChallenge: false, + challenge: hash(clientData.challenge).toString('hex') }); + + if (!challenge) { + await fail(403, { + error: 'non-existent challenge' + }); + return; + } + + await AttestationChallenges.delete({ + userId: user.id, + id: body.challengeId + }); + + if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) { + await fail(403, { + error: 'non-existent challenge' + }); + return; + } + + const securityKey = await UserSecurityKeys.findOne({ + id: Buffer.from( + body.credentialId + .replace(/\-/g, '+') + .replace(/_/g, '/'), + 'base64' + ).toString('hex') + }); + + if (!securityKey) { + await fail(403, { + error: 'invalid credentialId' + }); + return; + } + + const isValid = verifyLogin({ + publicKey: Buffer.from(securityKey.publicKey, 'hex'), + authenticatorData: Buffer.from(body.authenticatorData, 'hex'), + clientDataJSON, + clientData, + signature: Buffer.from(body.signature, 'hex'), + challenge: challenge.challenge + }); + + if (isValid) { + signin(ctx, user); + } else { + await fail(403, { + error: 'invalid challenge data' + }); + return; + } } - // Append signin history - const record = await Signins.save({ - id: genId(), - createdAt: new Date(), - userId: user.id, - ip: ctx.ip, - headers: ctx.headers, - success: same - }); - - // Publish signin event - publishMainStream(user.id, 'signin', await Signins.pack(record)); + await fail(); }; -- cgit v1.2.3-freya