diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index 2d641b4a0..2956be192 100644
--- a/backend/src/@types/fastify.d.ts
+++ b/backend/src/@types/fastify.d.ts
@@ -74,6 +74,7 @@ import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-a
import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
+import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types";
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
@@ -110,7 +111,6 @@ import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
-import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types";
declare module "@fastify/request-context" {
interface RequestContextData {
diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts
index b0b35cc2f..dfe9ac29d 100644
--- a/backend/src/lib/config/env.ts
+++ b/backend/src/lib/config/env.ts
@@ -193,6 +193,9 @@ const envSchema = z
PYLON_API_KEY: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
+ IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default(
+ "x-identity-tls-cert-auth-client-cert"
+ ),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index 3e5735fb9..f687d358d 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -193,6 +193,8 @@ import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
+import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal";
+import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service";
import { identityTokenAuthDALFactory } from "@app/services/identity-token-auth/identity-token-auth-dal";
import { identityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
@@ -301,8 +303,6 @@ import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
-import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal";
-import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service";
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
diff --git a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts
index 38190d7e7..2ae64cfe6 100644
--- a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts
+++ b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts
@@ -1,14 +1,17 @@
+import crypto from "node:crypto";
+
import { z } from "zod";
-// import { TLSSocket } from "tls";
+import { IdentityTlsCertAuthsSchema } from "@app/db/schemas";
+import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { ApiDocsTags, TLS_CERT_AUTH } from "@app/lib/api-docs";
+import { getConfig } from "@app/lib/config/env";
+import { BadRequestError } from "@app/lib/errors";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
-import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
-import { ApiDocsTags, TLS_CERT_AUTH } from "@app/lib/api-docs";
-import { IdentityTlsCertAuthsSchema } from "@app/db/schemas";
-import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
-import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
+import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
const validateCommonNames = z
.string()
@@ -21,44 +24,74 @@ const validateCommonNames = z
.join(",")
);
+const validateCaCertificate = (caCert: string) => {
+ if (!caCert) return true;
+ try {
+ // eslint-disable-next-line no-new
+ new crypto.X509Certificate(caCert);
+ return true;
+ } catch (err) {
+ return false;
+ }
+};
+
export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvider) => {
- // server.route({
- // method: "GET",
- // url: "/",
- // config: {
- // rateLimit: readLimit
- // },
- // schema: {
- // params: z.object({}),
- // response: {
- // 200: z.object({})
- // }
- // },
- // onRequest: verifyAuth([AuthMode.JWT]),
- // handler: async (req) => {
- // const { socket } = req;
- // if (socket instanceof TLSSocket && socket.encrypted) {
- // // Inside this block, TypeScript now knows `socket` is a TlsSocket
- // const certificate = socket.getPeerCertificate();
- //
- // if (Object.keys(certificate).length === 0) {
- // return reply.send({ message: "Client did not provide a certificate." });
- // }
- //
- // return reply.send({
- // message: "Certificate received!",
- // subject: certificate.subject,
- // issuer: certificate.issuer,
- // fingerprint: certificate.fingerprint
- // });
- // } else {
- // // This will handle plain HTTP requests gracefully
- // return reply
- // .status(400)
- // .send({ error: "This endpoint requires an HTTPS connection with a client certificate." });
- // }
- // }
- // });
+ server.route({
+ method: "POST",
+ url: "/login",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.TlsCertAuth],
+ description: "Login with TLS Certificate Auth",
+ body: z.object({
+ identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId)
+ }),
+ response: {
+ 200: z.object({
+ accessToken: z.string(),
+ expiresIn: z.coerce.number(),
+ accessTokenMaxTTL: z.coerce.number(),
+ tokenType: z.literal("Bearer")
+ })
+ }
+ },
+ handler: async (req) => {
+ const appCfg = getConfig();
+ const clientCertificate = req.headers[appCfg.IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY];
+ if (!clientCertificate) {
+ throw new BadRequestError({ message: "Missing TLS certificate in header" });
+ }
+
+ const { identityTlsCertAuth, accessToken, identityAccessToken, identityMembershipOrg } =
+ await server.services.identityTlsCertAuth.login({
+ identityId: req.body.identityId,
+ clientCertificate: clientCertificate as string
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: identityMembershipOrg?.orgId,
+ event: {
+ type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH,
+ metadata: {
+ identityId: identityTlsCertAuth.identityId,
+ identityAccessTokenId: identityAccessToken.id,
+ identityTlsCertAuthId: identityTlsCertAuth.id
+ }
+ }
+ });
+
+ return {
+ accessToken,
+ tokenType: "Bearer" as const,
+ expiresIn: identityTlsCertAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL
+ };
+ }
+ });
server.route({
method: "POST",
@@ -81,8 +114,16 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
}),
body: z
.object({
- allowedCommonNames: validateCommonNames.describe(TLS_CERT_AUTH.ATTACH.allowedCommonNames),
- caCertificate: z.string().min(1).describe(TLS_CERT_AUTH.ATTACH.caCertificate),
+ allowedCommonNames: validateCommonNames
+ .optional()
+ .nullable()
+ .describe(TLS_CERT_AUTH.ATTACH.allowedCommonNames),
+ caCertificate: z
+ .string()
+ .min(1)
+ .max(10240)
+ .refine(validateCaCertificate, "Invalid CA Certificate.")
+ .describe(TLS_CERT_AUTH.ATTACH.caCertificate),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
@@ -118,7 +159,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
),
response: {
200: z.object({
- identityTlsCloudAuth: IdentityTlsCertAuthsSchema
+ identityTlsCertAuth: IdentityTlsCertAuthsSchema
})
}
},
@@ -174,7 +215,17 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
}),
body: z
.object({
- allowedCommonNames: validateCommonNames.describe(TLS_CERT_AUTH.UPDATE.allowedCommonNames),
+ caCertificate: z
+ .string()
+ .min(1)
+ .max(10240)
+ .refine(validateCaCertificate, "Invalid CA Certificate.")
+ .optional()
+ .describe(TLS_CERT_AUTH.ATTACH.caCertificate),
+ allowedCommonNames: validateCommonNames
+ .optional()
+ .nullable()
+ .describe(TLS_CERT_AUTH.UPDATE.allowedCommonNames),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
@@ -210,7 +261,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
),
response: {
200: z.object({
- identityTlsCloudAuth: IdentityTlsCertAuthsSchema
+ identityTlsCertAuth: IdentityTlsCertAuthsSchema
})
}
},
@@ -265,7 +316,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
}),
response: {
200: z.object({
- identityTlsCloudAuth: IdentityTlsCertAuthsSchema.extend({
+ identityTlsCertAuth: IdentityTlsCertAuthsSchema.extend({
caCertificate: z.string()
})
})
@@ -315,7 +366,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
}),
response: {
200: z.object({
- identityTlsCloudAuth: IdentityTlsCertAuthsSchema
+ identityTlsCertAuth: IdentityTlsCertAuthsSchema
})
}
},
diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts
index bebdfbe65..e6aa2e83f 100644
--- a/backend/src/server/routes/v1/index.ts
+++ b/backend/src/server/routes/v1/index.ts
@@ -25,6 +25,7 @@ import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router";
+import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
import { registerIdentityUaRouter } from "./identity-universal-auth-router";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
@@ -53,7 +54,6 @@ import { registerUserEngagementRouter } from "./user-engagement-router";
import { registerUserRouter } from "./user-router";
import { registerWebhookRouter } from "./webhook-router";
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
-import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSsoRouter, { prefix: "/sso" });
diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts
index 8a416156d..11dd312ad 100644
--- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts
+++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts
@@ -11,6 +11,7 @@ import {
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
+import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@@ -81,7 +82,12 @@ export const identityTlsCertAuthServiceFactory = ({
cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate
}).toString();
- const clientCertificateX509 = new crypto.X509Certificate(Buffer.from(clientCertificate));
+ const leafCertificate = extractX509CertFromChain(decodeURIComponent(clientCertificate))?.[0];
+ if (!leafCertificate) {
+ throw new BadRequestError({ message: "Missing client certificate" });
+ }
+
+ const clientCertificateX509 = new crypto.X509Certificate(leafCertificate);
const caCertificateX509 = new crypto.X509Certificate(caCertificate);
const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey);
diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts
index 729f502a2..b7a08276b 100644
--- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts
+++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts
@@ -9,7 +9,7 @@ export type TLoginTlsCertAuthDTO = {
export type TAttachTlsCertAuthDTO = {
identityId: string;
caCertificate: string;
- allowedCommonNames?: string;
+ allowedCommonNames?: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
@@ -20,7 +20,7 @@ export type TAttachTlsCertAuthDTO = {
export type TUpdateTlsCertAuthDTO = {
identityId: string;
caCertificate?: string;
- allowedCommonNames?: string;
+ allowedCommonNames?: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts
index 3020d9c47..dee87fc49 100644
--- a/backend/src/services/identity/identity-fns.ts
+++ b/backend/src/services/identity/identity-fns.ts
@@ -11,7 +11,8 @@ export const buildAuthMethods = ({
azureId,
tokenId,
jwtId,
- ldapId
+ ldapId,
+ tlsCertId
}: {
uaId?: string;
gcpId?: string;
@@ -24,6 +25,7 @@ export const buildAuthMethods = ({
tokenId?: string;
jwtId?: string;
ldapId?: string;
+ tlsCertId?: string;
}) => {
return [
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
@@ -36,6 +38,7 @@ export const buildAuthMethods = ({
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null],
- ...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null]
+ ...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null],
+ ...[tlsCertId ? IdentityAuthMethod.TLS_CERT_AUTH : null]
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
};
diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts
index 28064c9bb..5ca6cbbc1 100644
--- a/backend/src/services/identity/identity-org-dal.ts
+++ b/backend/src/services/identity/identity-org-dal.ts
@@ -12,6 +12,7 @@ import {
TIdentityOciAuths,
TIdentityOidcAuths,
TIdentityOrgMemberships,
+ TIdentityTlsCertAuths,
TIdentityTokenAuths,
TIdentityUniversalAuths,
TOrgRoles
@@ -99,7 +100,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityLdapAuth}.identityId`
)
-
+ .leftJoin(
+ TableName.IdentityTlsCertAuth,
+ `${TableName.IdentityOrgMembership}.identityId`,
+ `${TableName.IdentityTlsCertAuth}.identityId`
+ )
.select(
selectAllTableCols(TableName.IdentityOrgMembership),
@@ -114,6 +119,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
+ db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth),
db.ref("name").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
);
@@ -238,7 +244,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId",
`${TableName.IdentityLdapAuth}.identityId`
)
-
+ .leftJoin(
+ TableName.IdentityTlsCertAuth,
+ "paginatedIdentity.identityId",
+ `${TableName.IdentityTlsCertAuth}.identityId`
+ )
.select(
db.ref("id").withSchema("paginatedIdentity"),
db.ref("role").withSchema("paginatedIdentity"),
@@ -260,7 +270,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
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("id").as("ldapId").withSchema(TableName.IdentityLdapAuth)
+ db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
+ db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth)
)
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@@ -306,6 +317,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
azureId,
tokenId,
ldapId,
+ tlsCertId,
createdAt,
updatedAt
}) => ({
@@ -313,7 +325,6 @@ export const identityOrgDALFactory = (db: TDbClient) => {
roleId,
identityId,
id,
-
orgId,
createdAt,
updatedAt,
@@ -341,7 +352,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
azureId,
tokenId,
jwtId,
- ldapId
+ ldapId,
+ tlsCertId
})
}
}),
diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx
index cc8d0cef6..b441f5015 100644
--- a/frontend/src/hooks/api/identities/constants.tsx
+++ b/frontend/src/hooks/api/identities/constants.tsx
@@ -11,5 +11,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.OCI_AUTH]: "OCI Auth",
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
[IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth",
- [IdentityAuthMethod.JWT_AUTH]: "JWT Auth"
+ [IdentityAuthMethod.JWT_AUTH]: "JWT Auth",
+ [IdentityAuthMethod.TLS_CERT_AUTH]: "TLS Certificate Auth"
};
diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx
index de329d393..c850005cf 100644
--- a/frontend/src/hooks/api/identities/enums.tsx
+++ b/frontend/src/hooks/api/identities/enums.tsx
@@ -9,7 +9,8 @@ export enum IdentityAuthMethod {
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth",
LDAP_AUTH = "ldap-auth",
- JWT_AUTH = "jwt-auth"
+ JWT_AUTH = "jwt-auth",
+ TLS_CERT_AUTH = "tls-cert-auth"
}
export enum IdentityJwtConfigurationType {
diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx
index 7abf1a015..613ca85b8 100644
--- a/frontend/src/hooks/api/identities/mutations.tsx
+++ b/frontend/src/hooks/api/identities/mutations.tsx
@@ -14,6 +14,7 @@ import {
AddIdentityLdapAuthDTO,
AddIdentityOciAuthDTO,
AddIdentityOidcAuthDTO,
+ AddIdentityTlsCertAuthDTO,
AddIdentityTokenAuthDTO,
AddIdentityUniversalAuthDTO,
ClientSecretData,
@@ -32,6 +33,7 @@ import {
DeleteIdentityLdapAuthDTO,
DeleteIdentityOciAuthDTO,
DeleteIdentityOidcAuthDTO,
+ DeleteIdentityTlsCertAuthDTO,
DeleteIdentityTokenAuthDTO,
DeleteIdentityUniversalAuthClientSecretDTO,
DeleteIdentityUniversalAuthDTO,
@@ -46,6 +48,7 @@ import {
IdentityLdapAuth,
IdentityOciAuth,
IdentityOidcAuth,
+ IdentityTlsCertAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
RevokeTokenDTO,
@@ -60,6 +63,7 @@ import {
UpdateIdentityLdapAuthDTO,
UpdateIdentityOciAuthDTO,
UpdateIdentityOidcAuthDTO,
+ UpdateIdentityTlsCertAuthDTO,
UpdateIdentityTokenAuthDTO,
UpdateIdentityUniversalAuthDTO,
UpdateTokenIdentityTokenAuthDTO
@@ -655,6 +659,107 @@ export const useDeleteIdentityAliCloudAuth = () => {
});
};
+export const useAddIdentityTlsCertAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ allowedCommonNames,
+ caCertificate,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityTlsCertAuth }
+ } = await apiRequest.post<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
+ `/api/v1/auth/tls-cert-auth/identities/${identityId}`,
+ {
+ allowedCommonNames,
+ caCertificate,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityTlsCertAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({
+ queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId)
+ });
+ }
+ });
+};
+
+export const useUpdateIdentityTlsCertAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ allowedCommonNames,
+ caCertificate,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityTlsCertAuth }
+ } = await apiRequest.patch<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
+ `/api/v1/auth/tls-cert-auth/identities/${identityId}`,
+ {
+ caCertificate,
+ allowedCommonNames,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityTlsCertAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({
+ queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId)
+ });
+ }
+ });
+};
+
+export const useDeleteIdentityTlsCertAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identityTlsCertAuth }
+ } = await apiRequest.delete(`/api/v1/auth/tls-cert-auth/identities/${identityId}`);
+ return identityTlsCertAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries({
+ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
+ });
+ queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
+ queryClient.invalidateQueries({
+ queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId)
+ });
+ }
+ });
+};
+
export const useUpdateIdentityOidcAuth = () => {
const queryClient = useQueryClient();
return useMutation({
diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx
index 806c1f0a4..b59526da1 100644
--- a/frontend/src/hooks/api/identities/queries.tsx
+++ b/frontend/src/hooks/api/identities/queries.tsx
@@ -17,6 +17,7 @@ import {
IdentityMembershipOrg,
IdentityOciAuth,
IdentityOidcAuth,
+ IdentityTlsCertAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
TSearchIdentitiesDTO
@@ -34,6 +35,8 @@ export const identitiesKeys = {
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const,
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
+ getIdentityTlsCertAuth: (identityId: string) =>
+ [{ identityId }, "identity-tls-cert-auth"] as const,
getIdentityAliCloudAuth: (identityId: string) =>
[{ identityId }, "identity-alicloud-auth"] as const,
getIdentityOciAuth: (identityId: string) => [{ identityId }, "identity-oci-auth"] as const,
@@ -175,6 +178,27 @@ export const useGetIdentityAwsAuth = (
});
};
+export const useGetIdentityTlsCertAuth = (
+ identityId: string,
+ options?: TReactQueryOptions["options"]
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityTlsCertAuth }
+ } = await apiRequest.get<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
+ `/api/v1/auth/tls-cert-auth/identities/${identityId}`
+ );
+ return identityTlsCertAuth;
+ },
+ staleTime: 0,
+ gcTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
+
export const useGetIdentityOciAuth = (
identityId: string,
options?: TReactQueryOptions["options"]
diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts
index 873338f09..1af1cc24c 100644
--- a/frontend/src/hooks/api/identities/types.ts
+++ b/frontend/src/hooks/api/identities/types.ts
@@ -485,6 +485,47 @@ export type DeleteIdentityKubernetesAuthDTO = {
identityId: string;
};
+export type IdentityTlsCertAuth = {
+ identityId: string;
+ caCertificate: string;
+ allowedCommonNames: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentityTlsCertAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ caCertificate: string;
+ allowedCommonNames?: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityTlsCertAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ caCertificate: string;
+ allowedCommonNames?: string | null;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityTlsCertAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
export type CreateIdentityUniversalAuthClientSecretDTO = {
identityId: string;
description?: string;
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx
index 0d44bfe64..57663f441 100644
--- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx
+++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx
@@ -17,6 +17,7 @@ import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm";
import { IdentityOciAuthForm } from "./IdentityOciAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
+import { IdentityTlsCertAuthForm } from "./IdentityTlsCertAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
@@ -52,6 +53,7 @@ const identityAuthMethods = [
{ label: "OCI Auth", value: IdentityAuthMethod.OCI_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH },
{ label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH },
+ { label: "TLS Certificate Auth", value: IdentityAuthMethod.TLS_CERT_AUTH },
{
label: "JWT Auth",
value: IdentityAuthMethod.JWT_AUTH
@@ -123,6 +125,15 @@ export const IdentityAuthMethodModalContent = ({
/>
)
},
+ [IdentityAuthMethod.TLS_CERT_AUTH]: {
+ render: () => (
+
+ )
+ },
[IdentityAuthMethod.OIDC_AUTH]: {
render: () => (
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx
new file mode 100644
index 000000000..e018bc770
--- /dev/null
+++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx
@@ -0,0 +1,358 @@
+import { useEffect, useState } from "react";
+import { Controller, useFieldArray, useForm } from "react-hook-form";
+import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import {
+ Button,
+ FormControl,
+ IconButton,
+ Input,
+ Tab,
+ TabList,
+ TabPanel,
+ Tabs,
+ TextArea
+} from "@app/components/v2";
+import { useOrganization, useSubscription } from "@app/context";
+import {
+ useAddIdentityTlsCertAuth,
+ useGetIdentityTlsCertAuth,
+ useUpdateIdentityTlsCertAuth
+} from "@app/hooks/api";
+import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
+import { UsePopUpState } from "@app/hooks/usePopUp";
+
+import { IdentityFormTab } from "./types";
+
+const schema = z.object({
+ allowedCommonNames: z.string().optional(),
+ caCertificate: z.string().min(1),
+ accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
+ message: "Access Token TTL cannot be greater than 315360000"
+ }),
+ accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, {
+ message: "Access Token Max TTL cannot be greater than 315360000"
+ }),
+ accessTokenNumUsesLimit: z.string(),
+ accessTokenTrustedIps: z
+ .array(
+ z.object({
+ ipAddress: z.string().max(50)
+ })
+ )
+ .min(1)
+});
+
+export type FormData = z.infer;
+
+type Props = {
+ handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
+ handlePopUpToggle: (
+ popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
+ state?: boolean
+ ) => void;
+ identityId?: string;
+ isUpdate?: boolean;
+};
+
+export const IdentityTlsCertAuthForm = ({
+ handlePopUpOpen,
+ handlePopUpToggle,
+ identityId,
+ isUpdate
+}: Props) => {
+ const { currentOrg } = useOrganization();
+ const orgId = currentOrg?.id || "";
+ const { subscription } = useSubscription();
+
+ const { mutateAsync: addMutateAsync } = useAddIdentityTlsCertAuth();
+ const { mutateAsync: updateMutateAsync } = useUpdateIdentityTlsCertAuth();
+ const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration);
+
+ const { data } = useGetIdentityTlsCertAuth(identityId ?? "", {
+ enabled: isUpdate
+ });
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ caCertificate: "",
+ accessTokenTTL: "2592000",
+ accessTokenMaxTTL: "2592000",
+ accessTokenNumUsesLimit: "0",
+ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
+ }
+ });
+
+ const {
+ fields: accessTokenTrustedIpsFields,
+ append: appendAccessTokenTrustedIp,
+ remove: removeAccessTokenTrustedIp
+ } = useFieldArray({ control, name: "accessTokenTrustedIps" });
+
+ useEffect(() => {
+ if (data) {
+ reset({
+ caCertificate: data.caCertificate,
+ allowedCommonNames: data.allowedCommonNames || undefined,
+ accessTokenTTL: String(data.accessTokenTTL),
+ accessTokenMaxTTL: String(data.accessTokenMaxTTL),
+ accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
+ accessTokenTrustedIps: data.accessTokenTrustedIps.map(
+ ({ ipAddress, prefix }: IdentityTrustedIp) => {
+ return {
+ ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
+ };
+ }
+ )
+ });
+ } else {
+ reset({
+ caCertificate: "",
+ accessTokenTTL: "2592000",
+ accessTokenMaxTTL: "2592000",
+ accessTokenNumUsesLimit: "0",
+ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
+ });
+ }
+ }, [data]);
+
+ const onFormSubmit = async ({
+ caCertificate,
+ allowedCommonNames,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }: FormData) => {
+ try {
+ if (!identityId) return;
+
+ if (data) {
+ await updateMutateAsync({
+ organizationId: orgId,
+ caCertificate,
+ allowedCommonNames: allowedCommonNames || null,
+ identityId,
+ accessTokenTTL: Number(accessTokenTTL),
+ accessTokenMaxTTL: Number(accessTokenMaxTTL),
+ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
+ accessTokenTrustedIps
+ });
+ } else {
+ await addMutateAsync({
+ organizationId: orgId,
+ identityId,
+ caCertificate,
+ allowedCommonNames: allowedCommonNames || undefined,
+ accessTokenTTL: Number(accessTokenTTL),
+ accessTokenMaxTTL: Number(accessTokenMaxTTL),
+ accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
+ accessTokenTrustedIps
+ });
+ }
+
+ handlePopUpToggle("identityAuthMethod", false);
+
+ createNotification({
+ text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
+ type: "success"
+ });
+
+ reset();
+ } catch {
+ createNotification({
+ text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
+ type: "error"
+ });
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx
index 8bfbd09b3..ca0a5e6f3 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx
@@ -15,6 +15,7 @@ import {
useDeleteIdentityLdapAuth,
useDeleteIdentityOciAuth,
useDeleteIdentityOidcAuth,
+ useDeleteIdentityTlsCertAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
@@ -29,6 +30,7 @@ import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthC
import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent";
import { ViewIdentityOciAuthContent } from "./ViewIdentityOciAuthContent";
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
+import { ViewIdentityTlsCertAuthContent } from "./ViewIdentityTlsCertAuthContent";
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
@@ -63,6 +65,7 @@ export const Content = ({
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
+ const { mutateAsync: revokeTlsCertAuth } = useDeleteIdentityTlsCertAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth();
@@ -93,6 +96,10 @@ export const Content = ({
revokeMethod = revokeGcpAuth;
Component = ViewIdentityGcpAuthContent;
break;
+ case IdentityAuthMethod.TLS_CERT_AUTH:
+ revokeMethod = revokeTlsCertAuth;
+ Component = ViewIdentityTlsCertAuthContent;
+ break;
case IdentityAuthMethod.AWS_AUTH:
revokeMethod = revokeAwsAuth;
Component = ViewIdentityAwsAuthContent;
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx
new file mode 100644
index 000000000..7f4ef7fa6
--- /dev/null
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx
@@ -0,0 +1,88 @@
+import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
+import { useGetIdentityTlsCertAuth } from "@app/hooks/api";
+import { IdentityTlsCertAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm";
+
+import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
+import { ViewAuthMethodProps } from "./types";
+import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
+
+export const ViewIdentityTlsCertAuthContent = ({
+ identityId,
+ handlePopUpToggle,
+ handlePopUpOpen,
+ onDelete,
+ popUp
+}: ViewAuthMethodProps) => {
+ const { data, isPending } = useGetIdentityTlsCertAuth(identityId);
+
+ if (isPending) {
+ return (
+
+
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+ );
+ }
+
+ if (popUp.identityAuthMethod.isOpen) {
+ return (
+
+ );
+ }
+
+ return (
+ handlePopUpOpen("identityAuthMethod")}
+ onDelete={onDelete}
+ >
+
+ {data.accessTokenTTL}
+
+
+ {data.accessTokenMaxTTL}
+
+
+ {data.accessTokenNumUsesLimit}
+
+
+ {data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
+
+
+ {data.caCertificate}
}
+ >
+
+
+
+ Reveal
+
+
+
+
+
+ {data.allowedCommonNames
+ ?.split(",")
+ .map((cn) => cn.trim())
+ .join(", ")}
+
+
+ );
+};