diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index 832f428a8..af743fb1d 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -137,7 +137,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) }, preValidation: [ async (req, res) => { - await server.services.identityLdapAuth.checkLdapLockout({ + const { lock } = await server.services.identityLdapAuth.checkLdapLockout({ identityId: req.body.identityId, username: req.body.username }); @@ -167,6 +167,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) } throw error; + } finally { + await lock.release(); } } ], diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index a8576dc0e..c279ca33d 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -15,11 +15,18 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + NotFoundError, + PermissionBoundaryError, + RateLimitError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -55,7 +62,10 @@ type TIdentityLdapAuthServiceFactoryDep = { kmsService: TKmsServiceFactory; identityDAL: TIdentityDALFactory; identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory; - keyStore: Pick; + keyStore: Pick< + TKeyStoreFactory, + "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" + >; }; export type TIdentityLdapAuthServiceFactory = ReturnType; @@ -646,17 +656,34 @@ export const identityLdapAuthServiceFactory = ({ const checkLdapLockout = async ({ identityId, username }: TCheckLdapAuthLockoutDTO) => { const LOCKOUT_KEY = `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:${username.trim().toLowerCase()}`; + let lock: Awaited>; + try { + lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 3000, { + retryCount: 3, + retryDelay: 1500, + retryJitter: 100 + }); + } catch (e) { + logger.info( + `identity login failed to acquire lock [identityId=${identityId}] [authMethod=${IdentityAuthMethod.LDAP_AUTH}]` + ); + throw new RateLimitError({ message: "Rate limit exceeded" }); + } + const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY); if (lockoutRaw) { const lockout = JSON.parse(lockoutRaw) as LockoutObject; if (lockout.lockedOut) { + await lock.release(); throw new UnauthorizedError({ message: "This identity auth method is temporarily locked, please try again later" }); } } + + return { lock }; }; const incrementLdapLockout = async ({ identityId, username }: TIncrementLdapAuthLockoutDTO) => { diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 969d00331..f216d6483 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -33,7 +33,7 @@ type TIdentityServiceFactoryDep = { identityProjectDAL: Pick; permissionService: Pick; licenseService: Pick; - keyStore: Pick; + keyStore: Pick; }; export type TIdentityServiceFactory = ReturnType; @@ -261,12 +261,18 @@ export const identityServiceFactory = ({ const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`); const activeLockoutAuthMethods = new Set(); - activeLockouts.forEach((key) => { + for await (const key of activeLockouts) { const parts = key.split(":"); if (parts.length > 3) { - activeLockoutAuthMethods.add(parts[3]); + const lockoutRaw = await keyStore.getItem(key); + if (lockoutRaw) { + const lockout = JSON.parse(lockoutRaw) as { lockedOut: boolean }; + if (lockout.lockedOut) { + activeLockoutAuthMethods.add(parts[3]); + } + } } - }); + } return { ...identity,