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:
かっこかり 2024-10-05 12:03:47 +09:00 committed by GitHub
parent fa06c59eae
commit ae3c155490
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 230 additions and 234 deletions

View file

@ -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();

View file

@ -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 });

View file

@ -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: {

View file

@ -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

View file

@ -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;
} }
} }

View file

@ -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);
// 後片付け // 後片付け

View file

@ -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',
}); });

View file

@ -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),
});
}
} }
} }

View file

@ -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 {

View file

@ -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();
} }

View file

@ -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)

View file

@ -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;

View file

@ -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];