Compare commits

...

41 Commits

Author SHA1 Message Date
973403c7f9 Merge branch 'feature/oidc' of https://github.com/Infisical/infisical into feature/oidc 2024-06-21 22:23:23 +08:00
52fcf53d0e misc: moved authenticate to preValidation 2024-06-21 22:22:34 +08:00
f31340cf53 Minor adjustments to oidc docs 2024-06-20 16:34:21 -07:00
193d6dad54 misc: removed read sso from org member 2024-06-21 00:39:58 +08:00
0f36fc46b3 docs: added docs for general oidc configuration 2024-06-21 00:37:37 +08:00
4a1a399fd8 docs: added documentation for auth0 oidc configuration 2024-06-20 22:56:53 +08:00
d19e2f64f0 misc: added oidc to user alias type 2024-06-20 21:14:47 +08:00
1e0f54d9a4 doc: added mentions of oidc 2024-06-20 21:14:09 +08:00
8d55c2802e misc: added redirect after user creation 2024-06-20 20:55:26 +08:00
e9639df8ce docs: added keycloak-oidc documentation 2024-06-20 20:25:57 +08:00
e0f5ecbe7b misc: added oidc to text label 2024-06-20 15:40:40 +08:00
3e230555fb misc: added oifc checks to signup 2024-06-20 13:59:50 +08:00
ad92565783 misc: grammar update 2024-06-20 03:30:03 +08:00
6c98c96a15 misc: added comment for samesite lax 2024-06-20 03:29:20 +08:00
f0a70d8769 misc: added samesite lax 2024-06-20 03:19:10 +08:00
d64e2fa243 misc: added client id and secret focus toggle 2024-06-20 01:37:52 +08:00
ecca6f4db5 Merge remote-tracking branch 'origin/main' into feature/oidc 2024-06-20 01:21:46 +08:00
b198f97930 misc: added oidc create validation in route 2024-06-20 00:28:14 +08:00
63a9e46936 misc: removed unnecessary zod assertions 2024-06-20 00:20:00 +08:00
7c067551a4 misc: added frontend validation for oidc form 2024-06-20 00:15:18 +08:00
1193ddbed1 misc: added rate limit for oidc login endpoint 2024-06-19 23:02:49 +08:00
6457c34712 misc: addressed eslint issue regarding configurationType 2024-06-19 22:41:33 +08:00
6a83b58de4 misc: added support for dynamic discovery of OIDC configuration 2024-06-19 22:33:50 +08:00
0100ddfb99 misc: addressed review comments 2024-06-19 19:35:02 +08:00
2bc6db1c47 misc: readded cookie nginx config for dev 2024-06-19 14:19:27 +08:00
92f2f16656 misc: added option for trusting OIDC emails by default 2024-06-19 13:46:17 +08:00
18e69578f0 feat: added support for limiting email domains 2024-06-19 01:29:26 +08:00
0685a5ea8b Merge remote-tracking branch 'origin/main' into feature/oidc 2024-06-18 23:54:06 +08:00
bdc7c018eb misc: added comment regarding session and redis usage 2024-06-18 20:36:09 +08:00
bcd65333c0 misc: added handling of inactive and undefined oidc config 2024-06-18 19:17:54 +08:00
371b96a13a misc: removed cookie path proxy for dev envs 2024-06-18 15:36:49 +08:00
c5c00b520c misc: added session regenerate for fresh state 2024-06-18 15:35:18 +08:00
8de4443be1 feat: added support for login via cli 2024-06-18 15:26:08 +08:00
96ad3b0264 misc: used redis for oic session managemen 2024-06-18 14:20:04 +08:00
df51d05c46 feat: integrated oidc with sso login 2024-06-18 02:10:08 +08:00
4f2f7b2f70 misc: moved oidc endpoints to /sso 2024-06-18 01:21:42 +08:00
d79ffbe37e misc: added license checks for oidc sso 2024-06-18 01:11:57 +08:00
2c237ee277 feat: moved oidc to ee directory 2024-06-18 00:53:56 +08:00
56cc248425 feature: finalized oidc core service methods 2024-06-17 23:28:27 +08:00
61fcb2b605 feat: finished oidc form functions 2024-06-17 22:37:23 +08:00
66e5edcfc0 feat: oidc poc 2024-06-17 20:32:47 +08:00
78 changed files with 2355 additions and 68 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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;

View File

@ -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<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
[TableName.OidcConfig]: Knex.CompositeTableType<TOidcConfigs, TOidcConfigsInsert, TOidcConfigsUpdate>;
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;

View File

@ -0,0 +1,49 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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");
});
}
}
}

View File

@ -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";

View File

@ -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",

View File

@ -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<typeof OidcConfigsSchema>;
export type TOidcConfigsInsert = Omit<z.input<typeof OidcConfigsSchema>, TImmutableDBKeys>;
export type TOidcConfigsUpdate = Partial<Omit<z.input<typeof OidcConfigsSchema>, TImmutableDBKeys>>;

View File

@ -16,7 +16,8 @@ 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()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@ -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" });

View File

@ -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<any>("oidcOrgSlug", orgSlug);
if (callbackPort) {
req.session.set<any>("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<any>("oidcOrgSlug");
const callbackPort = req.session.get<any>("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;
}
});
};

View File

@ -27,6 +27,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogStreams: false,
auditLogStreamLimit: 3,
samlSSO: false,
oidcSSO: false,
scim: false,
ldap: false,
groups: false,

View File

@ -44,6 +44,7 @@ export type TFeatureSet = {
auditLogStreams: false;
auditLogStreamLimit: 3;
samlSSO: false;
oidcSSO: false;
scim: false;
ldap: false;
groups: false;

View File

@ -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<typeof oidcConfigDALFactory>;
export const oidcConfigDALFactory = (db: TDbClient) => {
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
return { ...oidcCfgOrm };
};

View File

@ -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<TUserAliasDALFactory, "create" | "findOne">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
};
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
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 };
};

View File

@ -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;

View File

@ -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);

View File

@ -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);
@ -903,6 +906,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
@ -923,6 +939,7 @@ export const registerRoutes = async (
permission: permissionService,
org: orgService,
orgRole: orgRoleService,
oidc: oidcService,
apiKey: apiKeyService,
authToken: tokenService,
superAdmin: superAdminService,

View File

@ -51,7 +51,8 @@ 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()
}),
response: {
200: z.object({

View File

@ -201,7 +201,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;
}
}
@ -571,7 +574,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;

View File

@ -192,7 +192,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,

View File

@ -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 {

View File

@ -1,4 +1,5 @@
export enum UserAliasType {
LDAP = "ldap",
SAML = "saml"
SAML = "saml",
OIDC = "oidc"
}

View File

@ -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.
Next, head back to [Getting Started > Introduction](/documentation/getting-started/overview) to explore ways to fetch secrets from Infisical to your apps and infrastructure.

View File

@ -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.
<Info>
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.
</Info>
![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)
![organization usage and billing](../../images/platform/organization/organization-usage-billing.png)

View File

@ -0,0 +1,66 @@
---
title: "Auth0 OIDC"
description: "Learn how to configure Auth0 OIDC for Infisical SSO."
---
<Info>
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.
</Info>
<Steps>
<Step title="Setup application in Auth0">
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)
<Info>
If youre self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain.
</Info>
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)
</Step>
<Step title="Retrieve Identity Provider (IdP) Information from Auth0">
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.
</Step>
<Step title="Finish configuring OIDC in Infisical">
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.
</Step>
<Step title="Enable OIDC in Infisical">
Enabling OIDC allows members in your organization to log into Infisical via Auth0.
![OIDC auth0 enable OIDC](../../../images/sso/auth0-oidc/enable-oidc.png)
</Step>
</Steps>
<Note>
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)
</Note>

View File

@ -0,0 +1,69 @@
---
title: "General OIDC"
description: "Learn how to configure OIDC for Infisical SSO with any OIDC-compliant identity provider"
---
<Info>
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.
</Info>
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`.
<Steps>
<Step title="Setup Identity Provider">
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 IdPs OIDC discovery document (usually located at `https://<idp-domain>/.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys.
</Step>
<Step title="Finish configuring OIDC in Infisical">
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>
Note that the Discovery Document URL typically takes the form: `https://<idp-domain>/.well-known/openid-configuration`.
</Note>
![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.
</Step>
<Step title="Enable OIDC SSO in Infisical">
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)
</Step>
</Steps>
<Note>
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)
</Note>

View File

@ -0,0 +1,92 @@
---
title: "Keycloak OIDC"
description: "Learn how to configure Keycloak OIDC for Infisical SSO."
---
<Info>
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.
</Info>
<Steps>
<Step title="Create an OIDC client application in Keycloak">
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)
<Info>
You dont typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm.
</Info>
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)
<Info>
If youre self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain.
</Info>
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)
</Step>
<Step title="Retrieve Identity Provider (IdP) Information from Keycloak">
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)
</Step>
<Step title="Finish configuring OIDC in Infisical">
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.
</Step>
<Step title="Enable OIDC SSO in Infisical">
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)
</Step>
</Steps>
<Note>
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)
</Note>

View File

@ -7,13 +7,14 @@ description: "Learn how to log in to Infisical via SSO protocols."
<Info>
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.
</Info>
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).
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).
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).
@ -30,6 +31,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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

View File

@ -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.
</Note>
## 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).

View File

@ -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"
]
},
{

View File

@ -5,6 +5,7 @@ export type TServerConfig = {
isMigrationModeOn?: boolean;
trustSamlEmails: boolean;
trustLdapEmails: boolean;
trustOidcEmails: boolean;
isSecretScanningDisabled: boolean;
};

View File

@ -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";

View File

@ -0,0 +1 @@
export * from "./queries";

View File

@ -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));
}
});
};

View File

@ -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<OIDCConfigData>(
`/api/v1/sso/oidc/config?orgSlug=${orgSlug}`
);
return data;
},
enabled: true
});
};

View File

@ -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;
};

View File

@ -21,6 +21,7 @@ export type SubscriptionPlan = {
workspacesUsed: number;
environmentLimit: number;
samlSSO: boolean;
oidcSSO: boolean;
scim: boolean;
ldap: boolean;
groups: boolean;

View File

@ -29,7 +29,8 @@ export type User = {
export enum UserAliasType {
LDAP = "ldap",
SAML = "saml"
SAML = "saml",
OIDC = "oidc"
}
export type UserEnc = {

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { isLoggedIn } from "@app/reactQuery";
import { InitialStep, MFAStep, SAMLSSOStep } from "./components";
import { InitialStep, MFAStep, SSOStep } from "./components";
import { navigateUserToSelectOrg } from "./Login.utils";
export const Login = () => {
@ -57,7 +57,9 @@ export const Login = () => {
/>
);
case 2:
return <SAMLSSOStep setStep={setStep} />;
return <SSOStep setStep={setStep} type="SAML" />;
case 3:
return <SSOStep setStep={setStep} type="OIDC" />;
default:
return <div />;
}

View File

@ -40,11 +40,13 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const { data: serverDetails } = useFetchServerStatus();
useEffect(() => {
if (serverDetails?.samlDefaultOrgSlug){
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${serverDetails?.samlDefaultOrgSlug}${callbackPort ? `?callback_port=${callbackPort}` : ""}`
router.push(redirectUrl);
}
if (serverDetails?.samlDefaultOrgSlug) {
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${
serverDetails?.samlDefaultOrgSlug
}${callbackPort ? `?callback_port=${callbackPort}` : ""}`;
router.push(redirectUrl);
}
}, [serverDetails?.samlDefaultOrgSlug]);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
@ -217,6 +219,19 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
Continue with SAML
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(3);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with OIDC
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"

View File

@ -1 +0,0 @@
export { SAMLSSOStep } from "./SAMLSSOStep";

View File

@ -5,9 +5,10 @@ import { Button, Input } from "@app/components/v2";
type Props = {
setStep: (step: number) => void;
type: "SAML" | "OIDC";
};
export const SAMLSSOStep = ({ setStep }: Props) => {
export const SSOStep = ({ setStep, type }: Props) => {
const [ssoIdentifier, setSSOIdentifier] = useState("");
const { t } = useTranslation();
@ -16,21 +17,30 @@ export const SAMLSSOStep = ({ setStep }: Props) => {
const handleSubmission = (e: React.FormEvent) => {
e.preventDefault();
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/saml2/organizations/${ssoIdentifier}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
if (type === "SAML") {
window.open(
`/api/v1/sso/redirect/saml2/organizations/${ssoIdentifier}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
} else {
window.open(
`/api/v1/sso/oidc/login?orgSlug=${ssoIdentifier}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`
);
}
window.close();
};
return (
<div className="mx-auto w-full max-w-md md:px-6">
<p className="mx-auto mb-6 mb-8 flex w-max justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
<p className="mx-auto mb-8 flex w-max justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
What&apos;s your organization slug?
</p>
<form onSubmit={handleSubmission}>
<div className="relative mx-auto flex max-h-24 w-1/4 w-full min-w-[20rem] items-center justify-center rounded-lg md:max-h-28 md:min-w-[22rem] lg:w-1/6">
<div className="relative mx-auto flex max-h-24 w-full min-w-[20rem] items-center justify-center rounded-lg md:max-h-28 md:min-w-[22rem] lg:w-1/6">
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
<Input
value={ssoIdentifier}
@ -44,7 +54,7 @@ export const SAMLSSOStep = ({ setStep }: Props) => {
/>
</div>
</div>
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[20rem] items-center justify-center rounded-md text-center md:min-w-[22rem] lg:w-1/6">
<div className="mx-auto mt-4 flex w-full min-w-[20rem] items-center justify-center rounded-md text-center md:min-w-[22rem] lg:w-1/6">
<Button
type="submit"
colorSchema="primary"
@ -52,7 +62,7 @@ export const SAMLSSOStep = ({ setStep }: Props) => {
isFullWidth
className="h-14"
>
Continue with SAML
Continue with {type}
</Button>
</div>
</form>

View File

@ -0,0 +1 @@
export { SSOStep } from "./SSOStep";

View File

@ -1,6 +1,6 @@
export { InitialStep } from "./InitialStep";
export { MFAStep } from "./MFAStep";
export { SAMLSSOStep } from "./SAMLSSOStep";
export { SSOStep } from "./SSOStep";
// SSO-specific step
export { PasswordStep } from "./PasswordStep";

View File

@ -0,0 +1,404 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetOIDCConfig } from "@app/hooks/api";
import { useCreateOIDCConfig, useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations";
import { UsePopUpState } from "@app/hooks/usePopUp";
enum ConfigurationType {
CUSTOM = "custom",
DISCOVERY_URL = "discoveryURL"
}
type Props = {
popUp: UsePopUpState<["addOIDC"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addOIDC"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addOIDC"]>, state?: boolean) => void;
};
const schema = z
.object({
configurationType: z.string(),
issuer: z.string().optional(),
discoveryURL: z.string().optional(),
authorizationEndpoint: z.string().optional(),
jwksUri: z.string().optional(),
tokenEndpoint: z.string().optional(),
userinfoEndpoint: z.string().optional(),
clientId: z.string().min(1),
clientSecret: z.string().min(1),
allowedEmailDomains: z.string().optional()
})
.superRefine((data, ctx) => {
if (data.configurationType === ConfigurationType.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
});
}
}
});
export type OIDCFormData = z.infer<typeof schema>;
export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateOIDCConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateOIDCConfig();
const { data } = useGetOIDCConfig(currentOrg?.slug ?? "");
const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({
resolver: zodResolver(schema),
defaultValues: {
configurationType: ConfigurationType.DISCOVERY_URL
}
});
const [isClientIdFocused, setIsClientIdFocused] = useToggle();
const [isClientSecretFocused, setIsClientSecretFocused] = useToggle();
const configurationTypeValue = watch("configurationType");
useEffect(() => {
if (data) {
setValue("issuer", data.issuer);
setValue("authorizationEndpoint", data.authorizationEndpoint);
setValue("jwksUri", data.jwksUri);
setValue("tokenEndpoint", data.tokenEndpoint);
setValue("userinfoEndpoint", data.userinfoEndpoint);
setValue("discoveryURL", data.discoveryURL);
setValue("clientId", data.clientId);
setValue("clientSecret", data.clientSecret);
setValue("allowedEmailDomains", data.allowedEmailDomains);
setValue("configurationType", data.configurationType);
}
}, [data]);
const onOIDCModalSubmit = async ({
issuer,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
configurationType,
discoveryURL,
clientId,
clientSecret
}: OIDCFormData) => {
try {
if (!currentOrg) {
return;
}
if (!data) {
await createMutateAsync({
issuer,
configurationType,
discoveryURL,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret,
isActive: true,
orgSlug: currentOrg.slug
});
} else {
await updateMutateAsync({
issuer,
configurationType,
discoveryURL,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret,
isActive: true,
orgSlug: currentOrg.slug
});
}
handlePopUpClose("addOIDC");
createNotification({
text: `Successfully ${!data ? "added" : "updated"} OIDC SSO configuration`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${!data ? "add" : "update"} OIDC SSO configuration`,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.addOIDC?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addOIDC", isOpen);
reset();
}}
>
<ModalContent title="Manage OIDC configuration">
<form onSubmit={handleSubmit(onOIDCModalSubmit)}>
<Controller
control={control}
name="configurationType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full"
defaultValue="discoveryURL"
{...field}
onValueChange={(e) => onChange(e)}
>
<SelectItem value={ConfigurationType.DISCOVERY_URL}>Discovery URL</SelectItem>
<SelectItem value={ConfigurationType.CUSTOM}>Custom</SelectItem>
</Select>
</FormControl>
)}
/>
{configurationTypeValue === ConfigurationType.DISCOVERY_URL && (
<Controller
control={control}
name="discoveryURL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Discovery Document URL"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://accounts.google.com/.well-known/openid-configuration"
autoComplete="off"
/>
</FormControl>
)}
/>
)}
{configurationTypeValue === ConfigurationType.CUSTOM && (
<>
<Controller
control={control}
name="issuer"
render={({ field, fieldState: { error } }) => (
<FormControl label="Issuer" errorText={error?.message} isError={Boolean(error)}>
<Input
{...field}
placeholder="https://accounts.google.com"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="authorizationEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Authorization Endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://accounts.google.com/o/oauth2/v2/auth"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="tokenEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Token Endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://oauth2.googleapis.com/token"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="userinfoEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="User info endpoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://openidconnect.googleapis.com/v1/userinfo"
autoComplete="off"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="jwksUri"
render={({ field, fieldState: { error } }) => (
<FormControl label="JWKS URI" errorText={error?.message} isError={Boolean(error)}>
<Input
{...field}
placeholder="https://www.googleapis.com/oauth2/v3/certs"
autoComplete="off"
/>
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="allowedEmailDomains"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Email Domains (defaults to any)"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="infisical.com, google.com" autoComplete="off" />
</FormControl>
)}
/>
<Controller
control={control}
name="clientId"
render={({ field, fieldState: { error } }) => (
<FormControl label="Client ID" errorText={error?.message} isError={Boolean(error)}>
<Input
placeholder="Client ID"
type={isClientIdFocused ? "text" : "password"}
onFocus={() => setIsClientIdFocused.on()}
{...field}
onBlur={() => {
field.onBlur();
setIsClientIdFocused.off();
}}
autoComplete="off"
className="bg-mineshaft-800"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="Client Secret"
type={isClientSecretFocused ? "text" : "password"}
autoComplete="off"
onFocus={() => setIsClientSecretFocused.on()}
onBlur={() => {
field.onBlur();
setIsClientSecretFocused.off();
}}
className="bg-mineshaft-800"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!data ? "Add" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addOIDC")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

View File

@ -3,6 +3,7 @@ import { withPermission } from "@app/hoc";
import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection";
import { OrgLDAPSection } from "./OrgLDAPSection";
import { OrgOIDCSection } from "./OrgOIDCSection";
import { OrgScimSection } from "./OrgSCIMSection";
import { OrgSSOSection } from "./OrgSSOSection";
@ -12,6 +13,7 @@ export const OrgAuthTab = withPermission(
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgGeneralAuthSection />
<OrgSSOSection />
<OrgOIDCSection />
<OrgLDAPSection />
<OrgScimSection />
</div>

View File

@ -0,0 +1,118 @@
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { useGetOIDCConfig } from "@app/hooks/api";
import { useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations";
import { usePopUp } from "@app/hooks/usePopUp";
import { OIDCModal } from "./OIDCModal";
export const OrgOIDCSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { data, isLoading } = useGetOIDCConfig(currentOrg?.slug ?? "");
const { mutateAsync } = useUpdateOIDCConfig();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addOIDC",
"upgradePlan"
] as const);
const handleOIDCToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgSlug: currentOrg?.slug,
isActive: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} OIDC SSO`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${value ? "enable" : "disable"} OIDC SSO`,
type: "error"
});
}
};
const addOidcButtonClick = async () => {
if (subscription?.oidcSSO && currentOrg) {
handlePopUpOpen("addOIDC");
} else {
handlePopUpOpen("upgradePlan");
}
};
return (
<>
<hr className="border-mineshaft-600" />
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">OIDC</h2>
{!isLoading && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button
onClick={addOidcButtonClick}
colorSchema="secondary"
isDisabled={!isAllowed}
>
Manage
</Button>
)}
</OrgPermissionCan>
)}
</div>
<p className="text-sm text-mineshaft-300">Manage OIDC authentication configuration</p>
</div>
{data && (
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">Enable OIDC</h2>
{!isLoading && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enable-oidc-sso"
onCheckedChange={(value) => handleOIDCToggle(value)}
isChecked={data ? data.isActive : false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
)}
</div>
<p className="text-sm text-mineshaft-300">
Allow members to authenticate into Infisical with OIDC
</p>
</div>
)}
<OIDCModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use OIDC SSO if you switch to Infisical's Pro plan."
/>
</>
);
};

View File

@ -92,6 +92,10 @@ export const EmailConfirmationStep = ({
router.push(`/login/ldap?organizationSlug=${organizationSlug}`);
break;
}
case UserAliasType.OIDC: {
router.push(`/api/v1/sso/oidc/login?orgSlug=${organizationSlug}`);
break;
}
default: {
setStep(1);
break;

View File

@ -39,7 +39,8 @@ const formSchema = z.object({
signUpMode: z.nativeEnum(SignUpModes),
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean(),
trustLdapEmails: z.boolean()
trustLdapEmails: z.boolean(),
trustOidcEmails: z.boolean()
});
type TDashboardForm = z.infer<typeof formSchema>;
@ -60,7 +61,8 @@ export const AdminDashboardPage = () => {
signUpMode: config.allowSignUp ? SignUpModes.Anyone : SignUpModes.Disabled,
allowedSignUpDomain: config.allowedSignUpDomain,
trustSamlEmails: config.trustSamlEmails,
trustLdapEmails: config.trustLdapEmails
trustLdapEmails: config.trustLdapEmails,
trustOidcEmails: config.trustOidcEmails
}
});
@ -84,13 +86,15 @@ export const AdminDashboardPage = () => {
const onFormSubmit = async (formData: TDashboardForm) => {
try {
const { signUpMode, allowedSignUpDomain, trustSamlEmails, trustLdapEmails } = formData;
const { signUpMode, allowedSignUpDomain, trustSamlEmails, trustLdapEmails, trustOidcEmails } =
formData;
await updateServerConfig({
allowSignUp: signUpMode !== SignUpModes.Disabled,
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null,
trustSamlEmails,
trustLdapEmails
trustLdapEmails,
trustOidcEmails
});
createNotification({
text: "Successfully changed sign up setting.",
@ -190,9 +194,9 @@ export const AdminDashboardPage = () => {
<div className="mt-8 mb-8 flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Trust emails</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want Infisical to trust external emails from SAML/LDAP identity
providers. If set to false, then Infisical will prompt SAML/LDAP provisioned
users to verify their email upon their first login.
Select if you want Infisical to trust external emails from SAML/LDAP/OIDC
identity providers. If set to false, then Infisical will prompt SAML/LDAP/OIDC
provisioned users to verify their email upon their first login.
</div>
<Controller
control={control}
@ -228,6 +232,23 @@ export const AdminDashboardPage = () => {
);
}}
/>
<Controller
control={control}
name="trustOidcEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-oidc-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust OIDC emails</p>
</Switch>
</FormControl>
);
}}
/>
</div>
<Button
type="submit"

View File

@ -11,7 +11,7 @@ server {
proxy_pass http://backend:4000;
proxy_redirect off;
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location / {