diff options
| author | Mary <Ipadlover8322@gmail.com> | 2019-07-03 07:18:07 -0400 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2019-07-03 20:18:07 +0900 |
| commit | fd94b817abd8fa628586746eed3a1f61b4a2b3d8 (patch) | |
| tree | 53eccf1b923f9b29f73ec6651b361b1682af3247 /src/server/api/private | |
| parent | Resolve #5072 (diff) | |
| download | sharkey-fd94b817abd8fa628586746eed3a1f61b4a2b3d8.tar.gz sharkey-fd94b817abd8fa628586746eed3a1f61b4a2b3d8.tar.bz2 sharkey-fd94b817abd8fa628586746eed3a1f61b4a2b3d8.zip | |
Implement Webauthn ๐ (#5088)
* Implement Webauthn :tada:
* Share hexifyAB
* Move hr inside template and add AttestationChallenges janitor daemon
* Apply suggestions from code review
Co-Authored-By: Acid Chicken (็กซ้
ธ้ถ) <root@acid-chicken.com>
* Add newline at the end of file
* Fix stray newline in promise chain
* Ignore var in try{}catch(){} block
Co-Authored-By: Acid Chicken (็กซ้
ธ้ถ) <root@acid-chicken.com>
* 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
Diffstat (limited to 'src/server/api/private')
| -rw-r--r-- | src/server/api/private/signin.ts | 137 |
1 files changed, 107 insertions, 30 deletions
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 { - signin(ctx, user); + // Publish signin event + publishMainStream(user.id, 'signin', await Signins.pack(record)); + + if (status && failure) { + ctx.throw(status, failure); } - } else { - ctx.throw(403, { + } + + if (!same) { + await fail(403, { error: 'incorrect password' }); + 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 - }); + 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 { + 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; + } + } - // Publish signin event - publishMainStream(user.id, 'signin', await Signins.pack(record)); + await fail(); }; |