Compare commits

...

82 Commits

Author SHA1 Message Date
232b375f46 Merge pull request #2015 from Infisical/create-pull-request/patch-1719267521
GH Action: rename new migration file timestamp
2024-06-24 17:07:41 -07:00
d2acedf79e chore: renamed new migration files to latest timestamp (gh-action) 2024-06-24 22:18:39 +00:00
9d846319b0 Merge pull request #2014 from Infisical/cert-san
Add Certificate Support for Alt Names (SANs)
2024-06-24 15:18:17 -07:00
d69267a3ca remove console.log 2024-06-24 17:27:02 -04:00
051eee8701 Update altName example in docs 2024-06-24 14:14:51 -07:00
b5aa650899 Add cert support for alt names 2024-06-24 14:07:15 -07:00
376e185e2b Merge pull request #2006 from Infisical/daniel/expand-single-secret-ref
feat(api): Expand single secret references
2024-06-24 20:39:54 +02:00
6facce220c update select default org login 2024-06-24 14:06:31 -04:00
620a423cee update org selection error message 2024-06-24 13:43:56 -04:00
361496c644 Merge pull request #2012 from Infisical/create-pull-request/patch-1719249628
GH Action: rename new migration file timestamp
2024-06-24 13:20:49 -04:00
e03f77d9cf chore: renamed new migration files to latest timestamp (gh-action) 2024-06-24 17:20:27 +00:00
60cb420242 Merge pull request #2000 from Infisical/daniel/default-org
Feat: Default organization slug for LDAP/SAML
2024-06-24 13:20:02 -04:00
1b8a77f507 Merge pull request #2002 from Infisical/patch-ldap
Patch LDAP undefined userId, email confirmation code sending
2024-06-24 13:19:48 -04:00
5a957514df Feat: Clear select option 2024-06-24 19:12:38 +02:00
a6865585f3 Fix: Failing to create admin config on first run 2024-06-24 19:11:58 +02:00
1aaca12781 Update super-admin-dal.ts 2024-06-24 19:11:58 +02:00
7ab5c02000 Requested changes 2024-06-24 19:11:58 +02:00
c735beea32 Fix: Requested changes 2024-06-24 19:11:58 +02:00
2d98560255 Updated "defaultOrgId" and "defaultOrgSlug" to "defaultAuthOrgId" and "defaultAuthOrgSlug" 2024-06-24 19:10:22 +02:00
91bdd7ea6a Fix: UI descriptions 2024-06-24 19:09:48 +02:00
b0f3476e4a Fix: Completely hide org slug input field when org slug is passed or default slug is provided 2024-06-24 19:09:03 +02:00
14751df9de Feat: Default SAML/LDAP organization slug 2024-06-24 19:09:03 +02:00
e1a4185f76 Hide org slug input when default slug is set 2024-06-24 19:08:19 +02:00
4905ad1f48 Feat: Default SAML/LDAP organization slug 2024-06-24 19:08:19 +02:00
56bc25025a Update Login.utils.tsx 2024-06-24 19:08:19 +02:00
45da563465 Convert navigate function to hook 2024-06-24 19:08:19 +02:00
1930d40be8 Feat: Default SAML/LDAP organization slug 2024-06-24 19:05:46 +02:00
30b8d59796 Feat: Default SAML/LDAP organization slug 2024-06-24 19:05:46 +02:00
aa6cca738e Update index.ts 2024-06-24 19:05:46 +02:00
04dee70a55 Type changes 2024-06-24 19:05:46 +02:00
dfb53dd333 Helper omit function 2024-06-24 19:05:20 +02:00
ab19e7df6d Feat: Default SAML/LDAP organization slug 2024-06-24 19:05:20 +02:00
f9a1accf84 Merge pull request #2011 from Infisical/create-pull-request/patch-1719245983
GH Action: rename new migration file timestamp
2024-06-25 00:24:08 +08:00
ca86f3d2b6 chore: renamed new migration files to latest timestamp (gh-action) 2024-06-24 16:19:41 +00:00
de466b4b86 Merge pull request #1989 from Infisical/feature/oidc
feat: oidc
2024-06-25 00:19:15 +08:00
106dc261de Merge pull request #2008 from handotdev/patch-1
Update style.css in docs
2024-06-24 08:20:30 -07:00
548a0aed2a Merge pull request #2009 from Infisical/daniel/sdk-docs-typo
fix(docs): duplicate faq item
2024-06-24 07:28:52 -07:00
b7b606ab9a Update overview.mdx 2024-06-24 14:01:24 +02:00
00617ea7e8 Update style.css 2024-06-24 00:56:39 -07:00
c426ba517a Feat: Expand single secret references 2024-06-21 23:12:38 +02:00
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
91634fbe76 Patch LDAP 2024-06-20 17:49:09 -07: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
106 changed files with 2928 additions and 208 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

@ -0,0 +1,27 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
const DEFAULT_AUTH_ORG_ID_FIELD = "defaultAuthOrgId";
export async function up(knex: Knex): Promise<void> {
const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (!hasDefaultOrgColumn) {
t.uuid(DEFAULT_AUTH_ORG_ID_FIELD).nullable();
t.foreign(DEFAULT_AUTH_ORG_ID_FIELD).references("id").inTable(TableName.Organization).onDelete("SET NULL");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (hasDefaultOrgColumn) {
t.dropForeign([DEFAULT_AUTH_ORG_ID_FIELD]);
t.dropColumn(DEFAULT_AUTH_ORG_ID_FIELD);
}
});
}

View File

@ -0,0 +1,24 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.Certificate)) {
const hasAltNamesColumn = await knex.schema.hasColumn(TableName.Certificate, "altNames");
if (!hasAltNamesColumn) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.string("altNames").defaultTo("");
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.Certificate)) {
if (await knex.schema.hasColumn(TableName.Certificate, "altNames")) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("altNames");
});
}
}
}

View File

@ -19,7 +19,8 @@ export const CertificatesSchema = z.object({
notBefore: z.date(),
notAfter: z.date(),
revokedAt: z.date().nullable().optional(),
revocationReason: z.number().nullable().optional()
revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional()
});
export type TCertificates = z.infer<typeof CertificatesSchema>;

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

@ -17,8 +17,8 @@ export const ProjectsSchema = z.object({
updatedAt: z.date(),
version: z.number().default(1),
upgradeStatus: z.string().nullable().optional(),
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
pitVersionLimit: z.number().default(10)
pitVersionLimit: z.number().default(10),
kmsCertificateKeyId: z.string().uuid().nullable().optional()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@ -16,7 +16,9 @@ export const SuperAdminSchema = z.object({
allowedSignUpDomain: z.string().nullable().optional(),
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"),
trustSamlEmails: z.boolean().default(false).nullable().optional(),
trustLdapEmails: z.boolean().default(false).nullable().optional()
trustLdapEmails: z.boolean().default(false).nullable().optional(),
trustOidcEmails: z.boolean().default(false).nullable().optional(),
defaultAuthOrgId: z.string().uuid().nullable().optional()
});
export type TSuperAdmin = z.infer<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

@ -23,6 +23,8 @@ import {
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@ -30,6 +32,7 @@ import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membe
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
@ -84,6 +87,8 @@ type TLdapConfigServiceFactoryDep = {
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
};
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
@ -103,7 +108,9 @@ export const ldapConfigServiceFactory = ({
userDAL,
userAliasDAL,
permissionService,
licenseService
licenseService,
tokenService,
smtpService
}: TLdapConfigServiceFactoryDep) => {
const createLdapCfg = async ({
actor,
@ -494,7 +501,7 @@ export const ldapConfigServiceFactory = ({
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: userAlias.userId,
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
@ -627,6 +634,22 @@ export const ldapConfigServiceFactory = ({
}
);
if (user.email && !user.isEmailVerified) {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
}
return { isUserCompleted, providerAuthToken };
};

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

@ -347,6 +347,7 @@ export const RAW_SECRETS = {
tagIds: "The ID of the tags to be attached to the created secret."
},
GET: {
expand: "Whether or not to expand secret references",
secretName: "The name of the secret to get.",
workspaceId: "The ID of the project to get the secret from.",
workspaceSlug: "The slug of the project to get the secret from.",
@ -804,6 +805,8 @@ export const CERTIFICATE_AUTHORITIES = {
caId: "The ID of the CA to issue the certificate from",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
altNames:
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",

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);
@ -392,7 +395,9 @@ export const registerRoutes = async (
userDAL,
userAliasDAL,
permissionService,
licenseService
licenseService,
tokenService,
smtpService
});
const telemetryService = telemetryServiceFactory({
@ -903,6 +908,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 +941,7 @@ export const registerRoutes = async (
permission: permissionService,
org: orgService,
orgRole: orgRoleService,
oidc: oidcService,
apiKey: apiKeyService,
authToken: tokenService,
superAdmin: superAdminService,

View File

@ -22,6 +22,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
200: z.object({
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({
isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(),
isSecretScanningDisabled: z.boolean()
})
})
@ -51,11 +52,15 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
allowSignUp: z.boolean().optional(),
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean().optional(),
trustLdapEmails: z.boolean().optional()
trustLdapEmails: z.boolean().optional(),
trustOidcEmails: z.boolean().optional(),
defaultAuthOrgId: z.string().optional().nullable()
}),
response: {
200: z.object({
config: SuperAdminSchema
config: SuperAdminSchema.extend({
defaultAuthOrgSlug: z.string().nullable()
})
})
}
},

View File

@ -9,7 +9,10 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
import {
validateAltNamesField,
validateCaDateField
} from "@app/services/certificate-authority/certificate-authority-validators";
export const registerCaRouter = async (server: FastifyZodProvider) => {
server.route({
@ -452,6 +455,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
.object({
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")

View File

@ -300,6 +300,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type),
expandSecretReferences: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.GET.expand),
include_imports: z
.enum(["true", "false"])
.default("false")
@ -344,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
expandSecretReferences: req.query.expandSecretReferences,
environment,
projectId: workspaceId,
projectSlug: workspaceSlug,

View File

@ -202,7 +202,10 @@ export const authLoginServiceFactory = ({
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
authMethod = decodedProviderToken.authMethod;
if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId) {
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
) {
organizationId = decodedProviderToken.orgId;
}
}
@ -351,9 +354,12 @@ export const authLoginServiceFactory = ({
// Check if the user actually has access to the specified organization.
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId);
const selectedOrg = await orgDAL.findById(organizationId);
if (!hasOrganizationMembership) {
throw new UnauthorizedError({ message: "User does not have access to the organization" });
throw new UnauthorizedError({
message: `User does not have access to the organization named ${selectedOrg?.name}`
});
}
await tokenDAL.incrementTokenSessionVersion(user.id, decodedToken.tokenVersionId);
@ -578,7 +584,8 @@ export const authLoginServiceFactory = ({
const { authMethod, userName } = decodedProviderToken;
if (!userName) throw new BadRequestError({ message: "Missing user name" });
const organizationId =
(isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
? decodedProviderToken.orgId
: undefined;

View File

@ -193,7 +193,10 @@ export const authSignupServiceFactory = ({
tx
);
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && organizationId) {
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) &&
organizationId
) {
const [pendingOrgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited,

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

@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { z } from "zod";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -38,6 +39,7 @@ import {
TSignIntermediateDTO,
TUpdateCaDTO
} from "./certificate-authority-types";
import { hostnameRegex } from "./certificate-authority-validators";
type TCertificateAuthorityServiceFactoryDep = {
certificateAuthorityDAL: Pick<
@ -653,6 +655,7 @@ export const certificateAuthorityServiceFactory = ({
caId,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter,
@ -738,6 +741,45 @@ export const certificateAuthorityServiceFactory = ({
kmsService
});
const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
if (altNames) {
const altNamesArray: {
type: "email" | "dns";
value: string;
}[] = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
// check if the altName is a valid email
if (z.string().email().safeParse(altName).success) {
return {
type: "email",
value: altName
};
}
// check if the altName is a valid hostname
if (hostnameRegex.test(altName)) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
@ -748,12 +790,7 @@ export const certificateAuthorityServiceFactory = ({
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
@ -771,6 +808,7 @@ export const certificateAuthorityServiceFactory = ({
status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName,
commonName,
altNames,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate

View File

@ -75,6 +75,7 @@ export type TIssueCertFromCaDTO = {
caId: string;
friendlyName?: string;
commonName: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;

View File

@ -6,3 +6,29 @@ const isValidDate = (dateString: string) => {
};
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
export const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
export const validateAltNamesField = z
.string()
.trim()
.default("")
.transform((data) => {
if (data === "") return "";
// Trim each alt name and join with ', ' to ensure formatting
return data
.split(",")
.map((id) => id.trim())
.join(", ");
})
.refine(
(data) => {
if (data === "") return true;
// Split and validate each alt name
return data.split(", ").every((name) => {
return hostnameRegex.test(name) || z.string().email().safeParse(name).success;
});
},
{
message: "Each alt name must be a valid hostname or email address"
}
);

View File

@ -1078,6 +1078,7 @@ export const secretServiceFactory = ({
actor,
environment,
projectId: workspaceId,
expandSecretReferences,
projectSlug,
actorId,
actorOrgId,
@ -1091,7 +1092,7 @@ export const secretServiceFactory = ({
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const secret = await getSecretByName({
const encryptedSecret = await getSecretByName({
actorId,
projectId,
actorAuthMethod,
@ -1105,7 +1106,46 @@ export const secretServiceFactory = ({
version
});
return decryptSecretRaw(secret, botKey);
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) {
const expandSecrets = interpolateSecrets({
folderDAL,
projectId,
secretDAL,
secretEncKey: botKey
});
const expandSingleSecret = async (secret: {
secretKey: string;
secretValue: string;
secretComment?: string;
secretPath: string;
skipMultilineEncoding: boolean | null | undefined;
}) => {
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
> = {
[secret.secretKey]: {
value: secret.secretValue,
comment: secret.secretComment,
skipMultilineEncoding: secret.skipMultilineEncoding
}
};
await expandSecrets(secretRecord);
// Update the secret with the expanded value
// eslint-disable-next-line no-param-reassign
secret.secretValue = secretRecord[secret.secretKey].value;
};
// Expand the secret
await expandSingleSecret(decryptedSecret);
}
return decryptedSecret;
};
const createSecretRaw = async ({

View File

@ -151,6 +151,7 @@ export type TGetASecretRawDTO = {
secretName: string;
path: string;
environment: string;
expandSecretReferences?: boolean;
type: "shared" | "personal";
includeImports?: boolean;
version?: number;

View File

@ -1,7 +1,57 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TableName, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TSuperAdminDALFactory = ReturnType<typeof superAdminDALFactory>;
export const superAdminDALFactory = (db: TDbClient) => ormify(db, TableName.SuperAdmin, {});
export const superAdminDALFactory = (db: TDbClient) => {
const superAdminOrm = ormify(db, TableName.SuperAdmin);
const findById = async (id: string, tx?: Knex) => {
const config = await (tx || db)(TableName.SuperAdmin)
.where(`${TableName.SuperAdmin}.id`, id)
.leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`)
.select(
db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin,
db.ref("slug").withSchema(TableName.Organization).as("defaultAuthOrgSlug")
)
.first();
if (!config) {
return null;
}
return {
...config,
defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null
} as TSuperAdmin & { defaultAuthOrgSlug: string | null };
};
const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => {
const updatedConfig = await (superAdminOrm || tx).transaction(async (trx: Knex) => {
await superAdminOrm.updateById(id, data, trx);
const config = await findById(id, trx);
if (!config) {
throw new DatabaseError({
error: "Failed to find updated super admin config",
message: "Failed to update super admin config",
name: "UpdateById"
});
}
return config;
});
return updatedConfig;
};
return {
...superAdminOrm,
findById,
updateById
};
};

View File

@ -25,7 +25,7 @@ type TSuperAdminServiceFactoryDep = {
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
// eslint-disable-next-line
export let getServerCfg: () => Promise<TSuperAdmin>;
export let getServerCfg: () => Promise<TSuperAdmin & { defaultAuthOrgSlug: string | null }>;
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
@ -42,16 +42,20 @@ export const superAdminServiceFactory = ({
// TODO(akhilmhdh): bad pattern time less change this later to me itself
getServerCfg = async () => {
const config = await keyStore.getItem(ADMIN_CONFIG_KEY);
// missing in keystore means fetch from db
if (!config) {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg) {
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore
if (!serverCfg) {
throw new BadRequestError({ name: "Admin config", message: "Admin config not found" });
}
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore
return serverCfg;
}
const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin;
const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin & { defaultAuthOrgSlug: string | null };
return {
...keyStoreServerCfg,
// this is to allow admin router to work
@ -65,14 +69,21 @@ export const superAdminServiceFactory = ({
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg) return;
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true, id: ADMIN_CONFIG_DB_UUID });
const newCfg = await serverCfgDAL.create({
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
id: ADMIN_CONFIG_DB_UUID,
initialized: false,
allowSignUp: true,
defaultAuthOrgId: null
});
return newCfg;
};
const updateServerCfg = async (data: TSuperAdminUpdate) => {
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
return updatedServerCfg;
};

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

@ -56,9 +56,9 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
- Issuing CA: The CA under which to issue the certificate.
- Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty.
- Common Name (CN): The (common) name of the certificate.
- Common Name (CN): The (common) name for the certificate like `service.acme.com`.
- Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`.
- TTL: The lifetime of the certificate in seconds.
- Valid Until: The date until which the certificate is valid in the date time string format specified [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format). For example, the following formats would be valid: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `YYYY-MM-DDTHH:mm:ss.sssZ`.
</Step>
<Step title="Copying the certificate details">

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).
## Identity providers
@ -27,6 +28,9 @@ Infisical supports these and many other identity providers:
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud)
- [Keycloak SAML](/documentation/platform/sso/keycloak-saml)
- [Google SAML](/documentation/platform/sso/google-saml)
- [Keycloak OIDC](/documentation/platform/sso/keycloak-oidc)
- [Auth0 OIDC](/documentation/platform/sso/auth0-oidc)
- [General OIDC](/documentation/platform/sso/general-oidc)
If your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance.

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

@ -46,7 +46,4 @@ From local development to production, Infisical SDKs provide the easiest way for
Note: The exact parameter name may differ depending on the language.
</Accordion>
<Accordion title="What if a request for a secret fails?">
The SDK caches every secret and falls back to the cached value if a request fails. If no cached value is found, and the request fails, then the SDK throws an error.
</Accordion>
</AccordionGroup>

View File

@ -10,7 +10,6 @@
#sidebar {
left: 0;
padding-left: 48px;
padding-right: 30px;
border-right: 1px;
border-color: #cdd64b;
@ -18,6 +17,10 @@
border-right: 1px solid #ebebeb;
}
#sidebar-content {
padding-left: 2rem;
}
#sidebar .relative .sticky {
opacity: 0;
}
@ -154,4 +157,4 @@
.flex-1 .flex .items-center {
/* background-color: #f5f5f5; */
}
}

View File

@ -36,61 +36,73 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
ref
): JSX.Element => {
return (
<SelectPrimitive.Root {...props} disabled={isDisabled}>
<SelectPrimitive.Trigger
ref={ref}
className={twMerge(
`inline-flex items-center justify-between rounded-md
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`,
className,
isDisabled && "cursor-not-allowed opacity-50"
)}
>
<SelectPrimitive.Value placeholder={placeholder}>
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
</SelectPrimitive.Value>
<div className="flex items-center space-x-2">
<SelectPrimitive.Root
{...props}
onValueChange={(value) => {
if (!props.onValueChange) return;
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon
icon={faCaretDown}
size="sm"
className={twMerge(isDisabled && "opacity-30")}
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
const newValue = value === "EMPTY-VALUE" ? "" : value;
props.onValueChange(newValue);
}}
disabled={isDisabled}
>
<SelectPrimitive.Trigger
ref={ref}
className={twMerge(
"relative top-1 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
position === "popper" && "max-h-72",
dropdownContainerClassName
`inline-flex items-center justify-between rounded-md
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`,
className,
isDisabled && "cursor-not-allowed opacity-50"
)}
position={position}
style={{ width: "var(--radix-select-trigger-width)" }}
>
<SelectPrimitive.ScrollUpButton>
<div className="flex items-center justify-center">
<FontAwesomeIcon icon={faCaretUp} size="sm" />
</div>
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1">
{isLoading ? (
<div className="flex items-center justify-center">
<Spinner size="xs" />
<span className="ml-2 text-xs text-gray-500">Loading...</span>
</div>
) : (
children
<div className="flex items-center space-x-2">
{props.icon && <FontAwesomeIcon icon={props.icon} />}
<SelectPrimitive.Value placeholder={placeholder} />
</div>
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon
icon={faCaretDown}
size="sm"
className={twMerge(isDisabled && "opacity-30")}
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
className={twMerge(
"relative top-1 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
position === "popper" && "max-h-72",
dropdownContainerClassName
)}
</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton>
<div className="flex items-center justify-center">
<FontAwesomeIcon icon={faCaretDown} size="sm" />
</div>
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
position={position}
style={{ width: "var(--radix-select-trigger-width)" }}
>
<SelectPrimitive.ScrollUpButton>
<div className="flex items-center justify-center">
<FontAwesomeIcon icon={faCaretUp} size="sm" />
</div>
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1">
{isLoading ? (
<div className="flex items-center justify-center">
<Spinner size="xs" />
<span className="ml-2 text-xs text-gray-500">Loading...</span>
</div>
) : (
children
)}
</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton>
<div className="flex items-center justify-center">
<FontAwesomeIcon icon={faCaretDown} size="sm" />
</div>
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
</div>
);
}
);
@ -114,7 +126,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary",
isDisabled &&
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
className
)}
ref={forwardedRef}
@ -129,3 +141,45 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
);
SelectItem.displayName = "SelectItem";
export type SelectClearProps = Omit<SelectItemProps, "disabled" | "value"> & {
onClear: () => void;
selectValue: string;
};
export const SelectClear = forwardRef<HTMLDivElement, SelectClearProps>(
(
{ children, className, isSelected, isDisabled, onClear, selectValue, ...props },
forwardedRef
) => {
return (
<SelectPrimitive.Item
{...props}
value="EMPTY-VALUE"
onSelect={() => onClear()}
onClick={() => onClear()}
className={twMerge(
`relative mb-0.5 flex
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary",
isDisabled &&
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
className
)}
ref={forwardedRef}
>
<div
className={twMerge(
"absolute left-3.5 text-primary",
selectValue === "" ? "visible" : "hidden"
)}
>
<FontAwesomeIcon icon={faCheck} />
</div>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
);
SelectClear.displayName = "SelectClear";

View File

@ -1,2 +1,2 @@
export type { SelectItemProps, SelectProps } from "./Select";
export { Select, SelectItem } from "./Select";
export { Select, SelectClear, SelectItem } from "./Select";

View File

@ -5,7 +5,10 @@ export type TServerConfig = {
isMigrationModeOn?: boolean;
trustSamlEmails: boolean;
trustLdapEmails: boolean;
trustOidcEmails: boolean;
isSecretScanningDisabled: boolean;
defaultAuthOrgSlug: string | null;
defaultAuthOrgId: string | null;
};
export type TCreateAdminUserDTO = {

View File

@ -81,6 +81,7 @@ export type TCreateCertificateDTO = {
caId: string;
friendlyName?: string;
commonName: string;
altNames: string; // sans
ttl: string; // string compatible with ms
notBefore?: string;
notAfter?: string;

View File

@ -6,6 +6,7 @@ export type TCertificate = {
status: CertStatus;
friendlyName: string;
commonName: string;
altNames: string;
serialNumber: string;
notBefore: string;
notAfter: string;

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

@ -4,5 +4,5 @@ export type ServerStatus = {
emailConfigured: boolean;
secretScanningConfigured: boolean;
redisConfigured: boolean;
samlDefaultOrgSlug: boolean
samlDefaultOrgSlug: 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

@ -1,16 +1,15 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { isLoggedIn } from "@app/reactQuery";
import { InitialStep, MFAStep, SAMLSSOStep } from "./components";
import { navigateUserToSelectOrg } from "./Login.utils";
import { InitialStep, MFAStep, SSOStep } from "./components";
import { useNavigateToSelectOrganization } from "./Login.utils";
export const Login = () => {
const router = useRouter();
const [step, setStep] = useState(0);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
const queryParams = new URLSearchParams(window.location.search);
@ -21,10 +20,10 @@ export const Login = () => {
const callbackPort = queryParams?.get("callback_port");
// case: a callback port is set, meaning it's a cli login request: redirect to select org with callback port
if (callbackPort) {
navigateUserToSelectOrg(router, callbackPort);
navigateToSelectOrganization(callbackPort);
} else {
// case: no callback port, meaning it's a regular login request: redirect to select org
navigateUserToSelectOrg(router);
navigateToSelectOrganization();
}
} catch (error) {
console.log("Error - Not logged in yet");
@ -57,7 +56,9 @@ export const Login = () => {
/>
);
case 2:
return <SAMLSSOStep setStep={setStep} />;
return <SSOStep setStep={setStep} type="SAML" />;
case 3:
return <SSOStep setStep={setStep} type="OIDC" />;
default:
return <div />;
}

View File

@ -1,5 +1,7 @@
import { NextRouter } from "next/router";
import { NextRouter, useRouter } from "next/router";
import { useServerConfig } from "@app/context";
import { useSelectOrganization } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { userKeys } from "@app/hooks/api/users/queries";
import { queryClient } from "@app/reactQuery";
@ -27,14 +29,29 @@ export const navigateUserToOrg = async (router: NextRouter, organizationId?: str
}
};
export const navigateUserToSelectOrg = (router: NextRouter, cliCallbackPort?: string) => {
queryClient.invalidateQueries(userKeys.getUser);
export const useNavigateToSelectOrganization = () => {
const { config } = useServerConfig();
const selectOrganization = useSelectOrganization();
const router = useRouter();
let redirectTo = "/login/select-organization";
const navigate = async (cliCallbackPort?: string) => {
if (config.defaultAuthOrgId) {
await selectOrganization.mutateAsync({
organizationId: config.defaultAuthOrgId
});
if (cliCallbackPort) {
redirectTo += `?callback_port=${cliCallbackPort}`;
}
await navigateUserToOrg(router, config.defaultAuthOrgId);
}
router.push(redirectTo, undefined, { shallow: true });
queryClient.invalidateQueries(userKeys.getUser);
let redirectTo = "/login/select-organization";
if (cliCallbackPort) {
redirectTo += `?callback_port=${cliCallbackPort}`;
}
router.push(redirectTo, undefined, { shallow: true });
};
return { navigateToSelectOrganization: navigate };
};

View File

@ -4,15 +4,19 @@ import { useRouter } from "next/router";
import { createNotification } from "@app/components/notifications";
import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { loginLDAPRedirect } from "@app/hooks/api/auth/queries";
export const LoginLDAP = () => {
const router = useRouter();
const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const passedOrgSlug = queryParams.get("organizationSlug");
const passedUsername = queryParams.get("username");
const [organizationSlug, setOrganizationSlug] = useState(passedOrgSlug || "");
const [organizationSlug, setOrganizationSlug] = useState(
config.defaultAuthOrgSlug || passedOrgSlug || ""
);
const [username, setUsername] = useState(passedUsername || "");
const [password, setPassword] = useState("");
@ -63,21 +67,22 @@ export const LoginLDAP = () => {
What&apos;s your LDAP Login?
</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="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
<Input
value={organizationSlug}
onChange={(e) => setOrganizationSlug(e.target.value)}
type="text"
placeholder="Enter your organization slug..."
isRequired
autoComplete="email"
id="email"
className="h-12"
isDisabled={passedOrgSlug !== null}
/>
{!config.defaultAuthOrgSlug && !passedOrgSlug && (
<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="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
<Input
value={organizationSlug}
onChange={(e) => setOrganizationSlug(e.target.value)}
type="text"
placeholder="Enter your organization slug..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
</div>
)}
<div className="relative mx-auto mt-2 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="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
<Input

View File

@ -1,4 +1,4 @@
import { FormEvent, useEffect, useRef, useState } from "react";
import { FormEvent, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@ -16,7 +16,7 @@ import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useFetchServerStatus } from "@app/hooks/api";
import { navigateUserToSelectOrg } from "../../Login.utils";
import { useNavigateToSelectOrganization } from "../../Login.utils";
type Props = {
setStep: (step: number) => void;
@ -39,14 +39,28 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const captchaRef = useRef<HCaptcha>(null);
const { data: serverDetails } = useFetchServerStatus();
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
const redirectToSaml = (orgSlug: string) => {
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${orgSlug}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`;
router.push(redirectUrl);
};
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) redirectToSaml(serverDetails.samlDefaultOrgSlug);
}, [serverDetails?.samlDefaultOrgSlug]);
const handleSaml = useCallback((step: number) => {
if (config.defaultAuthOrgSlug) {
redirectToSaml(config.defaultAuthOrgSlug);
} else {
setStep(step);
}
}, []);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
@ -73,7 +87,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
return;
}
navigateUserToSelectOrg(router, callbackPort!);
navigateToSelectOrganization(callbackPort!);
} else {
setLoginError(true);
createNotification({
@ -98,7 +112,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
return;
}
navigateUserToSelectOrg(router);
navigateToSelectOrganization();
// case: login does not require MFA step
createNotification({
@ -209,7 +223,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(2);
handleSaml(2);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
@ -217,6 +231,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

@ -16,7 +16,7 @@ import { useSelectOrganization, verifyMfaToken } from "@app/hooks/api/auth/queri
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils";
import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils";
// The style for the verification code input
const props = {
@ -50,6 +50,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
const [isLoading, setIsLoading] = useState(false);
const [isLoadingResend, setIsLoadingResend] = useState(false);
const [mfaCode, setMfaCode] = useState("");
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
const { t } = useTranslation();
@ -93,7 +94,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
// case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) {
navigateUserToSelectOrg(router, callbackPort);
navigateToSelectOrganization(callbackPort);
}
// case: no orgs found, so we navigate the user to create an org
// cli login will fail in this case
@ -166,7 +167,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
// case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) {
navigateUserToSelectOrg(router, callbackPort);
navigateToSelectOrganization(callbackPort);
}
// case: no orgs found, so we navigate the user to create an org
// cli login will fail in this case
@ -195,7 +196,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
if (organizationId) {
await navigateUserToOrg(router, organizationId);
} else {
navigateUserToSelectOrg(router);
navigateToSelectOrganization();
}
} else {
createNotification({

View File

@ -1,4 +1,4 @@
import { useEffect, useRef,useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@ -16,7 +16,7 @@ import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils";
import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils";
type Props = {
providerAuthToken: string;
@ -39,8 +39,11 @@ export const PasswordStep = ({
const { mutateAsync: selectOrganization } = useSelectOrganization();
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
const { callbackPort, organizationId, hasExchangedPrivateKey } =
jwt_decode(providerAuthToken) as any;
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
const { callbackPort, organizationId, hasExchangedPrivateKey } = jwt_decode(
providerAuthToken
) as any;
const handleExchange = async () => {
try {
@ -92,7 +95,7 @@ export const PasswordStep = ({
// case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) {
navigateUserToSelectOrg(router, callbackPort);
navigateToSelectOrganization(callbackPort);
}
// case: no orgs found, so we navigate the user to create an org
else {
@ -176,7 +179,7 @@ export const PasswordStep = ({
// case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) {
navigateUserToSelectOrg(router, callbackPort);
navigateToSelectOrganization(callbackPort);
}
// case: no orgs found, so we navigate the user to create an org
else {
@ -220,7 +223,7 @@ export const PasswordStep = ({
const userOrgs = await fetchOrganizations();
if (userOrgs.length > 0) {
navigateUserToSelectOrg(router);
navigateToSelectOrganization();
} else {
await navigateUserToOrg(router);
}
@ -270,7 +273,7 @@ export const PasswordStep = ({
<form onSubmit={handleLogin} className="mx-auto h-full w-full max-w-md px-6 pt-8">
<div className="mb-8">
<p className="mx-auto mb-4 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 Infisical password?
What&apos;s your Infisical password?
</p>
</div>
<div className="relative mx-auto flex max-h-24 w-1/4 w-full min-w-[22rem] items-center justify-center rounded-lg md:max-h-28 lg:w-1/6">

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

@ -24,6 +24,7 @@ const schema = z.object({
caId: z.string(),
friendlyName: z.string(),
commonName: z.string().trim().min(1),
altNames: z.string(),
ttl: z.string().trim()
});
@ -71,6 +72,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
caId: cert.caId,
friendlyName: cert.friendlyName,
commonName: cert.commonName,
altNames: cert.altNames,
ttl: ""
});
} else {
@ -78,12 +80,13 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
caId: "",
friendlyName: "",
commonName: "",
altNames: "",
ttl: ""
});
}
}, [cert]);
const onFormSubmit = async ({ caId, friendlyName, commonName, ttl }: FormData) => {
const onFormSubmit = async ({ caId, friendlyName, commonName, altNames, ttl }: FormData) => {
try {
if (!currentWorkspace?.slug) return;
@ -92,6 +95,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
caId,
friendlyName,
commonName,
altNames,
ttl
});
@ -192,6 +196,24 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="altNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Alternative Names (SANs)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="app1.acme.com, app2.acme.com, ..."
isDisabled={Boolean(cert)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"

Some files were not shown because too many files have changed in this diff Show More