mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
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.
100 lines
2.9 KiB
TypeScript
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);
|
|
};
|