Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(frontend): サインイン画面の改善 #14658

Merged
merged 43 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5416198
wip
syuilo Sep 30, 2024
d5a3fb1
Merge remote-tracking branch 'msky/signin-captcha' into enh-tweak-sig…
kakkokari-gtyih Sep 30, 2024
a3fe00d
Update MkSignin.vue
syuilo Sep 30, 2024
4597848
Update MkSignin.vue
syuilo Sep 30, 2024
af7fc7c
wip
syuilo Sep 30, 2024
087b34b
Update CHANGELOG.md
syuilo Sep 30, 2024
80d068c
enhance(frontend): サインイン画面の改善
kakkokari-gtyih Sep 30, 2024
75854f5
Update Changelog
kakkokari-gtyih Sep 30, 2024
8865cef
Merge remote-tracking branch 'msky/signin-captcha' into enh-tweak-sig…
kakkokari-gtyih Sep 30, 2024
76e3950
Merge branch 'develop' into enh-tweak-signin-dialog
kakkokari-gtyih Sep 30, 2024
305925d
14655の変更取り込み
kakkokari-gtyih Sep 30, 2024
8ba9bd9
Merge branch 'enh-tweak-signin-dialog' of https://github.com/kakkokar…
kakkokari-gtyih Sep 30, 2024
6496df3
spdx
kakkokari-gtyih Sep 30, 2024
d568dda
fix
kakkokari-gtyih Sep 30, 2024
319cc79
fix
kakkokari-gtyih Sep 30, 2024
0636d6b
fix
kakkokari-gtyih Sep 30, 2024
c3f1d27
:art:
syuilo Oct 1, 2024
d4fa970
:art:
syuilo Oct 1, 2024
ee8e1b5
:art:
syuilo Oct 1, 2024
6983287
:art:
syuilo Oct 3, 2024
ae4c690
Merge branch 'develop' into enh-tweak-signin-dialog
kakkokari-gtyih Oct 3, 2024
65aac75
Captchaがリセットされない問題を修正
kakkokari-gtyih Oct 3, 2024
adabff7
次の処理をsignin apiから読み取るように
kakkokari-gtyih Oct 3, 2024
e91fc62
Add Comments
kakkokari-gtyih Oct 3, 2024
fdc696b
fix
kakkokari-gtyih Oct 3, 2024
1e88676
fix test
kakkokari-gtyih Oct 3, 2024
374e7d5
attempt to fix test
kakkokari-gtyih Oct 3, 2024
1134187
fix test
kakkokari-gtyih Oct 3, 2024
d5c96ed
fix test
kakkokari-gtyih Oct 3, 2024
51c5bc6
fix test
kakkokari-gtyih Oct 3, 2024
ee6e69c
fix
kakkokari-gtyih Oct 3, 2024
e8d93e7
fix test
kakkokari-gtyih Oct 3, 2024
46c9451
Merge branch 'develop' into enh-tweak-signin-dialog
kakkokari-gtyih Oct 3, 2024
88325d8
fix: 一部のエラーがちゃんと出るように
kakkokari-gtyih Oct 4, 2024
46f68cb
Update Changelog
kakkokari-gtyih Oct 4, 2024
9cf047c
:art:
kakkokari-gtyih Oct 4, 2024
70e772e
:art:
kakkokari-gtyih Oct 4, 2024
0d81649
Merge branch 'develop' into pr/14658
syuilo Oct 4, 2024
0db8ac0
Merge branch 'develop' into pr/14658
syuilo Oct 4, 2024
6efa4a3
remove border
kakkokari-gtyih Oct 4, 2024
9e45d62
Merge branch 'enh-tweak-signin-dialog' of https://github.com/kakkokar…
kakkokari-gtyih Oct 4, 2024
6cea42a
Merge branch 'develop' into pr/14658
syuilo Oct 4, 2024
e962140
Merge branch 'develop' into enh-tweak-signin-dialog
kakkokari-gtyih Oct 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
- サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)
- ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。
- なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。
- ユーザーデータを読み込む際の型が一部変更されました。
- `twoFactorEnabled`, `usePasswordLessLogin`, `securityKeys`: 自分とモデレーター以外のユーザーからは取得できなくなりました

### General
- Feat: サーバー初期設定時に初期パスワードを設定できるように
Expand All @@ -14,9 +16,11 @@

### Client
- Enhance: デザインの調整
- Enhance: ログイン画面の認証フローを改善

### Server
- Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
- Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように


## 2024.9.0
Expand Down
14 changes: 10 additions & 4 deletions cypress/e2e/basic.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,13 @@ describe('After user signup', () => {
cy.intercept('POST', '/api/signin').as('signin');

cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice');
// Enterキーでサインインできるかの確認も兼ねる

cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
// Enterキーで続行できるかの確認も兼ねる
cy.get('[data-cy-signin-username] input').type('alice{enter}');

cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
// Enterキーで続行できるかの確認も兼ねる
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');

cy.wait('@signin');
Expand All @@ -139,8 +144,9 @@ describe('After user signup', () => {
cy.visitHome();

cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice');
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');

cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
cy.get('[data-cy-signin-username] input').type('alice{enter}');

// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
Expand Down
4 changes: 3 additions & 1 deletion cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ Cypress.Commands.add('login', (username, password) => {
cy.intercept('POST', '/api/signin').as('signin');

cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type(username);
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
cy.get('[data-cy-signin-username] input').type(`${username}{enter}`);
cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);

cy.wait('@signin').as('signedIn');
Expand Down
4 changes: 4 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3714,6 +3714,10 @@ export interface Locale extends ILocale {
* パスワードが間違っています。
*/
"incorrectPassword": string;
/**
* ワンタイムパスワードが間違っているか、期限切れになっています。
*/
"incorrectTotp": string;
/**
* 「{choice}」に投票しますか?
*/
Expand Down
1 change: 1 addition & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,7 @@ followersVisibility: "フォロワーの公開範囲"
continueThread: "さらにスレッドを見る"
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
incorrectPassword: "パスワードが間違っています。"
incorrectTotp: "ワンタイムパスワードが間違っているか、期限切れになっています。"
voteConfirm: "「{choice}」に投票しますか?"
hide: "隠す"
useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示"
Expand Down
13 changes: 8 additions & 5 deletions packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit {
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
: false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
name: role.name,
Expand All @@ -564,6 +559,14 @@ export class UserEntityService implements OnModuleInit {
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),

...(isDetailed && (isMe || iAmModerator) ? {
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
: false,
} : {}),

...(isDetailed && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
Expand Down
42 changes: 27 additions & 15 deletions packages/backend/src/models/json-schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
},
twoFactorEnabled: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
usePasswordLessLogin: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
securityKeys: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
roles: {
type: 'array',
nullable: false, optional: false,
Expand All @@ -382,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string',
nullable: false, optional: true,
},
twoFactorEnabled: {
type: 'boolean',
nullable: false, optional: true,
},
usePasswordLessLogin: {
type: 'boolean',
nullable: false, optional: true,
},
securityKeys: {
type: 'boolean',
nullable: false, optional: true,
},
//#region relations
isFollowing: {
type: 'boolean',
Expand Down Expand Up @@ -630,6 +627,21 @@ export const packedMeDetailedOnlySchema = {
nullable: false, optional: false,
ref: 'RolePolicies',
},
twoFactorEnabled: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
usePasswordLessLogin: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
securityKeys: {
type: 'boolean',
nullable: false, optional: false,
default: false,
},
//#region secrets
email: {
type: 'string',
Expand Down
86 changes: 74 additions & 12 deletions packages/backend/src/server/api/SigninApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
MiMeta,
SigninsRepository,
UserProfilesRepository,
UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
Expand All @@ -25,9 +26,27 @@ import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';

/**
* next を指定すると、次にクライアント側で行うべき処理を指定できる。
*
* - `captcha`: パスワードと、(有効になっている場合は)CAPTCHAを求める
* - `password`: パスワードを求める
* - `totp`: ワンタイムパスワードを求める
* - `passkey`: WebAuthn認証を求める(WebAuthnに対応していないブラウザの場合はワンタイムパスワード)
*/

type SigninErrorResponse = {
id: string;
next?: 'captcha' | 'password' | 'totp';
} | {
id: string;
next: 'passkey';
authRequest: PublicKeyCredentialRequestOptionsJSON;
};

@Injectable()
export class SigninApiService {
constructor(
Expand All @@ -43,6 +62,9 @@ export class SigninApiService {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,

@Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository,

@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,

Expand All @@ -60,7 +82,7 @@ export class SigninApiService {
request: FastifyRequest<{
Body: {
username: string;
password: string;
password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string;
Expand All @@ -79,7 +101,7 @@ export class SigninApiService {
const password = body['password'];
const token = body['token'];

function error(status: number, error: { id: string }) {
function error(status: number, error: SigninErrorResponse) {
reply.code(status);
return { error };
}
Expand All @@ -103,11 +125,6 @@ export class SigninApiService {
return;
}

if (typeof password !== 'string') {
reply.code(400);
return;
}

if (token != null && typeof token !== 'string') {
reply.code(400);
return;
Expand All @@ -132,11 +149,36 @@ export class SigninApiService {
}

const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);

if (password == null) {
reply.code(403);
if (profile.twoFactorEnabled) {
return {
error: {
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'password',
},
} satisfies { error: SigninErrorResponse };
} else {
return {
error: {
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'captcha',
},
} satisfies { error: SigninErrorResponse };
}
}

if (typeof password !== 'string') {
reply.code(400);
return;
}

// Compare password
const same = await bcrypt.compare(password, profile.password!);

const fail = async (status?: number, failure?: { id: string }) => {
const fail = async (status?: number, failure?: SigninErrorResponse) => {
// Append signin history
await this.signinsRepository.insert({
id: this.idService.gen(),
Expand Down Expand Up @@ -217,7 +259,7 @@ export class SigninApiService {
id: '93b86c4b-72f9-40eb-9815-798928603d1e',
});
}
} else {
} else if (securityKeysAvailable) {
if (!same && !profile.usePasswordLessLogin) {
return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
Expand All @@ -226,8 +268,28 @@ export class SigninApiService {

const authRequest = await this.webAuthnService.initiateAuthentication(user.id);

reply.code(200);
return authRequest;
reply.code(403);
return {
error: {
id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4',
next: 'passkey',
authRequest,
},
} satisfies { error: SigninErrorResponse };
} else {
if (!same || !profile.twoFactorEnabled) {
return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
});
} else {
reply.code(403);
return {
error: {
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'totp',
},
} satisfies { error: SigninErrorResponse };
}
}
// never get here
}
Expand Down
Loading
Loading