Files
documenso/packages/auth/server/lib/utils/handle-oauth-organisation-callback-url.ts
David Nguyen 9ac7b94d9a feat: add organisation sso portal (#1946)
Allow organisations to manage an SSO OIDC compliant portal. This method
is intended to streamline the onboarding process and paves the way to
allow organisations to manage their members in a more strict way.
2025-09-09 17:14:07 +10:00

100 lines
2.9 KiB
TypeScript

import type { Context } from 'hono';
import { sendOrganisationAccountLinkConfirmationEmail } from '@documenso/ee/server-only/lib/send-organisation-account-link-confirmation-email';
import { AppError } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { formatOrganisationLoginUrl } from '@documenso/lib/utils/organisation-authentication-portal';
import { prisma } from '@documenso/prisma';
import { AuthenticationErrorCode } from '../errors/error-codes';
import { onAuthorize } from './authorizer';
import { validateOauth } from './handle-oauth-callback-url';
import { getOrganisationAuthenticationPortalOptions } from './organisation-portal';
type HandleOAuthOrganisationCallbackUrlOptions = {
c: Context;
orgUrl: string;
};
export const handleOAuthOrganisationCallbackUrl = async (
options: HandleOAuthOrganisationCallbackUrlOptions,
) => {
const { c, orgUrl } = options;
const { organisation, clientOptions } = await getOrganisationAuthenticationPortalOptions({
type: 'url',
organisationUrl: orgUrl,
});
const { email, name, sub, accessToken, accessTokenExpiresAt, idToken } = await validateOauth({
c,
clientOptions: {
...clientOptions,
bypassEmailVerification: true, // Bypass for organisation OIDC because we manually verify the email.
},
});
const allowedDomains = organisation.organisationAuthenticationPortal.allowedDomains;
if (allowedDomains.length > 0 && !allowedDomains.some((domain) => email.endsWith(`@${domain}`))) {
throw new AppError(AuthenticationErrorCode.InvalidRequest, {
message: 'Email domain not allowed',
});
}
// Find the account if possible.
const existingAccount = await prisma.account.findFirst({
where: {
provider: clientOptions.id,
providerAccountId: sub,
},
include: {
user: true,
},
});
// Directly log in user if account already exists.
if (existingAccount) {
await onAuthorize({ userId: existingAccount.user.id }, c);
return c.redirect(`/o/${orgUrl}`, 302);
}
let userToLink = await prisma.user.findFirst({
where: {
email,
},
});
// Handle new user.
if (!userToLink) {
userToLink = await prisma.user.create({
data: {
email: email,
name: name,
emailVerified: null, // Do not verify email.
},
});
await onCreateUserHook(userToLink).catch((err) => {
// Todo: (RR7) Add logging.
console.error(err);
});
}
await sendOrganisationAccountLinkConfirmationEmail({
type: userToLink.emailVerified ? 'link' : 'create',
userId: userToLink.id,
organisationId: organisation.id,
organisationName: organisation.name,
oauthConfig: {
accessToken,
idToken,
providerAccountId: sub,
expiresAt: Math.floor(accessTokenExpiresAt.getTime() / 1000),
},
});
return c.redirect(`${formatOrganisationLoginUrl(orgUrl)}?action=verification-required`, 302);
};