mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 08:42:12 +10:00
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.
This commit is contained in:
@ -0,0 +1,99 @@
|
||||
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);
|
||||
};
|
||||
Reference in New Issue
Block a user