diff --git a/.github/workflows/build-staging-and-deploy-aws.yml b/.github/workflows/build-staging-and-deploy-aws.yml
index a9b2046ae6..9af533ee3c 100644
--- a/.github/workflows/build-staging-and-deploy-aws.yml
+++ b/.github/workflows/build-staging-and-deploy-aws.yml
@@ -50,6 +50,13 @@ jobs:
environment:
name: Gamma
steps:
+ - uses: twingate/github-action@v1
+ with:
+ # The Twingate Service Key used to connect Twingate to the proper service
+ # Learn more about [Twingate Services](https://docs.twingate.com/docs/services)
+ #
+ # Required
+ service-key: ${{ secrets.TWINGATE_GAMMA_SERVICE_KEY }}
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
@@ -74,21 +81,21 @@ jobs:
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
- aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
+ aws ecs describe-task-definition --task-definition infisical-core-gamma-stage --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
- container-name: infisical-core-platform
+ container-name: infisical-core
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
- service: infisical-core-platform
- cluster: infisical-core-platform
+ service: infisical-core-gamma-stage
+ cluster: infisical-gamma-stage
wait-for-service-stability: true
production-postgres-deployment:
diff --git a/.infisicalignore b/.infisicalignore
index 855047fe4c..b7fc38b356 100644
--- a/.infisicalignore
+++ b/.infisicalignore
@@ -5,3 +5,4 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/M
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
+docs/mint.json:generic-api-key:651
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 8bd4a98a0b..97c3351399 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -38,6 +38,7 @@
"bcrypt": "^5.1.1",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
+ "connect-redis": "^7.1.1",
"cron": "^3.1.7",
"dotenv": "^16.4.1",
"fastify": "^4.26.0",
@@ -57,6 +58,7 @@
"mysql2": "^3.9.8",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
+ "openid-client": "^5.6.5",
"ora": "^7.0.1",
"oracledb": "^6.4.0",
"passport-github": "^1.1.0",
@@ -6790,6 +6792,17 @@
"integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
"dev": true
},
+ "node_modules/connect-redis": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-7.1.1.tgz",
+ "integrity": "sha512-M+z7alnCJiuzKa8/1qAYdGUXHYfDnLolOGAUjOioB07pP39qxjG+X9ibsud7qUBc4jMV5Mcy3ugGv8eFcgamJQ==",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "express-session": ">=1"
+ }
+ },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -7896,6 +7909,55 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/express-session": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
+ "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
+ "peer": true,
+ "dependencies": {
+ "cookie": "0.6.0",
+ "cookie-signature": "1.0.7",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.0.2",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/express-session/node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express-session/node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "peer": true
+ },
+ "node_modules/express-session/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "peer": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express-session/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "peer": true
+ },
"node_modules/express/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
@@ -9603,6 +9665,14 @@
"node": ">= 0.6.0"
}
},
+ "node_modules/jose": {
+ "version": "4.15.5",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz",
+ "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
@@ -10728,6 +10798,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
@@ -10851,6 +10929,14 @@
"@octokit/core": ">=5"
}
},
+ "node_modules/oidc-token-hash": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
+ "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
+ "engines": {
+ "node": "^10.13.0 || >=12.0.0"
+ }
+ },
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
@@ -10870,6 +10956,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -10897,6 +10992,20 @@
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="
},
+ "node_modules/openid-client": {
+ "version": "5.6.5",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz",
+ "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==",
+ "dependencies": {
+ "jose": "^4.15.5",
+ "lru-cache": "^6.0.0",
+ "object-hash": "^2.2.0",
+ "oidc-token-hash": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -11948,6 +12057,15 @@
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -14027,6 +14145,18 @@
"node": ">=0.8.0"
}
},
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "peer": true,
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
diff --git a/backend/package.json b/backend/package.json
index f2a8582d6d..ab397daa57 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -99,6 +99,7 @@
"bcrypt": "^5.1.1",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
+ "connect-redis": "^7.1.1",
"cron": "^3.1.7",
"dotenv": "^16.4.1",
"fastify": "^4.26.0",
@@ -118,6 +119,7 @@
"mysql2": "^3.9.8",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
+ "openid-client": "^5.6.5",
"ora": "^7.0.1",
"oracledb": "^6.4.0",
"passport-github": "^1.1.0",
diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index e5cf3f79f0..c7e58fb099 100644
--- a/backend/src/@types/fastify.d.ts
+++ b/backend/src/@types/fastify.d.ts
@@ -13,6 +13,7 @@ import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
+import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
@@ -102,6 +103,7 @@ declare module "fastify" {
permission: TPermissionServiceFactory;
org: TOrgServiceFactory;
orgRole: TOrgRoleServiceFactory;
+ oidc: TOidcConfigServiceFactory;
superAdmin: TSuperAdminServiceFactory;
user: TUserServiceFactory;
group: TGroupServiceFactory;
diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts
index 79342ea276..4fdfda70c0 100644
--- a/backend/src/@types/knex.d.ts
+++ b/backend/src/@types/knex.d.ts
@@ -134,6 +134,9 @@ import {
TLdapGroupMaps,
TLdapGroupMapsInsert,
TLdapGroupMapsUpdate,
+ TOidcConfigs,
+ TOidcConfigsInsert,
+ TOidcConfigsUpdate,
TOrganizations,
TOrganizationsInsert,
TOrganizationsUpdate,
@@ -549,6 +552,7 @@ declare module "knex/types/tables" {
TDynamicSecretLeasesUpdate
>;
[TableName.SamlConfig]: Knex.CompositeTableType;
+ [TableName.OidcConfig]: Knex.CompositeTableType;
[TableName.LdapConfig]: Knex.CompositeTableType;
[TableName.LdapGroupMap]: Knex.CompositeTableType;
[TableName.OrgBot]: Knex.CompositeTableType;
diff --git a/backend/src/db/migrations/20240624161942_add-oidc-auth.ts b/backend/src/db/migrations/20240624161942_add-oidc-auth.ts
new file mode 100644
index 0000000000..3f4b0636d2
--- /dev/null
+++ b/backend/src/db/migrations/20240624161942_add-oidc-auth.ts
@@ -0,0 +1,49 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasTable(TableName.OidcConfig))) {
+ await knex.schema.createTable(TableName.OidcConfig, (tb) => {
+ tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ tb.string("discoveryURL");
+ tb.string("issuer");
+ tb.string("authorizationEndpoint");
+ tb.string("jwksUri");
+ tb.string("tokenEndpoint");
+ tb.string("userinfoEndpoint");
+ tb.text("encryptedClientId").notNullable();
+ tb.string("configurationType").notNullable();
+ tb.string("clientIdIV").notNullable();
+ tb.string("clientIdTag").notNullable();
+ tb.text("encryptedClientSecret").notNullable();
+ tb.string("clientSecretIV").notNullable();
+ tb.string("clientSecretTag").notNullable();
+ tb.string("allowedEmailDomains").nullable();
+ tb.boolean("isActive").notNullable();
+ tb.timestamps(true, true, true);
+ tb.uuid("orgId").notNullable().unique();
+ tb.foreign("orgId").references("id").inTable(TableName.Organization);
+ });
+ }
+
+ if (await knex.schema.hasTable(TableName.SuperAdmin)) {
+ if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "trustOidcEmails"))) {
+ await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
+ tb.boolean("trustOidcEmails").defaultTo(false);
+ });
+ }
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.OidcConfig);
+
+ if (await knex.schema.hasTable(TableName.SuperAdmin)) {
+ if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustOidcEmails")) {
+ await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
+ t.dropColumn("trustOidcEmails");
+ });
+ }
+ }
+}
diff --git a/backend/src/db/migrations/20240624172027_default-saml-ldap-org.ts b/backend/src/db/migrations/20240624172027_default-saml-ldap-org.ts
new file mode 100644
index 0000000000..fec132df42
--- /dev/null
+++ b/backend/src/db/migrations/20240624172027_default-saml-ldap-org.ts
@@ -0,0 +1,27 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+const DEFAULT_AUTH_ORG_ID_FIELD = "defaultAuthOrgId";
+
+export async function up(knex: Knex): Promise {
+ const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD);
+
+ await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
+ if (!hasDefaultOrgColumn) {
+ t.uuid(DEFAULT_AUTH_ORG_ID_FIELD).nullable();
+ t.foreign(DEFAULT_AUTH_ORG_ID_FIELD).references("id").inTable(TableName.Organization).onDelete("SET NULL");
+ }
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD);
+
+ await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
+ if (hasDefaultOrgColumn) {
+ t.dropForeign([DEFAULT_AUTH_ORG_ID_FIELD]);
+ t.dropColumn(DEFAULT_AUTH_ORG_ID_FIELD);
+ }
+ });
+}
diff --git a/backend/src/db/migrations/20240624221840_certificate-alt-names.ts b/backend/src/db/migrations/20240624221840_certificate-alt-names.ts
new file mode 100644
index 0000000000..fa076b7b4b
--- /dev/null
+++ b/backend/src/db/migrations/20240624221840_certificate-alt-names.ts
@@ -0,0 +1,24 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ if (await knex.schema.hasTable(TableName.Certificate)) {
+ const hasAltNamesColumn = await knex.schema.hasColumn(TableName.Certificate, "altNames");
+ if (!hasAltNamesColumn) {
+ await knex.schema.alterTable(TableName.Certificate, (t) => {
+ t.string("altNames").defaultTo("");
+ });
+ }
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ if (await knex.schema.hasTable(TableName.Certificate)) {
+ if (await knex.schema.hasColumn(TableName.Certificate, "altNames")) {
+ await knex.schema.alterTable(TableName.Certificate, (t) => {
+ t.dropColumn("altNames");
+ });
+ }
+ }
+}
diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts
index b635420d51..833396fb16 100644
--- a/backend/src/db/schemas/certificates.ts
+++ b/backend/src/db/schemas/certificates.ts
@@ -19,7 +19,8 @@ export const CertificatesSchema = z.object({
notBefore: z.date(),
notAfter: z.date(),
revokedAt: z.date().nullable().optional(),
- revocationReason: z.number().nullable().optional()
+ revocationReason: z.number().nullable().optional(),
+ altNames: z.string().default("").nullable().optional()
});
export type TCertificates = z.infer;
diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts
index df126b7f03..af8c2070a1 100644
--- a/backend/src/db/schemas/index.ts
+++ b/backend/src/db/schemas/index.ts
@@ -43,6 +43,7 @@ export * from "./kms-root-config";
export * from "./ldap-configs";
export * from "./ldap-group-maps";
export * from "./models";
+export * from "./oidc-configs";
export * from "./org-bots";
export * from "./org-memberships";
export * from "./org-roles";
diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts
index d7a5e6de17..bdc574bcb8 100644
--- a/backend/src/db/schemas/models.ts
+++ b/backend/src/db/schemas/models.ts
@@ -78,6 +78,7 @@ export enum TableName {
SecretRotationOutput = "secret_rotation_outputs",
SamlConfig = "saml_configs",
LdapConfig = "ldap_configs",
+ OidcConfig = "oidc_configs",
LdapGroupMap = "ldap_group_maps",
AuditLog = "audit_logs",
AuditLogStream = "audit_log_streams",
diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts
new file mode 100644
index 0000000000..d78f9e9a7e
--- /dev/null
+++ b/backend/src/db/schemas/oidc-configs.ts
@@ -0,0 +1,34 @@
+// 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 { TImmutableDBKeys } from "./models";
+
+export const OidcConfigsSchema = z.object({
+ id: z.string().uuid(),
+ discoveryURL: z.string().nullable().optional(),
+ issuer: z.string().nullable().optional(),
+ authorizationEndpoint: z.string().nullable().optional(),
+ jwksUri: z.string().nullable().optional(),
+ tokenEndpoint: z.string().nullable().optional(),
+ userinfoEndpoint: z.string().nullable().optional(),
+ encryptedClientId: z.string(),
+ configurationType: z.string(),
+ clientIdIV: z.string(),
+ clientIdTag: z.string(),
+ encryptedClientSecret: z.string(),
+ clientSecretIV: z.string(),
+ clientSecretTag: z.string(),
+ allowedEmailDomains: z.string().nullable().optional(),
+ isActive: z.boolean(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ orgId: z.string().uuid()
+});
+
+export type TOidcConfigs = z.infer;
+export type TOidcConfigsInsert = Omit, TImmutableDBKeys>;
+export type TOidcConfigsUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts
index 91035ab8e0..211626b7c5 100644
--- a/backend/src/db/schemas/projects.ts
+++ b/backend/src/db/schemas/projects.ts
@@ -17,8 +17,8 @@ export const ProjectsSchema = z.object({
updatedAt: z.date(),
version: z.number().default(1),
upgradeStatus: z.string().nullable().optional(),
- kmsCertificateKeyId: z.string().uuid().nullable().optional(),
- pitVersionLimit: z.number().default(10)
+ pitVersionLimit: z.number().default(10),
+ kmsCertificateKeyId: z.string().uuid().nullable().optional()
});
export type TProjects = z.infer;
diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts
index 417d4e05e6..29e41c78ef 100644
--- a/backend/src/db/schemas/super-admin.ts
+++ b/backend/src/db/schemas/super-admin.ts
@@ -16,7 +16,9 @@ export const SuperAdminSchema = z.object({
allowedSignUpDomain: z.string().nullable().optional(),
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"),
trustSamlEmails: z.boolean().default(false).nullable().optional(),
- trustLdapEmails: z.boolean().default(false).nullable().optional()
+ trustLdapEmails: z.boolean().default(false).nullable().optional(),
+ trustOidcEmails: z.boolean().default(false).nullable().optional(),
+ defaultAuthOrgId: z.string().uuid().nullable().optional()
});
export type TSuperAdmin = z.infer;
diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts
index d04bd86fd1..6bd9176d8c 100644
--- a/backend/src/ee/routes/v1/index.ts
+++ b/backend/src/ee/routes/v1/index.ts
@@ -8,6 +8,7 @@ import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
+import { registerOidcRouter } from "./oidc-router";
import { registerOrgRoleRouter } from "./org-role-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerProjectRouter } from "./project-router";
@@ -64,7 +65,14 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ prefix: "/pki" }
);
- await server.register(registerSamlRouter, { prefix: "/sso" });
+ await server.register(
+ async (ssoRouter) => {
+ await ssoRouter.register(registerSamlRouter);
+ await ssoRouter.register(registerOidcRouter, { prefix: "/oidc" });
+ },
+ { prefix: "/sso" }
+ );
+
await server.register(registerScimRouter, { prefix: "/scim" });
await server.register(registerLdapRouter, { prefix: "/ldap" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts
new file mode 100644
index 0000000000..e675121e97
--- /dev/null
+++ b/backend/src/ee/routes/v1/oidc-router.ts
@@ -0,0 +1,355 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/no-unsafe-return */
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-unsafe-argument */
+// All the any rules are disabled because passport typesense with fastify is really poor
+
+import { Authenticator, Strategy } from "@fastify/passport";
+import fastifySession from "@fastify/session";
+import RedisStore from "connect-redis";
+import { Redis } from "ioredis";
+import { z } from "zod";
+
+import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs";
+import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types";
+import { getConfig } from "@app/lib/config/env";
+import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+export const registerOidcRouter = async (server: FastifyZodProvider) => {
+ const appCfg = getConfig();
+ const redis = new Redis(appCfg.REDIS_URL);
+ const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" });
+
+ /*
+ - OIDC protocol cannot work without sessions: https://github.com/panva/node-openid-client/issues/190
+ - Current redis usage is not ideal and will eventually have to be refactored to use a better structure
+ - Fastify session <> Redis structure is based on the ff: https://github.com/fastify/session/blob/master/examples/redis.js
+ */
+ const redisStore = new RedisStore({
+ client: redis,
+ prefix: "oidc-session:",
+ ttl: 600 // 10 minutes
+ });
+
+ await server.register(fastifySession, {
+ secret: appCfg.COOKIE_SECRET_SIGN_KEY,
+ store: redisStore,
+ cookie: {
+ secure: appCfg.HTTPS_ENABLED,
+ sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
+ }
+ });
+
+ await server.register(passport.initialize());
+ await server.register(passport.secureSession());
+
+ // redirect to IDP for login
+ server.route({
+ url: "/login",
+ method: "GET",
+ config: {
+ rateLimit: authRateLimit
+ },
+ schema: {
+ querystring: z.object({
+ orgSlug: z.string().trim(),
+ callbackPort: z.string().trim().optional()
+ })
+ },
+ preValidation: [
+ async (req, res) => {
+ const { orgSlug, callbackPort } = req.query;
+
+ // ensure fresh session state per login attempt
+ await req.session.regenerate();
+
+ req.session.set("oidcOrgSlug", orgSlug);
+
+ if (callbackPort) {
+ req.session.set("callbackPort", callbackPort);
+ }
+
+ const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(orgSlug, callbackPort);
+ return (
+ passport.authenticate(oidcStrategy as Strategy, {
+ scope: "profile email openid"
+ }) as any
+ )(req, res);
+ }
+ ],
+ handler: () => {}
+ });
+
+ // callback route after login from IDP
+ server.route({
+ url: "/callback",
+ method: "GET",
+ preValidation: [
+ async (req, res) => {
+ const oidcOrgSlug = req.session.get("oidcOrgSlug");
+ const callbackPort = req.session.get("callbackPort");
+ const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(oidcOrgSlug, callbackPort);
+
+ return (
+ passport.authenticate(oidcStrategy as Strategy, {
+ failureRedirect: "/api/v1/sso/oidc/login/error",
+ session: false,
+ failureMessage: true
+ }) as any
+ )(req, res);
+ }
+ ],
+ handler: async (req, res) => {
+ await req.session.destroy();
+
+ if (req.passportUser.isUserCompleted) {
+ return res.redirect(
+ `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
+ );
+ }
+
+ // signup
+ return res.redirect(
+ `${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
+ );
+ }
+ });
+
+ server.route({
+ url: "/login/error",
+ method: "GET",
+ handler: async (req, res) => {
+ await req.session.destroy();
+
+ return res.status(500).send({
+ error: "Authentication error",
+ details: req.query
+ });
+ }
+ });
+
+ server.route({
+ url: "/config",
+ method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ querystring: z.object({
+ orgSlug: z.string().trim()
+ }),
+ response: {
+ 200: OidcConfigsSchema.pick({
+ id: true,
+ issuer: true,
+ authorizationEndpoint: true,
+ jwksUri: true,
+ tokenEndpoint: true,
+ userinfoEndpoint: true,
+ configurationType: true,
+ discoveryURL: true,
+ isActive: true,
+ orgId: true,
+ allowedEmailDomains: true
+ }).extend({
+ clientId: z.string(),
+ clientSecret: z.string()
+ })
+ }
+ },
+ handler: async (req) => {
+ const { orgSlug } = req.query;
+ const oidc = await server.services.oidc.getOidc({
+ orgSlug,
+ type: "external",
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod
+ });
+
+ return oidc;
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/config",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ body: z
+ .object({
+ allowedEmailDomains: z
+ .string()
+ .trim()
+ .optional()
+ .default("")
+ .transform((data) => {
+ if (data === "") return "";
+ // Trim each ID and join with ', ' to ensure formatting
+ return data
+ .split(",")
+ .map((id) => id.trim())
+ .join(", ");
+ }),
+ discoveryURL: z.string().trim(),
+ configurationType: z.nativeEnum(OIDCConfigurationType),
+ issuer: z.string().trim(),
+ authorizationEndpoint: z.string().trim(),
+ jwksUri: z.string().trim(),
+ tokenEndpoint: z.string().trim(),
+ userinfoEndpoint: z.string().trim(),
+ clientId: z.string().trim(),
+ clientSecret: z.string().trim(),
+ isActive: z.boolean()
+ })
+ .partial()
+ .merge(z.object({ orgSlug: z.string() })),
+ response: {
+ 200: OidcConfigsSchema.pick({
+ id: true,
+ issuer: true,
+ authorizationEndpoint: true,
+ configurationType: true,
+ discoveryURL: true,
+ jwksUri: true,
+ tokenEndpoint: true,
+ userinfoEndpoint: true,
+ orgId: true,
+ allowedEmailDomains: true,
+ isActive: true
+ })
+ }
+ },
+ handler: async (req) => {
+ const oidc = await server.services.oidc.updateOidcCfg({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+ return oidc;
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/config",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ body: z
+ .object({
+ allowedEmailDomains: z
+ .string()
+ .trim()
+ .optional()
+ .default("")
+ .transform((data) => {
+ if (data === "") return "";
+ // Trim each ID and join with ', ' to ensure formatting
+ return data
+ .split(",")
+ .map((id) => id.trim())
+ .join(", ");
+ }),
+ configurationType: z.nativeEnum(OIDCConfigurationType),
+ issuer: z.string().trim().optional().default(""),
+ discoveryURL: z.string().trim().optional().default(""),
+ authorizationEndpoint: z.string().trim().optional().default(""),
+ jwksUri: z.string().trim().optional().default(""),
+ tokenEndpoint: z.string().trim().optional().default(""),
+ userinfoEndpoint: z.string().trim().optional().default(""),
+ clientId: z.string().trim(),
+ clientSecret: z.string().trim(),
+ isActive: z.boolean(),
+ orgSlug: z.string().trim()
+ })
+ .superRefine((data, ctx) => {
+ if (data.configurationType === OIDCConfigurationType.CUSTOM) {
+ if (!data.issuer) {
+ ctx.addIssue({
+ path: ["issuer"],
+ message: "Issuer is required",
+ code: z.ZodIssueCode.custom
+ });
+ }
+ if (!data.authorizationEndpoint) {
+ ctx.addIssue({
+ path: ["authorizationEndpoint"],
+ message: "Authorization endpoint is required",
+ code: z.ZodIssueCode.custom
+ });
+ }
+ if (!data.jwksUri) {
+ ctx.addIssue({
+ path: ["jwksUri"],
+ message: "JWKS URI is required",
+ code: z.ZodIssueCode.custom
+ });
+ }
+ if (!data.tokenEndpoint) {
+ ctx.addIssue({
+ path: ["tokenEndpoint"],
+ message: "Token endpoint is required",
+ code: z.ZodIssueCode.custom
+ });
+ }
+ if (!data.userinfoEndpoint) {
+ ctx.addIssue({
+ path: ["userinfoEndpoint"],
+ message: "Userinfo endpoint is required",
+ code: z.ZodIssueCode.custom
+ });
+ }
+ } else {
+ // eslint-disable-next-line no-lonely-if
+ if (!data.discoveryURL) {
+ ctx.addIssue({
+ path: ["discoveryURL"],
+ message: "Discovery URL is required",
+ code: z.ZodIssueCode.custom
+ });
+ }
+ }
+ }),
+ response: {
+ 200: OidcConfigsSchema.pick({
+ id: true,
+ issuer: true,
+ authorizationEndpoint: true,
+ configurationType: true,
+ discoveryURL: true,
+ jwksUri: true,
+ tokenEndpoint: true,
+ userinfoEndpoint: true,
+ orgId: true,
+ isActive: true,
+ allowedEmailDomains: true
+ })
+ }
+ },
+
+ handler: async (req) => {
+ const oidc = await server.services.oidc.createOidcCfg({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+ return oidc;
+ }
+ });
+};
diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts
index 8a0d2aef92..0c6ff51c8e 100644
--- a/backend/src/ee/services/audit-log/audit-log-types.ts
+++ b/backend/src/ee/services/audit-log/audit-log-types.ts
@@ -65,25 +65,31 @@ export enum EventType {
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
+ REVOKE_IDENTITY_UNIVERSAL_AUTH = "revoke-identity-universal-auth",
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth",
+ REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth",
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
+ GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth",
ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth",
UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth",
+ REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth",
GET_IDENTITY_GCP_AUTH = "get-identity-gcp-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",
+ REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth",
+ REVOKE_IDENTITY_AZURE_AUTH = "revoke-identity-azure-auth",
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
@@ -434,6 +440,13 @@ interface GetIdentityUniversalAuthEvent {
};
}
+interface DeleteIdentityUniversalAuthEvent {
+ type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
interface LoginIdentityKubernetesAuthEvent {
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH;
metadata: {
@@ -457,6 +470,13 @@ interface AddIdentityKubernetesAuthEvent {
};
}
+interface DeleteIdentityKubernetesAuthEvent {
+ type: EventType.REVOKE_IDENTITY_KUBERNETES_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
interface UpdateIdentityKubernetesAuthEvent {
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH;
metadata: {
@@ -493,6 +513,14 @@ interface GetIdentityUniversalAuthClientSecretsEvent {
};
}
+interface GetIdentityUniversalAuthClientSecretByIdEvent {
+ type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID;
+ metadata: {
+ identityId: string;
+ clientSecretId: string;
+ };
+}
+
interface RevokeIdentityUniversalAuthClientSecretEvent {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
metadata: {
@@ -525,6 +553,13 @@ interface AddIdentityGcpAuthEvent {
};
}
+interface DeleteIdentityGcpAuthEvent {
+ type: EventType.REVOKE_IDENTITY_GCP_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
interface UpdateIdentityGcpAuthEvent {
type: EventType.UPDATE_IDENTITY_GCP_AUTH;
metadata: {
@@ -570,6 +605,13 @@ interface AddIdentityAwsAuthEvent {
};
}
+interface DeleteIdentityAwsAuthEvent {
+ type: EventType.REVOKE_IDENTITY_AWS_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
interface UpdateIdentityAwsAuthEvent {
type: EventType.UPDATE_IDENTITY_AWS_AUTH;
metadata: {
@@ -613,6 +655,13 @@ interface AddIdentityAzureAuthEvent {
};
}
+interface DeleteIdentityAzureAuthEvent {
+ type: EventType.REVOKE_IDENTITY_AZURE_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
interface UpdateIdentityAzureAuthEvent {
type: EventType.UPDATE_IDENTITY_AZURE_AUTH;
metadata: {
@@ -1003,24 +1052,30 @@ export type Event =
| LoginIdentityUniversalAuthEvent
| AddIdentityUniversalAuthEvent
| UpdateIdentityUniversalAuthEvent
+ | DeleteIdentityUniversalAuthEvent
| GetIdentityUniversalAuthEvent
| LoginIdentityKubernetesAuthEvent
+ | DeleteIdentityKubernetesAuthEvent
| AddIdentityKubernetesAuthEvent
| UpdateIdentityKubernetesAuthEvent
| GetIdentityKubernetesAuthEvent
| CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent
+ | GetIdentityUniversalAuthClientSecretByIdEvent
| RevokeIdentityUniversalAuthClientSecretEvent
| LoginIdentityGcpAuthEvent
| AddIdentityGcpAuthEvent
+ | DeleteIdentityGcpAuthEvent
| UpdateIdentityGcpAuthEvent
| GetIdentityGcpAuthEvent
| LoginIdentityAwsAuthEvent
| AddIdentityAwsAuthEvent
| UpdateIdentityAwsAuthEvent
| GetIdentityAwsAuthEvent
+ | DeleteIdentityAwsAuthEvent
| LoginIdentityAzureAuthEvent
| AddIdentityAzureAuthEvent
+ | DeleteIdentityAzureAuthEvent
| UpdateIdentityAzureAuthEvent
| GetIdentityAzureAuthEvent
| CreateEnvironmentEvent
diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts
index f5b482c3a7..b63ecda109 100644
--- a/backend/src/ee/services/ldap-config/ldap-config-service.ts
+++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts
@@ -23,6 +23,8 @@ import {
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
+import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
+import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -30,6 +32,7 @@ import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membe
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
+import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
@@ -84,6 +87,8 @@ type TLdapConfigServiceFactoryDep = {
userAliasDAL: Pick;
permissionService: Pick;
licenseService: Pick;
+ tokenService: Pick;
+ smtpService: Pick;
};
export type TLdapConfigServiceFactory = ReturnType;
@@ -103,7 +108,9 @@ export const ldapConfigServiceFactory = ({
userDAL,
userAliasDAL,
permissionService,
- licenseService
+ licenseService,
+ tokenService,
+ smtpService
}: TLdapConfigServiceFactoryDep) => {
const createLdapCfg = async ({
actor,
@@ -509,7 +516,7 @@ export const ldapConfigServiceFactory = ({
if (!orgMembership) {
await orgMembershipDAL.create(
{
- userId: userAlias.userId,
+ userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
@@ -642,6 +649,22 @@ export const ldapConfigServiceFactory = ({
}
);
+ if (user.email && !user.isEmailVerified) {
+ const token = await tokenService.createTokenForUser({
+ type: TokenType.TOKEN_EMAIL_VERIFICATION,
+ userId: user.id
+ });
+
+ await smtpService.sendMail({
+ template: SmtpTemplates.EmailVerification,
+ subjectLine: "Infisical confirmation code",
+ recipients: [user.email],
+ substitutions: {
+ code: token
+ }
+ });
+ }
+
return { isUserCompleted, providerAuthToken };
};
diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts
index 45325d414d..d69f7bf955 100644
--- a/backend/src/ee/services/license/licence-fns.ts
+++ b/backend/src/ee/services/license/licence-fns.ts
@@ -29,6 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogStreams: false,
auditLogStreamLimit: 3,
samlSSO: false,
+ oidcSSO: false,
scim: false,
ldap: false,
groups: false,
diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts
index 565387296c..36b03ff803 100644
--- a/backend/src/ee/services/license/license-types.ts
+++ b/backend/src/ee/services/license/license-types.ts
@@ -46,6 +46,7 @@ export type TFeatureSet = {
auditLogStreams: false;
auditLogStreamLimit: 3;
samlSSO: false;
+ oidcSSO: false;
scim: false;
ldap: false;
groups: false;
diff --git a/backend/src/ee/services/oidc/oidc-config-dal.ts b/backend/src/ee/services/oidc/oidc-config-dal.ts
new file mode 100644
index 0000000000..470916c616
--- /dev/null
+++ b/backend/src/ee/services/oidc/oidc-config-dal.ts
@@ -0,0 +1,11 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify } from "@app/lib/knex";
+
+export type TOidcConfigDALFactory = ReturnType;
+
+export const oidcConfigDALFactory = (db: TDbClient) => {
+ const oidcCfgOrm = ormify(db, TableName.OidcConfig);
+
+ return { ...oidcCfgOrm };
+};
diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts
new file mode 100644
index 0000000000..b983492bc2
--- /dev/null
+++ b/backend/src/ee/services/oidc/oidc-config-service.ts
@@ -0,0 +1,637 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+import { ForbiddenError } from "@casl/ability";
+import jwt from "jsonwebtoken";
+import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
+
+import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
+import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
+import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
+import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
+import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { getConfig } from "@app/lib/config/env";
+import {
+ decryptSymmetric,
+ encryptSymmetric,
+ generateAsymmetricKeyPair,
+ generateSymmetricKey,
+ infisicalSymmetricDecrypt,
+ infisicalSymmetricEncypt
+} from "@app/lib/crypto/encryption";
+import { BadRequestError } from "@app/lib/errors";
+import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
+import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
+import { TokenType } from "@app/services/auth-token/auth-token-types";
+import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
+import { TOrgDALFactory } from "@app/services/org/org-dal";
+import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
+import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
+import { getServerCfg } from "@app/services/super-admin/super-admin-service";
+import { TUserDALFactory } from "@app/services/user/user-dal";
+import { normalizeUsername } from "@app/services/user/user-fns";
+import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
+import { UserAliasType } from "@app/services/user-alias/user-alias-types";
+
+import { TOidcConfigDALFactory } from "./oidc-config-dal";
+import {
+ OIDCConfigurationType,
+ TCreateOidcCfgDTO,
+ TGetOidcCfgDTO,
+ TOidcLoginDTO,
+ TUpdateOidcCfgDTO
+} from "./oidc-config-types";
+
+type TOidcConfigServiceFactoryDep = {
+ userDAL: Pick<
+ TUserDALFactory,
+ "create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
+ >;
+ userAliasDAL: Pick;
+ orgDAL: Pick<
+ TOrgDALFactory,
+ "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
+ >;
+ orgMembershipDAL: Pick;
+ orgBotDAL: Pick;
+ licenseService: Pick;
+ tokenService: Pick;
+ smtpService: Pick;
+ permissionService: Pick;
+ oidcConfigDAL: Pick;
+};
+
+export type TOidcConfigServiceFactory = ReturnType;
+
+export const oidcConfigServiceFactory = ({
+ orgDAL,
+ orgMembershipDAL,
+ userDAL,
+ userAliasDAL,
+ licenseService,
+ permissionService,
+ tokenService,
+ orgBotDAL,
+ smtpService,
+ oidcConfigDAL
+}: TOidcConfigServiceFactoryDep) => {
+ const getOidc = async (dto: TGetOidcCfgDTO) => {
+ const org = await orgDAL.findOne({ slug: dto.orgSlug });
+ if (!org) {
+ throw new BadRequestError({
+ message: "Organization not found",
+ name: "OrgNotFound"
+ });
+ }
+ if (dto.type === "external") {
+ const { permission } = await permissionService.getOrgPermission(
+ dto.actor,
+ dto.actorId,
+ org.id,
+ dto.actorAuthMethod,
+ dto.actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
+ }
+
+ const oidcCfg = await oidcConfigDAL.findOne({
+ orgId: org.id
+ });
+
+ if (!oidcCfg) {
+ throw new BadRequestError({
+ message: "Failed to find organization OIDC configuration"
+ });
+ }
+
+ // decrypt and return cfg
+ const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId });
+ if (!orgBot) {
+ throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
+ }
+
+ const key = infisicalSymmetricDecrypt({
+ ciphertext: orgBot.encryptedSymmetricKey,
+ iv: orgBot.symmetricKeyIV,
+ tag: orgBot.symmetricKeyTag,
+ keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
+ });
+
+ const { encryptedClientId, clientIdIV, clientIdTag, encryptedClientSecret, clientSecretIV, clientSecretTag } =
+ oidcCfg;
+
+ let clientId = "";
+ if (encryptedClientId && clientIdIV && clientIdTag) {
+ clientId = decryptSymmetric({
+ ciphertext: encryptedClientId,
+ key,
+ tag: clientIdTag,
+ iv: clientIdIV
+ });
+ }
+
+ let clientSecret = "";
+ if (encryptedClientSecret && clientSecretIV && clientSecretTag) {
+ clientSecret = decryptSymmetric({
+ key,
+ tag: clientSecretTag,
+ iv: clientSecretIV,
+ ciphertext: encryptedClientSecret
+ });
+ }
+
+ return {
+ id: oidcCfg.id,
+ issuer: oidcCfg.issuer,
+ authorizationEndpoint: oidcCfg.authorizationEndpoint,
+ configurationType: oidcCfg.configurationType,
+ discoveryURL: oidcCfg.discoveryURL,
+ jwksUri: oidcCfg.jwksUri,
+ tokenEndpoint: oidcCfg.tokenEndpoint,
+ userinfoEndpoint: oidcCfg.userinfoEndpoint,
+ orgId: oidcCfg.orgId,
+ isActive: oidcCfg.isActive,
+ allowedEmailDomains: oidcCfg.allowedEmailDomains,
+ clientId,
+ clientSecret
+ };
+ };
+
+ const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
+ const serverCfg = await getServerCfg();
+ const appCfg = getConfig();
+ const userAlias = await userAliasDAL.findOne({
+ externalId,
+ orgId,
+ aliasType: UserAliasType.OIDC
+ });
+
+ const organization = await orgDAL.findOrgById(orgId);
+ if (!organization) throw new BadRequestError({ message: "Org not found" });
+
+ let user: TUsers;
+ if (userAlias) {
+ user = await userDAL.transaction(async (tx) => {
+ const foundUser = await userDAL.findById(userAlias.userId, tx);
+ const [orgMembership] = await orgDAL.findMembership(
+ {
+ [`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id,
+ [`${TableName.OrgMembership}.orgId` as "id"]: orgId
+ },
+ { tx }
+ );
+ if (!orgMembership) {
+ await orgMembershipDAL.create(
+ {
+ userId: userAlias.userId,
+ inviteEmail: email,
+ orgId,
+ role: OrgMembershipRole.Member,
+ status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
+ },
+ tx
+ );
+ // Only update the membership to Accepted if the user account is already completed.
+ } else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) {
+ await orgDAL.updateMembershipById(
+ orgMembership.id,
+ {
+ status: OrgMembershipStatus.Accepted
+ },
+ tx
+ );
+ }
+
+ return foundUser;
+ });
+ } else {
+ user = await userDAL.transaction(async (tx) => {
+ let newUser: TUsers | undefined;
+
+ if (serverCfg.trustOidcEmails) {
+ newUser = await userDAL.findOne(
+ {
+ email,
+ isEmailVerified: true
+ },
+ tx
+ );
+ }
+
+ if (!newUser) {
+ const uniqueUsername = await normalizeUsername(externalId, userDAL);
+ newUser = await userDAL.create(
+ {
+ email,
+ firstName,
+ isEmailVerified: serverCfg.trustOidcEmails,
+ username: serverCfg.trustOidcEmails ? email : uniqueUsername,
+ lastName,
+ authMethods: [],
+ isGhost: false
+ },
+ tx
+ );
+ }
+
+ await userAliasDAL.create(
+ {
+ userId: newUser.id,
+ aliasType: UserAliasType.OIDC,
+ externalId,
+ emails: email ? [email] : [],
+ orgId
+ },
+ tx
+ );
+
+ const [orgMembership] = await orgDAL.findMembership(
+ {
+ [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
+ [`${TableName.OrgMembership}.orgId` as "id"]: orgId
+ },
+ { tx }
+ );
+
+ if (!orgMembership) {
+ await orgMembershipDAL.create(
+ {
+ userId: newUser.id,
+ inviteEmail: email,
+ orgId,
+ role: OrgMembershipRole.Member,
+ status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
+ },
+ tx
+ );
+ // Only update the membership to Accepted if the user account is already completed.
+ } else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
+ await orgDAL.updateMembershipById(
+ orgMembership.id,
+ {
+ status: OrgMembershipStatus.Accepted
+ },
+ tx
+ );
+ }
+
+ return newUser;
+ });
+ }
+
+ await licenseService.updateSubscriptionOrgMemberCount(organization.id);
+
+ const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
+ const isUserCompleted = Boolean(user.isAccepted);
+ const providerAuthToken = jwt.sign(
+ {
+ authTokenType: AuthTokenType.PROVIDER_TOKEN,
+ userId: user.id,
+ username: user.username,
+ ...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
+ firstName,
+ lastName,
+ organizationName: organization.name,
+ organizationId: organization.id,
+ organizationSlug: organization.slug,
+ hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
+ authMethod: AuthMethod.OIDC,
+ authType: UserAliasType.OIDC,
+ isUserCompleted,
+ ...(callbackPort && { callbackPort })
+ },
+ appCfg.AUTH_SECRET,
+ {
+ expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
+ }
+ );
+
+ if (user.email && !user.isEmailVerified) {
+ const token = await tokenService.createTokenForUser({
+ type: TokenType.TOKEN_EMAIL_VERIFICATION,
+ userId: user.id
+ });
+
+ await smtpService.sendMail({
+ template: SmtpTemplates.EmailVerification,
+ subjectLine: "Infisical confirmation code",
+ recipients: [user.email],
+ substitutions: {
+ code: token
+ }
+ });
+ }
+
+ return { isUserCompleted, providerAuthToken };
+ };
+
+ const updateOidcCfg = async ({
+ orgSlug,
+ allowedEmailDomains,
+ configurationType,
+ discoveryURL,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ actorId,
+ issuer,
+ isActive,
+ authorizationEndpoint,
+ jwksUri,
+ tokenEndpoint,
+ userinfoEndpoint,
+ clientId,
+ clientSecret
+ }: TUpdateOidcCfgDTO) => {
+ const org = await orgDAL.findOne({
+ slug: orgSlug
+ });
+
+ if (!org) {
+ throw new BadRequestError({
+ message: "Organization not found"
+ });
+ }
+
+ const plan = await licenseService.getPlan(org.id);
+ if (!plan.oidcSSO)
+ throw new BadRequestError({
+ message:
+ "Failed to update OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
+ });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ org.id,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
+
+ const orgBot = await orgBotDAL.findOne({ orgId: org.id });
+ if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
+ const key = infisicalSymmetricDecrypt({
+ ciphertext: orgBot.encryptedSymmetricKey,
+ iv: orgBot.symmetricKeyIV,
+ tag: orgBot.symmetricKeyTag,
+ keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
+ });
+
+ const updateQuery: TOidcConfigsUpdate = {
+ allowedEmailDomains,
+ configurationType,
+ discoveryURL,
+ issuer,
+ authorizationEndpoint,
+ tokenEndpoint,
+ userinfoEndpoint,
+ jwksUri,
+ isActive
+ };
+
+ if (clientId !== undefined) {
+ const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key);
+ updateQuery.encryptedClientId = encryptedClientId;
+ updateQuery.clientIdIV = clientIdIV;
+ updateQuery.clientIdTag = clientIdTag;
+ }
+
+ if (clientSecret !== undefined) {
+ const {
+ ciphertext: encryptedClientSecret,
+ iv: clientSecretIV,
+ tag: clientSecretTag
+ } = encryptSymmetric(clientSecret, key);
+
+ updateQuery.encryptedClientSecret = encryptedClientSecret;
+ updateQuery.clientSecretIV = clientSecretIV;
+ updateQuery.clientSecretTag = clientSecretTag;
+ }
+
+ const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
+ return ssoConfig;
+ };
+
+ const createOidcCfg = async ({
+ orgSlug,
+ allowedEmailDomains,
+ configurationType,
+ discoveryURL,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ actorId,
+ issuer,
+ isActive,
+ authorizationEndpoint,
+ jwksUri,
+ tokenEndpoint,
+ userinfoEndpoint,
+ clientId,
+ clientSecret
+ }: TCreateOidcCfgDTO) => {
+ const org = await orgDAL.findOne({
+ slug: orgSlug
+ });
+ if (!org) {
+ throw new BadRequestError({
+ message: "Organization not found"
+ });
+ }
+
+ const plan = await licenseService.getPlan(org.id);
+ if (!plan.oidcSSO)
+ throw new BadRequestError({
+ message:
+ "Failed to create OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
+ });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ org.id,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
+
+ const orgBot = await orgBotDAL.transaction(async (tx) => {
+ const doc = await orgBotDAL.findOne({ orgId: org.id }, tx);
+ if (doc) return doc;
+
+ const { privateKey, publicKey } = generateAsymmetricKeyPair();
+ const key = generateSymmetricKey();
+ const {
+ ciphertext: encryptedPrivateKey,
+ iv: privateKeyIV,
+ tag: privateKeyTag,
+ encoding: privateKeyKeyEncoding,
+ algorithm: privateKeyAlgorithm
+ } = infisicalSymmetricEncypt(privateKey);
+ const {
+ ciphertext: encryptedSymmetricKey,
+ iv: symmetricKeyIV,
+ tag: symmetricKeyTag,
+ encoding: symmetricKeyKeyEncoding,
+ algorithm: symmetricKeyAlgorithm
+ } = infisicalSymmetricEncypt(key);
+
+ return orgBotDAL.create(
+ {
+ name: "Infisical org bot",
+ publicKey,
+ privateKeyIV,
+ encryptedPrivateKey,
+ symmetricKeyIV,
+ symmetricKeyTag,
+ encryptedSymmetricKey,
+ symmetricKeyAlgorithm,
+ orgId: org.id,
+ privateKeyTag,
+ privateKeyAlgorithm,
+ privateKeyKeyEncoding,
+ symmetricKeyKeyEncoding
+ },
+ tx
+ );
+ });
+
+ const key = infisicalSymmetricDecrypt({
+ ciphertext: orgBot.encryptedSymmetricKey,
+ iv: orgBot.symmetricKeyIV,
+ tag: orgBot.symmetricKeyTag,
+ keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
+ });
+
+ const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key);
+ const {
+ ciphertext: encryptedClientSecret,
+ iv: clientSecretIV,
+ tag: clientSecretTag
+ } = encryptSymmetric(clientSecret, key);
+
+ const oidcCfg = await oidcConfigDAL.create({
+ issuer,
+ isActive,
+ configurationType,
+ discoveryURL,
+ authorizationEndpoint,
+ allowedEmailDomains,
+ jwksUri,
+ tokenEndpoint,
+ userinfoEndpoint,
+ orgId: org.id,
+ encryptedClientId,
+ clientIdIV,
+ clientIdTag,
+ encryptedClientSecret,
+ clientSecretIV,
+ clientSecretTag
+ });
+
+ return oidcCfg;
+ };
+
+ const getOrgAuthStrategy = async (orgSlug: string, callbackPort?: string) => {
+ const appCfg = getConfig();
+
+ const org = await orgDAL.findOne({
+ slug: orgSlug
+ });
+
+ if (!org) {
+ throw new BadRequestError({
+ message: "Organization not found."
+ });
+ }
+
+ const oidcCfg = await getOidc({
+ type: "internal",
+ orgSlug
+ });
+
+ if (!oidcCfg || !oidcCfg.isActive) {
+ throw new BadRequestError({
+ message: "Failed to authenticate with OIDC SSO"
+ });
+ }
+
+ let issuer: Issuer;
+ if (oidcCfg.configurationType === OIDCConfigurationType.DISCOVERY_URL) {
+ if (!oidcCfg.discoveryURL) {
+ throw new BadRequestError({
+ message: "OIDC not configured correctly"
+ });
+ }
+ issuer = await Issuer.discover(oidcCfg.discoveryURL);
+ } else {
+ if (
+ !oidcCfg.issuer ||
+ !oidcCfg.authorizationEndpoint ||
+ !oidcCfg.jwksUri ||
+ !oidcCfg.tokenEndpoint ||
+ !oidcCfg.userinfoEndpoint
+ ) {
+ throw new BadRequestError({
+ message: "OIDC not configured correctly"
+ });
+ }
+ issuer = new OpenIdIssuer({
+ issuer: oidcCfg.issuer,
+ authorization_endpoint: oidcCfg.authorizationEndpoint,
+ jwks_uri: oidcCfg.jwksUri,
+ token_endpoint: oidcCfg.tokenEndpoint,
+ userinfo_endpoint: oidcCfg.userinfoEndpoint
+ });
+ }
+
+ const client = new issuer.Client({
+ client_id: oidcCfg.clientId,
+ client_secret: oidcCfg.clientSecret,
+ redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`]
+ });
+
+ const strategy = new OpenIdStrategy(
+ {
+ client,
+ passReqToCallback: true
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (_req: any, tokenSet: TokenSet, cb: any) => {
+ const claims = tokenSet.claims();
+ if (!claims.email || !claims.given_name) {
+ throw new BadRequestError({
+ message: "Invalid request. Missing email or first name"
+ });
+ }
+
+ if (oidcCfg.allowedEmailDomains) {
+ const allowedDomains = oidcCfg.allowedEmailDomains.split(", ");
+ if (!allowedDomains.includes(claims.email.split("@")[1])) {
+ throw new BadRequestError({
+ message: "Email not allowed."
+ });
+ }
+ }
+
+ oidcLogin({
+ email: claims.email,
+ externalId: claims.sub,
+ firstName: claims.given_name ?? "",
+ lastName: claims.family_name ?? "",
+ orgId: org.id,
+ callbackPort
+ })
+ .then(({ isUserCompleted, providerAuthToken }) => {
+ cb(null, { isUserCompleted, providerAuthToken });
+ })
+ .catch((error) => {
+ cb(error);
+ });
+ }
+ );
+
+ return strategy;
+ };
+
+ return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg };
+};
diff --git a/backend/src/ee/services/oidc/oidc-config-types.ts b/backend/src/ee/services/oidc/oidc-config-types.ts
new file mode 100644
index 0000000000..6e36b796b3
--- /dev/null
+++ b/backend/src/ee/services/oidc/oidc-config-types.ts
@@ -0,0 +1,56 @@
+import { TGenericPermission } from "@app/lib/types";
+
+export enum OIDCConfigurationType {
+ CUSTOM = "custom",
+ DISCOVERY_URL = "discoveryURL"
+}
+
+export type TOidcLoginDTO = {
+ externalId: string;
+ email: string;
+ firstName: string;
+ lastName?: string;
+ orgId: string;
+ callbackPort?: string;
+};
+
+export type TGetOidcCfgDTO =
+ | ({
+ type: "external";
+ orgSlug: string;
+ } & TGenericPermission)
+ | {
+ type: "internal";
+ orgSlug: string;
+ };
+
+export type TCreateOidcCfgDTO = {
+ issuer?: string;
+ authorizationEndpoint?: string;
+ discoveryURL?: string;
+ configurationType: OIDCConfigurationType;
+ allowedEmailDomains?: string;
+ jwksUri?: string;
+ tokenEndpoint?: string;
+ userinfoEndpoint?: string;
+ clientId: string;
+ clientSecret: string;
+ isActive: boolean;
+ orgSlug: string;
+} & TGenericPermission;
+
+export type TUpdateOidcCfgDTO = Partial<{
+ issuer: string;
+ authorizationEndpoint: string;
+ allowedEmailDomains: string;
+ discoveryURL: string;
+ jwksUri: string;
+ configurationType: OIDCConfigurationType;
+ tokenEndpoint: string;
+ userinfoEndpoint: string;
+ clientId: string;
+ clientSecret: string;
+ isActive: boolean;
+ orgSlug: string;
+}> &
+ TGenericPermission;
diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts
index 9fece040bc..6b7b3b2b23 100644
--- a/backend/src/ee/services/permission/org-permission.ts
+++ b/backend/src/ee/services/permission/org-permission.ts
@@ -116,7 +116,6 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
- can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index d768066fa7..0e987f41b7 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -42,6 +42,13 @@ export const IDENTITIES = {
},
DELETE: {
identityId: "The ID of the identity to delete."
+ },
+ GET_BY_ID: {
+ identityId: "The ID of the identity to get details.",
+ orgId: "The ID of the org of the identity"
+ },
+ LIST: {
+ orgId: "The ID of the organization to list identities."
}
} as const;
@@ -65,6 +72,9 @@ export const UNIVERSAL_AUTH = {
RETRIEVE: {
identityId: "The ID of the identity to retrieve."
},
+ REVOKE: {
+ identityId: "The ID of the identity to revoke."
+ },
UPDATE: {
identityId: "The ID of the identity to update.",
clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.",
@@ -83,6 +93,10 @@ export const UNIVERSAL_AUTH = {
LIST_CLIENT_SECRETS: {
identityId: "The ID of the identity to list client secrets for."
},
+ GET_CLIENT_SECRET: {
+ identityId: "The ID of the identity to get the client secret from.",
+ clientSecretId: "The ID of the client secret to get details."
+ },
REVOKE_CLIENT_SECRET: {
identityId: "The ID of the identity to revoke the client secret from.",
clientSecretId: "The ID of the client secret to revoke."
@@ -104,6 +118,27 @@ export const AWS_AUTH = {
iamRequestBody:
"The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.",
iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request."
+ },
+ REVOKE: {
+ identityId: "The ID of the identity to revoke."
+ }
+} as const;
+
+export const AZURE_AUTH = {
+ REVOKE: {
+ identityId: "The ID of the identity to revoke."
+ }
+} as const;
+
+export const GCP_AUTH = {
+ REVOKE: {
+ identityId: "The ID of the identity to revoke."
+ }
+} as const;
+
+export const KUBERNETES_AUTH = {
+ REVOKE: {
+ identityId: "The ID of the identity to revoke."
}
} as const;
@@ -347,6 +382,7 @@ export const RAW_SECRETS = {
tagIds: "The ID of the tags to be attached to the created secret."
},
GET: {
+ expand: "Whether or not to expand secret references",
secretName: "The name of the secret to get.",
workspaceId: "The ID of the project to get the secret from.",
workspaceSlug: "The slug of the project to get the secret from.",
@@ -804,6 +840,8 @@ export const CERTIFICATE_AUTHORITIES = {
caId: "The ID of the CA to issue the certificate from",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
+ altNames:
+ "A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts
index 8d7ea656a6..29f7163063 100644
--- a/backend/src/lib/crypto/srp.ts
+++ b/backend/src/lib/crypto/srp.ts
@@ -101,33 +101,51 @@ export const getUserPrivateKey = async (
password: string,
user: Pick<
TUserEncryptionKeys,
- "protectedKeyTag" | "protectedKey" | "protectedKeyIV" | "encryptedPrivateKey" | "iv" | "salt" | "tag"
+ | "protectedKeyTag"
+ | "protectedKey"
+ | "protectedKeyIV"
+ | "encryptedPrivateKey"
+ | "iv"
+ | "salt"
+ | "tag"
+ | "encryptionVersion"
>
) => {
- const derivedKey = await argon2.hash(password, {
- salt: Buffer.from(user.salt),
- memoryCost: 65536,
- timeCost: 3,
- parallelism: 1,
- hashLength: 32,
- type: argon2.argon2id,
- raw: true
- });
- if (!derivedKey) throw new Error("Failed to derive key from password");
- const key = decryptSymmetric128BitHexKeyUTF8({
- ciphertext: user.protectedKey as string,
- iv: user.protectedKeyIV as string,
- tag: user.protectedKeyTag as string,
- key: derivedKey
- });
+ if (user.encryptionVersion === 1) {
+ return decryptSymmetric128BitHexKeyUTF8({
+ ciphertext: user.encryptedPrivateKey,
+ iv: user.iv,
+ tag: user.tag,
+ key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
+ });
+ }
+ if (user.encryptionVersion === 2 && user.protectedKey && user.protectedKeyIV && user.protectedKeyTag) {
+ const derivedKey = await argon2.hash(password, {
+ salt: Buffer.from(user.salt),
+ memoryCost: 65536,
+ timeCost: 3,
+ parallelism: 1,
+ hashLength: 32,
+ type: argon2.argon2id,
+ raw: true
+ });
+ if (!derivedKey) throw new Error("Failed to derive key from password");
+ const key = decryptSymmetric128BitHexKeyUTF8({
+ ciphertext: user.protectedKey,
+ iv: user.protectedKeyIV,
+ tag: user.protectedKeyTag,
+ key: derivedKey
+ });
- const privateKey = decryptSymmetric128BitHexKeyUTF8({
- ciphertext: user.encryptedPrivateKey,
- iv: user.iv,
- tag: user.tag,
- key: Buffer.from(key, "hex")
- });
- return privateKey;
+ const privateKey = decryptSymmetric128BitHexKeyUTF8({
+ ciphertext: user.encryptedPrivateKey,
+ iv: user.iv,
+ tag: user.tag,
+ key: Buffer.from(key, "hex")
+ });
+ return privateKey;
+ }
+ throw new Error(`GetUserPrivateKey: Encryption version not found`);
};
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index f20a622aba..a748396d4b 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -32,6 +32,8 @@ import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-conf
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
import { licenseDALFactory } from "@app/ee/services/license/license-dal";
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
+import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
+import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
@@ -250,6 +252,7 @@ export const registerRoutes = async (
const ldapConfigDAL = ldapConfigDALFactory(db);
const ldapGroupMapDAL = ldapGroupMapDALFactory(db);
+ const oidcConfigDAL = oidcConfigDALFactory(db);
const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db);
const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db);
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
@@ -392,7 +395,9 @@ export const registerRoutes = async (
userDAL,
userAliasDAL,
permissionService,
- licenseService
+ licenseService,
+ tokenService,
+ smtpService
});
const telemetryService = telemetryServiceFactory({
@@ -904,6 +909,19 @@ export const registerRoutes = async (
secretSharingDAL
});
+ const oidcService = oidcConfigServiceFactory({
+ orgDAL,
+ orgMembershipDAL,
+ userDAL,
+ userAliasDAL,
+ licenseService,
+ tokenService,
+ smtpService,
+ orgBotDAL,
+ permissionService,
+ oidcConfigDAL
+ });
+
await superAdminService.initServerCfg();
//
// setup the communication with license key server
@@ -924,6 +942,7 @@ export const registerRoutes = async (
permission: permissionService,
org: orgService,
orgRole: orgRoleService,
+ oidc: oidcService,
apiKey: apiKeyService,
authToken: tokenService,
superAdmin: superAdminService,
diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index 97c3449d9c..24c7e2a6e4 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -22,6 +22,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
200: z.object({
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({
isMigrationModeOn: z.boolean(),
+ defaultAuthOrgSlug: z.string().nullable(),
isSecretScanningDisabled: z.boolean()
})
})
@@ -51,11 +52,15 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
allowSignUp: z.boolean().optional(),
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean().optional(),
- trustLdapEmails: z.boolean().optional()
+ trustLdapEmails: z.boolean().optional(),
+ trustOidcEmails: z.boolean().optional(),
+ defaultAuthOrgId: z.string().optional().nullable()
}),
response: {
200: z.object({
- config: SuperAdminSchema
+ config: SuperAdminSchema.extend({
+ defaultAuthOrgSlug: z.string().nullable()
+ })
})
}
},
diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts
index 7573c0bd29..533f249f2b 100644
--- a/backend/src/server/routes/v1/certificate-authority-router.ts
+++ b/backend/src/server/routes/v1/certificate-authority-router.ts
@@ -9,7 +9,10 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
-import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
+import {
+ validateAltNamesField,
+ validateCaDateField
+} from "@app/services/certificate-authority/certificate-authority-validators";
export const registerCaRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -452,6 +455,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
.object({
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
+ altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts
index f8c0451683..8a85323a60 100644
--- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts
+++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts
@@ -266,4 +266,51 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
return { identityAwsAuth };
}
});
+
+ server.route({
+ method: "DELETE",
+ url: "/aws-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Delete AWS Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(AWS_AUTH.REVOKE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identityAwsAuth: IdentityAwsAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityAwsAuth = await server.services.identityAwsAuth.revokeIdentityAwsAuth({
+ 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: identityAwsAuth.orgId,
+ event: {
+ type: EventType.REVOKE_IDENTITY_AWS_AUTH,
+ metadata: {
+ identityId: identityAwsAuth.identityId
+ }
+ }
+ });
+
+ return { identityAwsAuth };
+ }
+ });
};
diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts
index d10cd131b2..6b4a7fb37d 100644
--- a/backend/src/server/routes/v1/identity-azure-auth-router.ts
+++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts
@@ -2,6 +2,7 @@ import { z } from "zod";
import { IdentityAzureAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { AZURE_AUTH } from "@app/lib/api-docs";
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";
@@ -259,4 +260,51 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
return { identityAzureAuth };
}
});
+
+ server.route({
+ method: "DELETE",
+ url: "/azure-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Delete Azure Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(AZURE_AUTH.REVOKE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identityAzureAuth: IdentityAzureAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityAzureAuth = await server.services.identityAzureAuth.revokeIdentityAzureAuth({
+ 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: identityAzureAuth.orgId,
+ event: {
+ type: EventType.REVOKE_IDENTITY_AZURE_AUTH,
+ metadata: {
+ identityId: identityAzureAuth.identityId
+ }
+ }
+ });
+
+ return { identityAzureAuth };
+ }
+ });
};
diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts
index 34940eb13e..0deeb95d30 100644
--- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts
+++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts
@@ -2,6 +2,7 @@ import { z } from "zod";
import { IdentityGcpAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { GCP_AUTH } from "@app/lib/api-docs";
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";
@@ -265,4 +266,51 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
return { identityGcpAuth };
}
});
+
+ server.route({
+ method: "DELETE",
+ url: "/gcp-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Delete GCP Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(GCP_AUTH.REVOKE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identityGcpAuth: IdentityGcpAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityGcpAuth = await server.services.identityGcpAuth.revokeIdentityGcpAuth({
+ 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: identityGcpAuth.orgId,
+ event: {
+ type: EventType.REVOKE_IDENTITY_GCP_AUTH,
+ metadata: {
+ identityId: identityGcpAuth.identityId
+ }
+ }
+ });
+
+ return { identityGcpAuth };
+ }
+ });
};
diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts
index 2273459166..4c54f1e7cb 100644
--- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts
+++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts
@@ -2,6 +2,7 @@ import { z } from "zod";
import { IdentityKubernetesAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { KUBERNETES_AUTH } from "@app/lib/api-docs";
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";
@@ -280,4 +281,54 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
return { identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.parse(identityKubernetesAuth) };
}
});
+
+ server.route({
+ method: "DELETE",
+ url: "/kubernetes-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Delete Kubernetes Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(KUBERNETES_AUTH.REVOKE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.omit({
+ caCert: true,
+ tokenReviewerJwt: true
+ })
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityKubernetesAuth = await server.services.identityKubernetesAuth.revokeIdentityKubernetesAuth({
+ 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: identityKubernetesAuth.orgId,
+ event: {
+ type: EventType.REVOKE_IDENTITY_KUBERNETES_AUTH,
+ metadata: {
+ identityId: identityKubernetesAuth.identityId
+ }
+ }
+ });
+
+ return { identityKubernetesAuth };
+ }
+ });
};
diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts
index e174cf974a..a425f963e1 100644
--- a/backend/src/server/routes/v1/identity-router.ts
+++ b/backend/src/server/routes/v1/identity-router.ts
@@ -1,9 +1,9 @@
import { z } from "zod";
-import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas";
+import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs";
-import { creationLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -170,4 +170,94 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
return { identity };
}
});
+
+ server.route({
+ method: "GET",
+ url: "/:identityId",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Get an identity by id",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(IDENTITIES.GET_BY_ID.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identity: IdentityOrgMembershipsSchema.extend({
+ customRole: OrgRolesSchema.pick({
+ id: true,
+ name: true,
+ slug: true,
+ permissions: true,
+ description: true
+ }).optional(),
+ identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
+ })
+ })
+ }
+ },
+ handler: async (req) => {
+ const identity = await server.services.identity.getIdentityById({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ id: req.params.identityId
+ });
+
+ return { identity };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "List identities",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ querystring: z.object({
+ orgId: z.string().describe(IDENTITIES.LIST.orgId)
+ }),
+ response: {
+ 200: z.object({
+ identities: IdentityOrgMembershipsSchema.extend({
+ customRole: OrgRolesSchema.pick({
+ id: true,
+ name: true,
+ slug: true,
+ permissions: true,
+ description: true
+ }).optional(),
+ identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
+ }).array()
+ })
+ }
+ },
+ handler: async (req) => {
+ const identities = await server.services.identity.listOrgIdentities({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ orgId: req.query.orgId
+ });
+
+ return { identities };
+ }
+ });
};
diff --git a/backend/src/server/routes/v1/identity-ua.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts
similarity index 81%
rename from backend/src/server/routes/v1/identity-ua.ts
rename to backend/src/server/routes/v1/identity-universal-auth-router.ts
index 670f52416d..b5a63f0dbc 100644
--- a/backend/src/server/routes/v1/identity-ua.ts
+++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts
@@ -134,7 +134,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
- const identityUniversalAuth = await server.services.identityUa.attachUa({
+ const identityUniversalAuth = await server.services.identityUa.attachUniversalAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
@@ -219,7 +219,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
- const identityUniversalAuth = await server.services.identityUa.updateUa({
+ const identityUniversalAuth = await server.services.identityUa.updateUniversalAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
@@ -272,7 +272,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
- const identityUniversalAuth = await server.services.identityUa.getIdentityUa({
+ const identityUniversalAuth = await server.services.identityUa.getIdentityUniversalAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -295,6 +295,53 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
});
+ server.route({
+ method: "DELETE",
+ url: "/universal-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Delete Universal Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(UNIVERSAL_AUTH.REVOKE.identityId)
+ }),
+ response: {
+ 200: z.object({
+ identityUniversalAuth: IdentityUniversalAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityUniversalAuth = await server.services.identityUa.revokeIdentityUniversalAuth({
+ 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: identityUniversalAuth.orgId,
+ event: {
+ type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH,
+ metadata: {
+ identityId: identityUniversalAuth.identityId
+ }
+ }
+ });
+
+ return { identityUniversalAuth };
+ }
+ });
+
server.route({
method: "POST",
url: "/universal-auth/identities/:identityId/client-secrets",
@@ -325,14 +372,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
- const { clientSecret, clientSecretData, orgId } = await server.services.identityUa.createUaClientSecret({
- actor: req.permission.type,
- actorId: req.permission.id,
- actorAuthMethod: req.permission.authMethod,
- actorOrgId: req.permission.orgId,
- identityId: req.params.identityId,
- ...req.body
- });
+ const { clientSecret, clientSecretData, orgId } =
+ await server.services.identityUa.createUniversalAuthClientSecret({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ identityId: req.params.identityId,
+ ...req.body
+ });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
@@ -374,24 +422,76 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
- const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUaClientSecrets({
+ const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUniversalAuthClientSecrets(
+ {
+ 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,
+ event: {
+ type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS,
+ metadata: {
+ identityId: req.params.identityId
+ }
+ }
+ });
+ return { clientSecretData };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Get Universal Auth Client Secret for identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.identityId),
+ clientSecretId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.clientSecretId)
+ }),
+ response: {
+ 200: z.object({
+ clientSecretData: sanitizedClientSecretSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const clientSecretData = await server.services.identityUa.getUniversalAuthClientSecretById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
- identityId: req.params.identityId
+ identityId: req.params.identityId,
+ clientSecretId: req.params.clientSecretId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
- orgId,
+ orgId: clientSecretData.orgId,
event: {
- type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS,
+ type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET,
metadata: {
- identityId: req.params.identityId
+ identityId: clientSecretData.identityId,
+ clientSecretId: clientSecretData.id
}
}
});
+
return { clientSecretData };
}
});
@@ -421,7 +521,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
- const clientSecretData = await server.services.identityUa.revokeUaClientSecret({
+ const clientSecretData = await server.services.identityUa.revokeUniversalAuthClientSecret({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts
index eee7dac65d..c2969b382e 100644
--- a/backend/src/server/routes/v1/index.ts
+++ b/backend/src/server/routes/v1/index.ts
@@ -9,7 +9,7 @@ import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityRouter } from "./identity-router";
-import { registerIdentityUaRouter } from "./identity-ua";
+import { registerIdentityUaRouter } from "./identity-universal-auth-router";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
import { registerIntegrationRouter } from "./integration-router";
import { registerInviteOrgRouter } from "./invite-org-router";
diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts
index e3f1528ade..b92138ec8f 100644
--- a/backend/src/server/routes/v3/secret-router.ts
+++ b/backend/src/server/routes/v3/secret-router.ts
@@ -300,6 +300,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type),
+ expandSecretReferences: z
+ .enum(["true", "false"])
+ .default("false")
+ .transform((value) => value === "true")
+ .describe(RAW_SECRETS.GET.expand),
include_imports: z
.enum(["true", "false"])
.default("false")
@@ -344,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
+ expandSecretReferences: req.query.expandSecretReferences,
environment,
projectId: workspaceId,
projectSlug: workspaceSlug,
diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts
index 29a2a176f8..8090b937ab 100644
--- a/backend/src/services/auth/auth-login-service.ts
+++ b/backend/src/services/auth/auth-login-service.ts
@@ -9,6 +9,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
+import { logger } from "@app/lib/logger";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TTokenDALFactory } from "../auth-token/auth-token-dal";
@@ -201,7 +202,10 @@ export const authLoginServiceFactory = ({
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
authMethod = decodedProviderToken.authMethod;
- if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId) {
+ if (
+ (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
+ decodedProviderToken.orgId
+ ) {
organizationId = decodedProviderToken.orgId;
}
}
@@ -258,7 +262,13 @@ export const authLoginServiceFactory = ({
});
// from password decrypt the private key
if (password) {
- const privateKey = await getUserPrivateKey(password, userEnc);
+ const privateKey = await getUserPrivateKey(password, userEnc).catch((err) => {
+ logger.error(
+ err,
+ `loginExchangeClientProof: private key generation failed for [userId=${user.id}] and [email=${user.email}] `
+ );
+ return "";
+ });
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
@@ -344,9 +354,12 @@ export const authLoginServiceFactory = ({
// Check if the user actually has access to the specified organization.
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId);
+ const selectedOrg = await orgDAL.findById(organizationId);
if (!hasOrganizationMembership) {
- throw new UnauthorizedError({ message: "User does not have access to the organization" });
+ throw new UnauthorizedError({
+ message: `User does not have access to the organization named ${selectedOrg?.name}`
+ });
}
await tokenDAL.incrementTokenSessionVersion(user.id, decodedToken.tokenVersionId);
@@ -571,7 +584,8 @@ export const authLoginServiceFactory = ({
const { authMethod, userName } = decodedProviderToken;
if (!userName) throw new BadRequestError({ message: "Missing user name" });
const organizationId =
- (isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId
+ (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
+ decodedProviderToken.orgId
? decodedProviderToken.orgId
: undefined;
diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts
index 8cf2c9d34f..7ac65c4686 100644
--- a/backend/src/services/auth/auth-signup-service.ts
+++ b/backend/src/services/auth/auth-signup-service.ts
@@ -165,7 +165,8 @@ export const authSignupServiceFactory = ({
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
- tag: encryptedPrivateKeyTag
+ tag: encryptedPrivateKeyTag,
+ encryptionVersion: 2
});
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
const updateduser = await authDAL.transaction(async (tx) => {
@@ -192,7 +193,10 @@ export const authSignupServiceFactory = ({
tx
);
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
- if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && organizationId) {
+ if (
+ (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) &&
+ organizationId
+ ) {
const [pendingOrgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited,
@@ -325,7 +329,8 @@ export const authSignupServiceFactory = ({
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
- tag: encryptedPrivateKeyTag
+ tag: encryptedPrivateKeyTag,
+ encryptionVersion: 2
});
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
const updateduser = await authDAL.transaction(async (tx) => {
diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts
index 8e7b922539..9210093ab7 100644
--- a/backend/src/services/auth/auth-type.ts
+++ b/backend/src/services/auth/auth-type.ts
@@ -8,7 +8,8 @@ export enum AuthMethod {
JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml",
KEYCLOAK_SAML = "keycloak-saml",
- LDAP = "ldap"
+ LDAP = "ldap",
+ OIDC = "oidc"
}
export enum AuthTokenType {
diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts
index 2345180a3f..7d87545e24 100644
--- a/backend/src/services/certificate-authority/certificate-authority-service.ts
+++ b/backend/src/services/certificate-authority/certificate-authority-service.ts
@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import ms from "ms";
+import { z } from "zod";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@@ -38,6 +39,7 @@ import {
TSignIntermediateDTO,
TUpdateCaDTO
} from "./certificate-authority-types";
+import { hostnameRegex } from "./certificate-authority-validators";
type TCertificateAuthorityServiceFactoryDep = {
certificateAuthorityDAL: Pick<
@@ -653,6 +655,7 @@ export const certificateAuthorityServiceFactory = ({
caId,
friendlyName,
commonName,
+ altNames,
ttl,
notBefore,
notAfter,
@@ -738,6 +741,45 @@ export const certificateAuthorityServiceFactory = ({
kmsService
});
+ const extensions: x509.Extension[] = [
+ new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
+ new x509.BasicConstraintsExtension(false),
+ await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
+ await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
+ ];
+
+ if (altNames) {
+ const altNamesArray: {
+ type: "email" | "dns";
+ value: string;
+ }[] = altNames
+ .split(",")
+ .map((name) => name.trim())
+ .map((altName) => {
+ // check if the altName is a valid email
+ if (z.string().email().safeParse(altName).success) {
+ return {
+ type: "email",
+ value: altName
+ };
+ }
+
+ // check if the altName is a valid hostname
+ if (hostnameRegex.test(altName)) {
+ return {
+ type: "dns",
+ value: altName
+ };
+ }
+
+ // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
+ throw new Error(`Invalid altName: ${altName}`);
+ });
+
+ const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
+ extensions.push(altNamesExtension);
+ }
+
const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
@@ -748,12 +790,7 @@ export const certificateAuthorityServiceFactory = ({
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
- extensions: [
- new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
- new x509.BasicConstraintsExtension(false),
- await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
- await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
- ]
+ extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
@@ -771,6 +808,7 @@ export const certificateAuthorityServiceFactory = ({
status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName,
commonName,
+ altNames,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate
diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts
index 3ba7624c0f..8af8b679c6 100644
--- a/backend/src/services/certificate-authority/certificate-authority-types.ts
+++ b/backend/src/services/certificate-authority/certificate-authority-types.ts
@@ -75,6 +75,7 @@ export type TIssueCertFromCaDTO = {
caId: string;
friendlyName?: string;
commonName: string;
+ altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts
index 77bf9ad2fc..a6d6c8c230 100644
--- a/backend/src/services/certificate-authority/certificate-authority-validators.ts
+++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts
@@ -6,3 +6,29 @@ const isValidDate = (dateString: string) => {
};
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
+
+export const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
+export const validateAltNamesField = z
+ .string()
+ .trim()
+ .default("")
+ .transform((data) => {
+ if (data === "") return "";
+ // Trim each alt name and join with ', ' to ensure formatting
+ return data
+ .split(",")
+ .map((id) => id.trim())
+ .join(", ");
+ })
+ .refine(
+ (data) => {
+ if (data === "") return true;
+ // Split and validate each alt name
+ return data.split(", ").every((name) => {
+ return hostnameRegex.test(name) || z.string().email().safeParse(name).success;
+ });
+ },
+ {
+ message: "Each alt name must be a valid hostname or email address"
+ }
+ );
diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts
index a589449093..9cb39aece0 100644
--- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts
+++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts
@@ -7,11 +7,12 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
-import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
+import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
-import { AuthTokenType } from "../auth/auth-type";
+import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
@@ -24,12 +25,13 @@ import {
TGetAwsAuthDTO,
TGetCallerIdentityResponse,
TLoginAwsAuthDTO,
+ TRevokeAwsAuthDTO,
TUpdateAwsAuthDTO
} from "./identity-aws-auth-types";
type TIdentityAwsAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick;
- identityAwsAuthDAL: Pick;
+ identityAwsAuthDAL: Pick;
identityOrgMembershipDAL: Pick;
identityDAL: Pick;
licenseService: Pick;
@@ -301,10 +303,54 @@ export const identityAwsAuthServiceFactory = ({
return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId };
};
+ const revokeIdentityAwsAuth = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TRevokeAwsAuthDTO) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
+ if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
+ throw new BadRequestError({
+ message: "The identity does not have aws auth"
+ });
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
+
+ const { permission: rolePermission } = await permissionService.getOrgPermission(
+ ActorType.IDENTITY,
+ identityMembershipOrg.identityId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
+ if (!hasPriviledge)
+ throw new ForbiddenRequestError({
+ message: "Failed to revoke aws auth of identity with more privileged role"
+ });
+
+ const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
+ const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx);
+ await identityDAL.updateById(identityId, { authMethod: null }, tx);
+ return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId };
+ });
+ return revokedIdentityAwsAuth;
+ };
+
return {
login,
attachAwsAuth,
updateAwsAuth,
- getAwsAuth
+ getAwsAuth,
+ revokeIdentityAwsAuth
};
};
diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts
index e45783ae1d..c24186ee0e 100644
--- a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts
+++ b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts
@@ -52,3 +52,7 @@ export type TGetCallerIdentityResponse = {
ResponseMetadata: { RequestId: string };
};
};
+
+export type TRevokeAwsAuthDTO = {
+ identityId: string;
+} & Omit;
diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts
index fa439bdc00..0d52bf6a05 100644
--- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts
+++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts
@@ -5,11 +5,12 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
-import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
+import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
-import { AuthTokenType } from "../auth/auth-type";
+import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
@@ -20,11 +21,15 @@ import {
TAttachAzureAuthDTO,
TGetAzureAuthDTO,
TLoginAzureAuthDTO,
+ TRevokeAzureAuthDTO,
TUpdateAzureAuthDTO
} from "./identity-azure-auth-types";
type TIdentityAzureAuthServiceFactoryDep = {
- identityAzureAuthDAL: Pick;
+ identityAzureAuthDAL: Pick<
+ TIdentityAzureAuthDALFactory,
+ "findOne" | "transaction" | "create" | "updateById" | "delete"
+ >;
identityOrgMembershipDAL: Pick;
identityAccessTokenDAL: Pick;
identityDAL: Pick;
@@ -277,10 +282,54 @@ export const identityAzureAuthServiceFactory = ({
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
};
+ const revokeIdentityAzureAuth = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TRevokeAzureAuthDTO) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
+ if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH)
+ throw new BadRequestError({
+ message: "The identity does not have azure auth"
+ });
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
+
+ const { permission: rolePermission } = await permissionService.getOrgPermission(
+ ActorType.IDENTITY,
+ identityMembershipOrg.identityId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
+ if (!hasPriviledge)
+ throw new ForbiddenRequestError({
+ message: "Failed to revoke azure auth of identity with more privileged role"
+ });
+
+ const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
+ const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx);
+ await identityDAL.updateById(identityId, { authMethod: null }, tx);
+ return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId };
+ });
+ return revokedIdentityAzureAuth;
+ };
+
return {
login,
attachAzureAuth,
updateAzureAuth,
- getAzureAuth
+ getAzureAuth,
+ revokeIdentityAzureAuth
};
};
diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts
index 65459003c0..ec03451dbc 100644
--- a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts
+++ b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts
@@ -118,3 +118,7 @@ export type TDecodedAzureAuthJwt = {
[key: string]: string;
};
};
+
+export type TRevokeAzureAuthDTO = {
+ identityId: string;
+} & Omit;
diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts
index 5f829cb335..edac7c132f 100644
--- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts
+++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts
@@ -5,11 +5,12 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
-import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
+import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
-import { AuthTokenType } from "../auth/auth-type";
+import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
@@ -21,11 +22,12 @@ import {
TGcpIdentityDetails,
TGetGcpAuthDTO,
TLoginGcpAuthDTO,
+ TRevokeGcpAuthDTO,
TUpdateGcpAuthDTO
} from "./identity-gcp-auth-types";
type TIdentityGcpAuthServiceFactoryDep = {
- identityGcpAuthDAL: Pick;
+ identityGcpAuthDAL: Pick;
identityOrgMembershipDAL: Pick;
identityAccessTokenDAL: Pick;
identityDAL: Pick;
@@ -315,10 +317,54 @@ export const identityGcpAuthServiceFactory = ({
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
};
+ const revokeIdentityGcpAuth = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TRevokeGcpAuthDTO) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
+ if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
+ throw new BadRequestError({
+ message: "The identity does not have gcp auth"
+ });
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
+
+ const { permission: rolePermission } = await permissionService.getOrgPermission(
+ ActorType.IDENTITY,
+ identityMembershipOrg.identityId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
+ if (!hasPriviledge)
+ throw new ForbiddenRequestError({
+ message: "Failed to revoke gcp auth of identity with more privileged role"
+ });
+
+ const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
+ const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx);
+ await identityDAL.updateById(identityId, { authMethod: null }, tx);
+ return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId };
+ });
+ return revokedIdentityGcpAuth;
+ };
+
return {
login,
attachGcpAuth,
updateGcpAuth,
- getGcpAuth
+ getGcpAuth,
+ revokeIdentityGcpAuth
};
};
diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts
index 60ab36b58a..45e64b24b6 100644
--- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts
+++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts
@@ -76,3 +76,7 @@ export type TDecodedGcpIamAuthJwt = {
[key: string]: string;
};
};
+
+export type TRevokeGcpAuthDTO = {
+ identityId: string;
+} & Omit;
diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts
index f1e1c6be0a..820777b465 100644
--- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts
+++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts
@@ -7,6 +7,7 @@ import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate }
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import {
decryptSymmetric,
@@ -16,11 +17,11 @@ import {
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
-import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
+import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
-import { AuthTokenType } from "../auth/auth-type";
+import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
@@ -32,13 +33,14 @@ import {
TCreateTokenReviewResponse,
TGetKubernetesAuthDTO,
TLoginKubernetesAuthDTO,
+ TRevokeKubernetesAuthDTO,
TUpdateKubernetesAuthDTO
} from "./identity-kubernetes-auth-types";
type TIdentityKubernetesAuthServiceFactoryDep = {
identityKubernetesAuthDAL: Pick<
TIdentityKubernetesAuthDALFactory,
- "create" | "findOne" | "transaction" | "updateById"
+ "create" | "findOne" | "transaction" | "updateById" | "delete"
>;
identityAccessTokenDAL: Pick;
identityOrgMembershipDAL: Pick;
@@ -533,10 +535,54 @@ export const identityKubernetesAuthServiceFactory = ({
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
};
+ const revokeIdentityKubernetesAuth = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TRevokeKubernetesAuthDTO) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
+ if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
+ throw new BadRequestError({
+ message: "The identity does not have kubenetes auth"
+ });
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
+
+ const { permission: rolePermission } = await permissionService.getOrgPermission(
+ ActorType.IDENTITY,
+ identityMembershipOrg.identityId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
+ if (!hasPriviledge)
+ throw new ForbiddenRequestError({
+ message: "Failed to revoke kubenetes auth of identity with more privileged role"
+ });
+
+ const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
+ const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx);
+ await identityDAL.updateById(identityId, { authMethod: null }, tx);
+ return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId };
+ });
+ return revokedIdentityKubernetesAuth;
+ };
+
return {
login,
attachKubernetesAuth,
updateKubernetesAuth,
- getKubernetesAuth
+ getKubernetesAuth,
+ revokeIdentityKubernetesAuth
};
};
diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts
index dbb42dce89..f1cde2be9a 100644
--- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts
+++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts
@@ -59,3 +59,7 @@ export type TCreateTokenReviewResponse = {
};
status: TCreateTokenReviewSuccessResponse | TCreateTokenReviewErrorResponse;
};
+
+export type TRevokeKubernetesAuthDTO = {
+ identityId: string;
+} & Omit;
diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts
index 5e940871b5..00dfa5d600 100644
--- a/backend/src/services/identity-ua/identity-ua-service.ts
+++ b/backend/src/services/identity-ua/identity-ua-service.ts
@@ -25,7 +25,9 @@ import {
TCreateUaClientSecretDTO,
TGetUaClientSecretsDTO,
TGetUaDTO,
+ TGetUniversalAuthClientSecretByIdDTO,
TRevokeUaClientSecretDTO,
+ TRevokeUaDTO,
TUpdateUaDTO
} from "./identity-ua-types";
@@ -136,7 +138,7 @@ export const identityUaServiceFactory = ({
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg };
};
- const attachUa = async ({
+ const attachUniversalAuth = async ({
accessTokenMaxTTL,
identityId,
accessTokenNumUsesLimit,
@@ -227,7 +229,7 @@ export const identityUaServiceFactory = ({
return { ...identityUa, orgId: identityMembershipOrg.orgId };
};
- const updateUa = async ({
+ const updateUniversalAuth = async ({
accessTokenMaxTTL,
identityId,
accessTokenNumUsesLimit,
@@ -312,7 +314,7 @@ export const identityUaServiceFactory = ({
return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId };
};
- const getIdentityUa = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
+ const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
@@ -333,7 +335,50 @@ export const identityUaServiceFactory = ({
return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId };
};
- const createUaClientSecret = async ({
+ const revokeIdentityUniversalAuth = async ({
+ identityId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TRevokeUaDTO) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
+ if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
+ throw new BadRequestError({
+ message: "The identity does not have universal auth"
+ });
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
+
+ const { permission: rolePermission } = await permissionService.getOrgPermission(
+ ActorType.IDENTITY,
+ identityMembershipOrg.identityId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
+ if (!hasPriviledge)
+ throw new ForbiddenRequestError({
+ message: "Failed to revoke universal auth of identity with more privileged role"
+ });
+
+ const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
+ const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx);
+ await identityDAL.updateById(identityId, { authMethod: null }, tx);
+ return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId };
+ });
+ return revokedIdentityUniversalAuth;
+ };
+
+ const createUniversalAuthClientSecret = async ({
actor,
actorId,
actorOrgId,
@@ -396,7 +441,7 @@ export const identityUaServiceFactory = ({
};
};
- const getUaClientSecrets = async ({
+ const getUniversalAuthClientSecrets = async ({
actor,
actorId,
actorOrgId,
@@ -442,7 +487,47 @@ export const identityUaServiceFactory = ({
return { clientSecrets, orgId: identityMembershipOrg.orgId };
};
- const revokeUaClientSecret = async ({
+ const getUniversalAuthClientSecretById = async ({
+ identityId,
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ clientSecretId
+ }: TGetUniversalAuthClientSecretByIdDTO) => {
+ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
+ if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
+ if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
+ throw new BadRequestError({
+ message: "The identity does not have universal auth"
+ });
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
+
+ const { permission: rolePermission } = await permissionService.getOrgPermission(
+ ActorType.IDENTITY,
+ identityMembershipOrg.identityId,
+ identityMembershipOrg.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
+ if (!hasPriviledge)
+ throw new ForbiddenRequestError({
+ message: "Failed to read identity client secret of project with more privileged role"
+ });
+
+ const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId);
+ return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId };
+ };
+
+ const revokeUniversalAuthClientSecret = async ({
identityId,
actorId,
actor,
@@ -475,7 +560,7 @@ export const identityUaServiceFactory = ({
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
- message: "Failed to add identity to project with more privileged role"
+ message: "Failed to revoke identity client secret with more privileged role"
});
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
@@ -486,11 +571,13 @@ export const identityUaServiceFactory = ({
return {
login,
- attachUa,
- updateUa,
- getIdentityUa,
- createUaClientSecret,
- getUaClientSecrets,
- revokeUaClientSecret
+ attachUniversalAuth,
+ updateUniversalAuth,
+ getIdentityUniversalAuth,
+ revokeIdentityUniversalAuth,
+ createUniversalAuthClientSecret,
+ getUniversalAuthClientSecrets,
+ revokeUniversalAuthClientSecret,
+ getUniversalAuthClientSecretById
};
};
diff --git a/backend/src/services/identity-ua/identity-ua-types.ts b/backend/src/services/identity-ua/identity-ua-types.ts
index 2cc4762a81..2045c21432 100644
--- a/backend/src/services/identity-ua/identity-ua-types.ts
+++ b/backend/src/services/identity-ua/identity-ua-types.ts
@@ -22,6 +22,10 @@ export type TGetUaDTO = {
identityId: string;
} & Omit;
+export type TRevokeUaDTO = {
+ identityId: string;
+} & Omit;
+
export type TCreateUaClientSecretDTO = {
identityId: string;
description: string;
@@ -37,3 +41,8 @@ export type TRevokeUaClientSecretDTO = {
identityId: string;
clientSecretId: string;
} & Omit;
+
+export type TGetUniversalAuthClientSecretByIdDTO = {
+ identityId: string;
+ clientSecretId: string;
+} & Omit;
diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts
index 95d742f334..0d31997254 100644
--- a/backend/src/services/identity/identity-org-dal.ts
+++ b/backend/src/services/identity/identity-org-dal.ts
@@ -27,10 +27,10 @@ export const identityOrgDALFactory = (db: TDbClient) => {
}
};
- const findByOrgId = async (orgId: string, tx?: Knex) => {
+ const find = async (filter: Partial, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.IdentityOrgMembership)
- .where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
+ .where(filter)
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.select(selectAllTableCols(TableName.IdentityOrgMembership))
@@ -79,5 +79,5 @@ export const identityOrgDALFactory = (db: TDbClient) => {
}
};
- return { ...identityOrgOrm, findOne, findByOrgId };
+ return { ...identityOrgOrm, find, findOne };
};
diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts
index 4659919fa8..f6eccfb99e 100644
--- a/backend/src/services/identity/identity-service.ts
+++ b/backend/src/services/identity/identity-service.ts
@@ -1,6 +1,6 @@
import { ForbiddenError } from "@casl/ability";
-import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
+import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -11,7 +11,7 @@ import { TOrgPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "./identity-dal";
import { TIdentityOrgDALFactory } from "./identity-org-dal";
-import { TCreateIdentityDTO, TDeleteIdentityDTO, TUpdateIdentityDTO } from "./identity-types";
+import { TCreateIdentityDTO, TDeleteIdentityDTO, TGetIdentityByIdDTO, TUpdateIdentityDTO } from "./identity-types";
type TIdentityServiceFactoryDep = {
identityDAL: TIdentityDALFactory;
@@ -138,6 +138,24 @@ export const identityServiceFactory = ({
return { ...identity, orgId: identityOrgMembership.orgId };
};
+ const getIdentityById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetIdentityByIdDTO) => {
+ const doc = await identityOrgMembershipDAL.find({
+ [`${TableName.IdentityOrgMembership}.identityId` as "identityId"]: id
+ });
+ const identity = doc[0];
+ if (!identity) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ identity.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
+ return identity;
+ };
+
const deleteIdentity = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteIdentityDTO) => {
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
@@ -172,7 +190,9 @@ export const identityServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
- const identityMemberships = await identityOrgMembershipDAL.findByOrgId(orgId);
+ const identityMemberships = await identityOrgMembershipDAL.find({
+ [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId
+ });
return identityMemberships;
};
@@ -180,6 +200,7 @@ export const identityServiceFactory = ({
createIdentity,
updateIdentity,
deleteIdentity,
- listOrgIdentities
+ listOrgIdentities,
+ getIdentityById
};
};
diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts
index 10b943667c..5125413e85 100644
--- a/backend/src/services/identity/identity-types.ts
+++ b/backend/src/services/identity/identity-types.ts
@@ -16,6 +16,10 @@ export type TDeleteIdentityDTO = {
id: string;
} & Omit;
+export type TGetIdentityByIdDTO = {
+ id: string;
+} & Omit;
+
export interface TIdentityTrustedIp {
ipAddress: string;
type: IPType;
diff --git a/backend/src/services/secret-blind-index/secret-blind-index-dal.ts b/backend/src/services/secret-blind-index/secret-blind-index-dal.ts
index 8fa60cde7c..825dea3a71 100644
--- a/backend/src/services/secret-blind-index/secret-blind-index-dal.ts
+++ b/backend/src/services/secret-blind-index/secret-blind-index-dal.ts
@@ -30,7 +30,6 @@ export const secretBlindIndexDALFactory = (db: TDbClient) => {
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.Secret}.folderId`)
.leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
.where({ projectId })
- .whereNull("secretBlindIndex")
.select(selectAllTableCols(TableName.Secret))
.select(
db.ref("slug").withSchema(TableName.Environment).as("environment"),
@@ -49,7 +48,6 @@ export const secretBlindIndexDALFactory = (db: TDbClient) => {
.leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
.where({ projectId })
.whereIn(`${TableName.Secret}.id`, secretIds)
- .whereNull("secretBlindIndex")
.select(selectAllTableCols(TableName.Secret))
.select(
db.ref("slug").withSchema(TableName.Environment).as("environment"),
diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts
index a5a469a8f8..cbfccba91c 100644
--- a/backend/src/services/secret/secret-service.ts
+++ b/backend/src/services/secret/secret-service.ts
@@ -1078,6 +1078,7 @@ export const secretServiceFactory = ({
actor,
environment,
projectId: workspaceId,
+ expandSecretReferences,
projectSlug,
actorId,
actorOrgId,
@@ -1091,7 +1092,7 @@ export const secretServiceFactory = ({
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
- const secret = await getSecretByName({
+ const encryptedSecret = await getSecretByName({
actorId,
projectId,
actorAuthMethod,
@@ -1105,7 +1106,46 @@ export const secretServiceFactory = ({
version
});
- return decryptSecretRaw(secret, botKey);
+ const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
+
+ if (expandSecretReferences) {
+ const expandSecrets = interpolateSecrets({
+ folderDAL,
+ projectId,
+ secretDAL,
+ secretEncKey: botKey
+ });
+
+ const expandSingleSecret = async (secret: {
+ secretKey: string;
+ secretValue: string;
+ secretComment?: string;
+ secretPath: string;
+ skipMultilineEncoding: boolean | null | undefined;
+ }) => {
+ const secretRecord: Record<
+ string,
+ { value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
+ > = {
+ [secret.secretKey]: {
+ value: secret.secretValue,
+ comment: secret.secretComment,
+ skipMultilineEncoding: secret.skipMultilineEncoding
+ }
+ };
+
+ await expandSecrets(secretRecord);
+
+ // Update the secret with the expanded value
+ // eslint-disable-next-line no-param-reassign
+ secret.secretValue = secretRecord[secret.secretKey].value;
+ };
+
+ // Expand the secret
+ await expandSingleSecret(decryptedSecret);
+ }
+
+ return decryptedSecret;
};
const createSecretRaw = async ({
diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts
index 1aac324c56..10df2f2583 100644
--- a/backend/src/services/secret/secret-types.ts
+++ b/backend/src/services/secret/secret-types.ts
@@ -151,6 +151,7 @@ export type TGetASecretRawDTO = {
secretName: string;
path: string;
environment: string;
+ expandSecretReferences?: boolean;
type: "shared" | "personal";
includeImports?: boolean;
version?: number;
diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts
index 64133ed7e1..7e707e6fa9 100644
--- a/backend/src/services/super-admin/super-admin-dal.ts
+++ b/backend/src/services/super-admin/super-admin-dal.ts
@@ -1,7 +1,57 @@
+import { Knex } from "knex";
+
import { TDbClient } from "@app/db";
-import { TableName } from "@app/db/schemas";
+import { TableName, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TSuperAdminDALFactory = ReturnType;
-export const superAdminDALFactory = (db: TDbClient) => ormify(db, TableName.SuperAdmin, {});
+export const superAdminDALFactory = (db: TDbClient) => {
+ const superAdminOrm = ormify(db, TableName.SuperAdmin);
+
+ const findById = async (id: string, tx?: Knex) => {
+ const config = await (tx || db)(TableName.SuperAdmin)
+ .where(`${TableName.SuperAdmin}.id`, id)
+ .leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`)
+ .select(
+ db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin,
+ db.ref("slug").withSchema(TableName.Organization).as("defaultAuthOrgSlug")
+ )
+ .first();
+
+ if (!config) {
+ return null;
+ }
+
+ return {
+ ...config,
+ defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null
+ } as TSuperAdmin & { defaultAuthOrgSlug: string | null };
+ };
+
+ const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => {
+ const updatedConfig = await (superAdminOrm || tx).transaction(async (trx: Knex) => {
+ await superAdminOrm.updateById(id, data, trx);
+ const config = await findById(id, trx);
+
+ if (!config) {
+ throw new DatabaseError({
+ error: "Failed to find updated super admin config",
+ message: "Failed to update super admin config",
+ name: "UpdateById"
+ });
+ }
+
+ return config;
+ });
+
+ return updatedConfig;
+ };
+
+ return {
+ ...superAdminOrm,
+ findById,
+ updateById
+ };
+};
diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts
index f1d931b204..41b97efa42 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -25,7 +25,7 @@ type TSuperAdminServiceFactoryDep = {
export type TSuperAdminServiceFactory = ReturnType;
// eslint-disable-next-line
-export let getServerCfg: () => Promise;
+export let getServerCfg: () => Promise;
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
@@ -42,16 +42,20 @@ export const superAdminServiceFactory = ({
// TODO(akhilmhdh): bad pattern time less change this later to me itself
getServerCfg = async () => {
const config = await keyStore.getItem(ADMIN_CONFIG_KEY);
+
// missing in keystore means fetch from db
if (!config) {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
- if (serverCfg) {
- await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore
+
+ if (!serverCfg) {
+ throw new BadRequestError({ name: "Admin config", message: "Admin config not found" });
}
+
+ await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore
return serverCfg;
}
- const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin;
+ const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin & { defaultAuthOrgSlug: string | null };
return {
...keyStoreServerCfg,
// this is to allow admin router to work
@@ -65,14 +69,21 @@ export const superAdminServiceFactory = ({
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg) return;
- // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
- const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true, id: ADMIN_CONFIG_DB_UUID });
+ const newCfg = await serverCfgDAL.create({
+ // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
+ id: ADMIN_CONFIG_DB_UUID,
+ initialized: false,
+ allowSignUp: true,
+ defaultAuthOrgId: null
+ });
return newCfg;
};
const updateServerCfg = async (data: TSuperAdminUpdate) => {
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data);
+
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
+
return updatedServerCfg;
};
@@ -98,6 +109,7 @@ export const superAdminServiceFactory = ({
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" });
const privateKey = await getUserPrivateKey(password, {
+ encryptionVersion: 2,
salt,
protectedKey,
protectedKeyIV,
diff --git a/backend/src/services/user-alias/user-alias-types.ts b/backend/src/services/user-alias/user-alias-types.ts
index 09204644f4..7207e8acf5 100644
--- a/backend/src/services/user-alias/user-alias-types.ts
+++ b/backend/src/services/user-alias/user-alias-types.ts
@@ -1,4 +1,5 @@
export enum UserAliasType {
LDAP = "ldap",
- SAML = "saml"
+ SAML = "saml",
+ OIDC = "oidc"
}
diff --git a/docs/api-reference/endpoints/identities/get-by-id.mdx b/docs/api-reference/endpoints/identities/get-by-id.mdx
new file mode 100644
index 0000000000..f721d3556a
--- /dev/null
+++ b/docs/api-reference/endpoints/identities/get-by-id.mdx
@@ -0,0 +1,5 @@
+---
+title: "Get By ID"
+openapi: "GET /api/v1/identities/{identityId}"
+---
+
diff --git a/docs/api-reference/endpoints/identities/list.mdx b/docs/api-reference/endpoints/identities/list.mdx
new file mode 100644
index 0000000000..d8972e3a91
--- /dev/null
+++ b/docs/api-reference/endpoints/identities/list.mdx
@@ -0,0 +1,4 @@
+---
+title: "List"
+openapi: "GET /api/v1/identities"
+---
diff --git a/docs/api-reference/endpoints/universal-auth/get-client-secret-by-id.mdx b/docs/api-reference/endpoints/universal-auth/get-client-secret-by-id.mdx
new file mode 100644
index 0000000000..477ee875cd
--- /dev/null
+++ b/docs/api-reference/endpoints/universal-auth/get-client-secret-by-id.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get Client Secret By ID"
+openapi: "GET /api/v1/auth/universal-auth/identities/{identityId}/client-secrets/{clientSecretId}"
+---
diff --git a/docs/api-reference/endpoints/universal-auth/revoke.mdx b/docs/api-reference/endpoints/universal-auth/revoke.mdx
new file mode 100644
index 0000000000..e2a19e93c3
--- /dev/null
+++ b/docs/api-reference/endpoints/universal-auth/revoke.mdx
@@ -0,0 +1,4 @@
+---
+title: "Revoke"
+openapi: "DELETE /api/v1/auth/universal-auth/identities/{identityId}"
+---
diff --git a/docs/documentation/getting-started/platform.mdx b/docs/documentation/getting-started/platform.mdx
index 1a1164a40a..7ce96a0ed8 100644
--- a/docs/documentation/getting-started/platform.mdx
+++ b/docs/documentation/getting-started/platform.mdx
@@ -12,14 +12,14 @@ From there, you can invite external members to the organization and start creati
### Projects
The **Projects** page shows you all the projects that you have access to within your organization.
-Here, you can also create a new project.
+Here, you can also create a new project.
![organization overview](../../images/organization-overview.png)
### Members
-The **Members** page lets you add or remove external members to your organization.
-Note that you can configure your organization in Infisical to have members authenticate with the platform via protocols like SAML 2.0.
+The **Members** page lets you add or remove external members to your organization.
+Note that you can configure your organization in Infisical to have members authenticate with the platform via protocols like SAML 2.0 and OpenID Connect.
![organization members](../../images/organization/platform/organization-members.png)
@@ -35,13 +35,14 @@ The **Secrets Overview** screen provides a bird's-eye view of all the secrets in
![dashboard secrets overview](../../images/dashboard-secrets-overview.png)
In the above image, you can already see that:
+
- `STRIPE_API_KEY` is missing from the **Staging** environment.
- `JWT_SECRET` is missing from the **Production** environment.
- `BAR` is `EMPTY` in the **Production** environment.
### Dashboard
-The secrets dashboard lets you manage secrets for a specific environment in a project.
+The secrets dashboard lets you manage secrets for a specific environment in a project.
Here, developers can override secrets, version secrets, rollback projects to any point in time and much more.
![dashboard](../../images/dashboard.png)
@@ -61,4 +62,4 @@ which you can assign to members.
That's it for the platform quickstart! — We encourage you to continue exploring the documentation to gain a deeper understanding of the extensive features and functionalities that Infisical has to offer.
-Next, head back to [Getting Started > Introduction](/documentation/getting-started/overview) to explore ways to fetch secrets from Infisical to your apps and infrastructure.
\ No newline at end of file
+Next, head back to [Getting Started > Introduction](/documentation/getting-started/overview) to explore ways to fetch secrets from Infisical to your apps and infrastructure.
diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx
index 9cc6c4c3d5..2db1834c47 100644
--- a/docs/documentation/platform/identities/machine-identities.mdx
+++ b/docs/documentation/platform/identities/machine-identities.mdx
@@ -26,13 +26,6 @@ A typical workflow for using identities consists of four steps:
3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back.
4. Authenticating subsequent requests with the Infisical API using the short-lived access token.
-
- Currently, identities can only be used to make authenticated requests to the Infisical API, SDKs, Terraform, Kubernetes Operator, and Infisical Agent. They do not work with clients such as CLI, Ansible look up plugin, etc.
-
-Machine Identity support for the rest of the clients is planned to be released in the current quarter.
-
-
-
## Authentication Methods
To interact with various resources in Infisical, Machine Identities are able to authenticate using:
diff --git a/docs/documentation/platform/ldap/overview.mdx b/docs/documentation/platform/ldap/overview.mdx
index 4d6c75e153..4502158d07 100644
--- a/docs/documentation/platform/ldap/overview.mdx
+++ b/docs/documentation/platform/ldap/overview.mdx
@@ -14,8 +14,6 @@ then you should contact sales@infisical.com to purchase an enterprise license to
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol).
-To note, configuring LDAP retains the end-to-end encrypted nature of authentication in Infisical because we decouple the authentication and decryption steps; the LDAP server cannot and will not have access to the decryption key needed to decrypt your secrets.
-
LDAP providers:
- Active Directory
diff --git a/docs/documentation/platform/organization.mdx b/docs/documentation/platform/organization.mdx
index d45bb6d4f8..5e75f1a3b2 100644
--- a/docs/documentation/platform/organization.mdx
+++ b/docs/documentation/platform/organization.mdx
@@ -21,20 +21,19 @@ The **Settings** page lets you manage information about your organization includ
![organization settings general](../../images/platform/organization/organization-settings-general.png)
-
-- Security and Authentication: A set of setting to enforce or manage [SAML](/documentation/platform/sso/overview), [SCIM](/documentation/platform/scim/overview), [LDAP](/documentation/platform/ldap/overview), and other authentication configurations.
+- Security and Authentication: A set of setting to enforce or manage [SAML](/documentation/platform/sso/overview), [OIDC](/documentation/platform/sso/overview), [SCIM](/documentation/platform/scim/overview), [LDAP](/documentation/platform/ldap/overview), and other authentication configurations.
![organization settings auth](../../images/platform/organization/organization-settings-auth.png)
## Access Control
-The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.
+The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.
You can add or remove additional members as well as modify their permissions.
![organization members](../../images/platform/organization/organization-members.png)
![organization identities](../../images/platform/organization/organization-machine-identities.png)
-In the **Organization Roles** tab, you can edit current or create new custom roles for members within the organization.
+In the **Organization Roles** tab, you can edit current or create new custom roles for members within the organization.
Note that Role-Based Access Management (RBAC) is partly a paid feature.
@@ -42,13 +41,14 @@ In the **Organization Roles** tab, you can edit current or create new custom rol
Infisical provides immutable roles like `admin`, `member`, etc.
at the organization and project level for free.
- If you're using Infisical Cloud, the ability to create custom roles is available under the **Pro Tier**.
- If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
+If you're using Infisical Cloud, the ability to create custom roles is available under the **Pro Tier**.
+If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
+
![organization roles](../../images/platform/organization/organization-members-roles.png)
-As you can see next, Infisical supports granular permissions that you can tailor to each role.
+As you can see next, Infisical supports granular permissions that you can tailor to each role.
If you need certain members to only be able to access billing details, for example, then you can
assign them that permission only.
@@ -66,4 +66,4 @@ This includes the following items:
- Receipts: The receipts of monthly/annual invoices.
- Billing: The billing details of your organization including payment methods on file, tax IDs (if applicable), etc.
-![organization usage and billing](../../images/platform/organization/organization-usage-billing.png)
\ No newline at end of file
+![organization usage and billing](../../images/platform/organization/organization-usage-billing.png)
diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx
index fe55466811..7dc3bca880 100644
--- a/docs/documentation/platform/pki/certificates.mdx
+++ b/docs/documentation/platform/pki/certificates.mdx
@@ -56,9 +56,9 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
- Issuing CA: The CA under which to issue the certificate.
- Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty.
- - Common Name (CN): The (common) name of the certificate.
+ - Common Name (CN): The (common) name for the certificate like `service.acme.com`.
+ - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`.
- TTL: The lifetime of the certificate in seconds.
- - Valid Until: The date until which the certificate is valid in the date time string format specified [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format). For example, the following formats would be valid: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `YYYY-MM-DDTHH:mm:ss.sssZ`.
diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx
new file mode 100644
index 0000000000..2b459d5ca3
--- /dev/null
+++ b/docs/documentation/platform/sso/auth0-oidc.mdx
@@ -0,0 +1,66 @@
+---
+title: "Auth0 OIDC"
+description: "Learn how to configure Auth0 OIDC for Infisical SSO."
+---
+
+
+ Auth0 OIDC SSO is a paid feature. If you're using Infisical Cloud, then it is
+ available under the **Pro Tier**. If you're self-hosting Infisical, then you
+ should contact sales@infisical.com to purchase an enterprise license to use
+ it.
+
+
+
+
+ 1.1. From the Application's Page, navigate to the settings tab of the Auth0 application you want to integrate with Infisical.
+ ![OIDC auth0 list of applications](../../../images/sso/auth0-oidc/application-settings.png)
+
+ 1.2. In the Application URIs section, set the **Application Login URI** and **Allowed Web Origins** fields to `https://app.infisical.com` and the **Allowed Callback URL** field to `https://app.infisical.com/api/v1/sso/oidc/callback`.
+ ![OIDC auth0 create application uris](../../../images/sso/auth0-oidc/application-uris.png)
+ ![OIDC auth0 create application origin](../../../images/sso/auth0-oidc/application-origin.png)
+
+ If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain.
+
+
+ Once done, click **Save Changes**.
+
+ 1.3. Proceed to the Connections Tab and enable desired connections.
+ ![OIDC auth0 application connections](../../../images/sso/auth0-oidc/application-connections.png)
+
+
+
+ 2.1. From the application settings page, retrieve the **Client ID** and **Client Secret**
+ ![OIDC auth0 application credential](../../../images/sso/auth0-oidc/application-credential.png)
+
+ 2.2. In the advanced settings (bottom-most section), retrieve the **OpenID Configuration URL** from the Endpoints tab.
+ ![OIDC auth0 application oidc url](../../../images/sso/auth0-oidc/application-urls.png)
+
+ Keep these values handy as we will need them in the next steps.
+
+
+
+ 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Manage**.
+ ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png)
+
+ 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2.
+ ![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png)
+
+ Once you've done that, press **Update** to complete the required configuration.
+
+
+
+ Enabling OIDC allows members in your organization to log into Infisical via Auth0.
+
+ ![OIDC auth0 enable OIDC](../../../images/sso/auth0-oidc/enable-oidc.png)
+
+
+
+
+
+ If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
+ sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
+ work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
+ can be a random 32-byte base64 string generated with `openssl rand -base64
+ 32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should
+ be an absolute URL including the protocol (e.g. https://app.infisical.com)
+
diff --git a/docs/documentation/platform/sso/general-oidc.mdx b/docs/documentation/platform/sso/general-oidc.mdx
new file mode 100644
index 0000000000..ae559cc597
--- /dev/null
+++ b/docs/documentation/platform/sso/general-oidc.mdx
@@ -0,0 +1,69 @@
+---
+title: "General OIDC"
+description: "Learn how to configure OIDC for Infisical SSO with any OIDC-compliant identity provider"
+---
+
+
+ OIDC SSO is a paid feature. If you're using Infisical Cloud, then it is
+ available under the **Pro Tier**. If you're self-hosting Infisical, then you
+ should contact sales@infisical.com to purchase an enterprise license to use
+ it.
+
+
+You can configure your organization in Infisical to have members authenticate with the platform through identity providers via [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html).
+
+Prerequisites:
+
+- The identity provider (Okta, Google, Azure AD, etc.) should support OIDC.
+- Users in the IdP should have a configured `email` and `given_name`.
+
+
+
+ 1.1. Register your application with the IdP to obtain a **Client ID** and **Client Secret**. These credentials are used by Infisical to authenticate with your IdP.
+
+ 1.2. Configure **Redirect URL** to be `https://app.infisical.com/api/v1/sso/oidc/callback`. If you're self-hosting Infisical, replace the domain with your own.
+
+ 1.3. Configure the scopes needed by Infisical (email, profile, openid) and ensure that they are mapped to the ID token claims.
+
+ 1.4. Access the IdP’s OIDC discovery document (usually located at `https:///.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys.
+
+
+ 2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage
+ ![OIDC general manage org Infisical](../../../images/sso/general-oidc/org-oidc-manage.png)
+
+ 2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints.
+
+ To configure OIDC via Discovery URL, set the **Configuration Type** field to **Discovery URL** and fill out the **Discovery Document URL** field.
+
+
+ Note that the Discovery Document URL typically takes the form: `https:///.well-known/openid-configuration`.
+
+
+ ![OIDC general discovery config](../../../images/sso/general-oidc/discovery-oidc-form.png)
+
+ To configure OIDC via the custom endpoints, set the **Configuration Type** field to **Custom** and input the required endpoint fields.
+ ![OIDC general custom config](../../../images/sso/general-oidc/custom-oidc-form.png)
+
+ 2.3. Optionally, you can define a whitelist of allowed email domains.
+
+ Finally, fill out the **Client ID** and **Client Secret** fields and press **Update** to complete the required configuration.
+
+
+
+
+ Enabling OIDC SSO allows members in your organization to log into Infisical via the configured Identity Provider
+
+ ![OIDC general enable OIDC](../../../images/sso/general-oidc/org-oidc-enable.png)
+
+
+
+
+
+
+ If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
+ sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
+ work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
+ can be a random 32-byte base64 string generated with `openssl rand -base64
+ 32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should
+ be an absolute URL including the protocol (e.g. https://app.infisical.com)
+
diff --git a/docs/documentation/platform/sso/keycloak-oidc.mdx b/docs/documentation/platform/sso/keycloak-oidc.mdx
new file mode 100644
index 0000000000..d3818f61ce
--- /dev/null
+++ b/docs/documentation/platform/sso/keycloak-oidc.mdx
@@ -0,0 +1,92 @@
+---
+title: "Keycloak OIDC"
+description: "Learn how to configure Keycloak OIDC for Infisical SSO."
+---
+
+
+ Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it
+ is available under the **Pro Tier**. If you're self-hosting Infisical, then
+ you should contact sales@infisical.com to purchase an enterprise license to
+ use it.
+
+
+
+
+ 1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application.
+
+ ![OIDC keycloak list of clients](../../../images/sso/keycloak-oidc/clients-list.png)
+
+
+ You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm.
+
+
+ 1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**.
+
+ ![OIDC keycloak create client general settings](../../../images/sso/keycloak-oidc/create-client-general-settings.png)
+
+ 1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section.
+
+ ![OIDC keycloak create client capability config settings](../../../images/sso/keycloak-oidc/create-client-capability.png)
+
+ 1.4. In the Login Settings step, set the following values:
+ - Root URL: `https://app.infisical.com`.
+ - Home URL: `https://app.infisical.com`.
+ - Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`.
+ - Web origins: `https://app.infisical.com`.
+
+ ![OIDC keycloak create client login settings](../../../images/sso/keycloak-oidc/create-client-login-settings.png)
+
+ If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain.
+
+
+ 1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope.
+
+ ![OIDC keycloak client scopes list](../../../images/sso/keycloak-oidc/client-scope-list.png)
+
+ 1.6. Next, click **Add predefined mapper**.
+
+ ![OIDC keycloak client mappers empty](../../../images/sso/keycloak-oidc/client-scope-mapper-menu.png)
+
+ 1.7. Select the **email**, **given name**, **family name** attributes and click **Add**.
+
+ ![OIDC keycloak client mappers predefined 1](../../../images/sso/keycloak-oidc/scope-predefined-mapper-1.png)
+ ![OIDC keycloak client mappers predefined 2](../../../images/sso/keycloak-oidc/scope-predefined-mapper-2.png)
+
+ Once you've completed the above steps, the list of mappers should look like the following:
+ ![OIDC keycloak client mappers completed](../../../images/sso/keycloak-oidc/client-scope-complete-overview.png)
+
+
+
+ 2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`.
+ ![OIDC keycloak realm OIDC metadata](../../../images/sso/keycloak-oidc/realm-setting-oidc-config.png)
+
+ 2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps.
+ ![OIDC keycloak realm OIDC secret](../../../images/sso/keycloak-oidc/client-secret.png)
+
+
+
+ 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage
+ ![OIDC keycloak manage org Infisical](../../../images/sso/keycloak-oidc/manage-org-oidc.png)
+
+ 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
+ ![OIDC keycloak paste values into Infisical](../../../images/sso/keycloak-oidc/create-oidc.png)
+
+ Once you've done that, press **Update** to complete the required configuration.
+
+
+
+ Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
+
+ ![OIDC keycloak enable OIDC](../../../images/sso/keycloak-oidc/enable-oidc.png)
+
+
+
+
+
+ If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
+ sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
+ work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
+ can be a random 32-byte base64 string generated with `openssl rand -base64
+ 32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should
+ be an absolute URL including the protocol (e.g. https://app.infisical.com)
+
diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx
index 9ab0acc3ae..4bb45cf48b 100644
--- a/docs/documentation/platform/sso/overview.mdx
+++ b/docs/documentation/platform/sso/overview.mdx
@@ -7,16 +7,14 @@ description: "Learn how to log in to Infisical via SSO protocols."
Infisical offers Google SSO and GitHub SSO for free across both Infisical
Cloud and Infisical Self-hosted. Infisical also offers SAML SSO authentication
- but as paid features that can be unlocked on Infisical Cloud's **Pro** tier or
- via enterprise license on self-hosted instances of Infisical. On this front,
- we support industry-leading providers including Okta, Azure AD, and JumpCloud;
- with any questions, please reach out to team@infisical.com.
+ and OpenID Connect (OIDC) but as paid features that can be unlocked on
+ Infisical Cloud's **Pro** tier or via enterprise license on self-hosted
+ instances of Infisical. On this front, we support industry-leading providers
+ including Okta, Azure AD, and JumpCloud; with any questions, please reach out
+ to team@infisical.com.
-You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
-
-To note, Infisical's SSO implementation decouples the **authentication** and **decryption** steps – which implies that no
-Identity Provider can have access to the decryption key needed to decrypt your secrets (this also implies that Infisical requires entering the user's Master Password on top of authenticating with SSO).
+You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0) or [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html).
## Identity providers
@@ -30,6 +28,9 @@ Infisical supports these and many other identity providers:
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud)
- [Keycloak SAML](/documentation/platform/sso/keycloak-saml)
- [Google SAML](/documentation/platform/sso/google-saml)
+- [Keycloak OIDC](/documentation/platform/sso/keycloak-oidc)
+- [Auth0 OIDC](/documentation/platform/sso/auth0-oidc)
+- [General OIDC](/documentation/platform/sso/general-oidc)
If your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance.
diff --git a/docs/images/integrations/bitbucket/integrations-bitbucket-env.png b/docs/images/integrations/bitbucket/integrations-bitbucket-env.png
new file mode 100644
index 0000000000..8c683a1001
Binary files /dev/null and b/docs/images/integrations/bitbucket/integrations-bitbucket-env.png differ
diff --git a/docs/images/sso/auth0-oidc/application-connections.png b/docs/images/sso/auth0-oidc/application-connections.png
new file mode 100644
index 0000000000..8307f3b248
Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-connections.png differ
diff --git a/docs/images/sso/auth0-oidc/application-credential.png b/docs/images/sso/auth0-oidc/application-credential.png
new file mode 100644
index 0000000000..157d824156
Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-credential.png differ
diff --git a/docs/images/sso/auth0-oidc/application-origin.png b/docs/images/sso/auth0-oidc/application-origin.png
new file mode 100644
index 0000000000..82394c6fd5
Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-origin.png differ
diff --git a/docs/images/sso/auth0-oidc/application-settings.png b/docs/images/sso/auth0-oidc/application-settings.png
new file mode 100644
index 0000000000..5f708ba2af
Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-settings.png differ
diff --git a/docs/images/sso/auth0-oidc/application-uris.png b/docs/images/sso/auth0-oidc/application-uris.png
new file mode 100644
index 0000000000..dadc6ce00e
Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-uris.png differ
diff --git a/docs/images/sso/auth0-oidc/application-urls.png b/docs/images/sso/auth0-oidc/application-urls.png
new file mode 100644
index 0000000000..b467d54c3b
Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-urls.png differ
diff --git a/docs/images/sso/auth0-oidc/enable-oidc.png b/docs/images/sso/auth0-oidc/enable-oidc.png
new file mode 100644
index 0000000000..32682c302e
Binary files /dev/null and b/docs/images/sso/auth0-oidc/enable-oidc.png differ
diff --git a/docs/images/sso/auth0-oidc/org-oidc-overview.png b/docs/images/sso/auth0-oidc/org-oidc-overview.png
new file mode 100644
index 0000000000..ce5fb0d600
Binary files /dev/null and b/docs/images/sso/auth0-oidc/org-oidc-overview.png differ
diff --git a/docs/images/sso/auth0-oidc/org-update-oidc.png b/docs/images/sso/auth0-oidc/org-update-oidc.png
new file mode 100644
index 0000000000..0b9e96b5b8
Binary files /dev/null and b/docs/images/sso/auth0-oidc/org-update-oidc.png differ
diff --git a/docs/images/sso/general-oidc/custom-oidc-form.png b/docs/images/sso/general-oidc/custom-oidc-form.png
new file mode 100644
index 0000000000..2aee026801
Binary files /dev/null and b/docs/images/sso/general-oidc/custom-oidc-form.png differ
diff --git a/docs/images/sso/general-oidc/discovery-oidc-form.png b/docs/images/sso/general-oidc/discovery-oidc-form.png
new file mode 100644
index 0000000000..ae99b35b25
Binary files /dev/null and b/docs/images/sso/general-oidc/discovery-oidc-form.png differ
diff --git a/docs/images/sso/general-oidc/org-oidc-enable.png b/docs/images/sso/general-oidc/org-oidc-enable.png
new file mode 100644
index 0000000000..69f0062bc6
Binary files /dev/null and b/docs/images/sso/general-oidc/org-oidc-enable.png differ
diff --git a/docs/images/sso/general-oidc/org-oidc-manage.png b/docs/images/sso/general-oidc/org-oidc-manage.png
new file mode 100644
index 0000000000..69a60f0f5b
Binary files /dev/null and b/docs/images/sso/general-oidc/org-oidc-manage.png differ
diff --git a/docs/images/sso/keycloak-oidc/client-scope-complete-overview.png b/docs/images/sso/keycloak-oidc/client-scope-complete-overview.png
new file mode 100644
index 0000000000..a0965db0b1
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/client-scope-complete-overview.png differ
diff --git a/docs/images/sso/keycloak-oidc/client-scope-list.png b/docs/images/sso/keycloak-oidc/client-scope-list.png
new file mode 100644
index 0000000000..c35a7691f2
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/client-scope-list.png differ
diff --git a/docs/images/sso/keycloak-oidc/client-scope-mapper-menu.png b/docs/images/sso/keycloak-oidc/client-scope-mapper-menu.png
new file mode 100644
index 0000000000..141bc5dd0b
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/client-scope-mapper-menu.png differ
diff --git a/docs/images/sso/keycloak-oidc/client-secret.png b/docs/images/sso/keycloak-oidc/client-secret.png
new file mode 100644
index 0000000000..c91ddb164b
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/client-secret.png differ
diff --git a/docs/images/sso/keycloak-oidc/clients-list.png b/docs/images/sso/keycloak-oidc/clients-list.png
new file mode 100644
index 0000000000..50e4e49cb9
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/clients-list.png differ
diff --git a/docs/images/sso/keycloak-oidc/create-client-capability.png b/docs/images/sso/keycloak-oidc/create-client-capability.png
new file mode 100644
index 0000000000..72aa508505
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/create-client-capability.png differ
diff --git a/docs/images/sso/keycloak-oidc/create-client-general-settings.png b/docs/images/sso/keycloak-oidc/create-client-general-settings.png
new file mode 100644
index 0000000000..87ec8d8370
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/create-client-general-settings.png differ
diff --git a/docs/images/sso/keycloak-oidc/create-client-login-settings.png b/docs/images/sso/keycloak-oidc/create-client-login-settings.png
new file mode 100644
index 0000000000..1c839f8d46
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/create-client-login-settings.png differ
diff --git a/docs/images/sso/keycloak-oidc/create-oidc.png b/docs/images/sso/keycloak-oidc/create-oidc.png
new file mode 100644
index 0000000000..358af1330a
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/create-oidc.png differ
diff --git a/docs/images/sso/keycloak-oidc/enable-oidc.png b/docs/images/sso/keycloak-oidc/enable-oidc.png
new file mode 100644
index 0000000000..6518319c45
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/enable-oidc.png differ
diff --git a/docs/images/sso/keycloak-oidc/manage-org-oidc.png b/docs/images/sso/keycloak-oidc/manage-org-oidc.png
new file mode 100644
index 0000000000..e6346cd24d
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/manage-org-oidc.png differ
diff --git a/docs/images/sso/keycloak-oidc/realm-setting-oidc-config.png b/docs/images/sso/keycloak-oidc/realm-setting-oidc-config.png
new file mode 100644
index 0000000000..9d3866c7e7
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/realm-setting-oidc-config.png differ
diff --git a/docs/images/sso/keycloak-oidc/scope-predefined-mapper-1.png b/docs/images/sso/keycloak-oidc/scope-predefined-mapper-1.png
new file mode 100644
index 0000000000..8b1cb16c48
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/scope-predefined-mapper-1.png differ
diff --git a/docs/images/sso/keycloak-oidc/scope-predefined-mapper-2.png b/docs/images/sso/keycloak-oidc/scope-predefined-mapper-2.png
new file mode 100644
index 0000000000..189cb34b27
Binary files /dev/null and b/docs/images/sso/keycloak-oidc/scope-predefined-mapper-2.png differ
diff --git a/docs/integrations/cicd/bitbucket.mdx b/docs/integrations/cicd/bitbucket.mdx
index 8f29e43a5f..2aa2106dac 100644
--- a/docs/integrations/cicd/bitbucket.mdx
+++ b/docs/integrations/cicd/bitbucket.mdx
@@ -7,26 +7,62 @@ Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
-
-
- Navigate to your project's integrations tab in Infisical.
+
+
+
+
+ Navigate to your project's integrations tab in Infisical.
- ![integrations](../../images/integrations.png)
+ ![integrations](../../images/integrations.png)
- Press on the Bitbucket tile and grant Infisical access to your Bitbucket account.
+ Press on the Bitbucket tile and grant Infisical access to your Bitbucket account.
- ![integrations bitbucket authorization](../../images/integrations/bitbucket/integrations-bitbucket-auth.png)
+ ![integrations bitbucket authorization](../../images/integrations/bitbucket/integrations-bitbucket-auth.png)
-
- If this is your project's first cloud integration, then you'll have to grant
- Infisical access to your project's environment variables. Although this step
- breaks E2EE, it's necessary for Infisical to sync the environment variables to
- the cloud platform.
-
-
-
- Select which Infisical environment secrets you want to sync to which Bitbucket repo and press start integration to start syncing secrets to the repo.
+
+
+ Select which Infisical environment secrets you want to sync to which Bitbucket repo and press start integration to start syncing secrets to the repo.
- ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png)
-
-
\ No newline at end of file
+ ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png)
+
+
+
+
+
+
+
+ Configure a [Machine Identity](https://infisical.com/docs/documentation/platform/identities/universal-auth) for your project and give it permissions to read secrets from your desired Infisical projects and environments.
+
+
+ Create Bitbucket variables (can be either workspace, repository, or deployment-level) to store Machine Identity Client ID and Client Secret.
+
+ ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket-env.png)
+
+
+ Edit your Bitbucket pipeline YAML file to include the use of the Infisical CLI to fetch and inject secrets into any script or command within the pipeline.
+
+ #### Example
+
+ ```yaml
+ image: atlassian/default-image:3
+
+ pipelines:
+ default:
+ - step:
+ name: Build application with secrets from Infisical
+ script:
+ - apt update && apt install -y curl
+ - curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash
+ - apt-get update && apt-get install -y infisical
+ - export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --silent --plain)
+ - infisical run --projectId=1d0443c1-cd43-4b3a-91a3-9d5f81254a89 --env=dev -- npm run build
+ ```
+
+
+ Set the values of `projectId` and `env` flags in the `infisical run` command to your intended source path. For more options, refer to the CLI command reference [here](https://infisical.com/docs/cli/commands/run).
+
+
+
+
+
+
diff --git a/docs/integrations/cloud/terraform-cloud.mdx b/docs/integrations/cloud/terraform-cloud.mdx
index 3fd70df73f..d68e8a14fd 100644
--- a/docs/integrations/cloud/terraform-cloud.mdx
+++ b/docs/integrations/cloud/terraform-cloud.mdx
@@ -27,12 +27,6 @@ Prerequisites:
![integrations terraform cloud authorization](../../images/integrations/terraform/integrations-terraformcloud-auth.png)
-
- If this is your project's first cloud integration, then you'll have to grant
- Infisical access to your project's environment variables. Although this step
- breaks E2EE, it's necessary for Infisical to sync the environment variables to
- the cloud platform.
-
Select which Infisical environment secrets and Terraform Cloud variable type you want to sync to which Terraform Cloud workspace/project and press create integration to start syncing secrets to Terraform Cloud.
@@ -40,4 +34,4 @@ Prerequisites:
![integrations terraform cloud](../../images/integrations/terraform/integrations-terraformcloud-create.png)
![integrations terraform cloud](../../images/integrations/terraform/integrations-terraformcloud.png)
-
\ No newline at end of file
+
diff --git a/docs/internals/security.mdx b/docs/internals/security.mdx
index 1b0fb9f325..02b6fd9fb1 100644
--- a/docs/internals/security.mdx
+++ b/docs/internals/security.mdx
@@ -79,10 +79,9 @@ Infisical uses AES-256-GCM for symmetric encryption and x25519-xsalsa20-poly1305
By default, Infisical employs a zero-knowledge-first approach to securely storing and sharing secrets.
- Each secret belongs to a project and is symmetrically encrypted by that project's unique key. Each member of a project is shared a copy of the project key, encrypted under their public key, when they are first invited to join the project.
-Since these encryption operations occur on the client-side, the Infisical API is not able to view the value of any secret and the default zero-knowledge property of Infisical is retained; as you'd expect, it follows that decryption operations also occur on the client-side.
+ Since these encryption operations occur on the client-side, the Infisical API is not able to view the value of any secret and the default zero-knowledge property of Infisical is retained; as you'd expect, it follows that decryption operations also occur on the client-side.
- An exception to the zero-knowledge property occurs when a member of a project explicitly shares that project's unique key with Infisical. It is often necessary to share the project key with Infisical in order to use features like native integrations and secret rotation that wouldn't be possible to offer otherwise.
-
## Infrastructure
### High availability
@@ -90,19 +89,22 @@ Since these encryption operations occur on the client-side, the Infisical API is
Infisical Cloud utilizes several strategies to ensure high availability, leveraging AWS services to maintain continuous operation and data integrity.
#### Multi-AZ AWS RDS
-Infisical Cloud uses AWS Relational Database Service (RDS) with Multi-AZ deployments.
-This configuration ensures that the database service is highly available and durable.
-AWS RDS automatically provisions and maintains a synchronous standby replica of the database in a different Availability Zone (AZ).
-This setup facilitates immediate failover to the standby in the event of an AZ failure, thereby ensuring that database operations can continue with minimal interruption.
+
+Infisical Cloud uses AWS Relational Database Service (RDS) with Multi-AZ deployments.
+This configuration ensures that the database service is highly available and durable.
+AWS RDS automatically provisions and maintains a synchronous standby replica of the database in a different Availability Zone (AZ).
+This setup facilitates immediate failover to the standby in the event of an AZ failure, thereby ensuring that database operations can continue with minimal interruption.
The continuous backup and replication to the standby instance safeguard data against loss and ensure its availability even during system failures.
#### Multi-AZ ECS for Container Orchestration
-Infisical Cloud leverages Amazon Elastic Container Service (ECS) in a Multi-AZ configuration for container orchestration.
-This arrangement enables the management and operation of containers across multiple availability zones, increasing the application's fault tolerance.
-Should there be an AZ failure, load is seamlessly sent to an operational AZ, thus minimizing downtime and preserving service availability.
+
+Infisical Cloud leverages Amazon Elastic Container Service (ECS) in a Multi-AZ configuration for container orchestration.
+This arrangement enables the management and operation of containers across multiple availability zones, increasing the application's fault tolerance.
+Should there be an AZ failure, load is seamlessly sent to an operational AZ, thus minimizing downtime and preserving service availability.
#### Standby Regions for Regional Failover
-To fight regional outages, secondary regions are always in standby mode and maintained with up-to-date configurations and data, ready to take over in case the primary region fails.
+
+To fight regional outages, secondary regions are always in standby mode and maintained with up-to-date configurations and data, ready to take over in case the primary region fails.
The standby regions enable a rapid transition and service continuity with minimal disruption in the event of a complete regional failure, ensuring that Infisical Cloud services remain accessible.
### Snapshots
@@ -127,7 +129,7 @@ JWT tokens are stored in browser memory and appended to outbound requests requir
### User authentication
-Infisical supports several authentication methods including email/password, Google SSO, GitHub SSO, and SAML 2.0 (Okta, Azure, JumpCloud); Infisical also currently offers email-based 2FA with authenticator app methods coming in Q1 2024.
+Infisical supports several authentication methods including email/password, Google SSO, GitHub SSO, SAML 2.0 (Okta, Azure, JumpCloud), and OpenID Connect; Infisical also currently offers email-based 2FA with authenticator app methods coming in Q1 2024.
Infisical uses the [secure remote password protocol](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol#:~:text=The%20SRP%20protocol%20has%20a,the%20user%20to%20the%20server), commonly found in other zero-knowledge platform architectures, for authentication.
Put simply, the protocol enables Infisical to validate a user's knowledge of their password without ever seeing it by constructing a mutual secret; we use this protocol because each user's password is used to seed the generation of a master encryption/decryption key via KDF for that user which the platform
@@ -141,6 +143,7 @@ Lastly, Infisical enforces strong password requirements according to the guidanc
to access the platform.
We strongly encourage users to generate and store their passwords / master decryption key in a password manager, such as 1Password, Bitwarden, or Dashlane.
+
## Role-based access control (RBAC)
@@ -172,7 +175,7 @@ Please email security@infisical.com to request any reports including a letter of
Whether or not Infisical or your employees can access data in the Infisical instance and/or storage backend depends on many factors how you use Infisical:
- Infisical Self-Hosted: Self-hosting Infisical is common amongst organizations that prefer to keep data on their own infrastructure usually to adhere to strict regulatory and compliance requirements. In this option, organizations retain full control over their data and therefore govern the data access policy of their Infisical instance and storage backend.
-- Infisical Cloud: Using Infisical's managed service, [Infisical Cloud](https://app.infisical.com) means delegating data oversight and management to Infisical. Under our policy controls, employees are only granted access to parts of infrastructure according to principle of least privilege; this is especially relevant to customer data can only be accessed currently by executive management of Infisical. Moreover, any changes to sensitive customer data is prohibited without explicit customer approval.
+- Infisical Cloud: Using Infisical's managed service, [Infisical Cloud](https://app.infisical.com) means delegating data oversight and management to Infisical. Under our policy controls, employees are only granted access to parts of infrastructure according to principle of least privilege; this is especially relevant to customer data can only be accessed currently by executive management of Infisical. Moreover, any changes to sensitive customer data is prohibited without explicit customer approval.
It should be noted that, even on Infisical Cloud, it is physically impossible for employees of Infisical to view the values of secrets if users have not explicitly granted Infisical access to their project (i.e. opted out of zero-knowledge).
diff --git a/docs/mint.json b/docs/mint.json
index 1caad07156..ac2a536c0a 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -178,7 +178,10 @@
"documentation/platform/sso/azure",
"documentation/platform/sso/jumpcloud",
"documentation/platform/sso/keycloak-saml",
- "documentation/platform/sso/google-saml"
+ "documentation/platform/sso/google-saml",
+ "documentation/platform/sso/keycloak-oidc",
+ "documentation/platform/sso/auth0-oidc",
+ "documentation/platform/sso/general-oidc"
]
},
{
@@ -416,7 +419,9 @@
"pages": [
"api-reference/endpoints/identities/create",
"api-reference/endpoints/identities/update",
- "api-reference/endpoints/identities/delete"
+ "api-reference/endpoints/identities/delete",
+ "api-reference/endpoints/identities/get-by-id",
+ "api-reference/endpoints/identities/list"
]
},
{
@@ -426,9 +431,11 @@
"api-reference/endpoints/universal-auth/attach",
"api-reference/endpoints/universal-auth/retrieve",
"api-reference/endpoints/universal-auth/update",
+ "api-reference/endpoints/universal-auth/revoke",
"api-reference/endpoints/universal-auth/create-client-secret",
"api-reference/endpoints/universal-auth/list-client-secrets",
"api-reference/endpoints/universal-auth/revoke-client-secret",
+ "api-reference/endpoints/universal-auth/get-client-secret-by-id",
"api-reference/endpoints/universal-auth/renew-access-token",
"api-reference/endpoints/universal-auth/revoke-access-token"
]
@@ -638,5 +645,10 @@
],
"integrations": {
"intercom": "hsg644ru"
+ },
+ "analytics": {
+ "koala": {
+ "publicApiKey": "pk_b50d7184e0e39ddd5cdb43cf6abeadd9b97d"
+ }
}
}
diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx
index 578e8ad0f3..3f779f1871 100644
--- a/docs/sdks/overview.mdx
+++ b/docs/sdks/overview.mdx
@@ -19,6 +19,9 @@ From local development to production, Infisical SDKs provide the easiest way for
Manage secrets for your Java application on demand
+
+ Manage secrets for your Go application on demand
+
Manage secrets for your C#/.NET application on demand
@@ -43,7 +46,4 @@ From local development to production, Infisical SDKs provide the easiest way for
Note: The exact parameter name may differ depending on the language.
-
- The SDK caches every secret and falls back to the cached value if a request fails. If no cached value is found, and the request fails, then the SDK throws an error.
-
diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx
index b225a3ace6..754409dfeb 100644
--- a/docs/self-hosting/configuration/envars.mdx
+++ b/docs/self-hosting/configuration/envars.mdx
@@ -25,6 +25,10 @@ Used to configure platform-specific security and operational settings
https://app.infisical.com).
+
+ Telemetry helps us improve Infisical but if you want to dsiable it you may set this to `false`.
+
+
## Data Layer
The platform utilizes Postgres to persist all of its data and Redis for caching and backgroud tasks
diff --git a/docs/style.css b/docs/style.css
index 3359151e46..4d9877c6cc 100644
--- a/docs/style.css
+++ b/docs/style.css
@@ -10,7 +10,6 @@
#sidebar {
left: 0;
- padding-left: 48px;
padding-right: 30px;
border-right: 1px;
border-color: #cdd64b;
@@ -18,6 +17,10 @@
border-right: 1px solid #ebebeb;
}
+#sidebar-content {
+ padding-left: 2rem;
+}
+
#sidebar .relative .sticky {
opacity: 0;
}
@@ -154,4 +157,4 @@
.flex-1 .flex .items-center {
/* background-color: #f5f5f5; */
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/dashboard/DropZone.tsx b/frontend/src/components/dashboard/DropZone.tsx
index ba0b9efdba..b196e8cd4b 100644
--- a/frontend/src/components/dashboard/DropZone.tsx
+++ b/frontend/src/components/dashboard/DropZone.tsx
@@ -6,6 +6,8 @@ import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { parseDocument, Scalar, YAMLMap } from "yaml";
+import { SecretType } from "@app/hooks/api/types";
+
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import { createNotification } from "../notifications";
@@ -33,7 +35,6 @@ const DropZone = ({
numCurrentRows
}: DropZoneProps) => {
const { t } = useTranslation();
-
const handleDragEnter = (e: DragEvent) => {
e.preventDefault();
@@ -66,7 +67,7 @@ const DropZone = ({
key,
value: keyPairs[key as keyof typeof keyPairs].value,
comment: keyPairs[key as keyof typeof keyPairs].comments.join("\n"),
- type: "shared",
+ type: SecretType.Shared,
tags: []
}));
break;
@@ -79,7 +80,7 @@ const DropZone = ({
key,
value: keyPairs[key as keyof typeof keyPairs],
comment: "",
- type: "shared",
+ type: SecretType.Shared,
tags: []
}));
break;
@@ -102,7 +103,7 @@ const DropZone = ({
key,
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? "",
comment,
- type: "shared",
+ type: SecretType.Shared,
tags: []
};
});
@@ -132,7 +133,7 @@ const DropZone = ({
if (file === undefined) {
createNotification({
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
- type: "error",
+ type: "error"
});
setLoading(false);
return;
diff --git a/frontend/src/components/notifications/Notifications.tsx b/frontend/src/components/notifications/Notifications.tsx
index 0d6b0d0615..befe79e4f3 100644
--- a/frontend/src/components/notifications/Notifications.tsx
+++ b/frontend/src/components/notifications/Notifications.tsx
@@ -26,4 +26,4 @@ export const createNotification = (
type: myProps?.type || "info",
});
-export const NotificationContainer = () => ;
+export const NotificationContainer = () => ;
diff --git a/frontend/src/components/utilities/secrets/checkOverrides.ts b/frontend/src/components/utilities/secrets/checkOverrides.ts
index 84ba0bbb7a..7f311a2529 100644
--- a/frontend/src/components/utilities/secrets/checkOverrides.ts
+++ b/frontend/src/components/utilities/secrets/checkOverrides.ts
@@ -1,5 +1,7 @@
import { SecretDataProps } from "public/data/frequentInterfaces";
+import { SecretType } from "@app/hooks/api/types";
+
/**
* This function downloads the secrets as a .env file
* @param {object} obj
@@ -10,8 +12,8 @@ const checkOverrides = async ({ data }: { data: SecretDataProps[] }) => {
let secrets: SecretDataProps[] = data!.map((secret) => Object.create(secret));
const overridenSecrets = data!.filter((secret) =>
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
- ? "shared"
- : "personal"
+ ? SecretType.Shared
+ : SecretType.Personal
);
if (overridenSecrets.length) {
overridenSecrets.forEach((secret) => {
diff --git a/frontend/src/components/utilities/secrets/encryptSecrets.ts b/frontend/src/components/utilities/secrets/encryptSecrets.ts
index 943502cdfb..4eb59ae9ff 100644
--- a/frontend/src/components/utilities/secrets/encryptSecrets.ts
+++ b/frontend/src/components/utilities/secrets/encryptSecrets.ts
@@ -3,6 +3,7 @@ import crypto from "crypto";
import { SecretDataProps, Tag } from "public/data/frequentInterfaces";
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
+import { SecretType } from "@app/hooks/api/types";
import { decryptAssymmetric, encryptSymmetric } from "../cryptography/crypto";
@@ -20,7 +21,7 @@ interface EncryptedSecretProps {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
- type: "personal" | "shared";
+ type: SecretType;
tags: Tag[];
}
@@ -108,8 +109,8 @@ const encryptSecrets = async ({
secretCommentTag,
type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
- ? "shared"
- : "personal",
+ ? SecretType.Shared
+ : SecretType.Personal,
tags: secret.tags
};
diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx
index 5e7bbef786..4ba089c462 100644
--- a/frontend/src/components/v2/SecretInput/SecretInput.tsx
+++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx
@@ -15,7 +15,7 @@ const replaceContentWithDot = (str: string) => {
};
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
- if (isImport) return "IMPORTED";
+ if (isImport && !content) return "IMPORTED";
if (content === "") return "EMPTY";
if (!content) return "EMPTY";
if (!isVisible) return replaceContentWithDot(content);
diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx
index 29dba23c72..015b16f98c 100644
--- a/frontend/src/components/v2/Select/Select.tsx
+++ b/frontend/src/components/v2/Select/Select.tsx
@@ -36,61 +36,73 @@ export const Select = forwardRef(
ref
): JSX.Element => {
return (
-
-
-
- {props.icon ? : placeholder}
-
+
+
{
+ if (!props.onValueChange) return;
-
-
-
-
-
-
+
-
-
-
-
-
-
- {isLoading ? (
+
+ {props.icon && }
+
+
+
+
+
+
+
+
+
+
-
- Loading...
+
- ) : (
- children
- )}
-
-
-
-
-
-
-
-
-
+
+
+ {isLoading ? (
+
+
+ Loading...
+
+ ) : (
+ children
+ )}
+
+
+
+
+
+
+
+
+
+
);
}
);
@@ -114,7 +126,7 @@ export const SelectItem = forwardRef(
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary",
isDisabled &&
- "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
+ "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
className
)}
ref={forwardedRef}
@@ -129,3 +141,45 @@ export const SelectItem = forwardRef(
);
SelectItem.displayName = "SelectItem";
+
+export type SelectClearProps = Omit & {
+ onClear: () => void;
+ selectValue: string;
+};
+
+export const SelectClear = forwardRef(
+ (
+ { children, className, isSelected, isDisabled, onClear, selectValue, ...props },
+ forwardedRef
+ ) => {
+ return (
+ onClear()}
+ onClick={() => onClear()}
+ className={twMerge(
+ `relative mb-0.5 flex
+ cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
+ outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
+ isSelected && "bg-primary",
+ isDisabled &&
+ "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
+ className
+ )}
+ ref={forwardedRef}
+ >
+
+
+
+ {children}
+
+ );
+ }
+);
+SelectClear.displayName = "SelectClear";
diff --git a/frontend/src/components/v2/Select/index.tsx b/frontend/src/components/v2/Select/index.tsx
index 6a783605ab..3765851d5c 100644
--- a/frontend/src/components/v2/Select/index.tsx
+++ b/frontend/src/components/v2/Select/index.tsx
@@ -1,2 +1,2 @@
export type { SelectItemProps, SelectProps } from "./Select";
-export { Select, SelectItem } from "./Select";
+export { Select, SelectClear, SelectItem } from "./Select";
diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts
index 8d5f59ede3..524bc6ace0 100644
--- a/frontend/src/hooks/api/admin/types.ts
+++ b/frontend/src/hooks/api/admin/types.ts
@@ -5,7 +5,10 @@ export type TServerConfig = {
isMigrationModeOn?: boolean;
trustSamlEmails: boolean;
trustLdapEmails: boolean;
+ trustOidcEmails: boolean;
isSecretScanningDisabled: boolean;
+ defaultAuthOrgSlug: string | null;
+ defaultAuthOrgId: string | null;
};
export type TCreateAdminUserDTO = {
diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts
index 64511253bd..7cb5dbf429 100644
--- a/frontend/src/hooks/api/ca/types.ts
+++ b/frontend/src/hooks/api/ca/types.ts
@@ -81,6 +81,7 @@ export type TCreateCertificateDTO = {
caId: string;
friendlyName?: string;
commonName: string;
+ altNames: string; // sans
ttl: string; // string compatible with ms
notBefore?: string;
notAfter?: string;
diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts
index 2dd6001877..d1ba46910a 100644
--- a/frontend/src/hooks/api/certificates/types.ts
+++ b/frontend/src/hooks/api/certificates/types.ts
@@ -6,6 +6,7 @@ export type TCertificate = {
status: CertStatus;
friendlyName: string;
commonName: string;
+ altNames: string;
serialNumber: string;
notBefore: string;
notAfter: string;
diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx
index b87c3f13f5..7e19ece330 100644
--- a/frontend/src/hooks/api/index.tsx
+++ b/frontend/src/hooks/api/index.tsx
@@ -17,6 +17,7 @@ export * from "./integrationAuth";
export * from "./integrations";
export * from "./keys";
export * from "./ldapConfig";
+export * from "./oidcConfig";
export * from "./organization";
export * from "./projectUserAdditionalPrivilege";
export * from "./rateLimit";
diff --git a/frontend/src/hooks/api/oidcConfig/index.tsx b/frontend/src/hooks/api/oidcConfig/index.tsx
new file mode 100644
index 0000000000..b69c251208
--- /dev/null
+++ b/frontend/src/hooks/api/oidcConfig/index.tsx
@@ -0,0 +1 @@
+export * from "./queries";
diff --git a/frontend/src/hooks/api/oidcConfig/mutations.tsx b/frontend/src/hooks/api/oidcConfig/mutations.tsx
new file mode 100644
index 0000000000..4a0d963cc1
--- /dev/null
+++ b/frontend/src/hooks/api/oidcConfig/mutations.tsx
@@ -0,0 +1,111 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { oidcConfigKeys } from "./queries";
+
+export const useUpdateOIDCConfig = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ issuer,
+ authorizationEndpoint,
+ configurationType,
+ discoveryURL,
+ jwksUri,
+ tokenEndpoint,
+ userinfoEndpoint,
+ allowedEmailDomains,
+ clientId,
+ clientSecret,
+ isActive,
+ orgSlug
+ }: {
+ allowedEmailDomains?: string;
+ issuer?: string;
+ authorizationEndpoint?: string;
+ discoveryURL?: string;
+ jwksUri?: string;
+ tokenEndpoint?: string;
+ userinfoEndpoint?: string;
+ clientId?: string;
+ clientSecret?: string;
+ isActive?: boolean;
+ configurationType?: string;
+ orgSlug: string;
+ }) => {
+ const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
+ issuer,
+ allowedEmailDomains,
+ authorizationEndpoint,
+ discoveryURL,
+ configurationType,
+ jwksUri,
+ tokenEndpoint,
+ userinfoEndpoint,
+ clientId,
+ orgSlug,
+ clientSecret,
+ isActive
+ });
+
+ return data;
+ },
+ onSuccess(_, dto) {
+ queryClient.invalidateQueries(oidcConfigKeys.getOIDCConfig(dto.orgSlug));
+ }
+ });
+};
+
+export const useCreateOIDCConfig = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ issuer,
+ configurationType,
+ discoveryURL,
+ authorizationEndpoint,
+ allowedEmailDomains,
+ jwksUri,
+ tokenEndpoint,
+ userinfoEndpoint,
+ clientId,
+ clientSecret,
+ isActive,
+ orgSlug
+ }: {
+ issuer?: string;
+ configurationType: string;
+ discoveryURL?: string;
+ authorizationEndpoint?: string;
+ jwksUri?: string;
+ tokenEndpoint?: string;
+ userinfoEndpoint?: string;
+ clientId: string;
+ clientSecret: string;
+ isActive: boolean;
+ orgSlug: string;
+ allowedEmailDomains?: string;
+ }) => {
+ const { data } = await apiRequest.post("/api/v1/sso/oidc/config", {
+ issuer,
+ configurationType,
+ discoveryURL,
+ authorizationEndpoint,
+ allowedEmailDomains,
+ jwksUri,
+ tokenEndpoint,
+ userinfoEndpoint,
+ clientId,
+ clientSecret,
+ isActive,
+ orgSlug
+ });
+
+ return data;
+ },
+ onSuccess(_, dto) {
+ queryClient.invalidateQueries(oidcConfigKeys.getOIDCConfig(dto.orgSlug));
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/oidcConfig/queries.tsx b/frontend/src/hooks/api/oidcConfig/queries.tsx
new file mode 100644
index 0000000000..08b38cbb88
--- /dev/null
+++ b/frontend/src/hooks/api/oidcConfig/queries.tsx
@@ -0,0 +1,23 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { OIDCConfigData } from "./types";
+
+export const oidcConfigKeys = {
+ getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const
+};
+
+export const useGetOIDCConfig = (orgSlug: string) => {
+ return useQuery({
+ queryKey: oidcConfigKeys.getOIDCConfig(orgSlug),
+ queryFn: async () => {
+ const { data } = await apiRequest.get(
+ `/api/v1/sso/oidc/config?orgSlug=${orgSlug}`
+ );
+
+ return data;
+ },
+ enabled: true
+ });
+};
diff --git a/frontend/src/hooks/api/oidcConfig/types.ts b/frontend/src/hooks/api/oidcConfig/types.ts
new file mode 100644
index 0000000000..1b8ede5e3e
--- /dev/null
+++ b/frontend/src/hooks/api/oidcConfig/types.ts
@@ -0,0 +1,15 @@
+export type OIDCConfigData = {
+ id: string;
+ issuer: string;
+ authorizationEndpoint: string;
+ configurationType: string;
+ discoveryURL: string;
+ jwksUri: string;
+ tokenEndpoint: string;
+ userinfoEndpoint: string;
+ isActive: boolean;
+ orgId: string;
+ clientId: string;
+ clientSecret: string;
+ allowedEmailDomains?: string;
+};
diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx
index 701a205435..a7c5106622 100644
--- a/frontend/src/hooks/api/secretImports/queries.tsx
+++ b/frontend/src/hooks/api/secretImports/queries.tsx
@@ -279,7 +279,28 @@ export const useGetImportedSecretsAllEnvs = ({
[(secretImports || []).map((response) => response.data)]
);
- return { secretImports, isImportedSecretPresentInEnv };
+ const getImportedSecretByKey = useCallback(
+ (envSlug: string, secretName: string) => {
+ const selectedEnvIndex = environments.indexOf(envSlug);
+
+ if (selectedEnvIndex !== -1) {
+ const secret = secretImports?.[selectedEnvIndex]?.data?.find(({ secrets }) =>
+ secrets.find((s) => s.key === secretName)
+ );
+
+ if (!secret) return undefined;
+
+ return {
+ secret: secret?.secrets.find((s) => s.key === secretName),
+ environmentInfo: secret?.environmentInfo
+ };
+ }
+ return undefined;
+ },
+ [(secretImports || []).map((response) => response.data)]
+ );
+
+ return { secretImports, isImportedSecretPresentInEnv, getImportedSecretByKey };
};
export const useGetImportedFoldersByEnv = ({
diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx
index ca1ec76fd4..cd81152a13 100644
--- a/frontend/src/hooks/api/secretSnapshots/queries.tsx
+++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx
@@ -7,7 +7,7 @@ import {
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
-import { DecryptedSecret } from "../secrets/types";
+import { DecryptedSecret, SecretType } from "../secrets/types";
import {
TGetSecretSnapshotsDTO,
TSecretRollbackDTO,
@@ -112,7 +112,7 @@ export const useGetSnapshotSecrets = ({ decryptFileKey, snapshotId }: TSnapshotD
version: encSecret.version
};
- if (encSecret.type === "personal") {
+ if (encSecret.type === SecretType.Personal) {
personalSecrets[decryptedSecret.key] = { id: encSecret.secretId, value: secretValue };
} else {
sharedSecrets.push(decryptedSecret);
diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx
index 28999389e9..382d01785b 100644
--- a/frontend/src/hooks/api/secrets/queries.tsx
+++ b/frontend/src/hooks/api/secrets/queries.tsx
@@ -14,6 +14,7 @@ import {
EncryptedSecret,
EncryptedSecretVersion,
GetSecretVersionsDTO,
+ SecretType,
TGetProjectSecretsAllEnvDTO,
TGetProjectSecretsDTO,
TGetProjectSecretsKey
@@ -77,7 +78,7 @@ export const decryptSecrets = (
skipMultilineEncoding: encSecret.skipMultilineEncoding
};
- if (encSecret.type === "personal") {
+ if (encSecret.type === SecretType.Personal) {
personalSecrets[decryptedSecret.key] = {
id: encSecret.id,
value: secretValue
diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts
index f36872e43a..378405c967 100644
--- a/frontend/src/hooks/api/secrets/types.ts
+++ b/frontend/src/hooks/api/secrets/types.ts
@@ -1,11 +1,16 @@
import type { UserWsKeyPair } from "../keys/types";
import type { WsTag } from "../tags/types";
+export enum SecretType {
+ Shared = "shared",
+ Personal = "personal"
+}
+
export type EncryptedSecret = {
id: string;
version: number;
workspace: string;
- type: "shared" | "personal";
+ type: SecretType;
environment: string;
secretKeyCiphertext: string;
secretKeyIV: string;
@@ -49,7 +54,7 @@ export type EncryptedSecretVersion = {
secretId: string;
version: number;
workspace: string;
- type: string;
+ type: SecretType;
isDeleted: boolean;
envId: string;
secretKeyCiphertext: string;
@@ -101,14 +106,14 @@ export type TCreateSecretsV3DTO = {
secretPath: string;
workspaceId: string;
environment: string;
- type: string;
+ type: SecretType;
};
export type TUpdateSecretsV3DTO = {
latestFileKey: UserWsKeyPair;
workspaceId: string;
environment: string;
- type: string;
+ type: SecretType;
secretPath: string;
skipMultilineEncoding?: boolean;
newSecretName?: string;
@@ -124,7 +129,7 @@ export type TUpdateSecretsV3DTO = {
export type TDeleteSecretsV3DTO = {
workspaceId: string;
environment: string;
- type: "shared" | "personal";
+ type: SecretType;
secretPath: string;
secretName: string;
secretId?: string;
@@ -140,7 +145,7 @@ export type TCreateSecretBatchDTO = {
secretValue: string;
secretComment: string;
skipMultilineEncoding?: boolean;
- type: "shared" | "personal";
+ type: SecretType;
metadata?: {
source?: string;
};
@@ -153,7 +158,7 @@ export type TUpdateSecretBatchDTO = {
secretPath: string;
latestFileKey: UserWsKeyPair;
secrets: Array<{
- type: "shared" | "personal";
+ type: SecretType;
secretName: string;
skipMultilineEncoding?: boolean;
secretValue: string;
@@ -168,14 +173,14 @@ export type TDeleteSecretBatchDTO = {
secretPath: string;
secrets: Array<{
secretName: string;
- type: "shared" | "personal";
+ type: SecretType;
}>;
};
export type CreateSecretDTO = {
workspaceId: string;
environment: string;
- type: "shared" | "personal";
+ type: SecretType;
secretKey: string;
secretKeyCiphertext: string;
secretKeyIV: string;
diff --git a/frontend/src/hooks/api/serverDetails/types.ts b/frontend/src/hooks/api/serverDetails/types.ts
index 911526404c..3e22c2684b 100644
--- a/frontend/src/hooks/api/serverDetails/types.ts
+++ b/frontend/src/hooks/api/serverDetails/types.ts
@@ -4,5 +4,5 @@ export type ServerStatus = {
emailConfigured: boolean;
secretScanningConfigured: boolean;
redisConfigured: boolean;
- samlDefaultOrgSlug: boolean
+ samlDefaultOrgSlug: string;
};
diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts
index 3d4a6dc36b..89635a9533 100644
--- a/frontend/src/hooks/api/subscriptions/types.ts
+++ b/frontend/src/hooks/api/subscriptions/types.ts
@@ -23,6 +23,7 @@ export type SubscriptionPlan = {
workspacesUsed: number;
environmentLimit: number;
samlSSO: boolean;
+ oidcSSO: boolean;
scim: boolean;
ldap: boolean;
groups: boolean;
diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts
index 649af434cf..43e4085716 100644
--- a/frontend/src/hooks/api/users/types.ts
+++ b/frontend/src/hooks/api/users/types.ts
@@ -29,7 +29,8 @@ export type User = {
export enum UserAliasType {
LDAP = "ldap",
- SAML = "saml"
+ SAML = "saml",
+ OIDC = "oidc"
}
export type UserEnc = {
diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx
index d560102783..1b6473bfe2 100644
--- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx
+++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx
@@ -297,6 +297,7 @@ export const IntegrationsSection = ({
(popUp?.deleteConfirmation?.data as TIntegration)?.app ||
(popUp?.deleteConfirmation?.data as TIntegration)?.owner ||
(popUp?.deleteConfirmation?.data as TIntegration)?.path ||
+ (popUp?.deleteConfirmation?.data as TIntegration)?.integration ||
""
}
onDeleteApproved={async () =>
diff --git a/frontend/src/views/Login/Login.tsx b/frontend/src/views/Login/Login.tsx
index 04a24d233c..36cd355f52 100644
--- a/frontend/src/views/Login/Login.tsx
+++ b/frontend/src/views/Login/Login.tsx
@@ -1,16 +1,15 @@
import { useEffect, useState } from "react";
-import { useRouter } from "next/router";
import { isLoggedIn } from "@app/reactQuery";
-import { InitialStep, MFAStep, SAMLSSOStep } from "./components";
-import { navigateUserToSelectOrg } from "./Login.utils";
+import { InitialStep, MFAStep, SSOStep } from "./components";
+import { useNavigateToSelectOrganization } from "./Login.utils";
export const Login = () => {
- const router = useRouter();
const [step, setStep] = useState(0);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
+ const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
const queryParams = new URLSearchParams(window.location.search);
@@ -21,10 +20,10 @@ export const Login = () => {
const callbackPort = queryParams?.get("callback_port");
// case: a callback port is set, meaning it's a cli login request: redirect to select org with callback port
if (callbackPort) {
- navigateUserToSelectOrg(router, callbackPort);
+ navigateToSelectOrganization(callbackPort);
} else {
// case: no callback port, meaning it's a regular login request: redirect to select org
- navigateUserToSelectOrg(router);
+ navigateToSelectOrganization();
}
} catch (error) {
console.log("Error - Not logged in yet");
@@ -57,7 +56,9 @@ export const Login = () => {
/>
);
case 2:
- return ;
+ return ;
+ case 3:
+ return ;
default:
return ;
}
diff --git a/frontend/src/views/Login/Login.utils.tsx b/frontend/src/views/Login/Login.utils.tsx
index b6e3c1a105..00f6037f16 100644
--- a/frontend/src/views/Login/Login.utils.tsx
+++ b/frontend/src/views/Login/Login.utils.tsx
@@ -1,5 +1,7 @@
-import { NextRouter } from "next/router";
+import { NextRouter, useRouter } from "next/router";
+import { useServerConfig } from "@app/context";
+import { useSelectOrganization } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { userKeys } from "@app/hooks/api/users/queries";
import { queryClient } from "@app/reactQuery";
@@ -27,14 +29,29 @@ export const navigateUserToOrg = async (router: NextRouter, organizationId?: str
}
};
-export const navigateUserToSelectOrg = (router: NextRouter, cliCallbackPort?: string) => {
- queryClient.invalidateQueries(userKeys.getUser);
+export const useNavigateToSelectOrganization = () => {
+ const { config } = useServerConfig();
+ const selectOrganization = useSelectOrganization();
+ const router = useRouter();
- let redirectTo = "/login/select-organization";
+ const navigate = async (cliCallbackPort?: string) => {
+ if (config.defaultAuthOrgId) {
+ await selectOrganization.mutateAsync({
+ organizationId: config.defaultAuthOrgId
+ });
- if (cliCallbackPort) {
- redirectTo += `?callback_port=${cliCallbackPort}`;
- }
+ await navigateUserToOrg(router, config.defaultAuthOrgId);
+ }
+
+ queryClient.invalidateQueries(userKeys.getUser);
+ let redirectTo = "/login/select-organization";
+
+ if (cliCallbackPort) {
+ redirectTo += `?callback_port=${cliCallbackPort}`;
+ }
+
+ router.push(redirectTo, undefined, { shallow: true });
+ };
- router.push(redirectTo, undefined, { shallow: true });
+ return { navigateToSelectOrganization: navigate };
};
diff --git a/frontend/src/views/Login/LoginLDAP.tsx b/frontend/src/views/Login/LoginLDAP.tsx
index 021ac73342..1e99c611df 100644
--- a/frontend/src/views/Login/LoginLDAP.tsx
+++ b/frontend/src/views/Login/LoginLDAP.tsx
@@ -4,15 +4,19 @@ import { useRouter } from "next/router";
import { createNotification } from "@app/components/notifications";
import { Button, Input } from "@app/components/v2";
+import { useServerConfig } from "@app/context";
import { loginLDAPRedirect } from "@app/hooks/api/auth/queries";
export const LoginLDAP = () => {
const router = useRouter();
+ const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const passedOrgSlug = queryParams.get("organizationSlug");
const passedUsername = queryParams.get("username");
- const [organizationSlug, setOrganizationSlug] = useState(passedOrgSlug || "");
+ const [organizationSlug, setOrganizationSlug] = useState(
+ config.defaultAuthOrgSlug || passedOrgSlug || ""
+ );
const [username, setUsername] = useState(passedUsername || "");
const [password, setPassword] = useState("");
@@ -63,21 +67,22 @@ export const LoginLDAP = () => {
What's your LDAP Login?