Fix WebAuthn login (#5103)

This commit is contained in:
Satsuki Yanagi 2019-07-05 07:48:12 +09:00 committed by syuilo
parent d5caf22d8c
commit 114523e69e
3 changed files with 51 additions and 74 deletions

View file

@ -107,9 +107,8 @@ export default Vue.extend({
})), })),
timeout: 60 * 1000 timeout: 60 * 1000
} }
}).catch(err => { }).catch(() => {
this.queryingKey = false; this.queryingKey = false;
console.warn(err);
return Promise.reject(null); return Promise.reject(null);
}).then(credential => { }).then(credential => {
this.queryingKey = false; this.queryingKey = false;
@ -127,8 +126,7 @@ export default Vue.extend({
localStorage.setItem('i', res.i); localStorage.setItem('i', res.i);
location.reload(); location.reload();
}).catch(err => { }).catch(err => {
if(err === null) return; if (err === null) return;
console.error(err);
this.$root.dialog({ this.$root.dialog({
type: 'error', type: 'error',
text: this.$t('login-failed') text: this.$t('login-failed')
@ -142,7 +140,7 @@ export default Vue.extend({
if (!this.totpLogin && this.user && this.user.twoFactorEnabled) { if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
if (window.PublicKeyCredential && this.user.securityKeys) { if (window.PublicKeyCredential && this.user.securityKeys) {
this.$root.api('i/2fa/getkeys', { this.$root.api('signin', {
username: this.username, username: this.username,
password: this.password password: this.password
}).then(res => { }).then(res => {
@ -150,6 +148,14 @@ export default Vue.extend({
this.signing = false; this.signing = false;
this.challengeData = res; this.challengeData = res;
return this.queryKey(); return this.queryKey();
}).catch(() => {
this.$root.dialog({
type: 'error',
text: this.$t('login-failed')
});
this.challengeData = null;
this.totpLogin = false;
this.signing = false;
}); });
} else { } else {
this.totpLogin = true; this.totpLogin = true;

View file

@ -1,67 +0,0 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import * as crypto from 'crypto';
import define from '../../../define';
import { UserProfiles, UserSecurityKeys, AttestationChallenges } from '../../../../../models';
import { ensure } from '../../../../../prelude/ensure';
import { promisify } from 'util';
import { hash } from '../../../2fa';
import { genId } from '../../../../../misc/gen-id';
export const meta = {
requireCredential: true,
secure: true,
params: {
password: {
validator: $.str
}
}
};
const randomBytes = promisify(crypto.randomBytes);
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
}
const keys = await UserSecurityKeys.find({
userId: user.id
});
if (keys.length === 0) {
throw new Error('no keys found');
}
// 32 byte challenge
const entropy = await randomBytes(32);
const challenge = entropy.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const challengeId = genId();
await AttestationChallenges.save({
userId: user.id,
id: challengeId,
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
createdAt: new Date(),
registrationChallenge: false
});
return {
challenge,
challengeId,
securityKeys: keys.map(key => ({
id: key.id
}))
};
});

View file

@ -9,6 +9,7 @@ import { ILocalUser } from '../../../models/entities/user';
import { genId } from '../../../misc/gen-id'; import { genId } from '../../../misc/gen-id';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
import { verifyLogin, hash } from '../2fa'; import { verifyLogin, hash } from '../2fa';
import { randomBytes } from 'crypto';
export default async (ctx: Koa.BaseContext) => { export default async (ctx: Koa.BaseContext) => {
ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Origin', config.url);
@ -99,7 +100,7 @@ export default async (ctx: Koa.BaseContext) => {
}); });
return; return;
} }
} else { } else if (body.credentialId) {
const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
const clientData = JSON.parse(clientDataJSON.toString('utf-8')); const clientData = JSON.parse(clientDataJSON.toString('utf-8'));
const challenge = await AttestationChallenges.findOne({ const challenge = await AttestationChallenges.findOne({
@ -131,7 +132,7 @@ export default async (ctx: Koa.BaseContext) => {
const securityKey = await UserSecurityKeys.findOne({ const securityKey = await UserSecurityKeys.findOne({
id: Buffer.from( id: Buffer.from(
body.credentialId body.credentialId
.replace(/\-/g, '+') .replace(/-/g, '+')
.replace(/_/g, '/'), .replace(/_/g, '/'),
'base64' 'base64'
).toString('hex') ).toString('hex')
@ -161,7 +162,44 @@ export default async (ctx: Koa.BaseContext) => {
}); });
return; return;
} }
} else {
const keys = await UserSecurityKeys.find({
userId: user.id
});
if (keys.length === 0) {
await fail(403, {
error: 'no keys found'
});
}
// 32 byte challenge
const challenge = randomBytes(32).toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const challengeId = genId();
await AttestationChallenges.save({
userId: user.id,
id: challengeId,
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
createdAt: new Date(),
registrationChallenge: false
});
ctx.body = {
challenge,
challengeId,
securityKeys: keys.map(key => ({
id: key.id
}))
};
ctx.status = 200;
return;
} }
await fail(); await fail();
return;
}; };