mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Add OIDC support for SSO (#15988)
Co-authored-by: Andreas Fitzek <andreas.fitzek@n8n.io>
This commit is contained in:
@@ -101,7 +101,7 @@ const onUserAction = (user: IUser, action: string) =>
|
||||
<N8nActionToggle
|
||||
v-if="
|
||||
!user.isOwner &&
|
||||
user.signInType !== 'ldap' &&
|
||||
!['ldap'].includes(user.signInType) &&
|
||||
!readonly &&
|
||||
getActions(user).length > 0 &&
|
||||
actions.length > 0
|
||||
|
||||
@@ -929,6 +929,7 @@
|
||||
"forgotPassword.returnToSignIn": "Back to sign in",
|
||||
"forgotPassword.sendingEmailError": "Problem sending email",
|
||||
"forgotPassword.ldapUserPasswordResetUnavailable": "Please contact your LDAP administrator to reset your password",
|
||||
"forgotPassword.oidcUserPasswordResetUnavailable": "Please contact your OIDC administrator to reset your password",
|
||||
"forgotPassword.smtpErrorContactAdministrator": "Please contact your administrator (problem with your SMTP setup)",
|
||||
"forgotPassword.tooManyRequests": "You’ve reached the password reset limit. Please try again in a few minutes.",
|
||||
"forms.resourceFiltersDropdown.filters": "Filters",
|
||||
@@ -2716,6 +2717,7 @@
|
||||
"settings.ldap.confirmMessage.beforeSaveForm.confirmButtonText": "Yes, disable it",
|
||||
"settings.ldap.confirmMessage.beforeSaveForm.headline": "Are you sure you want to disable LDAP login?",
|
||||
"settings.ldap.confirmMessage.beforeSaveForm.message": "If you do so, all LDAP users will be converted to email users.",
|
||||
|
||||
"settings.ldap.disabled.title": "Available on the Enterprise plan",
|
||||
"settings.ldap.disabled.description": "LDAP is available as a paid feature. Learn more about it.",
|
||||
"settings.ldap.disabled.buttonText": "See plans",
|
||||
@@ -2775,8 +2777,8 @@
|
||||
"settings.sso": "SSO",
|
||||
"settings.sso.title": "Single Sign On",
|
||||
"settings.sso.subtitle": "SAML 2.0 Configuration",
|
||||
"settings.sso.info": "Activate SAML SSO to enable passwordless login via your existing user management tool and enhance security through unified authentication.",
|
||||
"settings.sso.info.link": "Learn how to configure SAML 2.0.",
|
||||
"settings.sso.info": "Activate SAML or OIDC to enable passwordless login via your existing user management tool and enhance security through unified authentication.",
|
||||
"settings.sso.info.link": "Learn how to configure SAML or OIDC.",
|
||||
"settings.sso.activation.tooltip": "You need to save the settings first before activating SAML",
|
||||
"settings.sso.activated": "Activated",
|
||||
"settings.sso.deactivated": "Deactivated",
|
||||
@@ -2804,6 +2806,8 @@
|
||||
"settings.sso.actionBox.title": "Available on the Enterprise plan",
|
||||
"settings.sso.actionBox.description": "Use Single Sign On to consolidate authentication into a single platform to improve security and agility.",
|
||||
"settings.sso.actionBox.buttonText": "See plans",
|
||||
"settings.oidc.confirmMessage.beforeSaveForm.headline": "Are you sure you want to disable OIDC login?",
|
||||
"settings.oidc.confirmMessage.beforeSaveForm.message": "If you do so, all OIDC users will be converted to email users.",
|
||||
"settings.mfa.secret": "Secret {secret}",
|
||||
"settings.mfa": "MFA",
|
||||
"settings.mfa.title": "Multi-factor Authentication",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SamlPreferences, SamlToggleDto } from '@n8n/api-types';
|
||||
import type { OidcConfigDto, SamlPreferences, SamlToggleDto } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
@@ -39,3 +39,18 @@ export const toggleSamlConfig = async (
|
||||
export const testSamlConfig = async (context: IRestApiContext): Promise<string> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/sso/saml/config/test');
|
||||
};
|
||||
|
||||
export const getOidcConfig = async (context: IRestApiContext): Promise<OidcConfigDto> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/sso/oidc/config');
|
||||
};
|
||||
|
||||
export const saveOidcConfig = async (
|
||||
context: IRestApiContext,
|
||||
data: OidcConfigDto,
|
||||
): Promise<OidcConfigDto> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/sso/oidc/config', data);
|
||||
};
|
||||
|
||||
export const initOidcLogin = async (context: IRestApiContext): Promise<string> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/sso/oidc/login');
|
||||
};
|
||||
|
||||
@@ -608,6 +608,7 @@ export const enum UserManagementAuthenticationMethod {
|
||||
Email = 'email',
|
||||
Ldap = 'ldap',
|
||||
Saml = 'saml',
|
||||
Oidc = 'oidc',
|
||||
}
|
||||
|
||||
export interface IPermissionGroup {
|
||||
@@ -1423,6 +1424,7 @@ export type EnterpriseEditionFeatureKey =
|
||||
| 'LogStreaming'
|
||||
| 'Variables'
|
||||
| 'Saml'
|
||||
| 'Oidc'
|
||||
| 'SourceControl'
|
||||
| 'ExternalSecrets'
|
||||
| 'AuditLogs'
|
||||
|
||||
@@ -24,6 +24,7 @@ export const defaultSettings: FrontendSettings = {
|
||||
enterprise: {
|
||||
sharing: false,
|
||||
ldap: false,
|
||||
oidc: false,
|
||||
saml: false,
|
||||
logStreaming: false,
|
||||
debugInEditor: false,
|
||||
@@ -78,6 +79,7 @@ export const defaultSettings: FrontendSettings = {
|
||||
sso: {
|
||||
ldap: { loginEnabled: false, loginLabel: '' },
|
||||
saml: { loginEnabled: false, loginLabel: '' },
|
||||
oidc: { loginEnabled: false, loginUrl: '', callbackUrl: '' },
|
||||
},
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
|
||||
@@ -220,6 +220,7 @@ export function createMockEnterpriseSettings(
|
||||
sharing: false,
|
||||
ldap: false,
|
||||
saml: false,
|
||||
oidc: false,
|
||||
logStreaming: false,
|
||||
advancedExecutionFilters: false,
|
||||
variables: false,
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
import { useSSOStore } from '@/stores/sso.store';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
const i18n = useI18n();
|
||||
const ssoStore = useSSOStore();
|
||||
const toast = useToast();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const onSSOLogin = async () => {
|
||||
try {
|
||||
window.location.href = await ssoStore.getSSORedirectUrl();
|
||||
const redirectUrl = ssoStore.isDefaultAuthenticationSaml
|
||||
? await ssoStore.getSSORedirectUrl()
|
||||
: settingsStore.settings.sso.oidc.loginUrl;
|
||||
window.location.href = redirectUrl;
|
||||
} catch (error) {
|
||||
toast.showError(error, 'Error', error.message);
|
||||
}
|
||||
|
||||
@@ -644,6 +644,7 @@ export const EnterpriseEditionFeature: Record<
|
||||
LogStreaming: 'logStreaming',
|
||||
Variables: 'variables',
|
||||
Saml: 'saml',
|
||||
Oidc: 'oidc',
|
||||
SourceControl: 'sourceControl',
|
||||
ExternalSecrets: 'externalSecrets',
|
||||
AuditLogs: 'auditLogs',
|
||||
@@ -698,6 +699,7 @@ export const CURL_IMPORT_NODES_PROTOCOLS: { [key: string]: string } = {
|
||||
export const enum SignInType {
|
||||
LDAP = 'ldap',
|
||||
EMAIL = 'email',
|
||||
OIDC = 'oidc',
|
||||
}
|
||||
|
||||
export const N8N_SALES_EMAIL = 'sales@n8n.io';
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('permissions', () => {
|
||||
ldap: {},
|
||||
license: {},
|
||||
logStreaming: {},
|
||||
oidc: {},
|
||||
orchestration: {},
|
||||
project: {},
|
||||
saml: {},
|
||||
@@ -93,6 +94,7 @@ describe('permissions', () => {
|
||||
read: true,
|
||||
},
|
||||
saml: {},
|
||||
oidc: {},
|
||||
securityAudit: {},
|
||||
sourceControl: {},
|
||||
tag: {
|
||||
|
||||
@@ -33,6 +33,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => {
|
||||
license: {},
|
||||
logStreaming: {},
|
||||
saml: {},
|
||||
oidc: {},
|
||||
securityAudit: {},
|
||||
folder: {},
|
||||
insights: {},
|
||||
|
||||
@@ -46,6 +46,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
});
|
||||
const ldap = ref({ loginLabel: '', loginEnabled: false });
|
||||
const saml = ref({ loginLabel: '', loginEnabled: false });
|
||||
const oidc = ref({ loginEnabled: false, loginUrl: '', callbackUrl: '' });
|
||||
const mfa = ref({ enabled: false });
|
||||
const folders = ref({ enabled: false });
|
||||
|
||||
@@ -95,6 +96,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
|
||||
const isSamlLoginEnabled = computed(() => saml.value.loginEnabled);
|
||||
|
||||
const isOidcLoginEnabled = computed(() => oidc.value.loginEnabled);
|
||||
|
||||
const oidcCallBackUrl = computed(() => oidc.value.callbackUrl);
|
||||
|
||||
const isAiAssistantEnabled = computed(() => settings.value.aiAssistant?.enabled);
|
||||
|
||||
const isAskAiEnabled = computed(() => settings.value.askAi?.enabled);
|
||||
@@ -181,6 +186,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
() => userManagement.value.authenticationMethod === UserManagementAuthenticationMethod.Saml,
|
||||
);
|
||||
|
||||
const isDefaultAuthenticationOidc = computed(
|
||||
() => userManagement.value.authenticationMethod === UserManagementAuthenticationMethod.Oidc,
|
||||
);
|
||||
|
||||
const permanentlyDismissedBanners = computed(() => settings.value.banners?.dismissed ?? []);
|
||||
|
||||
const isBelowUserQuota = computed(
|
||||
@@ -210,6 +219,12 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
saml.value.loginLabel = settings.value.sso.saml.loginLabel;
|
||||
}
|
||||
|
||||
if (settings.value.sso?.oidc) {
|
||||
oidc.value.loginEnabled = settings.value.sso.oidc.loginEnabled;
|
||||
oidc.value.loginUrl = settings.value.sso.oidc.loginUrl || '';
|
||||
oidc.value.callbackUrl = settings.value.sso.oidc.callbackUrl || '';
|
||||
}
|
||||
|
||||
mfa.value.enabled = settings.value.mfa?.enabled;
|
||||
folders.value.enabled = settings.value.folders?.enabled;
|
||||
|
||||
@@ -420,6 +435,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
isLdapLoginEnabled,
|
||||
ldapLoginLabel,
|
||||
isSamlLoginEnabled,
|
||||
isOidcLoginEnabled,
|
||||
showSetupPage,
|
||||
deploymentType,
|
||||
isCloudDeployment,
|
||||
@@ -444,6 +460,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
isMultiMain,
|
||||
isWorkerViewAvailable,
|
||||
isDefaultAuthenticationSaml,
|
||||
isDefaultAuthenticationOidc,
|
||||
workflowCallerPolicyDefaultOption,
|
||||
permanentlyDismissedBanners,
|
||||
isBelowUserQuota,
|
||||
@@ -456,6 +473,8 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
isAiCreditsEnabled,
|
||||
aiCreditsQuota,
|
||||
experimental__minZoomNodeSettingsInCanvas,
|
||||
partialExecutionVersion,
|
||||
oidcCallBackUrl,
|
||||
reset,
|
||||
testLdapConnection,
|
||||
getLdapConfig,
|
||||
@@ -470,6 +489,5 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
getSettings,
|
||||
setSettings,
|
||||
initialize,
|
||||
partialExecutionVersion,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SamlPreferences } from '@n8n/api-types';
|
||||
import type { OidcConfigDto } from '@n8n/api-types';
|
||||
import { type SamlPreferences } from '@n8n/api-types';
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { defineStore } from 'pinia';
|
||||
@@ -17,17 +18,13 @@ export const useSSOStore = defineStore('sso', () => {
|
||||
const route = useRoute();
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
samlConfig: undefined as (SamlPreferences & SamlPreferencesExtractedData) | undefined,
|
||||
oidcConfig: undefined as OidcConfigDto | undefined,
|
||||
});
|
||||
|
||||
const isLoading = computed(() => state.loading);
|
||||
|
||||
const samlConfig = computed(() => state.samlConfig);
|
||||
|
||||
const setLoading = (loading: boolean) => {
|
||||
state.loading = loading;
|
||||
};
|
||||
const oidcConfig = computed(() => state.oidcConfig);
|
||||
|
||||
const isSamlLoginEnabled = computed({
|
||||
get: () => settingsStore.isSamlLoginEnabled,
|
||||
@@ -45,15 +42,43 @@ export const useSSOStore = defineStore('sso', () => {
|
||||
void toggleLoginEnabled(value);
|
||||
},
|
||||
});
|
||||
|
||||
const isOidcLoginEnabled = computed({
|
||||
get: () => settingsStore.isOidcLoginEnabled,
|
||||
set: (value: boolean) => {
|
||||
settingsStore.setSettings({
|
||||
...settingsStore.settings,
|
||||
sso: {
|
||||
...settingsStore.settings.sso,
|
||||
oidc: {
|
||||
...settingsStore.settings.sso.oidc,
|
||||
loginEnabled: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const isEnterpriseSamlEnabled = computed(
|
||||
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Saml],
|
||||
);
|
||||
|
||||
const isEnterpriseOidcEnabled = computed(
|
||||
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Oidc],
|
||||
);
|
||||
|
||||
const isDefaultAuthenticationSaml = computed(() => settingsStore.isDefaultAuthenticationSaml);
|
||||
|
||||
const isDefaultAuthenticationOidc = computed(() => settingsStore.isDefaultAuthenticationOidc);
|
||||
|
||||
const showSsoLoginButton = computed(
|
||||
() =>
|
||||
isSamlLoginEnabled.value &&
|
||||
isEnterpriseSamlEnabled.value &&
|
||||
isDefaultAuthenticationSaml.value,
|
||||
(isSamlLoginEnabled.value &&
|
||||
isEnterpriseSamlEnabled.value &&
|
||||
isDefaultAuthenticationSaml.value) ||
|
||||
(isOidcLoginEnabled.value &&
|
||||
isEnterpriseOidcEnabled.value &&
|
||||
isDefaultAuthenticationOidc.value),
|
||||
);
|
||||
|
||||
const getSSORedirectUrl = async () =>
|
||||
@@ -66,6 +91,9 @@ export const useSSOStore = defineStore('sso', () => {
|
||||
await ssoApi.toggleSamlConfig(rootStore.restApiContext, { loginEnabled: enabled });
|
||||
|
||||
const getSamlMetadata = async () => await ssoApi.getSamlMetadata(rootStore.restApiContext);
|
||||
|
||||
// const getOidcRedirectLUrl = async () => await ssoApi)
|
||||
|
||||
const getSamlConfig = async () => {
|
||||
const samlConfig = await ssoApi.getSamlConfig(rootStore.restApiContext);
|
||||
state.samlConfig = samlConfig;
|
||||
@@ -83,20 +111,36 @@ export const useSSOStore = defineStore('sso', () => {
|
||||
|
||||
const userData = computed(() => usersStore.currentUser);
|
||||
|
||||
const getOidcConfig = async () => {
|
||||
const oidcConfig = await ssoApi.getOidcConfig(rootStore.restApiContext);
|
||||
state.oidcConfig = oidcConfig;
|
||||
return oidcConfig;
|
||||
};
|
||||
|
||||
const saveOidcConfig = async (config: OidcConfigDto) => {
|
||||
const savedConfig = await ssoApi.saveOidcConfig(rootStore.restApiContext, config);
|
||||
state.oidcConfig = savedConfig;
|
||||
return savedConfig;
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
setLoading,
|
||||
isEnterpriseOidcEnabled,
|
||||
isSamlLoginEnabled,
|
||||
isOidcLoginEnabled,
|
||||
isEnterpriseSamlEnabled,
|
||||
isDefaultAuthenticationSaml,
|
||||
isDefaultAuthenticationOidc,
|
||||
showSsoLoginButton,
|
||||
samlConfig,
|
||||
oidcConfig,
|
||||
userData,
|
||||
getSSORedirectUrl,
|
||||
getSamlMetadata,
|
||||
getSamlConfig,
|
||||
saveSamlConfig,
|
||||
testSamlConfig,
|
||||
updateUser,
|
||||
userData,
|
||||
getOidcConfig,
|
||||
saveOidcConfig,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -72,7 +72,9 @@ const isExternalAuthEnabled = computed((): boolean => {
|
||||
settingsStore.settings.enterprise.ldap && currentUser.value?.signInType === 'ldap';
|
||||
const isSamlEnabled =
|
||||
settingsStore.isSamlLoginEnabled && settingsStore.isDefaultAuthenticationSaml;
|
||||
return isLdapEnabled || isSamlEnabled;
|
||||
const isOidcEnabled =
|
||||
settingsStore.settings.enterprise.oidc && currentUser.value?.signInType === 'oidc';
|
||||
return isLdapEnabled || isSamlEnabled || isOidcEnabled;
|
||||
});
|
||||
const isPersonalSecurityEnabled = computed((): boolean => {
|
||||
return usersStore.isInstanceOwner || !isExternalAuthEnabled.value;
|
||||
|
||||
@@ -65,6 +65,7 @@ describe('SettingsSso View', () => {
|
||||
const pinia = createTestingPinia();
|
||||
const ssoStore = mockedStore(useSSOStore);
|
||||
ssoStore.isEnterpriseSamlEnabled = false;
|
||||
ssoStore.isEnterpriseOidcEnabled = false;
|
||||
|
||||
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||
|
||||
@@ -82,6 +83,7 @@ describe('SettingsSso View', () => {
|
||||
|
||||
const ssoStore = mockedStore(useSSOStore);
|
||||
ssoStore.isEnterpriseSamlEnabled = true;
|
||||
ssoStore.isEnterpriseOidcEnabled = true;
|
||||
|
||||
ssoStore.getSamlConfig.mockResolvedValue(samlConfig);
|
||||
|
||||
@@ -102,6 +104,7 @@ describe('SettingsSso View', () => {
|
||||
const ssoStore = mockedStore(useSSOStore);
|
||||
ssoStore.isEnterpriseSamlEnabled = true;
|
||||
ssoStore.isSamlLoginEnabled = false;
|
||||
ssoStore.isEnterpriseOidcEnabled = true;
|
||||
|
||||
ssoStore.getSamlConfig.mockResolvedValue(samlConfig);
|
||||
|
||||
@@ -126,6 +129,7 @@ describe('SettingsSso View', () => {
|
||||
|
||||
const ssoStore = mockedStore(useSSOStore);
|
||||
ssoStore.isEnterpriseSamlEnabled = true;
|
||||
ssoStore.isEnterpriseOidcEnabled = true;
|
||||
|
||||
const { getByTestId } = renderView({ pinia });
|
||||
|
||||
@@ -163,6 +167,7 @@ describe('SettingsSso View', () => {
|
||||
|
||||
const ssoStore = mockedStore(useSSOStore);
|
||||
ssoStore.isEnterpriseSamlEnabled = true;
|
||||
ssoStore.isEnterpriseOidcEnabled = true;
|
||||
|
||||
const { getByTestId } = renderView({ pinia });
|
||||
|
||||
@@ -199,6 +204,7 @@ describe('SettingsSso View', () => {
|
||||
|
||||
const ssoStore = mockedStore(useSSOStore);
|
||||
ssoStore.isEnterpriseSamlEnabled = true;
|
||||
ssoStore.isEnterpriseOidcEnabled = true;
|
||||
|
||||
const { getByTestId } = renderView({ pinia });
|
||||
|
||||
@@ -228,6 +234,7 @@ describe('SettingsSso View', () => {
|
||||
|
||||
const ssoStore = mockedStore(useSSOStore);
|
||||
ssoStore.isEnterpriseSamlEnabled = true;
|
||||
ssoStore.isEnterpriseOidcEnabled = true;
|
||||
|
||||
const { getByTestId } = renderView({ pinia });
|
||||
|
||||
@@ -258,6 +265,8 @@ describe('SettingsSso View', () => {
|
||||
const ssoStore = mockedStore(useSSOStore);
|
||||
ssoStore.isEnterpriseSamlEnabled = true;
|
||||
ssoStore.isSamlLoginEnabled = true;
|
||||
ssoStore.isEnterpriseOidcEnabled = true;
|
||||
ssoStore.isOidcLoginEnabled = false;
|
||||
|
||||
const error = new Error('Request failed with status code 404');
|
||||
ssoStore.getSamlConfig.mockRejectedValue(error);
|
||||
|
||||
@@ -9,17 +9,27 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||
import { MODAL_CONFIRM } from '@/constants';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
type SupportedProtocolType = (typeof SupportedProtocols)[keyof typeof SupportedProtocols];
|
||||
|
||||
const IdentityProviderSettingsType = {
|
||||
URL: 'url',
|
||||
XML: 'xml',
|
||||
};
|
||||
|
||||
const SupportedProtocols = {
|
||||
SAML: 'saml',
|
||||
OIDC: 'oidc',
|
||||
} as const;
|
||||
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const rootStore = useRootStore();
|
||||
const ssoStore = useSSOStore();
|
||||
const message = useMessage();
|
||||
const settingsStore = useSettingsStore();
|
||||
const toast = useToast();
|
||||
const documentTitle = useDocumentTitle();
|
||||
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||
@@ -29,11 +39,24 @@ const ssoActivatedLabel = computed(() =>
|
||||
? i18n.baseText('settings.sso.activated')
|
||||
: i18n.baseText('settings.sso.deactivated'),
|
||||
);
|
||||
|
||||
const oidcActivatedLabel = computed(() =>
|
||||
ssoStore.isOidcLoginEnabled
|
||||
? i18n.baseText('settings.sso.activated')
|
||||
: i18n.baseText('settings.sso.deactivated'),
|
||||
);
|
||||
|
||||
const ssoSettingsSaved = ref(false);
|
||||
|
||||
const redirectUrl = ref();
|
||||
const entityId = ref();
|
||||
|
||||
const clientId = ref('');
|
||||
const clientSecret = ref('');
|
||||
|
||||
const discoveryEndpoint = ref('');
|
||||
|
||||
const authProtocol = ref<SupportedProtocolType>(SupportedProtocols.SAML);
|
||||
|
||||
const ipsOptions = ref([
|
||||
{
|
||||
label: i18n.baseText('settings.sso.settings.ips.options.url'),
|
||||
@@ -49,6 +72,8 @@ const ipsType = ref(IdentityProviderSettingsType.URL);
|
||||
const metadataUrl = ref();
|
||||
const metadata = ref();
|
||||
|
||||
const redirectUrl = ref();
|
||||
|
||||
const isSaveEnabled = computed(() => {
|
||||
if (ipsType.value === IdentityProviderSettingsType.URL) {
|
||||
return !!metadataUrl.value && metadataUrl.value !== ssoStore.samlConfig?.metadataUrl;
|
||||
@@ -67,6 +92,17 @@ const isTestEnabled = computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
async function loadSamlConfig() {
|
||||
if (!ssoStore.isEnterpriseSamlEnabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await getSamlConfig();
|
||||
} catch (error) {
|
||||
toast.showError(error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
const getSamlConfig = async () => {
|
||||
const config = await ssoStore.getSamlConfig();
|
||||
|
||||
@@ -167,39 +203,79 @@ const isToggleSsoDisabled = computed(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
documentTitle.set(i18n.baseText('settings.sso.title'));
|
||||
if (!ssoStore.isEnterpriseSamlEnabled) {
|
||||
await Promise.all([loadSamlConfig(), loadOidcConfig()]);
|
||||
|
||||
if (ssoStore.isDefaultAuthenticationSaml) {
|
||||
authProtocol.value = SupportedProtocols.SAML;
|
||||
} else if (ssoStore.isDefaultAuthenticationOidc) {
|
||||
authProtocol.value = SupportedProtocols.OIDC;
|
||||
}
|
||||
});
|
||||
|
||||
const getOidcConfig = async () => {
|
||||
const config = await ssoStore.getOidcConfig();
|
||||
|
||||
clientId.value = config.clientId;
|
||||
clientSecret.value = config.clientSecret;
|
||||
discoveryEndpoint.value = config.discoveryEndpoint;
|
||||
};
|
||||
|
||||
async function loadOidcConfig() {
|
||||
if (!ssoStore.isEnterpriseOidcEnabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await getSamlConfig();
|
||||
await getOidcConfig();
|
||||
} catch (error) {
|
||||
toast.showError(error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function onAuthProtocolUpdated(value: SupportedProtocolType) {
|
||||
authProtocol.value = value;
|
||||
}
|
||||
|
||||
const cannotSaveOidcSettings = computed(() => {
|
||||
return (
|
||||
ssoStore.oidcConfig?.clientId === clientId.value &&
|
||||
ssoStore.oidcConfig?.clientSecret === clientSecret.value &&
|
||||
ssoStore.oidcConfig?.discoveryEndpoint === discoveryEndpoint.value &&
|
||||
ssoStore.oidcConfig?.loginEnabled === ssoStore.isOidcLoginEnabled
|
||||
);
|
||||
});
|
||||
|
||||
async function onOidcSettingsSave() {
|
||||
if (ssoStore.oidcConfig?.loginEnabled && !ssoStore.isOidcLoginEnabled) {
|
||||
const confirmAction = await message.confirm(
|
||||
i18n.baseText('settings.oidc.confirmMessage.beforeSaveForm.message'),
|
||||
i18n.baseText('settings.oidc.confirmMessage.beforeSaveForm.headline'),
|
||||
{
|
||||
cancelButtonText: i18n.baseText(
|
||||
'settings.ldap.confirmMessage.beforeSaveForm.cancelButtonText',
|
||||
),
|
||||
confirmButtonText: i18n.baseText(
|
||||
'settings.ldap.confirmMessage.beforeSaveForm.confirmButtonText',
|
||||
),
|
||||
},
|
||||
);
|
||||
if (confirmAction !== MODAL_CONFIRM) return;
|
||||
}
|
||||
|
||||
const newConfig = await ssoStore.saveOidcConfig({
|
||||
clientId: clientId.value,
|
||||
clientSecret: clientSecret.value,
|
||||
discoveryEndpoint: discoveryEndpoint.value,
|
||||
loginEnabled: ssoStore.isOidcLoginEnabled,
|
||||
});
|
||||
|
||||
clientSecret.value = newConfig.clientSecret;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pb-3xl">
|
||||
<n8n-heading size="2xlarge">{{ i18n.baseText('settings.sso.title') }}</n8n-heading>
|
||||
<div :class="$style.top">
|
||||
<n8n-heading size="xlarge">{{ i18n.baseText('settings.sso.subtitle') }}</n8n-heading>
|
||||
<n8n-tooltip
|
||||
v-if="ssoStore.isEnterpriseSamlEnabled"
|
||||
:disabled="ssoStore.isSamlLoginEnabled || ssoSettingsSaved"
|
||||
>
|
||||
<template #content>
|
||||
<span>
|
||||
{{ i18n.baseText('settings.sso.activation.tooltip') }}
|
||||
</span>
|
||||
</template>
|
||||
<el-switch
|
||||
v-model="ssoStore.isSamlLoginEnabled"
|
||||
data-test-id="sso-toggle"
|
||||
:disabled="isToggleSsoDisabled"
|
||||
:class="$style.switch"
|
||||
:inactive-text="ssoActivatedLabel"
|
||||
/>
|
||||
</n8n-tooltip>
|
||||
<div class="pb-2xl">
|
||||
<div :class="$style.heading">
|
||||
<n8n-heading size="2xlarge">{{ i18n.baseText('settings.sso.title') }}</n8n-heading>
|
||||
</div>
|
||||
<n8n-info-tip>
|
||||
{{ i18n.baseText('settings.sso.info') }}
|
||||
@@ -209,67 +285,173 @@ onMounted(async () => {
|
||||
</n8n-info-tip>
|
||||
<div v-if="ssoStore.isEnterpriseSamlEnabled" data-test-id="sso-content-licensed">
|
||||
<div :class="$style.group">
|
||||
<label>{{ i18n.baseText('settings.sso.settings.redirectUrl.label') }}</label>
|
||||
<CopyInput
|
||||
:value="redirectUrl"
|
||||
:copy-button-text="i18n.baseText('generic.clickToCopy')"
|
||||
:toast-title="i18n.baseText('settings.sso.settings.redirectUrl.copied')"
|
||||
/>
|
||||
<small>{{ i18n.baseText('settings.sso.settings.redirectUrl.help') }}</small>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<label>{{ i18n.baseText('settings.sso.settings.entityId.label') }}</label>
|
||||
<CopyInput
|
||||
:value="entityId"
|
||||
:copy-button-text="i18n.baseText('generic.clickToCopy')"
|
||||
:toast-title="i18n.baseText('settings.sso.settings.entityId.copied')"
|
||||
/>
|
||||
<small>{{ i18n.baseText('settings.sso.settings.entityId.help') }}</small>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<label>{{ i18n.baseText('settings.sso.settings.ips.label') }}</label>
|
||||
<div class="mt-2xs mb-s">
|
||||
<n8n-radio-buttons v-model="ipsType" :options="ipsOptions" />
|
||||
<label>Select Authentication Protocol</label>
|
||||
<div>
|
||||
<N8nSelect
|
||||
filterable
|
||||
:model-value="authProtocol"
|
||||
:placeholder="i18n.baseText('parameterInput.select')"
|
||||
@update:model-value="onAuthProtocolUpdated"
|
||||
@keydown.stop
|
||||
>
|
||||
<N8nOption
|
||||
v-for="protocol in Object.values(SupportedProtocols)"
|
||||
:key="protocol"
|
||||
:value="protocol"
|
||||
:label="protocol.toUpperCase()"
|
||||
data-test-id="credential-select-option"
|
||||
>
|
||||
</N8nOption>
|
||||
</N8nSelect>
|
||||
</div>
|
||||
<div v-show="ipsType === IdentityProviderSettingsType.URL">
|
||||
<n8n-input
|
||||
v-model="metadataUrl"
|
||||
type="text"
|
||||
name="metadataUrl"
|
||||
</div>
|
||||
<div v-if="authProtocol === SupportedProtocols.SAML">
|
||||
<div :class="$style.group">
|
||||
<label>{{ i18n.baseText('settings.sso.settings.redirectUrl.label') }}</label>
|
||||
<CopyInput
|
||||
:value="redirectUrl"
|
||||
:copy-button-text="i18n.baseText('generic.clickToCopy')"
|
||||
:toast-title="i18n.baseText('settings.sso.settings.redirectUrl.copied')"
|
||||
/>
|
||||
<small>{{ i18n.baseText('settings.sso.settings.redirectUrl.help') }}</small>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<label>{{ i18n.baseText('settings.sso.settings.entityId.label') }}</label>
|
||||
<CopyInput
|
||||
:value="entityId"
|
||||
:copy-button-text="i18n.baseText('generic.clickToCopy')"
|
||||
:toast-title="i18n.baseText('settings.sso.settings.entityId.copied')"
|
||||
/>
|
||||
<small>{{ i18n.baseText('settings.sso.settings.entityId.help') }}</small>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<label>{{ i18n.baseText('settings.sso.settings.ips.label') }}</label>
|
||||
<div class="mt-2xs mb-s">
|
||||
<n8n-radio-buttons v-model="ipsType" :options="ipsOptions" />
|
||||
</div>
|
||||
<div v-show="ipsType === IdentityProviderSettingsType.URL">
|
||||
<n8n-input
|
||||
v-model="metadataUrl"
|
||||
type="text"
|
||||
name="metadataUrl"
|
||||
size="large"
|
||||
:placeholder="i18n.baseText('settings.sso.settings.ips.url.placeholder')"
|
||||
data-test-id="sso-provider-url"
|
||||
/>
|
||||
<small>{{ i18n.baseText('settings.sso.settings.ips.url.help') }}</small>
|
||||
</div>
|
||||
<div v-show="ipsType === IdentityProviderSettingsType.XML">
|
||||
<n8n-input
|
||||
v-model="metadata"
|
||||
type="textarea"
|
||||
name="metadata"
|
||||
:rows="4"
|
||||
data-test-id="sso-provider-xml"
|
||||
/>
|
||||
<small>{{ i18n.baseText('settings.sso.settings.ips.xml.help') }}</small>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<n8n-tooltip
|
||||
v-if="ssoStore.isEnterpriseSamlEnabled"
|
||||
:disabled="ssoStore.isSamlLoginEnabled || ssoSettingsSaved"
|
||||
>
|
||||
<template #content>
|
||||
<span>
|
||||
{{ i18n.baseText('settings.sso.activation.tooltip') }}
|
||||
</span>
|
||||
</template>
|
||||
<el-switch
|
||||
v-model="ssoStore.isSamlLoginEnabled"
|
||||
data-test-id="sso-toggle"
|
||||
:disabled="isToggleSsoDisabled"
|
||||
:class="$style.switch"
|
||||
:inactive-text="ssoActivatedLabel"
|
||||
/>
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.buttons">
|
||||
<n8n-button
|
||||
:disabled="!isSaveEnabled"
|
||||
size="large"
|
||||
:placeholder="i18n.baseText('settings.sso.settings.ips.url.placeholder')"
|
||||
data-test-id="sso-provider-url"
|
||||
/>
|
||||
<small>{{ i18n.baseText('settings.sso.settings.ips.url.help') }}</small>
|
||||
data-test-id="sso-save"
|
||||
@click="onSave"
|
||||
>
|
||||
{{ i18n.baseText('settings.sso.settings.save') }}
|
||||
</n8n-button>
|
||||
<n8n-button
|
||||
:disabled="!isTestEnabled"
|
||||
size="large"
|
||||
type="tertiary"
|
||||
data-test-id="sso-test"
|
||||
@click="onTest"
|
||||
>
|
||||
{{ i18n.baseText('settings.sso.settings.test') }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
<div v-show="ipsType === IdentityProviderSettingsType.XML">
|
||||
<n8n-input
|
||||
v-model="metadata"
|
||||
type="textarea"
|
||||
name="metadata"
|
||||
:rows="4"
|
||||
data-test-id="sso-provider-xml"
|
||||
|
||||
<footer :class="$style.footer">
|
||||
{{ i18n.baseText('settings.sso.settings.footer.hint') }}
|
||||
</footer>
|
||||
</div>
|
||||
<div v-if="authProtocol === SupportedProtocols.OIDC">
|
||||
<div :class="$style.group">
|
||||
<label>Redirect URL</label>
|
||||
<CopyInput
|
||||
:value="settingsStore.oidcCallBackUrl"
|
||||
:copy-button-text="i18n.baseText('generic.clickToCopy')"
|
||||
toast-title="Redirect URL copied to clipboard"
|
||||
/>
|
||||
<small>{{ i18n.baseText('settings.sso.settings.ips.xml.help') }}</small>
|
||||
<small>Copy the Redirect URL to configure your OIDC provider </small>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<label>Discovery Endpoint</label>
|
||||
<N8nInput
|
||||
:model-value="discoveryEndpoint"
|
||||
type="text"
|
||||
placeholder="https://accounts.google.com/.well-known/openid-configuration"
|
||||
@update:model-value="(v: string) => (discoveryEndpoint = v)"
|
||||
/>
|
||||
<small>Paste here your discovery endpoint</small>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<label>Client ID</label>
|
||||
<N8nInput
|
||||
:model-value="clientId"
|
||||
type="text"
|
||||
@update:model-value="(v: string) => (clientId = v)"
|
||||
/>
|
||||
<small
|
||||
>The client ID you received when registering your application with your provider</small
|
||||
>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<label>Client Secret</label>
|
||||
<N8nInput
|
||||
:model-value="clientSecret"
|
||||
type="password"
|
||||
@update:model-value="(v: string) => (clientSecret = v)"
|
||||
/>
|
||||
<small
|
||||
>The client Secret you received when registering your application with your
|
||||
provider</small
|
||||
>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<el-switch
|
||||
v-model="ssoStore.isOidcLoginEnabled"
|
||||
data-test-id="sso-oidc-toggle"
|
||||
:class="$style.switch"
|
||||
:inactive-text="oidcActivatedLabel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="$style.buttons">
|
||||
<n8n-button size="large" :disabled="cannotSaveOidcSettings" @click="onOidcSettingsSave">
|
||||
{{ i18n.baseText('settings.sso.settings.save') }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.buttons">
|
||||
<n8n-button :disabled="!isSaveEnabled" size="large" data-test-id="sso-save" @click="onSave">
|
||||
{{ i18n.baseText('settings.sso.settings.save') }}
|
||||
</n8n-button>
|
||||
<n8n-button
|
||||
:disabled="!isTestEnabled"
|
||||
size="large"
|
||||
type="tertiary"
|
||||
data-test-id="sso-test"
|
||||
@click="onTest"
|
||||
>
|
||||
{{ i18n.baseText('settings.sso.settings.test') }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
<footer :class="$style.footer">
|
||||
{{ i18n.baseText('settings.sso.settings.footer.hint') }}
|
||||
</footer>
|
||||
</div>
|
||||
<n8n-action-box
|
||||
v-else
|
||||
@@ -287,11 +469,8 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-2xl) 0 var(--spacing-xl);
|
||||
.heading {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.switch {
|
||||
|
||||
Reference in New Issue
Block a user