diff --git a/apps/backend/prisma/migrations/20240905201445_ms_tenant/migration.sql b/apps/backend/prisma/migrations/20240905201445_ms_tenant/migration.sql new file mode 100644 index 000000000..b6b4c8b5a --- /dev/null +++ b/apps/backend/prisma/migrations/20240905201445_ms_tenant/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "StandardOAuthProviderConfig" ADD COLUMN "microsoftTenantId" TEXT; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 1690a9ad2..81655092c 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -566,7 +566,9 @@ model StandardOAuthProviderConfig { // optional extra parameters for specific oauth providers // Facebook business integration requires a config_id - facebookConfigId String? + facebookConfigId String? + // Microsoft organizational directory (tenant) + microsoftTenantId String? @@id([projectConfigId, id]) } diff --git a/apps/backend/src/app/api/v1/internal/projects/crud.tsx b/apps/backend/src/app/api/v1/internal/projects/crud.tsx index 3d61e8705..4722590bd 100644 --- a/apps/backend/src/app/api/v1/internal/projects/crud.tsx +++ b/apps/backend/src/app/api/v1/internal/projects/crud.tsx @@ -72,6 +72,7 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand clientId: item.client_id ?? throwErr('client_id is required'), clientSecret: item.client_secret ?? throwErr('client_secret is required'), facebookConfigId: item.facebook_config_id, + microsoftTenantId: item.microsoft_tenant_id, } } : undefined, })) diff --git a/apps/backend/src/app/api/v1/projects/current/crud.tsx b/apps/backend/src/app/api/v1/projects/current/crud.tsx index f84d384e6..4a98d7b20 100644 --- a/apps/backend/src/app/api/v1/projects/current/crud.tsx +++ b/apps/backend/src/app/api/v1/projects/current/crud.tsx @@ -194,6 +194,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro clientId: providerUpdate.client_id ?? throwErr('client_id is required'), clientSecret: providerUpdate.client_secret ?? throwErr('client_secret is required'), facebookConfigId: providerUpdate.facebook_config_id, + microsoftTenantId: providerUpdate.microsoft_tenant_id, }, }, }; @@ -227,6 +228,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro clientId: provider.update.client_id ?? throwErr('client_id is required'), clientSecret: provider.update.client_secret ?? throwErr('client_secret is required'), facebookConfigId: provider.update.facebook_config_id, + microsoftTenantId: provider.update.microsoft_tenant_id, }, }, }; diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index f07b10117..5fd4da785 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -68,6 +68,7 @@ export function projectPrismaToCrud( client_id?: string, client_secret?: string , facebook_config_id?: string, + microsoft_tenant_id?: string, }[] => { if (provider.proxiedOAuthConfig) { return [{ @@ -83,6 +84,7 @@ export function projectPrismaToCrud( client_id: provider.standardOAuthConfig.clientId, client_secret: provider.standardOAuthConfig.clientSecret, facebook_config_id: provider.standardOAuthConfig.facebookConfigId ?? undefined, + microsoft_tenant_id: provider.standardOAuthConfig.microsoftTenantId ?? undefined, }]; } else { throw new StackAssertionError(`Exactly one of the provider configs should be set on provider config '${provider.id}' of project '${prisma.id}'`, { prisma }); diff --git a/apps/backend/src/oauth/index.tsx b/apps/backend/src/oauth/index.tsx index 8cc1279de..c3403410c 100644 --- a/apps/backend/src/oauth/index.tsx +++ b/apps/backend/src/oauth/index.tsx @@ -51,7 +51,8 @@ export async function getProvider(provider: ProjectsCrud['Admin']['Read']['confi return await _providers[provider.id].create({ clientId: provider.client_id || throwErr("Client ID is required for standard providers"), clientSecret: provider.client_secret || throwErr("Client secret is required for standard providers"), - facebookConfigId: provider.facebook_config_id + facebookConfigId: provider.facebook_config_id, + microsoftTenantId: provider.microsoft_tenant_id, }); } } diff --git a/apps/backend/src/oauth/providers/microsoft.tsx b/apps/backend/src/oauth/providers/microsoft.tsx index 7c8948a2a..858ae5357 100644 --- a/apps/backend/src/oauth/providers/microsoft.tsx +++ b/apps/backend/src/oauth/providers/microsoft.tsx @@ -12,11 +12,12 @@ export class MicrosoftProvider extends OAuthBaseProvider { static async create(options: { clientId: string, clientSecret: string, + microsoftTenantId?: string, }) { return new MicrosoftProvider(...await OAuthBaseProvider.createConstructorArgs({ - issuer: "https://login.microsoftonline.com", - authorizationEndpoint: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize", - tokenEndpoint: "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", + issuer: `https://login.microsoftonline.com${"/" + options.microsoftTenantId || ""}`, + authorizationEndpoint: `https://login.microsoftonline.com/${options.microsoftTenantId || 'consumers'}/oauth2/v2.0/authorize`, + tokenEndpoint: `https://login.microsoftonline.com/${options.microsoftTenantId || 'consumers'}/oauth2/v2.0/token`, redirectUri: getEnvVariable("STACK_BASE_URL") + "/api/v1/auth/oauth/callback/microsoft", baseScope: "User.Read", ...options, diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx index 865e66d4a..db7c89c5a 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx @@ -42,6 +42,7 @@ export const providerFormSchema = yup.object({ otherwise: (schema) => schema.optional() }), facebookConfigId: yup.string().optional(), + microsoftTenantId: yup.string().optional(), }); export type ProviderFormValues = yup.InferType @@ -53,6 +54,7 @@ export function ProviderSettingDialog(props: Props & { open: boolean, onClose: ( clientId: (props.provider as any)?.clientId ?? "", clientSecret: (props.provider as any)?.clientSecret ?? "", facebookConfigId: (props.provider as any)?.facebookConfigId ?? "", + microsoftTenantId: (props.provider as any)?.microsoftTenantId ?? "", }; const onSubmit = async (values: ProviderFormValues) => { @@ -66,6 +68,7 @@ export function ProviderSettingDialog(props: Props & { open: boolean, onClose: ( clientId: values.clientId || "", clientSecret: values.clientSecret || "", facebookConfigId: values.facebookConfigId, + microsoftTenantId: values.microsoftTenantId, }); } }; @@ -130,6 +133,15 @@ export function ProviderSettingDialog(props: Props & { open: boolean, onClose: ( placeholder="Facebook Config ID" /> )} + + {props.id === 'microsoft' && ( + + )} )} diff --git a/packages/stack-shared/src/interface/crud/projects.ts b/packages/stack-shared/src/interface/crud/projects.ts index 7c1f313f4..3f2fdbd47 100644 --- a/packages/stack-shared/src/interface/crud/projects.ts +++ b/packages/stack-shared/src/interface/crud/projects.ts @@ -15,6 +15,7 @@ const oauthProviderSchema = yupObject({ // extra params facebook_config_id: yupString().optional().meta({ openapiField: { description: 'This parameter is the configuration id for Facebook business login (for things like ads and marketing).' } }), + microsoft_tenant_id: yupString().optional().meta({ openapiField: { description: 'This parameter is the Microsoft tenant id for Microsoft directory' } }), }); const enabledOAuthProviderSchema = yupObject({ diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 238e162b7..37a8233c7 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -2006,6 +2006,7 @@ class _StackAdminAppImpl