feat: added support for limiting email domains

This commit is contained in:
Sheen Capadngan
2024-06-19 01:29:26 +08:00
parent 0685a5ea8b
commit 18e69578f0
8 changed files with 154 additions and 83 deletions

View File

@@ -17,6 +17,7 @@ export async function up(knex: Knex): Promise<void> {
tb.text("encryptedClientSecret").notNullable(); tb.text("encryptedClientSecret").notNullable();
tb.string("clientSecretIV").notNullable(); tb.string("clientSecretIV").notNullable();
tb.string("clientSecretTag").notNullable(); tb.string("clientSecretTag").notNullable();
tb.string("allowedEmailDomains").nullable();
tb.boolean("isActive").notNullable(); tb.boolean("isActive").notNullable();
tb.timestamps(true, true, true); tb.timestamps(true, true, true);
tb.uuid("orgId").notNullable().unique(); tb.uuid("orgId").notNullable().unique();

View File

@@ -20,6 +20,7 @@ export const OidcConfigsSchema = z.object({
encryptedClientSecret: z.string(), encryptedClientSecret: z.string(),
clientSecretIV: z.string(), clientSecretIV: z.string(),
clientSecretTag: z.string(), clientSecretTag: z.string(),
allowedEmailDomains: z.string().nullable().optional(),
isActive: z.boolean(), isActive: z.boolean(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),

View File

@@ -137,7 +137,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
tokenEndpoint: true, tokenEndpoint: true,
userinfoEndpoint: true, userinfoEndpoint: true,
isActive: true, isActive: true,
orgId: true orgId: true,
allowedEmailDomains: true
}).extend({ }).extend({
clientId: z.string(), clientId: z.string(),
clientSecret: z.string() clientSecret: z.string()
@@ -169,6 +170,19 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z body: z
.object({ .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(), issuer: z.string().trim(),
authorizationEndpoint: z.string().trim(), authorizationEndpoint: z.string().trim(),
jwksUri: z.string().trim(), jwksUri: z.string().trim(),
@@ -189,6 +203,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
tokenEndpoint: true, tokenEndpoint: true,
userinfoEndpoint: true, userinfoEndpoint: true,
orgId: true, orgId: true,
allowedEmailDomains: true,
isActive: true isActive: true
}) })
} }
@@ -215,6 +230,19 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z.object({ body: z.object({
issuer: z.string().trim(), 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(), authorizationEndpoint: z.string().trim(),
jwksUri: z.string().trim(), jwksUri: z.string().trim(),
tokenEndpoint: z.string().trim(), tokenEndpoint: z.string().trim(),
@@ -233,7 +261,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
tokenEndpoint: true, tokenEndpoint: true,
userinfoEndpoint: true, userinfoEndpoint: true,
orgId: true, orgId: true,
isActive: true isActive: true,
allowedEmailDomains: true
}) })
} }
}, },

View File

@@ -63,6 +63,86 @@ export const oidcConfigServiceFactory = ({
smtpService, smtpService,
oidcConfigDAL oidcConfigDAL
}: TOidcConfigServiceFactoryDep) => { }: 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 oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const userAlias = await userAliasDAL.findOne({ const userAlias = await userAliasDAL.findOne({
@@ -216,87 +296,9 @@ export const oidcConfigServiceFactory = ({
return { isUserCompleted, providerAuthToken }; 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 ({ const updateOidcCfg = async ({
orgSlug, orgSlug,
allowedEmailDomains,
actor, actor,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
@@ -346,6 +348,7 @@ export const oidcConfigServiceFactory = ({
}); });
const updateQuery: TOidcConfigsUpdate = { const updateQuery: TOidcConfigsUpdate = {
allowedEmailDomains,
issuer, issuer,
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
@@ -374,12 +377,12 @@ export const oidcConfigServiceFactory = ({
} }
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
return ssoConfig; return ssoConfig;
}; };
const createOidcCfg = async ({ const createOidcCfg = async ({
orgSlug, orgSlug,
allowedEmailDomains,
actor, actor,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
@@ -477,6 +480,7 @@ export const oidcConfigServiceFactory = ({
issuer, issuer,
isActive, isActive,
authorizationEndpoint, authorizationEndpoint,
allowedEmailDomains,
jwksUri, jwksUri,
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, 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({ oidcLogin({
email: claims.email, email: claims.email,
externalId: claims.sub, externalId: claims.sub,

View File

@@ -22,6 +22,7 @@ export type TGetOidcCfgDTO =
export type TCreateOidcCfgDTO = { export type TCreateOidcCfgDTO = {
issuer: string; issuer: string;
authorizationEndpoint: string; authorizationEndpoint: string;
allowedEmailDomains: string;
jwksUri: string; jwksUri: string;
tokenEndpoint: string; tokenEndpoint: string;
userinfoEndpoint: string; userinfoEndpoint: string;
@@ -34,6 +35,7 @@ export type TCreateOidcCfgDTO = {
export type TUpdateOidcCfgDTO = Partial<{ export type TUpdateOidcCfgDTO = Partial<{
issuer: string; issuer: string;
authorizationEndpoint: string; authorizationEndpoint: string;
allowedEmailDomains: string;
jwksUri: string; jwksUri: string;
tokenEndpoint: string; tokenEndpoint: string;
userinfoEndpoint: string; userinfoEndpoint: string;

View File

@@ -13,11 +13,13 @@ export const useUpdateOIDCConfig = () => {
jwksUri, jwksUri,
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,
allowedEmailDomains,
clientId, clientId,
clientSecret, clientSecret,
isActive, isActive,
orgSlug orgSlug
}: { }: {
allowedEmailDomains?: string;
issuer?: string; issuer?: string;
authorizationEndpoint?: string; authorizationEndpoint?: string;
jwksUri?: string; jwksUri?: string;
@@ -30,6 +32,7 @@ export const useUpdateOIDCConfig = () => {
}) => { }) => {
const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", { const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
issuer, issuer,
allowedEmailDomains,
authorizationEndpoint, authorizationEndpoint,
jwksUri, jwksUri,
tokenEndpoint, tokenEndpoint,
@@ -54,6 +57,7 @@ export const useCreateOIDCConfig = () => {
mutationFn: async ({ mutationFn: async ({
issuer, issuer,
authorizationEndpoint, authorizationEndpoint,
allowedEmailDomains,
jwksUri, jwksUri,
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,
@@ -71,10 +75,12 @@ export const useCreateOIDCConfig = () => {
clientSecret: string; clientSecret: string;
isActive: boolean; isActive: boolean;
orgSlug: string; orgSlug: string;
allowedEmailDomains?: string;
}) => { }) => {
const { data } = await apiRequest.post("/api/v1/sso/oidc/config", { const { data } = await apiRequest.post("/api/v1/sso/oidc/config", {
issuer, issuer,
authorizationEndpoint, authorizationEndpoint,
allowedEmailDomains,
jwksUri, jwksUri,
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,

View File

@@ -9,4 +9,5 @@ export type OIDCConfigData = {
orgId: string; orgId: string;
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
allowedEmailDomains?: string;
}; };

View File

@@ -23,7 +23,8 @@ const schema = z.object({
tokenEndpoint: z.string().min(1), tokenEndpoint: z.string().min(1),
userinfoEndpoint: z.string().min(1), userinfoEndpoint: z.string().min(1),
clientId: 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<typeof schema>; export type OIDCFormData = z.infer<typeof schema>;
@@ -48,12 +49,14 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
setValue("userinfoEndpoint", data.userinfoEndpoint); setValue("userinfoEndpoint", data.userinfoEndpoint);
setValue("clientId", data.clientId); setValue("clientId", data.clientId);
setValue("clientSecret", data.clientSecret); setValue("clientSecret", data.clientSecret);
setValue("allowedEmailDomains", data.allowedEmailDomains);
} }
}, [data]); }, [data]);
const onOIDCModalSubmit = async ({ const onOIDCModalSubmit = async ({
issuer, issuer,
authorizationEndpoint, authorizationEndpoint,
allowedEmailDomains,
jwksUri, jwksUri,
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,
@@ -67,6 +70,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
await createMutateAsync({ await createMutateAsync({
issuer, issuer,
authorizationEndpoint, authorizationEndpoint,
allowedEmailDomains,
jwksUri, jwksUri,
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,
@@ -79,6 +83,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
await updateMutateAsync({ await updateMutateAsync({
issuer, issuer,
authorizationEndpoint, authorizationEndpoint,
allowedEmailDomains,
jwksUri, jwksUri,
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,
@@ -187,6 +192,19 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="allowedEmailDomains"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Email Domains (defaults to any)"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="infisical.com, google.com" autoComplete="off" />
</FormControl>
)}
/>
<Controller <Controller
control={control} control={control}
name="clientId" name="clientId"