fix: enforce SSO in invitation signups (#1258)

This commit is contained in:
Philip Okugbe
2025-06-15 20:25:15 +01:00
committed by GitHub
parent 1c674efddd
commit 44445fbf46
7 changed files with 78 additions and 47 deletions

View File

@ -18,6 +18,7 @@ import classes from "@/features/auth/components/auth.module.css";
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
import { useTranslation } from "react-i18next";
import SsoLogin from "@/ee/components/sso-login.tsx";
const formSchema = z.object({
name: z.string().trim().min(1),
@ -71,39 +72,43 @@ export function InviteSignUpForm() {
{t("Join the workspace")}
</Title>
<Stack align="stretch" justify="center" gap="xl">
<form onSubmit={form.onSubmit(onSubmit)}>
<TextInput
id="name"
type="text"
label={t("Name")}
placeholder={t("enter your full name")}
variant="filled"
{...form.getInputProps("name")}
/>
<SsoLogin />
<TextInput
id="email"
type="email"
label={t("Email")}
value={invitation.email}
disabled
variant="filled"
mt="md"
/>
{!invitation.enforceSso && (
<Stack align="stretch" justify="center" gap="xl">
<form onSubmit={form.onSubmit(onSubmit)}>
<TextInput
id="name"
type="text"
label={t("Name")}
placeholder={t("enter your full name")}
variant="filled"
{...form.getInputProps("name")}
/>
<PasswordInput
label={t("Password")}
placeholder={t("Your password")}
variant="filled"
mt="md"
{...form.getInputProps("password")}
/>
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
{t("Sign Up")}
</Button>
</form>
</Stack>
<TextInput
id="email"
type="email"
label={t("Email")}
value={invitation.email}
disabled
variant="filled"
mt="md"
/>
<PasswordInput
label={t("Password")}
placeholder={t("Your password")}
variant="filled"
mt="md"
{...form.getInputProps("password")}
/>
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
{t("Sign Up")}
</Button>
</form>
</Stack>
)}
</Box>
</Container>
);

View File

@ -173,7 +173,7 @@ export function useRevokeInvitationMutation() {
export function useGetInvitationQuery(
invitationId: string,
): UseQueryResult<any, Error> {
): UseQueryResult<IInvitation, Error> {
return useQuery({
queryKey: ["invitations", invitationId],
queryFn: () => getInvitationById({ invitationId }),

View File

@ -35,6 +35,7 @@ export interface IInvitation {
workspaceId: string;
invitedById: string;
createdAt: Date;
enforceSso: boolean;
}
export interface IInvitationLink {

View File

@ -6,3 +6,16 @@ export function validateSsoEnforcement(workspace: Workspace) {
throw new BadRequestException('This workspace has enforced SSO login.');
}
}
export function validateAllowedEmail(userEmail: string, workspace: Workspace) {
const emailParts = userEmail.split('@');
const emailDomain = emailParts[1].toLowerCase();
if (
workspace.emailDomains?.length > 0 &&
!workspace.emailDomains.includes(emailDomain)
) {
throw new BadRequestException(
`The email domain "${emailDomain}" is not approved for this workspace.`,
);
}
}

View File

@ -180,10 +180,13 @@ export class WorkspaceController {
@Public()
@HttpCode(HttpStatus.OK)
@Post('invites/info')
async getInvitationById(@Body() dto: InvitationIdDto, @Req() req: any) {
async getInvitationById(
@Body() dto: InvitationIdDto,
@AuthWorkspace() workspace: Workspace,
) {
return this.workspaceInvitationService.getInvitationById(
dto.invitationId,
req.raw.workspaceId,
workspace,
);
}
@ -253,12 +256,12 @@ export class WorkspaceController {
@Post('invites/accept')
async acceptInvite(
@Body() acceptInviteDto: AcceptInviteDto,
@Req() req: any,
@AuthWorkspace() workspace: Workspace,
@Res({ passthrough: true }) res: FastifyReply,
) {
const authToken = await this.workspaceInvitationService.acceptInvitation(
acceptInviteDto,
req.raw.workspaceId,
workspace,
);
res.setCookie('authToken', authToken, {

View File

@ -28,6 +28,10 @@ import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
import { Queue } from 'bullmq';
import { EnvironmentService } from '../../../integrations/environment/environment.service';
import {
validateAllowedEmail,
validateSsoEnforcement,
} from '../../auth/auth.util';
@Injectable()
export class WorkspaceInvitationService {
@ -63,19 +67,19 @@ export class WorkspaceInvitationService {
return result;
}
async getInvitationById(invitationId: string, workspaceId: string) {
async getInvitationById(invitationId: string, workspace: Workspace) {
const invitation = await this.db
.selectFrom('workspaceInvitations')
.select(['id', 'email', 'createdAt'])
.where('id', '=', invitationId)
.where('workspaceId', '=', workspaceId)
.where('workspaceId', '=', workspace.id)
.executeTakeFirst();
if (!invitation) {
throw new NotFoundException('Invitation not found');
}
return invitation;
return { ...invitation, enforceSso: workspace.enforceSso };
}
async getInvitationTokenById(invitationId: string, workspaceId: string) {
@ -169,12 +173,12 @@ export class WorkspaceInvitationService {
}
}
async acceptInvitation(dto: AcceptInviteDto, workspaceId: string) {
async acceptInvitation(dto: AcceptInviteDto, workspace: Workspace) {
const invitation = await this.db
.selectFrom('workspaceInvitations')
.selectAll()
.where('id', '=', dto.invitationId)
.where('workspaceId', '=', workspaceId)
.where('workspaceId', '=', workspace.id)
.executeTakeFirst();
if (!invitation) {
@ -185,6 +189,9 @@ export class WorkspaceInvitationService {
throw new BadRequestException('Invalid invitation token');
}
validateSsoEnforcement(workspace);
validateAllowedEmail(invitation.email, workspace);
let newUser: User;
try {
@ -197,7 +204,7 @@ export class WorkspaceInvitationService {
password: dto.password,
role: invitation.role,
invitedById: invitation.invitedById,
workspaceId: workspaceId,
workspaceId: workspace.id,
},
trx,
);
@ -205,7 +212,7 @@ export class WorkspaceInvitationService {
// add user to default group
await this.groupUserRepo.addUserToDefaultGroup(
newUser.id,
workspaceId,
workspace.id,
trx,
);
@ -215,7 +222,7 @@ export class WorkspaceInvitationService {
.selectFrom('groups')
.select(['id', 'name'])
.where('groups.id', 'in', invitation.groupIds)
.where('groups.workspaceId', '=', workspaceId)
.where('groups.workspaceId', '=', workspace.id)
.execute();
if (validGroups && validGroups.length > 0) {
@ -256,7 +263,7 @@ export class WorkspaceInvitationService {
// notify the inviter
const invitedByUser = await this.userRepo.findById(
invitation.invitedById,
workspaceId,
workspace.id,
);
if (invitedByUser) {
@ -273,7 +280,9 @@ export class WorkspaceInvitationService {
}
if (this.environmentService.isCloud()) {
await this.billingQueue.add(QueueJob.STRIPE_SEATS_SYNC, { workspaceId });
await this.billingQueue.add(QueueJob.STRIPE_SEATS_SYNC, {
workspaceId: workspace.id,
});
}
return this.tokenService.generateAccessToken(newUser);