import { Container } from 'typedi'; import type { FlowResult } from 'samlify/types/src/flow'; import { randomString } from 'n8n-workflow'; import config from '@/config'; import { AuthIdentity } from '@db/entities/AuthIdentity'; import type { User } from '@db/entities/User'; import { UserRepository } from '@db/repositories/user.repository'; import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { AuthError } from '@/errors/response-errors/auth.error'; import { License } from '@/License'; import { PasswordUtility } from '@/services/password.utility'; import type { SamlPreferences } from './types/samlPreferences'; import type { SamlUserAttributes } from './types/samlUserAttributes'; import type { SamlAttributeMapping } from './types/samlAttributeMapping'; import { SAML_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants'; import { getCurrentAuthenticationMethod, isEmailCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod, setCurrentAuthenticationMethod, } from '../ssoHelpers'; import { getServiceProviderConfigTestReturnUrl } from './serviceProvider.ee'; import type { SamlConfiguration } from './types/requests'; /** * Check whether the SAML feature is licensed and enabled in the instance */ export function isSamlLoginEnabled(): boolean { return config.getEnv(SAML_LOGIN_ENABLED); } export function getSamlLoginLabel(): string { return config.getEnv(SAML_LOGIN_LABEL); } // can only toggle between email and saml, not directly to e.g. ldap export async function setSamlLoginEnabled(enabled: boolean): Promise { if (isEmailCurrentAuthenticationMethod() || isSamlCurrentAuthenticationMethod()) { if (enabled) { config.set(SAML_LOGIN_ENABLED, true); await setCurrentAuthenticationMethod('saml'); } else if (!enabled) { config.set(SAML_LOGIN_ENABLED, false); await setCurrentAuthenticationMethod('email'); } } else { throw new InternalServerError( `Cannot switch SAML login enabled state when an authentication method other than email or saml is active (current: ${getCurrentAuthenticationMethod()})`, ); } } export function setSamlLoginLabel(label: string): void { config.set(SAML_LOGIN_LABEL, label); } export function isSamlLicensed(): boolean { return Container.get(License).isSamlEnabled(); } export function isSamlLicensedAndEnabled(): boolean { return isSamlLoginEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod(); } export const isSamlPreferences = (candidate: unknown): candidate is SamlPreferences => { const o = candidate as SamlPreferences; return ( typeof o === 'object' && typeof o.metadata === 'string' && typeof o.mapping === 'object' && o.mapping !== null && o.loginEnabled !== undefined ); }; export async function createUserFromSamlAttributes(attributes: SamlUserAttributes): Promise { const randomPassword = randomString(18); const userRepository = Container.get(UserRepository); return await userRepository.manager.transaction(async (trx) => { const { user } = await userRepository.createUserWithProject( { email: attributes.email.toLowerCase(), firstName: attributes.firstName, lastName: attributes.lastName, role: 'global:member', // generates a password that is not used or known to the user password: await Container.get(PasswordUtility).hash(randomPassword), }, trx, ); await trx.save( trx.create(AuthIdentity, { providerId: attributes.userPrincipalName, providerType: 'saml', userId: user.id, }), ); return user; }); } export async function updateUserFromSamlAttributes( user: User, attributes: SamlUserAttributes, ): Promise { if (!attributes.email) throw new AuthError('Email is required to update user'); if (!user) throw new AuthError('User not found'); let samlAuthIdentity = user?.authIdentities.find((e) => e.providerType === 'saml'); if (!samlAuthIdentity) { samlAuthIdentity = new AuthIdentity(); samlAuthIdentity.providerId = attributes.userPrincipalName; samlAuthIdentity.providerType = 'saml'; samlAuthIdentity.user = user; user.authIdentities.push(samlAuthIdentity); } else { samlAuthIdentity.providerId = attributes.userPrincipalName; } await Container.get(AuthIdentityRepository).save(samlAuthIdentity, { transaction: false }); user.firstName = attributes.firstName; user.lastName = attributes.lastName; const resultUser = await Container.get(UserRepository).save(user, { transaction: false }); if (!resultUser) throw new AuthError('Could not create User'); return resultUser; } type GetMappedSamlReturn = { attributes: SamlUserAttributes | undefined; missingAttributes: string[]; }; export function getMappedSamlAttributesFromFlowResult( flowResult: FlowResult, attributeMapping: SamlAttributeMapping, ): GetMappedSamlReturn { const result: GetMappedSamlReturn = { attributes: undefined, missingAttributes: [] as string[], }; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (flowResult?.extract?.attributes) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const attributes = flowResult.extract.attributes as { [key: string]: string }; // TODO:SAML: fetch mapped attributes from flowResult.extract.attributes and create or login user const email = attributes[attributeMapping.email]; const firstName = attributes[attributeMapping.firstName]; const lastName = attributes[attributeMapping.lastName]; const userPrincipalName = attributes[attributeMapping.userPrincipalName]; result.attributes = { email, firstName, lastName, userPrincipalName, }; if (!email) result.missingAttributes.push(attributeMapping.email); if (!userPrincipalName) result.missingAttributes.push(attributeMapping.userPrincipalName); if (!firstName) result.missingAttributes.push(attributeMapping.firstName); if (!lastName) result.missingAttributes.push(attributeMapping.lastName); } return result; } export function isConnectionTestRequest(req: SamlConfiguration.AcsRequest): boolean { return req.body.RelayState === getServiceProviderConfigTestReturnUrl(); }