mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 18:29:28 +00:00
464 lines
17 KiB
TypeScript
464 lines
17 KiB
TypeScript
import crypto from "node:crypto";
|
|
|
|
import { ForbiddenError } from "@casl/ability";
|
|
import bcrypt from "bcrypt";
|
|
import jwt from "jsonwebtoken";
|
|
|
|
import { IdentityAuthMethod } from "@app/db/schemas";
|
|
import {
|
|
OrgPermissionActions,
|
|
OrgPermissionSubjects
|
|
} from "@app/ee/services/permission/org-permission";
|
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
|
import { getConfig } from "@app/lib/config/env";
|
|
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
|
|
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
|
import { TIdentityDalFactory } from "../identity/identity-dal";
|
|
import { TIdentityOrgDalFactory } from "../identity/identity-org-dal";
|
|
import { TIdentityAccessTokenDalFactory } from "../identity-access-token/identity-access-token-dal";
|
|
import { TUaClientSecretDalFactory } from "./ua-client-secret-dal";
|
|
import { TUniversalAuthDalFactory } from "./universal-auth-dal";
|
|
import {
|
|
TAttachUaDTO,
|
|
TCreateUaClientSecretDTO,
|
|
TGetUaClientSecretsDTO,
|
|
TGetUaDTO,
|
|
TRevokeUaClientSecretDTO,
|
|
TUpdateUaDTO
|
|
} from "./universal-auth-types";
|
|
|
|
type TUniversalAuthServiceFactoryDep = {
|
|
universalAuthDal: TUniversalAuthDalFactory;
|
|
uaClientSecretDal: TUaClientSecretDalFactory;
|
|
identityAccessTokenDal: TIdentityAccessTokenDalFactory;
|
|
identityOrgDal: TIdentityOrgDalFactory;
|
|
identityDal: Pick<TIdentityDalFactory, "updateById">;
|
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
|
};
|
|
|
|
export type TUniversalAuthServiceFactory = ReturnType<typeof universalAuthServiceFactory>;
|
|
|
|
export const universalAuthServiceFactory = ({
|
|
universalAuthDal,
|
|
uaClientSecretDal,
|
|
identityAccessTokenDal,
|
|
identityOrgDal,
|
|
identityDal,
|
|
permissionService
|
|
}: TUniversalAuthServiceFactoryDep) => {
|
|
const login = async (clientId: string, clientSecret: string) => {
|
|
const identityUa = await universalAuthDal.findOne({ clientId });
|
|
if (!identityUa) throw new UnauthorizedError();
|
|
|
|
// TODO(akhilmhdh-pg): add ip checking
|
|
const clientSecrtInfo = await uaClientSecretDal.find({
|
|
identityUAId: identityUa.id,
|
|
isClientSecretRevoked: false
|
|
});
|
|
|
|
const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) =>
|
|
bcrypt.compareSync(clientSecret, clientSecretHash)
|
|
);
|
|
if (!validClientSecretInfo) throw new UnauthorizedError();
|
|
|
|
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } =
|
|
validClientSecretInfo;
|
|
if (clientSecretTTL > 0) {
|
|
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
|
const ttlInMilliseconds = clientSecretTTL * 1000;
|
|
const currentDate = new Date();
|
|
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
|
|
|
if (currentDate > expirationTime) {
|
|
await uaClientSecretDal.updateById(validClientSecretInfo.id, {
|
|
isClientSecretRevoked: true
|
|
});
|
|
|
|
throw new UnauthorizedError({
|
|
message: "Failed to authenticate identity credentials due to expired client secret"
|
|
});
|
|
}
|
|
}
|
|
|
|
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses === clientSecretNumUsesLimit) {
|
|
// number of times client secret can be used for
|
|
// a login operation reached
|
|
await uaClientSecretDal.updateById(validClientSecretInfo.id, { isClientSecretRevoked: true });
|
|
throw new UnauthorizedError({
|
|
message:
|
|
"Failed to authenticate identity credentials due to client secret number of uses limit reached"
|
|
});
|
|
}
|
|
|
|
const identityAccessToken = await universalAuthDal.transaction(async (tx) => {
|
|
const uaClientSecretDoc = await uaClientSecretDal.incrementUsage(
|
|
validClientSecretInfo.id,
|
|
tx
|
|
);
|
|
const newToken = await identityAccessTokenDal.create(
|
|
{
|
|
identityId: identityUa.identityId,
|
|
authType: IdentityAuthMethod.Univeral,
|
|
isAccessTokenRevoked: false,
|
|
identityUAClientSecretId: uaClientSecretDoc.id,
|
|
accessTokenTTL: identityUa.accessTokenTTL,
|
|
accessTokenMaxTTL: identityUa.accessTokenMaxTTL,
|
|
accessTokenNumUses: 0,
|
|
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit
|
|
},
|
|
tx
|
|
);
|
|
return newToken;
|
|
});
|
|
|
|
const appCfg = getConfig();
|
|
const accessToken = jwt.sign(
|
|
{
|
|
identityId: identityUa.identityId,
|
|
clientSecretId: validClientSecretInfo.id,
|
|
identityAccessTokenId: identityAccessToken.id,
|
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
|
},
|
|
appCfg.JWT_AUTH_SECRET,
|
|
{
|
|
expiresIn:
|
|
identityAccessToken.accessTokenMaxTTL === 0
|
|
? undefined
|
|
: identityAccessToken.accessTokenMaxTTL
|
|
}
|
|
);
|
|
return { accessToken, identityUa };
|
|
};
|
|
|
|
const attachUa = async ({
|
|
accessTokenMaxTTL,
|
|
identityId,
|
|
accessTokenNumUsesLimit,
|
|
accessTokenTTL,
|
|
accessTokenTrustedIps,
|
|
clientSecretTrustedIps,
|
|
actorId,
|
|
actor
|
|
}: TAttachUaDTO) => {
|
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
|
if (identityMembershipOrg.identity.authMethod)
|
|
throw new BadRequestError({
|
|
message: "Failed to add universal auth to already configured identity"
|
|
});
|
|
|
|
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
|
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
|
}
|
|
|
|
const { permission } = await permissionService.getOrgPermission(
|
|
actor,
|
|
actorId,
|
|
identityMembershipOrg.orgId
|
|
);
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
OrgPermissionActions.Create,
|
|
OrgPermissionSubjects.Identity
|
|
);
|
|
const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map(
|
|
(clientSecretTrustedIp) => {
|
|
// TODO(akhilmhdh-pg): add licence server here
|
|
if (/* !plan.ipAllowlisting && */ clientSecretTrustedIp.ipAddress !== "0.0.0.0/0")
|
|
throw new BadRequestError({
|
|
message:
|
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
|
});
|
|
if (!isValidIpOrCidr(clientSecretTrustedIp.ipAddress))
|
|
throw new BadRequestError({
|
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
|
});
|
|
return extractIPDetails(clientSecretTrustedIp.ipAddress);
|
|
}
|
|
);
|
|
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
|
// TODO(akhilmhdh-pg): add licence server here
|
|
if (/* !plan.ipAllowlisting && */ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0")
|
|
throw new BadRequestError({
|
|
message:
|
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
|
});
|
|
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
|
throw new BadRequestError({
|
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
|
});
|
|
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
|
});
|
|
|
|
const identityUa = await universalAuthDal.transaction(async (tx) => {
|
|
const doc = await universalAuthDal.create(
|
|
{
|
|
identityId: identityMembershipOrg.identityId,
|
|
clientId: crypto.randomUUID(),
|
|
clientSecretTrustedIps: JSON.stringify(reformattedClientSecretTrustedIps),
|
|
accessTokenMaxTTL,
|
|
accessTokenTTL,
|
|
accessTokenNumUsesLimit,
|
|
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
|
},
|
|
tx
|
|
);
|
|
await identityDal.updateById(
|
|
identityMembershipOrg.identityId,
|
|
{
|
|
authMethod: IdentityAuthMethod.Univeral
|
|
},
|
|
tx
|
|
);
|
|
return doc;
|
|
});
|
|
return identityUa;
|
|
};
|
|
|
|
const updateUa = async ({
|
|
accessTokenMaxTTL,
|
|
identityId,
|
|
accessTokenNumUsesLimit,
|
|
accessTokenTTL,
|
|
accessTokenTrustedIps,
|
|
clientSecretTrustedIps,
|
|
actorId,
|
|
actor
|
|
}: TUpdateUaDTO) => {
|
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
|
throw new BadRequestError({
|
|
message: "Failed to updated universal auth"
|
|
});
|
|
|
|
const uaIdentityAuth = await universalAuthDal.findOne({ identityId });
|
|
|
|
if (
|
|
(accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) > 0 &&
|
|
(accessTokenTTL || uaIdentityAuth.accessTokenMaxTTL) >
|
|
(accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL)
|
|
) {
|
|
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
|
}
|
|
|
|
const { permission } = await permissionService.getOrgPermission(
|
|
actor,
|
|
actorId,
|
|
identityMembershipOrg.orgId
|
|
);
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
OrgPermissionActions.Edit,
|
|
OrgPermissionSubjects.Identity
|
|
);
|
|
const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map(
|
|
(clientSecretTrustedIp) => {
|
|
// TODO(akhilmhdh-pg): add licence server here
|
|
if (/* !plan.ipAllowlisting && */ clientSecretTrustedIp.ipAddress !== "0.0.0.0/0")
|
|
throw new BadRequestError({
|
|
message:
|
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
|
});
|
|
if (!isValidIpOrCidr(clientSecretTrustedIp.ipAddress))
|
|
throw new BadRequestError({
|
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
|
});
|
|
return extractIPDetails(clientSecretTrustedIp.ipAddress);
|
|
}
|
|
);
|
|
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
|
// TODO(akhilmhdh-pg): add licence server here
|
|
if (/* !plan.ipAllowlisting && */ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0")
|
|
throw new BadRequestError({
|
|
message:
|
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
|
});
|
|
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
|
throw new BadRequestError({
|
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
|
});
|
|
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
|
});
|
|
|
|
const updatedUaAuth = await universalAuthDal.updateById(uaIdentityAuth.id, {
|
|
clientSecretTrustedIps: reformattedClientSecretTrustedIps
|
|
? JSON.stringify(reformattedClientSecretTrustedIps)
|
|
: undefined,
|
|
accessTokenMaxTTL,
|
|
accessTokenTTL,
|
|
accessTokenNumUsesLimit,
|
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
|
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
|
: undefined
|
|
});
|
|
return updatedUaAuth;
|
|
};
|
|
|
|
const getIdentityUa = async ({ identityId, actorId, actor }: TGetUaDTO) => {
|
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
|
throw new BadRequestError({
|
|
message: "The identity does not have universal auth"
|
|
});
|
|
|
|
const uaIdentityAuth = await universalAuthDal.findOne({ identityId });
|
|
|
|
const { permission } = await permissionService.getOrgPermission(
|
|
actor,
|
|
actorId,
|
|
identityMembershipOrg.orgId
|
|
);
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
OrgPermissionActions.Read,
|
|
OrgPermissionSubjects.Identity
|
|
);
|
|
return uaIdentityAuth;
|
|
};
|
|
|
|
const createUaClientSecret = async ({
|
|
actor,
|
|
actorId,
|
|
identityId,
|
|
ttl,
|
|
description,
|
|
numUsesLimit
|
|
}: TCreateUaClientSecretDTO) => {
|
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
|
throw new BadRequestError({
|
|
message: "The identity does not have universal auth"
|
|
});
|
|
const { permission } = await permissionService.getOrgPermission(
|
|
actor,
|
|
actorId,
|
|
identityMembershipOrg.orgId
|
|
);
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
OrgPermissionActions.Create,
|
|
OrgPermissionSubjects.Identity
|
|
);
|
|
|
|
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
|
ActorType.IDENTITY,
|
|
identityMembershipOrg.identityId,
|
|
identityMembershipOrg.orgId
|
|
);
|
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
|
if (!hasPriviledge)
|
|
throw new ForbiddenRequestError({
|
|
message: "Failed to add identity to project with more privileged role"
|
|
});
|
|
|
|
const appCfg = getConfig();
|
|
const clientSecret = crypto.randomBytes(32).toString("hex");
|
|
const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS);
|
|
const identityUniversalAuth = await universalAuthDal.findOne({
|
|
identityId
|
|
});
|
|
|
|
const identityUaClientSecret = await uaClientSecretDal.create({
|
|
identityUAId: identityUniversalAuth.id,
|
|
description,
|
|
clientSecretPrefix: clientSecret.slice(0, 4),
|
|
clientSecretHash,
|
|
clientSecretNumUses: 0,
|
|
clientSecretNumUsesLimit: numUsesLimit,
|
|
clientSecretTTL: ttl,
|
|
isClientSecretRevoked: false
|
|
});
|
|
return { clientSecret: identityUaClientSecret, uaAuth: identityUniversalAuth };
|
|
};
|
|
|
|
const getUaClientSecrets = async ({ actor, actorId, identityId }: TGetUaClientSecretsDTO) => {
|
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
|
throw new BadRequestError({
|
|
message: "The identity does not have universal auth"
|
|
});
|
|
const { permission } = await permissionService.getOrgPermission(
|
|
actor,
|
|
actorId,
|
|
identityMembershipOrg.orgId
|
|
);
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
OrgPermissionActions.Read,
|
|
OrgPermissionSubjects.Identity
|
|
);
|
|
|
|
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
|
ActorType.IDENTITY,
|
|
identityMembershipOrg.identityId,
|
|
identityMembershipOrg.orgId
|
|
);
|
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
|
if (!hasPriviledge)
|
|
throw new ForbiddenRequestError({
|
|
message: "Failed to add identity to project with more privileged role"
|
|
});
|
|
|
|
const identityUniversalAuth = await universalAuthDal.findOne({
|
|
identityId
|
|
});
|
|
|
|
const clientSecrets = await uaClientSecretDal.findOne({
|
|
identityUAId: identityUniversalAuth.id,
|
|
isClientSecretRevoked: false
|
|
});
|
|
return clientSecrets;
|
|
};
|
|
|
|
const revokeUaClientSecret = async ({
|
|
identityId,
|
|
actorId,
|
|
actor,
|
|
clientSecretId
|
|
}: TRevokeUaClientSecretDTO) => {
|
|
const identityMembershipOrg = await identityOrgDal.findOne({ identityId });
|
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
|
throw new BadRequestError({
|
|
message: "The identity does not have universal auth"
|
|
});
|
|
const { permission } = await permissionService.getOrgPermission(
|
|
actor,
|
|
actorId,
|
|
identityMembershipOrg.orgId
|
|
);
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
OrgPermissionActions.Delete,
|
|
OrgPermissionSubjects.Identity
|
|
);
|
|
|
|
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
|
ActorType.IDENTITY,
|
|
identityMembershipOrg.identityId,
|
|
identityMembershipOrg.orgId
|
|
);
|
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
|
if (!hasPriviledge)
|
|
throw new ForbiddenRequestError({
|
|
message: "Failed to add identity to project with more privileged role"
|
|
});
|
|
|
|
const clientSecret = await uaClientSecretDal.updateById(clientSecretId, {
|
|
isClientSecretRevoked: true
|
|
});
|
|
return clientSecret;
|
|
};
|
|
|
|
return {
|
|
login,
|
|
attachUa,
|
|
updateUa,
|
|
getIdentityUa,
|
|
createUaClientSecret,
|
|
getUaClientSecrets,
|
|
revokeUaClientSecret
|
|
};
|
|
};
|