mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 05:19:49 +00:00
@@ -37,7 +37,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
|
||||
|
||||
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
|
||||
.select(selectAllTableCols(TableName.IdentityAccessToken))
|
||||
.select(
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
|
||||
@@ -47,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
)
|
||||
.first();
|
||||
@@ -61,7 +62,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
|
||||
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
|
||||
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
|
||||
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
|
||||
|
||||
@@ -171,7 +171,8 @@ export const identityAccessTokenServiceFactory = ({
|
||||
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
|
||||
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
|
||||
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
|
||||
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth
|
||||
};
|
||||
|
||||
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityJwtAuthDALFactory = ReturnType<typeof identityJwtAuthDALFactory>;
|
||||
|
||||
export const identityJwtAuthDALFactory = (db: TDbClient) => {
|
||||
const jwtAuthOrm = ormify(db, TableName.IdentityJwtAuth);
|
||||
|
||||
return jwtAuthOrm;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import picomatch from "picomatch";
|
||||
|
||||
export const doesFieldValueMatchJwtPolicy = (fieldValue: string | boolean | number, policyValue: string) => {
|
||||
if (typeof fieldValue === "boolean") {
|
||||
return fieldValue === (policyValue === "true");
|
||||
}
|
||||
|
||||
if (typeof fieldValue === "number") {
|
||||
return fieldValue === parseInt(policyValue, 10);
|
||||
}
|
||||
|
||||
return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||
};
|
||||
@@ -0,0 +1,534 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import https from "https";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { JwksClient } from "jwks-rsa";
|
||||
|
||||
import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
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, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal";
|
||||
import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns";
|
||||
import {
|
||||
JwtConfigurationType,
|
||||
TAttachJwtAuthDTO,
|
||||
TGetJwtAuthDTO,
|
||||
TLoginJwtAuthDTO,
|
||||
TRevokeJwtAuthDTO,
|
||||
TUpdateJwtAuthDTO
|
||||
} from "./identity-jwt-auth-types";
|
||||
|
||||
type TIdentityJwtAuthServiceFactoryDep = {
|
||||
identityJwtAuthDAL: TIdentityJwtAuthDALFactory;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TIdentityJwtAuthServiceFactory = ReturnType<typeof identityJwtAuthServiceFactory>;
|
||||
|
||||
export const identityJwtAuthServiceFactory = ({
|
||||
identityJwtAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
identityAccessTokenDAL,
|
||||
kmsService
|
||||
}: TIdentityJwtAuthServiceFactoryDep) => {
|
||||
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
|
||||
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||
if (!identityJwtAuth) {
|
||||
throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" });
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
|
||||
identityId: identityJwtAuth.identityId
|
||||
});
|
||||
if (!identityMembershipOrg) {
|
||||
throw new NotFoundError({
|
||||
message: `Identity organization membership for identity with ID '${identityJwtAuth.identityId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const decodedToken = jwt.decode(jwtValue, { complete: true });
|
||||
if (!decodedToken) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid JWT"
|
||||
});
|
||||
}
|
||||
|
||||
let tokenData: Record<string, string | boolean | number> = {};
|
||||
|
||||
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
|
||||
const decryptedJwksCaCert = orgDataKeyDecryptor({
|
||||
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
|
||||
}).toString();
|
||||
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
|
||||
const client = new JwksClient({
|
||||
jwksUri: identityJwtAuth.jwksUrl,
|
||||
requestAgent
|
||||
});
|
||||
|
||||
const { kid } = decodedToken.header;
|
||||
const jwtSigningKey = await client.getSigningKey(kid);
|
||||
|
||||
try {
|
||||
tokenData = jwt.verify(jwtValue, jwtSigningKey.getPublicKey()) as Record<string, string>;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
|
||||
.toString()
|
||||
.split(",");
|
||||
|
||||
const errors: string[] = [];
|
||||
let isMatchAnyKey = false;
|
||||
for (const publicKey of decryptedPublicKeys) {
|
||||
try {
|
||||
tokenData = jwt.verify(jwtValue, publicKey) as Record<string, string>;
|
||||
isMatchAnyKey = true;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
errors.push(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMatchAnyKey) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (identityJwtAuth.boundIssuer) {
|
||||
if (tokenData.iss !== identityJwtAuth.boundIssuer) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: issuer mismatch"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (identityJwtAuth.boundSubject) {
|
||||
if (!tokenData.sub) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: token has no subject field"
|
||||
});
|
||||
}
|
||||
|
||||
if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: subject not allowed"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (identityJwtAuth.boundAudiences) {
|
||||
if (!tokenData.aud) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: token has no audience field"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!identityJwtAuth.boundAudiences
|
||||
.split(", ")
|
||||
.some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue))
|
||||
) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: token audience not allowed"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (identityJwtAuth.boundClaims) {
|
||||
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
|
||||
if (!tokenData[claimKey]) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
}
|
||||
|
||||
// handle both single and multi-valued claims
|
||||
if (
|
||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry))
|
||||
) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: claim mismatch for field ${claimKey}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityJwtAuth.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
accessTokenTTL: identityJwtAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.JWT_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
identityId: identityJwtAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
{
|
||||
expiresIn:
|
||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
||||
? undefined
|
||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
||||
}
|
||||
);
|
||||
|
||||
return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg };
|
||||
};
|
||||
|
||||
const attachJwtAuth = async ({
|
||||
identityId,
|
||||
configurationType,
|
||||
jwksUrl,
|
||||
jwksCaCert,
|
||||
publicKeys,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TAttachJwtAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
}
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add JWT 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,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access 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 { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(jwksCaCert)
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(publicKeys.join(","))
|
||||
});
|
||||
|
||||
const identityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityJwtAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
configurationType,
|
||||
jwksUrl,
|
||||
encryptedJwksCaCert,
|
||||
encryptedPublicKeys,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return doc;
|
||||
});
|
||||
return { ...identityJwtAuth, orgId: identityMembershipOrg.orgId, jwksCaCert, publicKeys };
|
||||
};
|
||||
|
||||
const updateJwtAuth = async ({
|
||||
identityId,
|
||||
configurationType,
|
||||
jwksUrl,
|
||||
jwksCaCert,
|
||||
publicKeys,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateJwtAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update JWT Auth"
|
||||
});
|
||||
}
|
||||
|
||||
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityJwtAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityJwtAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityJwtAuth.accessTokenMaxTTL)
|
||||
) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access 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 updateQuery: TIdentityJwtAuthsUpdate = {
|
||||
boundIssuer,
|
||||
configurationType,
|
||||
jwksUrl,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
};
|
||||
|
||||
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (jwksCaCert !== undefined) {
|
||||
const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(jwksCaCert)
|
||||
});
|
||||
|
||||
updateQuery.encryptedJwksCaCert = encryptedJwksCaCert;
|
||||
}
|
||||
|
||||
if (publicKeys) {
|
||||
const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(publicKeys.join(","))
|
||||
});
|
||||
|
||||
updateQuery.encryptedPublicKeys = encryptedPublicKeys;
|
||||
}
|
||||
|
||||
const updatedJwtAuth = await identityJwtAuthDAL.updateById(identityJwtAuth.id, updateQuery);
|
||||
const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedJwksCaCert }).toString();
|
||||
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedPublicKeys })
|
||||
.toString()
|
||||
.split(",");
|
||||
|
||||
return {
|
||||
...updatedJwtAuth,
|
||||
orgId: identityMembershipOrg.orgId,
|
||||
jwksCaCert: decryptedJwksCaCert,
|
||||
publicKeys: decryptedPublicKeys
|
||||
};
|
||||
};
|
||||
|
||||
const getJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetJwtAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have JWT Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedJwksCaCert }).toString();
|
||||
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
|
||||
.toString()
|
||||
.split(",");
|
||||
|
||||
return {
|
||||
...identityJwtAuth,
|
||||
orgId: identityMembershipOrg.orgId,
|
||||
jwksCaCert: decryptedJwksCaCert,
|
||||
publicKeys: decryptedPublicKeys
|
||||
};
|
||||
};
|
||||
|
||||
const revokeJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeJwtAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new NotFoundError({ message: "Failed to find identity" });
|
||||
}
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have JWT auth"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to revoke JWT auth of identity with more privileged role"
|
||||
});
|
||||
}
|
||||
|
||||
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
|
||||
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);
|
||||
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.JWT_AUTH }, tx);
|
||||
|
||||
return { ...deletedJwtAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
|
||||
return revokedIdentityJwtAuth;
|
||||
};
|
||||
|
||||
return {
|
||||
login,
|
||||
attachJwtAuth,
|
||||
updateJwtAuth,
|
||||
getJwtAuth,
|
||||
revokeJwtAuth
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum JwtConfigurationType {
|
||||
JWKS = "jwks",
|
||||
STATIC = "static"
|
||||
}
|
||||
|
||||
export type TAttachJwtAuthDTO = {
|
||||
identityId: string;
|
||||
configurationType: JwtConfigurationType;
|
||||
jwksUrl: string;
|
||||
jwksCaCert: string;
|
||||
publicKeys: string[];
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateJwtAuthDTO = {
|
||||
identityId: string;
|
||||
configurationType?: JwtConfigurationType;
|
||||
jwksUrl?: string;
|
||||
jwksCaCert?: string;
|
||||
publicKeys?: string[];
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetJwtAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRevokeJwtAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TLoginJwtAuthDTO = {
|
||||
identityId: string;
|
||||
jwt: string;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const validateJwtAuthAudiencesField = z
|
||||
.string()
|
||||
.trim()
|
||||
.default("")
|
||||
.transform((data) => {
|
||||
if (data === "") return "";
|
||||
return data
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.join(", ");
|
||||
});
|
||||
|
||||
export const validateJwtBoundClaimsField = z.record(z.string()).transform((data) => {
|
||||
const formattedClaims: Record<string, string> = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
formattedClaims[key] = data[key]
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.join(", ");
|
||||
});
|
||||
|
||||
return formattedClaims;
|
||||
});
|
||||
@@ -7,7 +7,8 @@ export const buildAuthMethods = ({
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId
|
||||
tokenId,
|
||||
jwtId
|
||||
}: {
|
||||
uaId?: string;
|
||||
gcpId?: string;
|
||||
@@ -16,6 +17,7 @@ export const buildAuthMethods = ({
|
||||
oidcId?: string;
|
||||
azureId?: string;
|
||||
tokenId?: string;
|
||||
jwtId?: string;
|
||||
}) => {
|
||||
return [
|
||||
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
|
||||
@@ -24,6 +26,7 @@ export const buildAuthMethods = ({
|
||||
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
|
||||
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
|
||||
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
|
||||
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null]
|
||||
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
|
||||
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null]
|
||||
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAzureAuths,
|
||||
TIdentityGcpAuths,
|
||||
TIdentityJwtAuths,
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOrgMemberships,
|
||||
@@ -70,6 +71,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityJwtAuths>(
|
||||
TableName.IdentityJwtAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityJwtAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
selectAllTableCols(TableName.IdentityOrgMembership),
|
||||
@@ -81,6 +87,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
|
||||
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
);
|
||||
@@ -183,6 +190,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityJwtAuths>(
|
||||
TableName.IdentityJwtAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityJwtAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("id").withSchema("paginatedIdentity"),
|
||||
@@ -200,7 +212,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
|
||||
)
|
||||
// cr stands for custom role
|
||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||
@@ -237,6 +250,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
jwtId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
@@ -271,7 +285,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId
|
||||
tokenId,
|
||||
jwtId
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user