Skip to content

Commit

Permalink
feat: added support for limiting email domains
Browse files Browse the repository at this point in the history
  • Loading branch information
sheensantoscapadngan committed Jun 18, 2024
1 parent 0685a5e commit 18e6957
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 83 deletions.
1 change: 1 addition & 0 deletions backend/src/db/migrations/20240617041053_add-oidc-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export async function up(knex: Knex): Promise<void> {
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();
Expand Down
1 change: 1 addition & 0 deletions backend/src/db/schemas/oidc-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
33 changes: 31 additions & 2 deletions backend/src/ee/routes/v1/oidc-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(),
Expand All @@ -189,6 +203,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
tokenEndpoint: true,
userinfoEndpoint: true,
orgId: true,
allowedEmailDomains: true,
isActive: true
})
}
Expand All @@ -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(),
Expand All @@ -233,7 +261,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
tokenEndpoint: true,
userinfoEndpoint: true,
orgId: true,
isActive: true
isActive: true,
allowedEmailDomains: true
})
}
},
Expand Down
173 changes: 93 additions & 80 deletions backend/src/ee/services/oidc/oidc-config-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -346,6 +348,7 @@ export const oidcConfigServiceFactory = ({
});

const updateQuery: TOidcConfigsUpdate = {
allowedEmailDomains,
issuer,
authorizationEndpoint,
tokenEndpoint,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -477,6 +480,7 @@ export const oidcConfigServiceFactory = ({
issuer,
isActive,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions backend/src/ee/services/oidc/oidc-config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type TGetOidcCfgDTO =
export type TCreateOidcCfgDTO = {
issuer: string;
authorizationEndpoint: string;
allowedEmailDomains: string;
jwksUri: string;
tokenEndpoint: string;
userinfoEndpoint: string;
Expand All @@ -34,6 +35,7 @@ export type TCreateOidcCfgDTO = {
export type TUpdateOidcCfgDTO = Partial<{
issuer: string;
authorizationEndpoint: string;
allowedEmailDomains: string;
jwksUri: string;
tokenEndpoint: string;
userinfoEndpoint: string;
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/hooks/api/oidcConfig/mutations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ export const useUpdateOIDCConfig = () => {
jwksUri,
tokenEndpoint,
userinfoEndpoint,
allowedEmailDomains,
clientId,
clientSecret,
isActive,
orgSlug
}: {
allowedEmailDomains?: string;
issuer?: string;
authorizationEndpoint?: string;
jwksUri?: string;
Expand All @@ -30,6 +32,7 @@ export const useUpdateOIDCConfig = () => {
}) => {
const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
issuer,
allowedEmailDomains,
authorizationEndpoint,
jwksUri,
tokenEndpoint,
Expand All @@ -54,6 +57,7 @@ export const useCreateOIDCConfig = () => {
mutationFn: async ({
issuer,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/api/oidcConfig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export type OIDCConfigData = {
orgId: string;
clientId: string;
clientSecret: string;
allowedEmailDomains?: string;
};
Loading

0 comments on commit 18e6957

Please sign in to comment.