Implement Webauthn 🎉 (#5088)
* Implement Webauthn 🎉
* 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
This commit is contained in:
parent
f17e229c1e
commit
fd94b817ab
21 changed files with 1376 additions and 64 deletions
|
|
@ -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
|
||||
});
|
||||
|
||||
if (verified) {
|
||||
signin(ctx, user);
|
||||
} else {
|
||||
ctx.throw(403, {
|
||||
error: 'invalid token'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
signin(ctx, user);
|
||||
}
|
||||
} else {
|
||||
ctx.throw(403, {
|
||||
error: 'incorrect password'
|
||||
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)
|
||||
});
|
||||
|
||||
// Publish signin event
|
||||
publishMainStream(user.id, 'signin', await Signins.pack(record));
|
||||
|
||||
if (status && failure) {
|
||||
ctx.throw(status, failure);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (!same) {
|
||||
await fail(403, {
|
||||
error: 'incorrect password'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Publish signin event
|
||||
publishMainStream(user.id, 'signin', await Signins.pack(record));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
await fail();
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue