mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 09:26:47 +00:00
feat: oidc poc
This commit is contained in:
Generated
+39
@@ -55,6 +55,7 @@
|
|||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
|
"openid-client": "^5.6.5",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
@@ -9445,6 +9446,14 @@
|
|||||||
"node": ">= 0.6.0"
|
"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": {
|
"node_modules/joycon": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||||
@@ -10570,6 +10579,14 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.1",
|
"version": "1.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||||
@@ -10693,6 +10710,14 @@
|
|||||||
"@octokit/core": ">=5"
|
"@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": {
|
"node_modules/on-exit-leak-free": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||||
@@ -10739,6 +10764,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||||
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="
|
"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": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.3",
|
"version": "0.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
|
"openid-client": "^5.6.5",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
|
|||||||
Vendored
+2
@@ -40,6 +40,7 @@ import { TIdentityProjectServiceFactory } from "@app/services/identity-project/i
|
|||||||
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||||
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
|
import { TOidcConfigServiceFactory } from "@app/services/oidc/oidc-config-service";
|
||||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||||
@@ -99,6 +100,7 @@ declare module "fastify" {
|
|||||||
permission: TPermissionServiceFactory;
|
permission: TPermissionServiceFactory;
|
||||||
org: TOrgServiceFactory;
|
org: TOrgServiceFactory;
|
||||||
orgRole: TOrgRoleServiceFactory;
|
orgRole: TOrgRoleServiceFactory;
|
||||||
|
oidc: TOidcConfigServiceFactory;
|
||||||
superAdmin: TSuperAdminServiceFactory;
|
superAdmin: TSuperAdminServiceFactory;
|
||||||
user: TUserServiceFactory;
|
user: TUserServiceFactory;
|
||||||
group: TGroupServiceFactory;
|
group: TGroupServiceFactory;
|
||||||
|
|||||||
Vendored
+2
@@ -255,6 +255,7 @@ import {
|
|||||||
TWebhooksInsert,
|
TWebhooksInsert,
|
||||||
TWebhooksUpdate
|
TWebhooksUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { TOidcConfigs, TOidcConfigsInsert, TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
||||||
|
|
||||||
declare module "knex/types/tables" {
|
declare module "knex/types/tables" {
|
||||||
interface Tables {
|
interface Tables {
|
||||||
@@ -497,6 +498,7 @@ declare module "knex/types/tables" {
|
|||||||
TDynamicSecretLeasesUpdate
|
TDynamicSecretLeasesUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
|
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
|
||||||
|
[TableName.OidcConfig]: Knex.CompositeTableType<TOidcConfigs, TOidcConfigsInsert, TOidcConfigsUpdate>;
|
||||||
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
|
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
|
||||||
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
|
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
|
||||||
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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("issuer");
|
||||||
|
tb.string("authorizationEndpoint");
|
||||||
|
tb.string("jwksUri");
|
||||||
|
tb.string("tokenEndpoint");
|
||||||
|
tb.string("userinfoEndpoint");
|
||||||
|
tb.text("encryptedClientId");
|
||||||
|
tb.string("clientIdIV");
|
||||||
|
tb.string("clientIdTag");
|
||||||
|
tb.text("encryptedClientSecret");
|
||||||
|
tb.string("clientSecretIV");
|
||||||
|
tb.string("clientSecretTag");
|
||||||
|
tb.boolean("isActive").notNullable();
|
||||||
|
tb.timestamps(true, true, true);
|
||||||
|
tb.uuid("orgId").notNullable().unique();
|
||||||
|
tb.foreign("orgId").references("id").inTable(TableName.Organization);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.OidcConfig);
|
||||||
|
}
|
||||||
@@ -71,6 +71,7 @@ export enum TableName {
|
|||||||
SecretRotationOutput = "secret_rotation_outputs",
|
SecretRotationOutput = "secret_rotation_outputs",
|
||||||
SamlConfig = "saml_configs",
|
SamlConfig = "saml_configs",
|
||||||
LdapConfig = "ldap_configs",
|
LdapConfig = "ldap_configs",
|
||||||
|
OidcConfig = "oidc_configs",
|
||||||
LdapGroupMap = "ldap_group_maps",
|
LdapGroupMap = "ldap_group_maps",
|
||||||
AuditLog = "audit_logs",
|
AuditLog = "audit_logs",
|
||||||
AuditLogStream = "audit_log_streams",
|
AuditLogStream = "audit_log_streams",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// 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(),
|
||||||
|
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().nullable().optional(),
|
||||||
|
clientIdIV: z.string().nullable().optional(),
|
||||||
|
clientIdTag: z.string().nullable().optional(),
|
||||||
|
encryptedClientSecret: z.string().nullable().optional(),
|
||||||
|
clientSecretIV: z.string().nullable().optional(),
|
||||||
|
clientSecretTag: 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>>;
|
||||||
@@ -101,6 +101,7 @@ import { integrationAuthServiceFactory } from "@app/services/integration-auth/in
|
|||||||
import { kmsDALFactory } from "@app/services/kms/kms-dal";
|
import { kmsDALFactory } from "@app/services/kms/kms-dal";
|
||||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { oidcConfigServiceFactory } from "@app/services/oidc/oidc-config-service";
|
||||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||||
@@ -838,6 +839,16 @@ export const registerRoutes = async (
|
|||||||
secretSharingDAL
|
secretSharingDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const oidcService = oidcConfigServiceFactory({
|
||||||
|
orgDAL,
|
||||||
|
orgMembershipDAL,
|
||||||
|
userDAL,
|
||||||
|
userAliasDAL,
|
||||||
|
licenseService,
|
||||||
|
tokenService,
|
||||||
|
smtpService
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
//
|
//
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@@ -858,6 +869,7 @@ export const registerRoutes = async (
|
|||||||
permission: permissionService,
|
permission: permissionService,
|
||||||
org: orgService,
|
org: orgService,
|
||||||
orgRole: orgRoleService,
|
orgRole: orgRoleService,
|
||||||
|
oidc: oidcService,
|
||||||
apiKey: apiKeyService,
|
apiKey: apiKeyService,
|
||||||
authToken: tokenService,
|
authToken: tokenService,
|
||||||
superAdmin: superAdminService,
|
superAdmin: superAdminService,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { registerIdentityUaRouter } from "./identity-ua";
|
|||||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||||
import { registerIntegrationRouter } from "./integration-router";
|
import { registerIntegrationRouter } from "./integration-router";
|
||||||
import { registerInviteOrgRouter } from "./invite-org-router";
|
import { registerInviteOrgRouter } from "./invite-org-router";
|
||||||
|
import { registerOidcRouter } from "./oidc-router";
|
||||||
import { registerOrgRouter } from "./organization-router";
|
import { registerOrgRouter } from "./organization-router";
|
||||||
import { registerPasswordRouter } from "./password-router";
|
import { registerPasswordRouter } from "./password-router";
|
||||||
import { registerProjectEnvRouter } from "./project-env-router";
|
import { registerProjectEnvRouter } from "./project-env-router";
|
||||||
@@ -29,6 +30,7 @@ import { registerWebhookRouter } from "./webhook-router";
|
|||||||
|
|
||||||
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||||
await server.register(registerSsoRouter, { prefix: "/sso" });
|
await server.register(registerSsoRouter, { prefix: "/sso" });
|
||||||
|
await server.register(registerOidcRouter, { prefix: "/oidc" });
|
||||||
await server.register(
|
await server.register(
|
||||||
async (authRouter) => {
|
async (authRouter) => {
|
||||||
await authRouter.register(registerAuthRoutes);
|
await authRouter.register(registerAuthRoutes);
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/* 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 { z } from "zod";
|
||||||
|
|
||||||
|
import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
|
export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" });
|
||||||
|
await server.register(fastifySession, {
|
||||||
|
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
|
||||||
|
cookie: {
|
||||||
|
secure: false // has to be set to false if testing locally
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await server.register(passport.initialize());
|
||||||
|
await server.register(passport.secureSession());
|
||||||
|
|
||||||
|
// redirect to IDP for login
|
||||||
|
server.route({
|
||||||
|
url: "/login",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
orgSlug: z.string().trim()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
// get params, save to session
|
||||||
|
const { orgSlug } = req.params;
|
||||||
|
req.session.set<any>("oidcOrgSlug", orgSlug);
|
||||||
|
const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(orgSlug);
|
||||||
|
(
|
||||||
|
passport.authenticate(oidcStrategy as Strategy, {
|
||||||
|
scope: "profile email openid"
|
||||||
|
}) as any
|
||||||
|
)(req, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// callback route after login from IDP
|
||||||
|
server.route({
|
||||||
|
url: "/callback",
|
||||||
|
method: "GET",
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const oidcOrgSlug = req.session.get<any>("oidcOrgSlug");
|
||||||
|
const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(oidcOrgSlug);
|
||||||
|
await (
|
||||||
|
passport.authenticate(oidcStrategy as Strategy, {
|
||||||
|
failureRedirect: "/api/v1/oidc/login/error",
|
||||||
|
session: false,
|
||||||
|
failureMessage: true
|
||||||
|
}) as any
|
||||||
|
)(req, res);
|
||||||
|
|
||||||
|
if (req.passportUser.isUserCompleted) {
|
||||||
|
return res.redirect(
|
||||||
|
`http://localhost:8080/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// signup
|
||||||
|
return res.redirect(
|
||||||
|
`http://localhost:8080/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/login/error",
|
||||||
|
method: "GET",
|
||||||
|
handler: (req, res) => {
|
||||||
|
return res.status(500).send({
|
||||||
|
error: "Authentication error",
|
||||||
|
details: req.query
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/config",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
orgSlug: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: OidcConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
issuer: true,
|
||||||
|
authorizationEndpoint: true,
|
||||||
|
jwksUri: true,
|
||||||
|
tokenEndpoint: true,
|
||||||
|
userinfoEndpoint: true,
|
||||||
|
orgId: 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -196,7 +196,10 @@ export const authLoginServiceFactory = ({
|
|||||||
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
|
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
|
||||||
|
|
||||||
authMethod = decodedProviderToken.authMethod;
|
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;
|
organizationId = decodedProviderToken.orgId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export enum AuthMethod {
|
|||||||
JUMPCLOUD_SAML = "jumpcloud-saml",
|
JUMPCLOUD_SAML = "jumpcloud-saml",
|
||||||
GOOGLE_SAML = "google-saml",
|
GOOGLE_SAML = "google-saml",
|
||||||
KEYCLOAK_SAML = "keycloak-saml",
|
KEYCLOAK_SAML = "keycloak-saml",
|
||||||
LDAP = "ldap"
|
LDAP = "ldap",
|
||||||
|
OIDC = "oidc"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthTokenType {
|
export enum AuthTokenType {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
};
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||||
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
|
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { normalizeUsername } from "../user/user-fns";
|
||||||
|
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||||
|
import { UserAliasType } from "../user-alias/user-alias-types";
|
||||||
|
import { TOidcLoginDTO } from "./oidc-config-types";
|
||||||
|
|
||||||
|
type TOidcConfigServiceFactoryDep = {
|
||||||
|
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">;
|
||||||
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
|
orgDAL: Pick<
|
||||||
|
TOrgDALFactory,
|
||||||
|
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||||
|
>;
|
||||||
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
|
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||||
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
|
||||||
|
|
||||||
|
export const oidcConfigServiceFactory = ({
|
||||||
|
orgDAL,
|
||||||
|
orgMembershipDAL,
|
||||||
|
userDAL,
|
||||||
|
userAliasDAL,
|
||||||
|
licenseService,
|
||||||
|
tokenService,
|
||||||
|
smtpService
|
||||||
|
}: TOidcConfigServiceFactoryDep) => {
|
||||||
|
const oidcLogin = async ({ externalId, email, firstName, lastName, orgId }: TOidcLoginDTO) => {
|
||||||
|
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 (!newUser) {
|
||||||
|
const uniqueUsername = await normalizeUsername(externalId, userDAL);
|
||||||
|
newUser = await userDAL.create(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
isEmailVerified: false,
|
||||||
|
username: 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 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,
|
||||||
|
authMethod: AuthMethod.OIDC,
|
||||||
|
authType: UserAliasType.OIDC,
|
||||||
|
isUserCompleted
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Sheen update oidc config
|
||||||
|
// await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
||||||
|
return { oidcLogin };
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { TGenericPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TOidcLoginDTO = {
|
||||||
|
externalId: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName?: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetOidcCfgDTO = {
|
||||||
|
type: "internal" | "external";
|
||||||
|
orgSlug: string;
|
||||||
|
} & TGenericPermission;
|
||||||
|
|
||||||
|
export type TUpdateOidcCfgDTO = Partial<{
|
||||||
|
issuer: string;
|
||||||
|
authorizationEndpoint: string;
|
||||||
|
jwksUri: string;
|
||||||
|
tokenEndpoint: string;
|
||||||
|
userinfoEndpoint: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
isActive: boolean;
|
||||||
|
orgSlug: string;
|
||||||
|
}> &
|
||||||
|
TGenericPermission;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum UserAliasType {
|
export enum UserAliasType {
|
||||||
LDAP = "ldap",
|
LDAP = "ldap",
|
||||||
SAML = "saml"
|
SAML = "saml",
|
||||||
|
OIDC = "oidc"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export * from "./integrationAuth";
|
|||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
export * from "./ldapConfig";
|
export * from "./ldapConfig";
|
||||||
|
export * from "./oidcConfig";
|
||||||
export * from "./organization";
|
export * from "./organization";
|
||||||
export * from "./projectUserAdditionalPrivilege";
|
export * from "./projectUserAdditionalPrivilege";
|
||||||
export * from "./rateLimit";
|
export * from "./rateLimit";
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./queries";
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
export const oidcConfigKeys = {
|
||||||
|
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetOIDCConfig = (orgSlug: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: oidcConfigKeys.getOIDCConfig(orgSlug),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get(`/api/v1/oidc/config?orgSlug=${orgSlug}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user