summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/SigninApiService.ts
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-12-15 17:27:12 +0000
committerdakkar <dakkar@thenautilus.net>2024-12-15 17:27:12 +0000
commite2352839e4639b09e2e52b2ada1399097fad1d8d (patch)
tree9268cda477b8c1dcfb2c78eaabcb173a1566a469 /packages/backend/src/server/api/SigninApiService.ts
parentmerge: Fix rate limits under multi-node environments (!809) (diff)
parentupstream merge checklist: remember to check federated profile fields (diff)
downloadsharkey-e2352839e4639b09e2e52b2ada1399097fad1d8d.tar.gz
sharkey-e2352839e4639b09e2e52b2ada1399097fad1d8d.tar.bz2
sharkey-e2352839e4639b09e2e52b2ada1399097fad1d8d.zip
merge: upstream changes for 2024.11 (!742)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/742 Closes #645 and #646 Approved-by: Hazelnoot <acomputerdog@gmail.com> Approved-by: Marie <github@yuugi.dev>
Diffstat (limited to 'packages/backend/src/server/api/SigninApiService.ts')
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts105
1 files changed, 94 insertions, 11 deletions
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 1a4ce0a54c..fa9155d82d 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -6,12 +6,14 @@
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
-import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm';
+import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js';
import type {
+ MiMeta,
SigninsRepository,
UserProfilesRepository,
+ UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
@@ -21,8 +23,9 @@ import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
+import { CaptchaService } from '@/core/CaptchaService.js';
+import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
-import type { MiMeta } from '@/models/_.js';
import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SigninService } from './SigninService.js';
@@ -44,6 +47,9 @@ export class SigninApiService {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
@@ -52,6 +58,7 @@ export class SigninApiService {
private signinService: SigninService,
private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService,
+ private captchaService: CaptchaService,
) {
}
@@ -60,9 +67,15 @@ export class SigninApiService {
request: FastifyRequest<{
Body: {
username: string;
- password: string;
+ password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
+ 'hcaptcha-response'?: string;
+ 'g-recaptcha-response'?: string;
+ 'turnstile-response'?: string;
+ 'frc-captcha-solution'?: string;
+ 'm-captcha-response'?: string;
+ 'testcaptcha-response'?: string;
};
}>,
reply: FastifyReply,
@@ -101,11 +114,6 @@ export class SigninApiService {
return;
}
- if (typeof password !== 'string') {
- reply.code(400);
- return;
- }
-
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
@@ -136,6 +144,27 @@ 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(200);
+ if (profile.twoFactorEnabled) {
+ return {
+ finished: false,
+ next: 'password',
+ } satisfies Misskey.entities.SigninFlowResponse;
+ } else {
+ return {
+ finished: false,
+ next: 'captcha',
+ } satisfies Misskey.entities.SigninFlowResponse;
+ }
+ }
+
+ if (typeof password !== 'string') {
+ reply.code(400);
+ return;
+ }
if (!user.approved && this.meta.approvalRequiredForSignup) {
reply.code(403);
@@ -151,7 +180,7 @@ export class SigninApiService {
// Compare password
const same = await argon2.verify(profile.password!, password) || bcrypt.compareSync(password, profile.password!);
- const fail = async (status?: number, failure?: { id: string }) => {
+ const fail = async (status?: number, failure?: { id: string; }) => {
// Append signin history
await this.signinsRepository.insert({
id: this.idService.gen(),
@@ -165,6 +194,44 @@ export class SigninApiService {
};
if (!profile.twoFactorEnabled) {
+ if (process.env.NODE_ENV !== 'test') {
+ if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
+ await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
+ await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableFC && this.meta.fcSecretKey) {
+ await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
+ await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
+ await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableTestcaptcha) {
+ await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+ }
+
if (same) {
if (profile.password!.startsWith('$2')) {
const newHash = await argon2.hash(password);
@@ -223,7 +290,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',
@@ -233,7 +300,23 @@ export class SigninApiService {
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
reply.code(200);
- return authRequest;
+ return {
+ finished: false,
+ next: 'passkey',
+ authRequest,
+ } satisfies Misskey.entities.SigninFlowResponse;
+ } else {
+ if (!same || !profile.twoFactorEnabled) {
+ return await fail(403, {
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
+ });
+ } else {
+ reply.code(200);
+ return {
+ finished: false,
+ next: 'totp',
+ } satisfies Misskey.entities.SigninFlowResponse;
+ }
}
// never get here
}