mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3852 from akhilmhdh/feat/tls-identity-auth
feat: TLS cert identity auth
This commit is contained in:
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -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";
|
||||
@@ -218,6 +219,7 @@ declare module "fastify" {
|
||||
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
|
||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||
identityAliCloudAuth: TIdentityAliCloudAuthServiceFactory;
|
||||
identityTlsCertAuth: TIdentityTlsCertAuthServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||
identityOciAuth: TIdentityOciAuthServiceFactory;
|
||||
|
||||
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -164,6 +164,9 @@ import {
|
||||
TIdentityProjectMemberships,
|
||||
TIdentityProjectMembershipsInsert,
|
||||
TIdentityProjectMembershipsUpdate,
|
||||
TIdentityTlsCertAuths,
|
||||
TIdentityTlsCertAuthsInsert,
|
||||
TIdentityTlsCertAuthsUpdate,
|
||||
TIdentityTokenAuths,
|
||||
TIdentityTokenAuthsInsert,
|
||||
TIdentityTokenAuthsUpdate,
|
||||
@@ -794,6 +797,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityAlicloudAuthsInsert,
|
||||
TIdentityAlicloudAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityTlsCertAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityTlsCertAuths,
|
||||
TIdentityTlsCertAuthsInsert,
|
||||
TIdentityTlsCertAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAwsAuthsInsert,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.IdentityTlsCertAuth))) {
|
||||
await knex.schema.createTable(TableName.IdentityTlsCertAuth, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
|
||||
t.jsonb("accessTokenTrustedIps").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("identityId").notNullable().unique();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
t.string("allowedCommonNames").nullable();
|
||||
t.binary("encryptedCaCertificate").notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityTlsCertAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth);
|
||||
}
|
||||
27
backend/src/db/schemas/identity-tls-cert-auths.ts
Normal file
27
backend/src/db/schemas/identity-tls-cert-auths.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IdentityTlsCertAuthsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
accessTokenTTL: z.coerce.number().default(7200),
|
||||
accessTokenMaxTTL: z.coerce.number().default(7200),
|
||||
accessTokenNumUsesLimit: z.coerce.number().default(0),
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid(),
|
||||
allowedCommonNames: z.string().nullable().optional(),
|
||||
encryptedCaCertificate: zodBuffer
|
||||
});
|
||||
|
||||
export type TIdentityTlsCertAuths = z.infer<typeof IdentityTlsCertAuthsSchema>;
|
||||
export type TIdentityTlsCertAuthsInsert = Omit<z.input<typeof IdentityTlsCertAuthsSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityTlsCertAuthsUpdate = Partial<Omit<z.input<typeof IdentityTlsCertAuthsSchema>, TImmutableDBKeys>>;
|
||||
@@ -52,6 +52,7 @@ export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
export * from "./identity-project-membership-role";
|
||||
export * from "./identity-project-memberships";
|
||||
export * from "./identity-tls-cert-auths";
|
||||
export * from "./identity-token-auths";
|
||||
export * from "./identity-ua-client-secrets";
|
||||
export * from "./identity-universal-auths";
|
||||
|
||||
@@ -86,6 +86,7 @@ export enum TableName {
|
||||
IdentityOidcAuth = "identity_oidc_auths",
|
||||
IdentityJwtAuth = "identity_jwt_auths",
|
||||
IdentityLdapAuth = "identity_ldap_auths",
|
||||
IdentityTlsCertAuth = "identity_tls_cert_auths",
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
@@ -251,6 +252,7 @@ export enum IdentityAuthMethod {
|
||||
ALICLOUD_AUTH = "alicloud-auth",
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth",
|
||||
TLS_CERT_AUTH = "tls-cert-auth",
|
||||
OCI_AUTH = "oci-auth",
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
JWT_AUTH = "jwt-auth",
|
||||
|
||||
@@ -202,6 +202,12 @@ export enum EventType {
|
||||
REVOKE_IDENTITY_ALICLOUD_AUTH = "revoke-identity-alicloud-auth",
|
||||
GET_IDENTITY_ALICLOUD_AUTH = "get-identity-alicloud-auth",
|
||||
|
||||
LOGIN_IDENTITY_TLS_CERT_AUTH = "login-identity-tls-cert-auth",
|
||||
ADD_IDENTITY_TLS_CERT_AUTH = "add-identity-tls-cert-auth",
|
||||
UPDATE_IDENTITY_TLS_CERT_AUTH = "update-identity-tls-cert-auth",
|
||||
REVOKE_IDENTITY_TLS_CERT_AUTH = "revoke-identity-tls-cert-auth",
|
||||
GET_IDENTITY_TLS_CERT_AUTH = "get-identity-tls-cert-auth",
|
||||
|
||||
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
|
||||
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
|
||||
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
|
||||
@@ -1141,6 +1147,53 @@ interface GetIdentityAliCloudAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityTlsCertAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
identityTlsCertAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityTlsCertAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
allowedCommonNames: string | null | undefined;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIdentityTlsCertAuthEvent {
|
||||
type: EventType.REVOKE_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityTlsCertAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
allowedCommonNames: string | null | undefined;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityTlsCertAuthEvent {
|
||||
type: EventType.GET_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityOciAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
@@ -3358,6 +3411,11 @@ export type Event =
|
||||
| UpdateIdentityAliCloudAuthEvent
|
||||
| GetIdentityAliCloudAuthEvent
|
||||
| DeleteIdentityAliCloudAuthEvent
|
||||
| LoginIdentityTlsCertAuthEvent
|
||||
| AddIdentityTlsCertAuthEvent
|
||||
| UpdateIdentityTlsCertAuthEvent
|
||||
| GetIdentityTlsCertAuthEvent
|
||||
| DeleteIdentityTlsCertAuthEvent
|
||||
| LoginIdentityOciAuthEvent
|
||||
| AddIdentityOciAuthEvent
|
||||
| UpdateIdentityOciAuthEvent
|
||||
|
||||
@@ -22,6 +22,7 @@ export enum ApiDocsTags {
|
||||
UniversalAuth = "Universal Auth",
|
||||
GcpAuth = "GCP Auth",
|
||||
AliCloudAuth = "Alibaba Cloud Auth",
|
||||
TlsCertAuth = "TLS Certificate Auth",
|
||||
AwsAuth = "AWS Auth",
|
||||
OciAuth = "OCI Auth",
|
||||
AzureAuth = "Azure Auth",
|
||||
@@ -283,6 +284,38 @@ export const ALICLOUD_AUTH = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const TLS_CERT_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login."
|
||||
},
|
||||
ATTACH: {
|
||||
identityId: "The ID of the identity to attach the configuration onto.",
|
||||
allowedCommonNames:
|
||||
"The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
|
||||
caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
|
||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
|
||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
|
||||
},
|
||||
UPDATE: {
|
||||
identityId: "The ID of the identity to update the auth method for.",
|
||||
allowedCommonNames:
|
||||
"The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
|
||||
caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
|
||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
|
||||
},
|
||||
RETRIEVE: {
|
||||
identityId: "The ID of the identity to retrieve the auth method for."
|
||||
},
|
||||
REVOKE: {
|
||||
identityId: "The ID of the identity to revoke the auth method for."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const AWS_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login.",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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";
|
||||
@@ -384,6 +386,7 @@ export const registerRoutes = async (
|
||||
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
const identityAliCloudAuthDAL = identityAliCloudAuthDALFactory(db);
|
||||
const identityTlsCertAuthDAL = identityTlsCertAuthDALFactory(db);
|
||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
||||
const identityOciAuthDAL = identityOciAuthDALFactory(db);
|
||||
@@ -1492,6 +1495,15 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityTlsCertAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityAwsAuthDAL,
|
||||
@@ -1946,6 +1958,7 @@ export const registerRoutes = async (
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
identityAzureAuth: identityAzureAuthService,
|
||||
identityOciAuth: identityOciAuthService,
|
||||
identityTlsCertAuth: identityTlsCertAuthService,
|
||||
identityOidcAuth: identityOidcAuthService,
|
||||
identityJwtAuth: identityJwtAuthService,
|
||||
identityLdapAuth: identityLdapAuthService,
|
||||
|
||||
396
backend/src/server/routes/v1/identity-tls-cert-auth-router.ts
Normal file
396
backend/src/server/routes/v1/identity-tls-cert-auth-router.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
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 { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
|
||||
|
||||
const validateCommonNames = z
|
||||
.string()
|
||||
.min(1)
|
||||
.trim()
|
||||
.transform((el) =>
|
||||
el
|
||||
.split(",")
|
||||
.map((i) => i.trim())
|
||||
.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: "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",
|
||||
url: "/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.TlsCertAuth],
|
||||
description: "Attach TLS Certificate Auth configuration onto identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(TLS_CERT_AUTH.ATTACH.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
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()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||
.describe(TLS_CERT_AUTH.ATTACH.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(TLS_CERT_AUTH.ATTACH.accessTokenTTL),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(TLS_CERT_AUTH.ATTACH.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe(TLS_CERT_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityTlsCertAuth: IdentityTlsCertAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityTlsCertAuth = await server.services.identityTlsCertAuth.attachTlsCertAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId,
|
||||
isActorSuperAdmin: isSuperAdmin(req.auth)
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.ADD_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
identityId: identityTlsCertAuth.identityId,
|
||||
allowedCommonNames: identityTlsCertAuth.allowedCommonNames,
|
||||
accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityTlsCertAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityTlsCertAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.TlsCertAuth],
|
||||
description: "Update TLS Certificate Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(TLS_CERT_AUTH.UPDATE.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
caCertificate: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(10240)
|
||||
.refine(validateCaCertificate, "Invalid CA Certificate.")
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.caCertificate),
|
||||
allowedCommonNames: validateCommonNames
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.allowedCommonNames),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.accessTokenMaxTTL)
|
||||
})
|
||||
.refine(
|
||||
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityTlsCertAuth: IdentityTlsCertAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityTlsCertAuth = await server.services.identityTlsCertAuth.updateTlsCertAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
identityId: identityTlsCertAuth.identityId,
|
||||
allowedCommonNames: identityTlsCertAuth.allowedCommonNames,
|
||||
accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityTlsCertAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityTlsCertAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.TlsCertAuth],
|
||||
description: "Retrieve TLS Certificate Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(TLS_CERT_AUTH.RETRIEVE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityTlsCertAuth: IdentityTlsCertAuthsSchema.extend({
|
||||
caCertificate: z.string()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityTlsCertAuth = await server.services.identityTlsCertAuth.getTlsCertAuth({
|
||||
identityId: req.params.identityId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
identityId: identityTlsCertAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
return { identityTlsCertAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.TlsCertAuth],
|
||||
description: "Delete TLS Certificate Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(TLS_CERT_AUTH.REVOKE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityTlsCertAuth: IdentityTlsCertAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityTlsCertAuth = await server.services.identityTlsCertAuth.revokeTlsCertAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.REVOKE_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
identityId: identityTlsCertAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityTlsCertAuth };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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";
|
||||
@@ -66,6 +67,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||
await authRouter.register(registerIdentityAliCloudAuthRouter);
|
||||
await authRouter.register(registerIdentityAwsAuthRouter);
|
||||
await authRouter.register(registerIdentityTlsCertAuthRouter, { prefix: "/tls-cert-auth" });
|
||||
await authRouter.register(registerIdentityAzureAuthRouter);
|
||||
await authRouter.register(registerIdentityOciAuthRouter);
|
||||
await authRouter.register(registerIdentityOidcAuthRouter);
|
||||
|
||||
@@ -45,6 +45,11 @@ 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`)
|
||||
.leftJoin(
|
||||
TableName.IdentityTlsCertAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityTlsCertAuth}.identityId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.IdentityAccessToken))
|
||||
.select(
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
|
||||
@@ -61,6 +66,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityLdapAuth).as("accessTokenTrustedIpsLdap"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTlsCertAuth).as("accessTokenTrustedIpsTlsCert"),
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
)
|
||||
.first();
|
||||
@@ -79,7 +85,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
|
||||
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,
|
||||
trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap
|
||||
trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap,
|
||||
trustedIpsAccessTlsCertAuth: doc.accessTokenTrustedIpsTlsCert
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
|
||||
|
||||
@@ -201,7 +201,8 @@ export const identityAccessTokenServiceFactory = ({
|
||||
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
|
||||
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth,
|
||||
[IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth
|
||||
[IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth,
|
||||
[IdentityAuthMethod.TLS_CERT_AUTH]: identityAccessToken.trustedIpsAccessTlsCertAuth
|
||||
};
|
||||
|
||||
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, TOrmify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityTlsCertAuthDALFactory = TOrmify<TableName.IdentityTlsCertAuth>;
|
||||
|
||||
export const identityTlsCertAuthDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.IdentityTlsCertAuth);
|
||||
return orm;
|
||||
};
|
||||
@@ -0,0 +1,423 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
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";
|
||||
|
||||
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 { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
||||
import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
|
||||
import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
|
||||
|
||||
type TIdentityTlsCertAuthServiceFactoryDep = {
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
|
||||
identityTlsCertAuthDAL: Pick<
|
||||
TIdentityTlsCertAuthDALFactory,
|
||||
"findOne" | "transaction" | "create" | "updateById" | "delete"
|
||||
>;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
const parseSubjectDetails = (data: string) => {
|
||||
const values: Record<string, string> = {};
|
||||
data.split("\n").forEach((el) => {
|
||||
const [key, value] = el.split("=");
|
||||
values[key.trim()] = value.trim();
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
export const identityTlsCertAuthServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityTlsCertAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService,
|
||||
kmsService
|
||||
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
|
||||
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
|
||||
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
||||
if (!identityTlsCertAuth) {
|
||||
throw new NotFoundError({
|
||||
message: "TLS Certificate auth method not found for identity, did you configure TLS Certificate auth?"
|
||||
});
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
|
||||
identityId: identityTlsCertAuth.identityId
|
||||
});
|
||||
|
||||
if (!identityMembershipOrg) {
|
||||
throw new NotFoundError({
|
||||
message: `Identity organization membership for identity with ID '${identityTlsCertAuth.identityId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const caCertificate = decryptor({
|
||||
cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate
|
||||
}).toString();
|
||||
|
||||
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);
|
||||
if (!isValidCertificate)
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: Certificate not issued by the provided CA."
|
||||
});
|
||||
|
||||
if (new Date(clientCertificateX509.validTo) < new Date()) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: Certificate has expired."
|
||||
});
|
||||
}
|
||||
|
||||
if (new Date(clientCertificateX509.validFrom) > new Date()) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: Certificate not yet valid."
|
||||
});
|
||||
}
|
||||
|
||||
const subjectDetails = parseSubjectDetails(clientCertificateX509.subject);
|
||||
if (identityTlsCertAuth.allowedCommonNames) {
|
||||
const isValidCommonName = identityTlsCertAuth.allowedCommonNames.split(",").includes(subjectDetails.CN);
|
||||
if (!isValidCommonName) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: TLS Certificate Auth common name not allowed."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the token
|
||||
const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityTlsCertAuth.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.TLS_CERT_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
identityId: identityTlsCertAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
Number(identityAccessToken.accessTokenTTL) === 0
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
identityTlsCertAuth,
|
||||
accessToken,
|
||||
identityAccessToken,
|
||||
identityMembershipOrg
|
||||
};
|
||||
};
|
||||
|
||||
const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({
|
||||
identityId,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
isActorSuperAdmin,
|
||||
caCertificate,
|
||||
allowedCommonNames
|
||||
}) => {
|
||||
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
||||
|
||||
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.TLS_CERT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add TLS Certificate 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(OrgPermissionIdentityActions.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 } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const identityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityTlsCertAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
accessTokenMaxTTL,
|
||||
allowedCommonNames,
|
||||
accessTokenTTL,
|
||||
encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCertificate) }).cipherTextBlob,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityTlsCertAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const updateTlsCertAuth: TIdentityTlsCertAuthServiceFactory["updateTlsCertAuth"] = async ({
|
||||
identityId,
|
||||
caCertificate,
|
||||
allowedCommonNames,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}) => {
|
||||
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.TLS_CERT_AUTH)) {
|
||||
throw new NotFoundError({
|
||||
message: "The identity does not have TLS Certificate Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityTlsCertAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityTlsCertAuth.accessTokenTTL) >
|
||||
(accessTokenMaxTTL || identityTlsCertAuth.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(OrgPermissionIdentityActions.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 { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const updatedTlsCertAuth = await identityTlsCertAuthDAL.updateById(identityTlsCertAuth.id, {
|
||||
allowedCommonNames,
|
||||
encryptedCaCertificate: caCertificate
|
||||
? encryptor({ plainText: Buffer.from(caCertificate) }).cipherTextBlob
|
||||
: undefined,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
});
|
||||
|
||||
return { ...updatedTlsCertAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const getTlsCertAuth: TIdentityTlsCertAuthServiceFactory["getTlsCertAuth"] = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}) => {
|
||||
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.TLS_CERT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have TLS Certificate Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
let caCertificate = "";
|
||||
if (identityAuth.encryptedCaCertificate) {
|
||||
caCertificate = decryptor({ cipherTextBlob: identityAuth.encryptedCaCertificate }).toString();
|
||||
}
|
||||
|
||||
return { ...identityAuth, caCertificate, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const revokeTlsCertAuth: TIdentityTlsCertAuthServiceFactory["revokeTlsCertAuth"] = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}) => {
|
||||
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.TLS_CERT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have TLS Certificate auth"
|
||||
});
|
||||
}
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke TLS Certificate auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const revokedIdentityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => {
|
||||
const deletedTlsCertAuth = await identityTlsCertAuthDAL.delete({ identityId }, tx);
|
||||
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.TLS_CERT_AUTH }, tx);
|
||||
|
||||
return { ...deletedTlsCertAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityTlsCertAuth;
|
||||
};
|
||||
|
||||
return {
|
||||
login,
|
||||
attachTlsCertAuth,
|
||||
updateTlsCertAuth,
|
||||
getTlsCertAuth,
|
||||
revokeTlsCertAuth
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { TIdentityAccessTokens, TIdentityOrgMemberships, TIdentityTlsCertAuths } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TLoginTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
clientCertificate: string;
|
||||
};
|
||||
|
||||
export type TAttachTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
caCertificate: string;
|
||||
allowedCommonNames?: string | null;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
isActorSuperAdmin?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
caCertificate?: string;
|
||||
allowedCommonNames?: string | null;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRevokeTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIdentityTlsCertAuthServiceFactory = {
|
||||
login: (dto: TLoginTlsCertAuthDTO) => Promise<{
|
||||
identityTlsCertAuth: TIdentityTlsCertAuths;
|
||||
accessToken: string;
|
||||
identityAccessToken: TIdentityAccessTokens;
|
||||
identityMembershipOrg: TIdentityOrgMemberships;
|
||||
}>;
|
||||
attachTlsCertAuth: (dto: TAttachTlsCertAuthDTO) => Promise<TIdentityTlsCertAuths>;
|
||||
updateTlsCertAuth: (dto: TUpdateTlsCertAuthDTO) => Promise<TIdentityTlsCertAuths>;
|
||||
revokeTlsCertAuth: (dto: TRevokeTlsCertAuthDTO) => Promise<TIdentityTlsCertAuths>;
|
||||
getTlsCertAuth: (dto: TGetTlsCertAuthDTO) => Promise<TIdentityTlsCertAuths & { caCertificate: string }>;
|
||||
};
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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<TIdentityTlsCertAuths>(
|
||||
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<TIdentityTlsCertAuths>(
|
||||
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
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
nginx:
|
||||
container_name: infisical-dev-nginx
|
||||
image: nginx
|
||||
restart: always
|
||||
restart: "always"
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8443:443
|
||||
|
||||
4
docs/api-reference/endpoints/tls-cert-auth/attach.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/attach.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Attach"
|
||||
openapi: "POST /api/v1/auth/tls-cert-auth/identities/{identityId}"
|
||||
---
|
||||
4
docs/api-reference/endpoints/tls-cert-auth/login.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/login.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Login"
|
||||
openapi: "POST /api/v1/auth/tls-cert-auth/login"
|
||||
---
|
||||
4
docs/api-reference/endpoints/tls-cert-auth/retrieve.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/retrieve.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Retrieve"
|
||||
openapi: "GET /api/v1/auth/tls-cert-auth/identities/{identityId}"
|
||||
---
|
||||
4
docs/api-reference/endpoints/tls-cert-auth/revoke.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/revoke.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Revoke"
|
||||
openapi: "DELETE /api/v1/auth/tls-cert-auth/identities/{identityId}"
|
||||
---
|
||||
4
docs/api-reference/endpoints/tls-cert-auth/update.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/update.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/auth/tls-cert-auth/identities/{identityId}"
|
||||
---
|
||||
@@ -288,6 +288,7 @@
|
||||
"documentation/platform/identities/kubernetes-auth",
|
||||
"documentation/platform/identities/oci-auth",
|
||||
"documentation/platform/identities/token-auth",
|
||||
"documentation/platform/identities/tls-cert-auth",
|
||||
"documentation/platform/identities/universal-auth",
|
||||
{
|
||||
"group": "OIDC Auth",
|
||||
@@ -752,6 +753,16 @@
|
||||
"api-reference/endpoints/alicloud-auth/revoke"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "TLS Certificate Auth",
|
||||
"pages": [
|
||||
"api-reference/endpoints/tls-cert-auth/login",
|
||||
"api-reference/endpoints/tls-cert-auth/attach",
|
||||
"api-reference/endpoints/tls-cert-auth/retrieve",
|
||||
"api-reference/endpoints/tls-cert-auth/update",
|
||||
"api-reference/endpoints/tls-cert-auth/revoke"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "AWS Auth",
|
||||
"pages": [
|
||||
|
||||
176
docs/documentation/platform/identities/tls-cert-auth.mdx
Normal file
176
docs/documentation/platform/identities/tls-cert-auth.mdx
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: TLS Certificate Auth
|
||||
description: "Learn how to authenticate with Infisical using TLS Certificate."
|
||||
---
|
||||
|
||||
**TLS Certificate Auth** is an authentication method that verifies a user's TLS Client certificate using the provided CA Certificate, allowing secure access to Infisical resources.
|
||||
|
||||
## Diagram
|
||||
|
||||
The following sequence diagram illustrates the TLS Certificate Auth workflow for authenticating users with Infisical.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Infisical
|
||||
|
||||
Note over Client,Client: Step 1: Setup your TLS request with the client certificate
|
||||
|
||||
Note over Client,Infisical: Step 2: Login Operation
|
||||
Client->>Infisical: Send request to /api/v1/auth/tls-cert-auth/login
|
||||
|
||||
Note over Infisical: Step 3: Request verification using CA Certificate
|
||||
|
||||
Infisical->>Client: Return short-lived access token
|
||||
|
||||
Note over Client,Infisical: Step 5: Access Infisical API with token
|
||||
Client->>Infisical: Make authenticated requests using the short-lived access token
|
||||
```
|
||||
|
||||
## Concept
|
||||
|
||||
At a high level, Infisical authenticates the client's TLS Certificate by verifying its identity and checking that it meets specific requirements (e.g., it is bound to the allowed common names) at the `/api/v1/auth/tls-cert-auth/login` endpoint. If successful, Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
|
||||
|
||||
To be more specific:
|
||||
|
||||
1. The client sends a TLS request with the client certificate to Infisical at the `/api/v1/auth/tls-cert-auth/login` endpoint.
|
||||
2. Infisical verifies the incoming request using the provided CA certificate.
|
||||
3. Infisical checks the user's properties against set criteria such as Allowed Common Names.
|
||||
4. If all checks pass, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
|
||||
|
||||
<Accordion title="TLS with Load Balancer/Proxy">
|
||||
Most of the time, the Infisical server will be behind a load balancer or
|
||||
proxy. To propagate the TLS certificate from the load balancer to the
|
||||
instance, you can configure the TLS to send the client certificate as a header
|
||||
that is set as an [environment
|
||||
variable](/self-hosting/configuration/envars#param-identity-tls-cert-auth-client-certificate-header-key).
|
||||
</Accordion>
|
||||
|
||||
## Guide
|
||||
|
||||
In the following steps, we explore how to create and use identities for your workloads and applications on TLS Certificate to
|
||||
access the Infisical API using request signing.
|
||||
|
||||
<Warning>
|
||||
**Self-Hosted Users:** Before using TLS Certificate Auth, please review the
|
||||
[Security Requirements for Self-Hosted
|
||||
Deployments](#security-requirements-for-self-hosted-deployments) section below
|
||||
to ensure proper configuration and avoid security vulnerabilities.
|
||||
</Warning>
|
||||
|
||||
### Creating an identity
|
||||
|
||||
To create an identity, head to your Organization Settings > Access Control > [Identities](https://app.infisical.com/organization/access-management?selectedTab=identities) and press **Create identity**.
|
||||
|
||||

|
||||
|
||||
When creating an identity, you specify an organization-level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
|
||||
|
||||

|
||||
|
||||
Input some details for your new identity:
|
||||
|
||||
- **Name (required):** A friendly name for the identity.
|
||||
- **Role (required):** A role from the [**Organization Roles**](https://app.infisical.com/organization/access-management?selectedTab=roles) tab for the identity to assume. The organization role assigned will determine what organization-level resources this identity can have access to.
|
||||
|
||||
Once you've created an identity, you'll be redirected to a page where you can manage the identity.
|
||||
|
||||

|
||||
|
||||
Since the identity has been configured with [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) by default, you should reconfigure it to use TLS Certificate Auth instead. To do this, click the cog next to **Universal Auth** and then select **Delete** in the options dropdown.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Now create a new TLS Certificate Auth Method.
|
||||
|
||||

|
||||
|
||||
Here's some information about each field:
|
||||
|
||||
- **CA Certificate:** A PEM encoded CA Certificate used to validate incoming TLS request client certificate.
|
||||
- **Allowed Common Names:** A comma separated list of client certificate common names allowed.
|
||||
- **Access Token TTL (default is `2592000` equivalent to 30 days):** The lifetime for an access token in seconds. This value will be referenced at renewal time.
|
||||
- **Access Token Max TTL (default is `2592000` equivalent to 30 days):** The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.
|
||||
- **Access Token Max Number of Uses (default is `0`):** The maximum number of times that an access token can be used; a value of `0` implies an infinite number of uses.
|
||||
- **Access Token Trusted IPs:** The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
|
||||
|
||||
### Adding an identity to a project
|
||||
|
||||
In order to allow an identity to access project-level resources such as secrets, you must add it to the relevant projects.
|
||||
|
||||
To do this, head over to the project you want to add the identity to and navigate to Project Settings > Access Control > Machine Identities and press **Add Identity**.
|
||||
|
||||

|
||||
|
||||
Select the identity you want to add to the project and the project-level role you want it to assume. The project role given to the identity will determine what project-level resources this identity can access.
|
||||
|
||||

|
||||
|
||||
### Accessing the Infisical API with the identity
|
||||
|
||||
To access the Infisical API as the identity, you need to send a TLS request to `/api/v1/auth/tls-cert-auth/login` endpoint.
|
||||
|
||||
Below is an example of how you can authenticate with Infisical using NodeJS.
|
||||
|
||||
```javascript
|
||||
const fs = require("fs");
|
||||
const https = require("https");
|
||||
const axios = require("axios");
|
||||
|
||||
try {
|
||||
const clientCertificate = fs.readFileSync("client-cert.pem", "utf8");
|
||||
const clientKeyCertificate = fs.readFileSync("client-key.pem", "utf8");
|
||||
|
||||
const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
|
||||
const identityId = "<your-identity-id>";
|
||||
|
||||
// Create HTTPS agent with client certificate and key
|
||||
const httpsAgent = new https.Agent({
|
||||
cert: clientCertificate,
|
||||
key: clientKeyCertificate,
|
||||
});
|
||||
|
||||
const { data } = await axios.post(
|
||||
`${infisicalUrl}/api/v1/auth/tls-cert-auth/login`,
|
||||
{
|
||||
identityId,
|
||||
},
|
||||
{
|
||||
httpsAgent: httpsAgent, // Pass the HTTPS agent with client cert
|
||||
}
|
||||
);
|
||||
|
||||
console.log("result data: ", data); // access token here
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; the default TTL is `7200` seconds, which can be adjusted.
|
||||
|
||||
If an identity access token expires, it can no longer access the Infisical API. A new access token should be obtained by performing another login operation.
|
||||
|
||||
</Note>
|
||||
|
||||
## Security Requirements for Self-Hosted Deployments
|
||||
|
||||
ALL TLS cert [login](/api-reference/endpoints/tls-cert-auth/login) requests **MUST** go through a load balancer/proxy that verifies certificate ownership:
|
||||
|
||||
- **REQUIRED:** Configure your load balancer/proxy to **require a proper TLS handshake with client certificate presentation**
|
||||
- **REQUIRED:** Ensure the load balancer **verifies the client possesses the private key** corresponding to the certificate (standard TLS behavior)
|
||||
- **NEVER** allow direct connections to Infisical for TLS cert auth - this enables header injection attacks
|
||||
- **NEVER** forward certificate headers without requiring proper TLS certificate presentation
|
||||
|
||||
### Load Balancer Configuration Examples
|
||||
|
||||
- **AWS ALB:** Use mTLS listeners which require client certificate presentation during the TLS handshake
|
||||
- **NGINX/HAProxy:** Configure SSL client certificate requirement with proper TLS handshake verification
|
||||
|
||||
<Note>
|
||||
Infisical will handle the actual certificate validation against the configured
|
||||
CA certificate and determine authentication permissions. The load balancer's
|
||||
role is to ensure certificate ownership, not certificate trust validation.
|
||||
</Note>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 467 KiB |
@@ -32,7 +32,7 @@ Used to configure platform-specific security and operational settings
|
||||
<ParamField query="HOST" type="string" default="localhost" optional>
|
||||
Specifies the network interface Infisical will bind to when accepting incoming connections.
|
||||
|
||||
By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
|
||||
By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
|
||||
|
||||
To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces.
|
||||
|
||||
@@ -122,6 +122,7 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}]
|
||||
</ParamField>
|
||||
|
||||
### Redis
|
||||
|
||||
Redis is used for caching and background tasks. You can use either a standalone Redis instance or a Redis Sentinel setup.
|
||||
|
||||
<Tabs>
|
||||
@@ -199,8 +200,17 @@ Without email configuration, Infisical's core functions like sign-up/login and s
|
||||
connection can not be encrypted then message is not sent.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SMTP_TLS_REJECT_UNAUTHORIZED" type="bool" default="true" optional>
|
||||
If this is `true`, Infisical will validate the server's SSL/TLS certificate and reject the connection if the certificate is invalid or not trusted. If set to `false`, the client will accept the server's certificate regardless of its validity, which can be useful in development or testing environments but is not recommended for production use.
|
||||
<ParamField
|
||||
query="SMTP_TLS_REJECT_UNAUTHORIZED"
|
||||
type="bool"
|
||||
default="true"
|
||||
optional
|
||||
>
|
||||
If this is `true`, Infisical will validate the server's SSL/TLS certificate
|
||||
and reject the connection if the certificate is invalid or not trusted. If set
|
||||
to `false`, the client will accept the server's certificate regardless of its
|
||||
validity, which can be useful in development or testing environments but is
|
||||
not recommended for production use.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SMTP_CUSTOM_CA_CERT" type="string" default="none" optional>
|
||||
@@ -211,6 +221,7 @@ Without email configuration, Infisical's core functions like sign-up/login and s
|
||||
Infisical highly encourages the following variables be used alongside this one for maximum security:
|
||||
- `SMTP_REQUIRE_TLS=true`
|
||||
- `SMTP_TLS_REJECT_UNAUTHORIZED=true`
|
||||
|
||||
</ParamField>
|
||||
</Accordion>
|
||||
|
||||
@@ -577,6 +588,7 @@ You can configure third-party app connections for re-use across Infisical Projec
|
||||
<ParamField query="INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET" type="string" default="none" optional>
|
||||
The webhook secret configured for payload verification in the GitHub Radar App
|
||||
</ParamField>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="GitHub OAuth Connection">
|
||||
@@ -771,3 +783,14 @@ If export type is set to `otlp`, you will have to configure a value for `OTEL_EX
|
||||
<ParamField query="OTEL_COLLECTOR_BASIC_AUTH_PASSWORD" type="string">
|
||||
The password for authenticating with the telemetry collector.
|
||||
</ParamField>
|
||||
|
||||
## Identity Auth Method
|
||||
|
||||
<ParamField
|
||||
query="IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY"
|
||||
type="string"
|
||||
default="x-identity-tls-cert-auth-client-cert"
|
||||
>
|
||||
The TLS header used to propagate the client certificate from the load balancer
|
||||
to the server.
|
||||
</ParamField>
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<IdentityTlsCertAuth, object, AddIdentityTlsCertAuthDTO>({
|
||||
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.getIdentityTlsCertAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityTlsCertAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityTlsCertAuth, object, UpdateIdentityTlsCertAuthDTO>({
|
||||
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.getIdentityTlsCertAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIdentityTlsCertAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityTlsCertAuth, object, DeleteIdentityTlsCertAuthDTO>({
|
||||
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.getIdentityTlsCertAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityOidcAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityOidcAuth, object, UpdateIdentityOidcAuthDTO>({
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: () => (
|
||||
<IdentityTlsCertAuthForm
|
||||
identityId={identityAuthMethodData.identityId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.OIDC_AUTH]: {
|
||||
render: () => (
|
||||
|
||||
@@ -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<typeof schema>;
|
||||
|
||||
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>(IdentityFormTab.Configuration);
|
||||
|
||||
const { data } = useGetIdentityTlsCertAuth(identityId ?? "", {
|
||||
enabled: isUpdate
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
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 (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||
<TabList>
|
||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IdentityFormTab.Configuration}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCertificate"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
tooltipText="A PEM-encoded CA certificate. This will be used to validate client certificate."
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedCommonNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Common Names"
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
tooltipText="Comma separated common names allowed to authenticate against the identity. Leave empty to allow any certificate."
|
||||
>
|
||||
<Input {...field} placeholder="" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
tooltipText="The lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update" : "Add"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Spinner className="text-mineshaft-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={faBan}
|
||||
title="Could not find TLS Certificate Auth associated with this Identity."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (popUp.identityAuthMethod.isOpen) {
|
||||
return (
|
||||
<IdentityTlsCertAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
{data.accessTokenNumUsesLimit}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-2"
|
||||
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCertificate}</p>}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Common Names">
|
||||
{data.allowedCommonNames
|
||||
?.split(",")
|
||||
.map((cn) => cn.trim())
|
||||
.join(", ")}
|
||||
</IdentityAuthFieldDisplay>
|
||||
</ViewIdentityContentWrapper>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user