lock to prevent parallel logins

This commit is contained in:
x032205
2025-09-02 20:49:51 -04:00
parent 642b56fbda
commit 77d56468f1
3 changed files with 43 additions and 8 deletions
@@ -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();
}
}
],
@@ -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<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems">;
keyStore: Pick<
TKeyStoreFactory,
"setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock"
>;
};
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
@@ -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<ReturnType<typeof keyStore.acquireLock>>;
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) => {
@@ -33,7 +33,7 @@ type TIdentityServiceFactoryDep = {
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
keyStore: Pick<TKeyStoreFactory, "getKeysByPattern">;
keyStore: Pick<TKeyStoreFactory, "getKeysByPattern" | "getItem">;
};
export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
@@ -261,12 +261,18 @@ export const identityServiceFactory = ({
const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`);
const activeLockoutAuthMethods = new Set<string>();
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,