diff --git a/backend/src/db/migrations/20240617041053_add-oidc-auth.ts b/backend/src/db/migrations/20240617041053_add-oidc-auth.ts index b11e6bbab..8ef34854b 100644 --- a/backend/src/db/migrations/20240617041053_add-oidc-auth.ts +++ b/backend/src/db/migrations/20240617041053_add-oidc-auth.ts @@ -17,6 +17,7 @@ export async function up(knex: Knex): Promise { 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(); diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts index b06114df6..3de22d1d0 100644 --- a/backend/src/db/schemas/oidc-configs.ts +++ b/backend/src/db/schemas/oidc-configs.ts @@ -20,6 +20,7 @@ export const OidcConfigsSchema = z.object({ encryptedClientSecret: z.string(), clientSecretIV: z.string(), clientSecretTag: z.string(), + allowedEmailDomains: z.string().nullable().optional(), isActive: z.boolean(), createdAt: z.date(), updatedAt: z.date(), diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts index 8b68a8aef..d64704dce 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -137,7 +137,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { tokenEndpoint: true, userinfoEndpoint: true, isActive: true, - orgId: true + orgId: true, + allowedEmailDomains: true }).extend({ clientId: z.string(), clientSecret: z.string() @@ -169,6 +170,19 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { 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(", "); + }), issuer: z.string().trim(), authorizationEndpoint: z.string().trim(), jwksUri: z.string().trim(), @@ -189,6 +203,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { tokenEndpoint: true, userinfoEndpoint: true, orgId: true, + allowedEmailDomains: true, isActive: true }) } @@ -215,6 +230,19 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { schema: { body: z.object({ issuer: z.string().trim(), + 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(", "); + }), authorizationEndpoint: z.string().trim(), jwksUri: z.string().trim(), tokenEndpoint: z.string().trim(), @@ -233,7 +261,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { tokenEndpoint: true, userinfoEndpoint: true, orgId: true, - isActive: true + isActive: true, + allowedEmailDomains: true }) } }, diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 7c4b7e87e..3f58036b6 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -63,6 +63,86 @@ export const oidcConfigServiceFactory = ({ 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, + 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 appCfg = getConfig(); const userAlias = await userAliasDAL.findOne({ @@ -216,87 +296,9 @@ export const oidcConfigServiceFactory = ({ return { isUserCompleted, providerAuthToken }; }; - 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, - jwksUri: oidcCfg.jwksUri, - tokenEndpoint: oidcCfg.tokenEndpoint, - userinfoEndpoint: oidcCfg.userinfoEndpoint, - orgId: oidcCfg.orgId, - isActive: oidcCfg.isActive, - clientId, - clientSecret - }; - }; - const updateOidcCfg = async ({ orgSlug, + allowedEmailDomains, actor, actorOrgId, actorAuthMethod, @@ -346,6 +348,7 @@ export const oidcConfigServiceFactory = ({ }); const updateQuery: TOidcConfigsUpdate = { + allowedEmailDomains, issuer, authorizationEndpoint, tokenEndpoint, @@ -374,12 +377,12 @@ export const oidcConfigServiceFactory = ({ } const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); - return ssoConfig; }; const createOidcCfg = async ({ orgSlug, + allowedEmailDomains, actor, actorOrgId, actorAuthMethod, @@ -477,6 +480,7 @@ export const oidcConfigServiceFactory = ({ issuer, isActive, authorizationEndpoint, + allowedEmailDomains, jwksUri, tokenEndpoint, userinfoEndpoint, @@ -544,6 +548,15 @@ export const oidcConfigServiceFactory = ({ }); } + 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, diff --git a/backend/src/ee/services/oidc/oidc-config-types.ts b/backend/src/ee/services/oidc/oidc-config-types.ts index 4c729ceea..f773bbe92 100644 --- a/backend/src/ee/services/oidc/oidc-config-types.ts +++ b/backend/src/ee/services/oidc/oidc-config-types.ts @@ -22,6 +22,7 @@ export type TGetOidcCfgDTO = export type TCreateOidcCfgDTO = { issuer: string; authorizationEndpoint: string; + allowedEmailDomains: string; jwksUri: string; tokenEndpoint: string; userinfoEndpoint: string; @@ -34,6 +35,7 @@ export type TCreateOidcCfgDTO = { export type TUpdateOidcCfgDTO = Partial<{ issuer: string; authorizationEndpoint: string; + allowedEmailDomains: string; jwksUri: string; tokenEndpoint: string; userinfoEndpoint: string; diff --git a/frontend/src/hooks/api/oidcConfig/mutations.tsx b/frontend/src/hooks/api/oidcConfig/mutations.tsx index bfcf92d1f..39467e84c 100644 --- a/frontend/src/hooks/api/oidcConfig/mutations.tsx +++ b/frontend/src/hooks/api/oidcConfig/mutations.tsx @@ -13,11 +13,13 @@ export const useUpdateOIDCConfig = () => { jwksUri, tokenEndpoint, userinfoEndpoint, + allowedEmailDomains, clientId, clientSecret, isActive, orgSlug }: { + allowedEmailDomains?: string; issuer?: string; authorizationEndpoint?: string; jwksUri?: string; @@ -30,6 +32,7 @@ export const useUpdateOIDCConfig = () => { }) => { const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", { issuer, + allowedEmailDomains, authorizationEndpoint, jwksUri, tokenEndpoint, @@ -54,6 +57,7 @@ export const useCreateOIDCConfig = () => { mutationFn: async ({ issuer, authorizationEndpoint, + allowedEmailDomains, jwksUri, tokenEndpoint, userinfoEndpoint, @@ -71,10 +75,12 @@ export const useCreateOIDCConfig = () => { clientSecret: string; isActive: boolean; orgSlug: string; + allowedEmailDomains?: string; }) => { const { data } = await apiRequest.post("/api/v1/sso/oidc/config", { issuer, authorizationEndpoint, + allowedEmailDomains, jwksUri, tokenEndpoint, userinfoEndpoint, diff --git a/frontend/src/hooks/api/oidcConfig/types.ts b/frontend/src/hooks/api/oidcConfig/types.ts index 579590e09..457a86cb7 100644 --- a/frontend/src/hooks/api/oidcConfig/types.ts +++ b/frontend/src/hooks/api/oidcConfig/types.ts @@ -9,4 +9,5 @@ export type OIDCConfigData = { orgId: string; clientId: string; clientSecret: string; + allowedEmailDomains?: string; }; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OIDCModal.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OIDCModal.tsx index 55b7924a0..229f437f9 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OIDCModal.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OIDCModal.tsx @@ -23,7 +23,8 @@ const schema = z.object({ tokenEndpoint: z.string().min(1), userinfoEndpoint: z.string().min(1), clientId: z.string().min(1), - clientSecret: z.string().min(1) + clientSecret: z.string().min(1), + allowedEmailDomains: z.string().optional() }); export type OIDCFormData = z.infer; @@ -48,12 +49,14 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) setValue("userinfoEndpoint", data.userinfoEndpoint); setValue("clientId", data.clientId); setValue("clientSecret", data.clientSecret); + setValue("allowedEmailDomains", data.allowedEmailDomains); } }, [data]); const onOIDCModalSubmit = async ({ issuer, authorizationEndpoint, + allowedEmailDomains, jwksUri, tokenEndpoint, userinfoEndpoint, @@ -67,6 +70,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) await createMutateAsync({ issuer, authorizationEndpoint, + allowedEmailDomains, jwksUri, tokenEndpoint, userinfoEndpoint, @@ -79,6 +83,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) await updateMutateAsync({ issuer, authorizationEndpoint, + allowedEmailDomains, jwksUri, tokenEndpoint, userinfoEndpoint, @@ -187,6 +192,19 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) )} /> + ( + + + + )} + />