summaryrefslogtreecommitdiff
path: root/src/server/api/private
diff options
context:
space:
mode:
authorMary <Ipadlover8322@gmail.com>2019-07-03 07:18:07 -0400
committersyuilo <Syuilotan@yahoo.co.jp>2019-07-03 20:18:07 +0900
commitfd94b817abd8fa628586746eed3a1f61b4a2b3d8 (patch)
tree53eccf1b923f9b29f73ec6651b361b1682af3247 /src/server/api/private
parentResolve #5072 (diff)
downloadsharkey-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.ts137
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();
};