mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-13 13:37:31 -07:00
Support password-less login with WebAuthn (#5112)
* Support password-less login with WebAuthn * Fix initial value of usePasswordLessLogin
This commit is contained in:
parent
15384920c4
commit
80c55fbc76
8 changed files with 90 additions and 10 deletions
|
@ -1112,6 +1112,7 @@ desktop/views/components/settings.2fa.vue:
|
|||
register-security-key: "キーの登録を完了"
|
||||
something-went-wrong: "わー! キーを登録する際に問題が発生しました:"
|
||||
key-unregistered: "キーが削除されました"
|
||||
use-password-less-login: "パスワードなしのログインを使用"
|
||||
|
||||
common/views/components/media-image.vue:
|
||||
sensitive: "閲覧注意"
|
||||
|
|
13
migration/1562422242907-PasswordLessLogin.ts
Normal file
13
migration/1562422242907-PasswordLessLogin.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class PasswordLessLogin1562422242907 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "usePasswordLessLogin" boolean DEFAULT false NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "usePasswordLessLogin"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -28,6 +28,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ui-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">
|
||||
{{ $t('use-password-less-login') }}
|
||||
</ui-switch>
|
||||
|
||||
<ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info>
|
||||
<ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button>
|
||||
|
||||
|
@ -80,6 +84,7 @@ export default Vue.extend({
|
|||
return {
|
||||
data: null,
|
||||
supportsCredentials: !!navigator.credentials,
|
||||
usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
|
||||
registration: null,
|
||||
keyName: '',
|
||||
token: null
|
||||
|
@ -112,6 +117,9 @@ export default Vue.extend({
|
|||
if (canceled) return;
|
||||
this.$root.api('i/2fa/unregister', {
|
||||
password: password
|
||||
}).then(() => {
|
||||
this.usePasswordLessLogin = false;
|
||||
this.updatePasswordLessLogin();
|
||||
}).then(() => {
|
||||
this.$notify(this.$t('unregistered'));
|
||||
this.$store.state.i.twoFactorEnabled = false;
|
||||
|
@ -157,6 +165,9 @@ export default Vue.extend({
|
|||
return this.$root.api('i/2fa/remove-key', {
|
||||
password,
|
||||
credentialId: key.id
|
||||
}).then(() => {
|
||||
this.usePasswordLessLogin = false;
|
||||
this.updatePasswordLessLogin();
|
||||
}).then(() => {
|
||||
this.$notify(this.$t('key-unregistered'));
|
||||
});
|
||||
|
@ -213,6 +224,11 @@ export default Vue.extend({
|
|||
this.registration.stage = -1;
|
||||
});
|
||||
});
|
||||
},
|
||||
updatePasswordLessLogin() {
|
||||
this.$root.api('i/2fa/password-less', {
|
||||
value: !!this.usePasswordLessLogin
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</ui-input>
|
||||
<ui-input v-model="password" type="password" :with-password-toggle="true" required>
|
||||
<ui-input v-model="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
|
||||
<span>{{ $t('password') }}</span>
|
||||
<template #prefix><fa icon="lock"/></template>
|
||||
</ui-input>
|
||||
|
@ -28,6 +28,10 @@
|
|||
</div>
|
||||
<div class="twofa-group totp-group">
|
||||
<p style="margin-bottom:0;">{{ $t('enter-2fa-code') }}</p>
|
||||
<ui-input v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required>
|
||||
<span>{{ $t('password') }}</span>
|
||||
<template #prefix><fa icon="lock"/></template>
|
||||
</ui-input>
|
||||
<ui-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
|
||||
<span>{{ $t('@.2fa') }}</span>
|
||||
<template #prefix><fa icon="gavel"/></template>
|
||||
|
|
|
@ -81,6 +81,11 @@ export class UserProfile {
|
|||
})
|
||||
public securityKeysAvailable: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public usePasswordLessLogin: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The password hash of the User. It will be null if the origin of the user is local.'
|
||||
|
|
|
@ -156,6 +156,7 @@ export class UserRepository extends Repository<User> {
|
|||
detail: true
|
||||
}),
|
||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
||||
securityKeys: profile!.twoFactorEnabled
|
||||
? UserSecurityKeys.count({
|
||||
userId: user.id
|
||||
|
@ -208,7 +209,6 @@ export class UserRepository extends Repository<User> {
|
|||
select: ['id', 'name', 'lastUsed']
|
||||
})
|
||||
: []
|
||||
|
||||
} : {}),
|
||||
|
||||
...(relation ? {
|
||||
|
|
21
src/server/api/endpoints/i/2fa/password-less.ts
Normal file
21
src/server/api/endpoints/i/2fa/password-less.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { UserProfiles } from '../../../../../models';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
value: {
|
||||
validator: $.boolean
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
await UserProfiles.update(user.id, {
|
||||
usePasswordLessLogin: ps.value
|
||||
});
|
||||
});
|
|
@ -72,19 +72,25 @@ export default async (ctx: Koa.BaseContext) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (!same) {
|
||||
await fail(403, {
|
||||
error: 'incorrect password'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profile.twoFactorEnabled) {
|
||||
signin(ctx, user);
|
||||
if (same) {
|
||||
signin(ctx, user);
|
||||
} else {
|
||||
await fail(403, {
|
||||
error: 'incorrect password'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
if (!same) {
|
||||
await fail(403, {
|
||||
error: 'incorrect password'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const verified = (speakeasy as any).totp.verify({
|
||||
secret: profile.twoFactorSecret,
|
||||
encoding: 'base32',
|
||||
|
@ -101,6 +107,13 @@ export default async (ctx: Koa.BaseContext) => {
|
|||
return;
|
||||
}
|
||||
} else if (body.credentialId) {
|
||||
if (!same && !profile.usePasswordLessLogin) {
|
||||
await fail(403, {
|
||||
error: 'incorrect password'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
|
||||
const clientData = JSON.parse(clientDataJSON.toString('utf-8'));
|
||||
const challenge = await AttestationChallenges.findOne({
|
||||
|
@ -163,6 +176,13 @@ export default async (ctx: Koa.BaseContext) => {
|
|||
return;
|
||||
}
|
||||
} else {
|
||||
if (!same && !profile.usePasswordLessLogin) {
|
||||
await fail(403, {
|
||||
error: 'incorrect password'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = await UserSecurityKeys.find({
|
||||
userId: user.id
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue