fix: signin の資格情報が足りないだけの場合はエラーにせず200を返すように (#14700)
* fix: signin の資格情報が足りないだけの場合はエラーにせず200を返すように * run api extractor * fix * fix * fix test * /signin -> /signin-flow * fix * fix lint * rename * fix * fix
This commit is contained in:
parent
fa06c59eae
commit
ae3c155490
13 changed files with 230 additions and 234 deletions
|
@ -120,7 +120,7 @@ describe('After user signup', () => {
|
||||||
it('signin', () => {
|
it('signin', () => {
|
||||||
cy.visitHome();
|
cy.visitHome();
|
||||||
|
|
||||||
cy.intercept('POST', '/api/signin').as('signin');
|
cy.intercept('POST', '/api/signin-flow').as('signin');
|
||||||
|
|
||||||
cy.get('[data-cy-signin]').click();
|
cy.get('[data-cy-signin]').click();
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
|
||||||
Cypress.Commands.add('login', (username, password) => {
|
Cypress.Commands.add('login', (username, password) => {
|
||||||
cy.visitHome();
|
cy.visitHome();
|
||||||
|
|
||||||
cy.intercept('POST', '/api/signin').as('signin');
|
cy.intercept('POST', '/api/signin-flow').as('signin');
|
||||||
|
|
||||||
cy.get('[data-cy-signin]').click();
|
cy.get('[data-cy-signin]').click();
|
||||||
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
|
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
|
||||||
|
|
|
@ -133,7 +133,7 @@ export class ApiServerService {
|
||||||
'turnstile-response'?: string;
|
'turnstile-response'?: string;
|
||||||
'm-captcha-response'?: string;
|
'm-captcha-response'?: string;
|
||||||
};
|
};
|
||||||
}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
|
}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
|
||||||
|
|
||||||
fastify.post<{
|
fastify.post<{
|
||||||
Body: {
|
Body: {
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import * as OTPAuth from 'otpauth';
|
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type {
|
import type {
|
||||||
MiMeta,
|
MiMeta,
|
||||||
|
@ -26,27 +26,9 @@ import { CaptchaService } from '@/core/CaptchaService.js';
|
||||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||||
import { RateLimiterService } from './RateLimiterService.js';
|
import { RateLimiterService } from './RateLimiterService.js';
|
||||||
import { SigninService } from './SigninService.js';
|
import { SigninService } from './SigninService.js';
|
||||||
import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
|
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
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()
|
@Injectable()
|
||||||
export class SigninApiService {
|
export class SigninApiService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -101,7 +83,7 @@ export class SigninApiService {
|
||||||
const password = body['password'];
|
const password = body['password'];
|
||||||
const token = body['token'];
|
const token = body['token'];
|
||||||
|
|
||||||
function error(status: number, error: SigninErrorResponse) {
|
function error(status: number, error: { id: string }) {
|
||||||
reply.code(status);
|
reply.code(status);
|
||||||
return { error };
|
return { error };
|
||||||
}
|
}
|
||||||
|
@ -152,21 +134,17 @@ export class SigninApiService {
|
||||||
const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
|
const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
|
||||||
|
|
||||||
if (password == null) {
|
if (password == null) {
|
||||||
reply.code(403);
|
reply.code(200);
|
||||||
if (profile.twoFactorEnabled) {
|
if (profile.twoFactorEnabled) {
|
||||||
return {
|
return {
|
||||||
error: {
|
finished: false,
|
||||||
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
|
next: 'password',
|
||||||
next: 'password',
|
} satisfies Misskey.entities.SigninFlowResponse;
|
||||||
},
|
|
||||||
} satisfies { error: SigninErrorResponse };
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
error: {
|
finished: false,
|
||||||
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
|
next: 'captcha',
|
||||||
next: 'captcha',
|
} satisfies Misskey.entities.SigninFlowResponse;
|
||||||
},
|
|
||||||
} satisfies { error: SigninErrorResponse };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +156,7 @@ export class SigninApiService {
|
||||||
// Compare password
|
// Compare password
|
||||||
const same = await bcrypt.compare(password, profile.password!);
|
const same = await bcrypt.compare(password, profile.password!);
|
||||||
|
|
||||||
const fail = async (status?: number, failure?: SigninErrorResponse) => {
|
const fail = async (status?: number, failure?: { id: string; }) => {
|
||||||
// Append signin history
|
// Append signin history
|
||||||
await this.signinsRepository.insert({
|
await this.signinsRepository.insert({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
|
@ -268,27 +246,23 @@ export class SigninApiService {
|
||||||
|
|
||||||
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
|
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
|
||||||
|
|
||||||
reply.code(403);
|
reply.code(200);
|
||||||
return {
|
return {
|
||||||
error: {
|
finished: false,
|
||||||
id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4',
|
next: 'passkey',
|
||||||
next: 'passkey',
|
authRequest,
|
||||||
authRequest,
|
} satisfies Misskey.entities.SigninFlowResponse;
|
||||||
},
|
|
||||||
} satisfies { error: SigninErrorResponse };
|
|
||||||
} else {
|
} else {
|
||||||
if (!same || !profile.twoFactorEnabled) {
|
if (!same || !profile.twoFactorEnabled) {
|
||||||
return await fail(403, {
|
return await fail(403, {
|
||||||
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reply.code(403);
|
reply.code(200);
|
||||||
return {
|
return {
|
||||||
error: {
|
finished: false,
|
||||||
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
|
next: 'totp',
|
||||||
next: 'totp',
|
} satisfies Misskey.entities.SigninFlowResponse;
|
||||||
},
|
|
||||||
} satisfies { error: SigninErrorResponse };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// never get here
|
// never get here
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
|
import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
@ -57,9 +58,10 @@ export class SigninService {
|
||||||
|
|
||||||
reply.code(200);
|
reply.code(200);
|
||||||
return {
|
return {
|
||||||
|
finished: true,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
i: user.token,
|
i: user.token!,
|
||||||
};
|
} satisfies Misskey.entities.SigninFlowResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,7 @@ describe('2要素認証', () => {
|
||||||
keyName: string,
|
keyName: string,
|
||||||
credentialId: Buffer,
|
credentialId: Buffer,
|
||||||
requestOptions: PublicKeyCredentialRequestOptionsJSON,
|
requestOptions: PublicKeyCredentialRequestOptionsJSON,
|
||||||
}): misskey.entities.SigninRequest => {
|
}): misskey.entities.SigninFlowRequest => {
|
||||||
// AuthenticatorAssertionResponse.authenticatorData
|
// AuthenticatorAssertionResponse.authenticatorData
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
|
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
|
||||||
const authenticatorData = Buffer.concat([
|
const authenticatorData = Buffer.concat([
|
||||||
|
@ -196,22 +196,21 @@ describe('2要素認証', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(doneResponse.status, 200);
|
assert.strictEqual(doneResponse.status, 200);
|
||||||
|
|
||||||
const signinWithoutTokenResponse = await api('signin', {
|
const signinWithoutTokenResponse = await api('signin-flow', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
});
|
});
|
||||||
assert.strictEqual(signinWithoutTokenResponse.status, 403);
|
assert.strictEqual(signinWithoutTokenResponse.status, 200);
|
||||||
assert.deepStrictEqual(signinWithoutTokenResponse.body, {
|
assert.deepStrictEqual(signinWithoutTokenResponse.body, {
|
||||||
error: {
|
finished: false,
|
||||||
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
|
next: 'totp',
|
||||||
next: 'totp',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const signinResponse = await api('signin', {
|
const signinResponse = await api('signin-flow', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
token: otpToken(registerResponse.body.secret),
|
token: otpToken(registerResponse.body.secret),
|
||||||
});
|
});
|
||||||
assert.strictEqual(signinResponse.status, 200);
|
assert.strictEqual(signinResponse.status, 200);
|
||||||
|
assert.strictEqual(signinResponse.body.finished, true);
|
||||||
assert.notEqual(signinResponse.body.i, undefined);
|
assert.notEqual(signinResponse.body.i, undefined);
|
||||||
|
|
||||||
// 後片付け
|
// 後片付け
|
||||||
|
@ -252,29 +251,23 @@ describe('2要素認証', () => {
|
||||||
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
|
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
|
||||||
assert.strictEqual(keyDoneResponse.body.name, keyName);
|
assert.strictEqual(keyDoneResponse.body.name, keyName);
|
||||||
|
|
||||||
const signinResponse = await api('signin', {
|
const signinResponse = await api('signin-flow', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
});
|
});
|
||||||
const signinResponseBody = signinResponse.body as unknown as {
|
assert.strictEqual(signinResponse.status, 200);
|
||||||
error: {
|
assert.strictEqual(signinResponse.body.finished, false);
|
||||||
id: string;
|
assert.strictEqual(signinResponse.body.next, 'passkey');
|
||||||
next: 'passkey';
|
assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
|
||||||
authRequest: PublicKeyCredentialRequestOptionsJSON;
|
assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
|
||||||
};
|
assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
|
||||||
};
|
|
||||||
assert.strictEqual(signinResponse.status, 403);
|
|
||||||
assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4');
|
|
||||||
assert.strictEqual(signinResponseBody.error.next, 'passkey');
|
|
||||||
assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined);
|
|
||||||
assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined);
|
|
||||||
assert.strictEqual(signinResponseBody.error.authRequest.allowCredentials && signinResponseBody.error.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
|
|
||||||
|
|
||||||
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
|
const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
requestOptions: signinResponseBody.error.authRequest,
|
requestOptions: signinResponse.body.authRequest,
|
||||||
}));
|
}));
|
||||||
assert.strictEqual(signinResponse2.status, 200);
|
assert.strictEqual(signinResponse2.status, 200);
|
||||||
|
assert.strictEqual(signinResponse2.body.finished, true);
|
||||||
assert.notEqual(signinResponse2.body.i, undefined);
|
assert.notEqual(signinResponse2.body.i, undefined);
|
||||||
|
|
||||||
// 後片付け
|
// 後片付け
|
||||||
|
@ -320,32 +313,26 @@ describe('2要素認証', () => {
|
||||||
assert.strictEqual(iResponse.status, 200);
|
assert.strictEqual(iResponse.status, 200);
|
||||||
assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
|
assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
|
||||||
|
|
||||||
const signinResponse = await api('signin', {
|
const signinResponse = await api('signin-flow', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
password: '',
|
password: '',
|
||||||
});
|
});
|
||||||
const signinResponseBody = signinResponse.body as unknown as {
|
assert.strictEqual(signinResponse.status, 200);
|
||||||
error: {
|
assert.strictEqual(signinResponse.body.finished, false);
|
||||||
id: string;
|
assert.strictEqual(signinResponse.body.next, 'passkey');
|
||||||
next: 'passkey';
|
assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
|
||||||
authRequest: PublicKeyCredentialRequestOptionsJSON;
|
assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
|
||||||
};
|
|
||||||
};
|
|
||||||
assert.strictEqual(signinResponse.status, 403);
|
|
||||||
assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4');
|
|
||||||
assert.strictEqual(signinResponseBody.error.next, 'passkey');
|
|
||||||
assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined);
|
|
||||||
assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined);
|
|
||||||
|
|
||||||
const signinResponse2 = await api('signin', {
|
const signinResponse2 = await api('signin-flow', {
|
||||||
...signinWithSecurityKeyParam({
|
...signinWithSecurityKeyParam({
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
requestOptions: signinResponseBody.error.authRequest,
|
requestOptions: signinResponse.body.authRequest,
|
||||||
} as any),
|
} as any),
|
||||||
password: '',
|
password: '',
|
||||||
});
|
});
|
||||||
assert.strictEqual(signinResponse2.status, 200);
|
assert.strictEqual(signinResponse2.status, 200);
|
||||||
|
assert.strictEqual(signinResponse2.body.finished, true);
|
||||||
assert.notEqual(signinResponse2.body.i, undefined);
|
assert.notEqual(signinResponse2.body.i, undefined);
|
||||||
|
|
||||||
// 後片付け
|
// 後片付け
|
||||||
|
@ -450,11 +437,12 @@ describe('2要素認証', () => {
|
||||||
assert.strictEqual(afterIResponse.status, 200);
|
assert.strictEqual(afterIResponse.status, 200);
|
||||||
assert.strictEqual(afterIResponse.body.securityKeys, false);
|
assert.strictEqual(afterIResponse.body.securityKeys, false);
|
||||||
|
|
||||||
const signinResponse = await api('signin', {
|
const signinResponse = await api('signin-flow', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
token: otpToken(registerResponse.body.secret),
|
token: otpToken(registerResponse.body.secret),
|
||||||
});
|
});
|
||||||
assert.strictEqual(signinResponse.status, 200);
|
assert.strictEqual(signinResponse.status, 200);
|
||||||
|
assert.strictEqual(signinResponse.body.finished, true);
|
||||||
assert.notEqual(signinResponse.body.i, undefined);
|
assert.notEqual(signinResponse.body.i, undefined);
|
||||||
|
|
||||||
// 後片付け
|
// 後片付け
|
||||||
|
@ -485,10 +473,11 @@ describe('2要素認証', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(unregisterResponse.status, 204);
|
assert.strictEqual(unregisterResponse.status, 204);
|
||||||
|
|
||||||
const signinResponse = await api('signin', {
|
const signinResponse = await api('signin-flow', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
});
|
});
|
||||||
assert.strictEqual(signinResponse.status, 200);
|
assert.strictEqual(signinResponse.status, 200);
|
||||||
|
assert.strictEqual(signinResponse.body.finished, true);
|
||||||
assert.notEqual(signinResponse.body.i, undefined);
|
assert.notEqual(signinResponse.body.i, undefined);
|
||||||
|
|
||||||
// 後片付け
|
// 後片付け
|
||||||
|
|
|
@ -66,9 +66,9 @@ describe('Endpoints', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('signin', () => {
|
describe('signin-flow', () => {
|
||||||
test('間違ったパスワードでサインインできない', async () => {
|
test('間違ったパスワードでサインインできない', async () => {
|
||||||
const res = await api('signin', {
|
const res = await api('signin-flow', {
|
||||||
username: 'test1',
|
username: 'test1',
|
||||||
password: 'bar',
|
password: 'bar',
|
||||||
});
|
});
|
||||||
|
@ -77,7 +77,7 @@ describe('Endpoints', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('クエリをインジェクションできない', async () => {
|
test('クエリをインジェクションできない', async () => {
|
||||||
const res = await api('signin', {
|
const res = await api('signin-flow', {
|
||||||
username: 'test1',
|
username: 'test1',
|
||||||
// @ts-expect-error password must be string
|
// @ts-expect-error password must be string
|
||||||
password: {
|
password: {
|
||||||
|
@ -89,7 +89,7 @@ describe('Endpoints', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('正しい情報でサインインできる', async () => {
|
test('正しい情報でサインインできる', async () => {
|
||||||
const res = await api('signin', {
|
const res = await api('signin-flow', {
|
||||||
username: 'test1',
|
username: 'test1',
|
||||||
password: 'test1',
|
password: 'test1',
|
||||||
});
|
});
|
||||||
|
|
|
@ -83,7 +83,7 @@ import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/br
|
||||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'login', v: Misskey.entities.SigninResponse): void;
|
(ev: 'login', v: Misskey.entities.SigninFlowResponse): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
|
@ -212,23 +212,63 @@ async function onTotpSubmitted(token: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryLogin(req: Partial<Misskey.entities.SigninRequest>): Promise<Misskey.entities.SigninResponse> {
|
async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promise<Misskey.entities.SigninFlowResponse> {
|
||||||
const _req = {
|
const _req = {
|
||||||
username: req.username ?? userInfo.value?.username,
|
username: req.username ?? userInfo.value?.username,
|
||||||
...req,
|
...req,
|
||||||
};
|
};
|
||||||
|
|
||||||
function assertIsSigninRequest(x: Partial<Misskey.entities.SigninRequest>): x is Misskey.entities.SigninRequest {
|
function assertIsSigninFlowRequest(x: Partial<Misskey.entities.SigninFlowRequest>): x is Misskey.entities.SigninFlowRequest {
|
||||||
return x.username != null;
|
return x.username != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!assertIsSigninRequest(_req)) {
|
if (!assertIsSigninFlowRequest(_req)) {
|
||||||
throw new Error('Invalid request');
|
throw new Error('Invalid request');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await misskeyApi('signin', _req).then(async (res) => {
|
return await misskeyApi('signin-flow', _req).then(async (res) => {
|
||||||
emit('login', res);
|
if (res.finished) {
|
||||||
await onLoginSucceeded(res);
|
emit('login', res);
|
||||||
|
await onLoginSucceeded(res);
|
||||||
|
} else {
|
||||||
|
switch (res.next) {
|
||||||
|
case 'captcha': {
|
||||||
|
needCaptcha.value = true;
|
||||||
|
page.value = 'password';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'password': {
|
||||||
|
needCaptcha.value = false;
|
||||||
|
page.value = 'password';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'totp': {
|
||||||
|
page.value = 'totp';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'passkey': {
|
||||||
|
if (webAuthnSupported()) {
|
||||||
|
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||||
|
publicKey: res.authRequest,
|
||||||
|
});
|
||||||
|
page.value = 'passkey';
|
||||||
|
} else {
|
||||||
|
page.value = 'totp';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doingPasskeyFromInputPage.value === true) {
|
||||||
|
doingPasskeyFromInputPage.value = false;
|
||||||
|
page.value = 'input';
|
||||||
|
password.value = '';
|
||||||
|
}
|
||||||
|
passwordPageEl.value?.resetCaptcha();
|
||||||
|
nextTick(() => {
|
||||||
|
waiting.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
return res;
|
return res;
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
onSigninApiError(err);
|
onSigninApiError(err);
|
||||||
|
@ -236,7 +276,7 @@ async function tryLogin(req: Partial<Misskey.entities.SigninRequest>): Promise<M
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
|
async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true; }) {
|
||||||
if (props.autoSet) {
|
if (props.autoSet) {
|
||||||
await login(res.i);
|
await login(res.i);
|
||||||
}
|
}
|
||||||
|
@ -245,112 +285,82 @@ async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
|
||||||
function onSigninApiError(err?: any): void {
|
function onSigninApiError(err?: any): void {
|
||||||
const id = err?.id ?? null;
|
const id = err?.id ?? null;
|
||||||
|
|
||||||
if (typeof err === 'object' && 'next' in err) {
|
switch (id) {
|
||||||
switch (err.next) {
|
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
|
||||||
case 'captcha': {
|
os.alert({
|
||||||
needCaptcha.value = true;
|
type: 'error',
|
||||||
page.value = 'password';
|
title: i18n.ts.loginFailed,
|
||||||
break;
|
text: i18n.ts.noSuchUser,
|
||||||
}
|
});
|
||||||
case 'password': {
|
break;
|
||||||
needCaptcha.value = false;
|
|
||||||
page.value = 'password';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'totp': {
|
|
||||||
page.value = 'totp';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'passkey': {
|
|
||||||
if (webAuthnSupported() && 'authRequest' in err) {
|
|
||||||
credentialRequest.value = parseRequestOptionsFromJSON({
|
|
||||||
publicKey: err.authRequest,
|
|
||||||
});
|
|
||||||
page.value = 'passkey';
|
|
||||||
} else {
|
|
||||||
page.value = 'totp';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
|
||||||
switch (id) {
|
os.alert({
|
||||||
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
|
type: 'error',
|
||||||
os.alert({
|
title: i18n.ts.loginFailed,
|
||||||
type: 'error',
|
text: i18n.ts.incorrectPassword,
|
||||||
title: i18n.ts.loginFailed,
|
});
|
||||||
text: i18n.ts.noSuchUser,
|
break;
|
||||||
});
|
}
|
||||||
break;
|
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
|
||||||
}
|
showSuspendedDialog();
|
||||||
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
|
break;
|
||||||
os.alert({
|
}
|
||||||
type: 'error',
|
case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
|
||||||
title: i18n.ts.loginFailed,
|
os.alert({
|
||||||
text: i18n.ts.incorrectPassword,
|
type: 'error',
|
||||||
});
|
title: i18n.ts.loginFailed,
|
||||||
break;
|
text: i18n.ts.rateLimitExceeded,
|
||||||
}
|
});
|
||||||
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
|
break;
|
||||||
showSuspendedDialog();
|
}
|
||||||
break;
|
case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
|
||||||
}
|
os.alert({
|
||||||
case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
|
type: 'error',
|
||||||
os.alert({
|
title: i18n.ts.loginFailed,
|
||||||
type: 'error',
|
text: i18n.ts.incorrectTotp,
|
||||||
title: i18n.ts.loginFailed,
|
});
|
||||||
text: i18n.ts.rateLimitExceeded,
|
break;
|
||||||
});
|
}
|
||||||
break;
|
case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
|
||||||
}
|
os.alert({
|
||||||
case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
|
type: 'error',
|
||||||
os.alert({
|
title: i18n.ts.loginFailed,
|
||||||
type: 'error',
|
text: i18n.ts.unknownWebAuthnKey,
|
||||||
title: i18n.ts.loginFailed,
|
});
|
||||||
text: i18n.ts.incorrectTotp,
|
break;
|
||||||
});
|
}
|
||||||
break;
|
case '93b86c4b-72f9-40eb-9815-798928603d1e': {
|
||||||
}
|
os.alert({
|
||||||
case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
|
type: 'error',
|
||||||
os.alert({
|
title: i18n.ts.loginFailed,
|
||||||
type: 'error',
|
text: i18n.ts.passkeyVerificationFailed,
|
||||||
title: i18n.ts.loginFailed,
|
});
|
||||||
text: i18n.ts.unknownWebAuthnKey,
|
break;
|
||||||
});
|
}
|
||||||
break;
|
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
|
||||||
}
|
os.alert({
|
||||||
case '93b86c4b-72f9-40eb-9815-798928603d1e': {
|
type: 'error',
|
||||||
os.alert({
|
title: i18n.ts.loginFailed,
|
||||||
type: 'error',
|
text: i18n.ts.passkeyVerificationFailed,
|
||||||
title: i18n.ts.loginFailed,
|
});
|
||||||
text: i18n.ts.passkeyVerificationFailed,
|
break;
|
||||||
});
|
}
|
||||||
break;
|
case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
|
||||||
}
|
os.alert({
|
||||||
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
|
type: 'error',
|
||||||
os.alert({
|
title: i18n.ts.loginFailed,
|
||||||
type: 'error',
|
text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
|
||||||
title: i18n.ts.loginFailed,
|
});
|
||||||
text: i18n.ts.passkeyVerificationFailed,
|
break;
|
||||||
});
|
}
|
||||||
break;
|
default: {
|
||||||
}
|
console.error(err);
|
||||||
case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
|
os.alert({
|
||||||
os.alert({
|
type: 'error',
|
||||||
type: 'error',
|
title: i18n.ts.loginFailed,
|
||||||
title: i18n.ts.loginFailed,
|
text: JSON.stringify(err),
|
||||||
text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
|
});
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
console.error(err);
|
|
||||||
os.alert({
|
|
||||||
type: 'error',
|
|
||||||
title: i18n.ts.loginFailed,
|
|
||||||
text: JSON.stringify(err),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'signup', user: Misskey.entities.SigninResponse): void;
|
(ev: 'signup', user: Misskey.entities.SigninFlowResponse): void;
|
||||||
(ev: 'signupEmailPending'): void;
|
(ev: 'signupEmailPending'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -269,14 +269,19 @@ async function onSubmit(): Promise<void> {
|
||||||
});
|
});
|
||||||
emit('signupEmailPending');
|
emit('signupEmailPending');
|
||||||
} else {
|
} else {
|
||||||
const res = await misskeyApi('signin', {
|
const res = await misskeyApi('signin-flow', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
});
|
});
|
||||||
emit('signup', res);
|
emit('signup', res);
|
||||||
|
|
||||||
if (props.autoSet) {
|
if (props.autoSet && res.finished) {
|
||||||
return login(res.i);
|
return login(res.i);
|
||||||
|
} else {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: i18n.ts.somethingHappened,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', res: Misskey.entities.SigninResponse): void;
|
(ev: 'done', res: Misskey.entities.SigninFlowResponse): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
|
||||||
const isAcceptedServerRule = ref(false);
|
const isAcceptedServerRule = ref(false);
|
||||||
|
|
||||||
function onSignup(res: Misskey.entities.SigninResponse) {
|
function onSignup(res: Misskey.entities.SigninFlowResponse) {
|
||||||
emit('done', res);
|
emit('done', res);
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1158,9 +1158,9 @@ export type Endpoints = Overwrite<Endpoints_2, {
|
||||||
req: SignupPendingRequest;
|
req: SignupPendingRequest;
|
||||||
res: SignupPendingResponse;
|
res: SignupPendingResponse;
|
||||||
};
|
};
|
||||||
'signin': {
|
'signin-flow': {
|
||||||
req: SigninRequest;
|
req: SigninFlowRequest;
|
||||||
res: SigninResponse;
|
res: SigninFlowResponse;
|
||||||
};
|
};
|
||||||
'signin-with-passkey': {
|
'signin-with-passkey': {
|
||||||
req: SigninWithPasskeyRequest;
|
req: SigninWithPasskeyRequest;
|
||||||
|
@ -1208,11 +1208,11 @@ declare namespace entities {
|
||||||
SignupResponse,
|
SignupResponse,
|
||||||
SignupPendingRequest,
|
SignupPendingRequest,
|
||||||
SignupPendingResponse,
|
SignupPendingResponse,
|
||||||
SigninRequest,
|
SigninFlowRequest,
|
||||||
|
SigninFlowResponse,
|
||||||
SigninWithPasskeyRequest,
|
SigninWithPasskeyRequest,
|
||||||
SigninWithPasskeyInitResponse,
|
SigninWithPasskeyInitResponse,
|
||||||
SigninWithPasskeyResponse,
|
SigninWithPasskeyResponse,
|
||||||
SigninResponse,
|
|
||||||
PartialRolePolicyOverride,
|
PartialRolePolicyOverride,
|
||||||
EmptyRequest,
|
EmptyRequest,
|
||||||
EmptyResponse,
|
EmptyResponse,
|
||||||
|
@ -3038,7 +3038,7 @@ type ServerStatsLog = ServerStats[];
|
||||||
type Signin = components['schemas']['Signin'];
|
type Signin = components['schemas']['Signin'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type SigninRequest = {
|
type SigninFlowRequest = {
|
||||||
username: string;
|
username: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
@ -3050,9 +3050,17 @@ type SigninRequest = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type SigninResponse = {
|
type SigninFlowResponse = {
|
||||||
|
finished: true;
|
||||||
id: User['id'];
|
id: User['id'];
|
||||||
i: string;
|
i: string;
|
||||||
|
} | {
|
||||||
|
finished: false;
|
||||||
|
next: 'captcha' | 'password' | 'totp';
|
||||||
|
} | {
|
||||||
|
finished: false;
|
||||||
|
next: 'passkey';
|
||||||
|
authRequest: PublicKeyCredentialRequestOptionsJSON;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -3069,7 +3077,7 @@ type SigninWithPasskeyRequest = {
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type SigninWithPasskeyResponse = {
|
type SigninWithPasskeyResponse = {
|
||||||
signinResponse: SigninResponse;
|
signinResponse: SigninFlowResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { UserDetailed } from './autogen/models.js';
|
||||||
import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
|
import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
|
||||||
import {
|
import {
|
||||||
PartialRolePolicyOverride,
|
PartialRolePolicyOverride,
|
||||||
SigninRequest,
|
SigninFlowRequest,
|
||||||
SigninResponse,
|
SigninFlowResponse,
|
||||||
SigninWithPasskeyInitResponse,
|
SigninWithPasskeyInitResponse,
|
||||||
SigninWithPasskeyRequest,
|
SigninWithPasskeyRequest,
|
||||||
SigninWithPasskeyResponse,
|
SigninWithPasskeyResponse,
|
||||||
|
@ -81,9 +81,9 @@ export type Endpoints = Overwrite<
|
||||||
res: SignupPendingResponse;
|
res: SignupPendingResponse;
|
||||||
},
|
},
|
||||||
// api.jsonには載せないものなのでここで定義
|
// api.jsonには載せないものなのでここで定義
|
||||||
'signin': {
|
'signin-flow': {
|
||||||
req: SigninRequest;
|
req: SigninFlowRequest;
|
||||||
res: SigninResponse;
|
res: SigninFlowResponse;
|
||||||
},
|
},
|
||||||
'signin-with-passkey': {
|
'signin-with-passkey': {
|
||||||
req: SigninWithPasskeyRequest;
|
req: SigninWithPasskeyRequest;
|
||||||
|
|
|
@ -267,7 +267,7 @@ export type SignupPendingResponse = {
|
||||||
i: string,
|
i: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SigninRequest = {
|
export type SigninFlowRequest = {
|
||||||
username: string;
|
username: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
@ -278,6 +278,19 @@ export type SigninRequest = {
|
||||||
'm-captcha-response'?: string | null;
|
'm-captcha-response'?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SigninFlowResponse = {
|
||||||
|
finished: true;
|
||||||
|
id: User['id'];
|
||||||
|
i: string;
|
||||||
|
} | {
|
||||||
|
finished: false;
|
||||||
|
next: 'captcha' | 'password' | 'totp';
|
||||||
|
} | {
|
||||||
|
finished: false;
|
||||||
|
next: 'passkey';
|
||||||
|
authRequest: PublicKeyCredentialRequestOptionsJSON;
|
||||||
|
};
|
||||||
|
|
||||||
export type SigninWithPasskeyRequest = {
|
export type SigninWithPasskeyRequest = {
|
||||||
credential?: AuthenticationResponseJSON;
|
credential?: AuthenticationResponseJSON;
|
||||||
context?: string;
|
context?: string;
|
||||||
|
@ -289,12 +302,7 @@ export type SigninWithPasskeyInitResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SigninWithPasskeyResponse = {
|
export type SigninWithPasskeyResponse = {
|
||||||
signinResponse: SigninResponse;
|
signinResponse: SigninFlowResponse;
|
||||||
};
|
|
||||||
|
|
||||||
export type SigninResponse = {
|
|
||||||
id: User['id'],
|
|
||||||
i: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];
|
type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];
|
||||||
|
|
Loading…
Reference in a new issue