mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): SSO login button (#5615)
* feat(editor): SSO login button * feat(editor): SSO login button * feat(editor): SSO login button
This commit is contained in:
parent
e0ea97af8d
commit
6916628a9f
|
@ -137,9 +137,9 @@ export class SamlController {
|
||||||
const result = this.samlService.getLoginRequestUrl();
|
const result = this.samlService.getLoginRequestUrl();
|
||||||
if (result?.binding === 'redirect') {
|
if (result?.binding === 'redirect') {
|
||||||
// forced client side redirect through the use of a javascript redirect
|
// forced client side redirect through the use of a javascript redirect
|
||||||
return res.send(getInitSSOPostView(result.context));
|
// return res.send(getInitSSOPostView(result.context));
|
||||||
// TODO:SAML: If we want the frontend to handle the redirect, we will send the redirect URL instead:
|
// Return the redirect URL directly
|
||||||
// return res.status(301).send(result.context.context);
|
return res.send(result.context.context);
|
||||||
} else if (result?.binding === 'post') {
|
} else if (result?.binding === 'post') {
|
||||||
return res.send(getInitSSOFormView(result.context as PostBindingContext));
|
return res.send(getInitSSOFormView(result.context as PostBindingContext));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
{{ redirectText }}
|
{{ redirectText }}
|
||||||
</n8n-link>
|
</n8n-link>
|
||||||
</div>
|
</div>
|
||||||
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -623,10 +623,17 @@ export interface IN8nPromptResponse {
|
||||||
updated: boolean;
|
updated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum UserManagementAuthenticationMethod {
|
||||||
|
Email = 'email',
|
||||||
|
Ldap = 'ldap',
|
||||||
|
Saml = 'saml',
|
||||||
|
}
|
||||||
|
|
||||||
export interface IUserManagementConfig {
|
export interface IUserManagementConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
showSetupOnFirstLoad?: boolean;
|
showSetupOnFirstLoad?: boolean;
|
||||||
smtpSetup: boolean;
|
smtpSetup: boolean;
|
||||||
|
authenticationMethod: UserManagementAuthenticationMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPermissionGroup {
|
export interface IPermissionGroup {
|
||||||
|
|
6
packages/editor-ui/src/api/sso.ts
Normal file
6
packages/editor-ui/src/api/sso.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { makeRestApiRequest } from '@/utils';
|
||||||
|
import { IRestApiContext } from '@/Interface';
|
||||||
|
|
||||||
|
export const initSSO = (context: IRestApiContext): Promise<string> => {
|
||||||
|
return makeRestApiRequest(context, 'GET', '/sso/saml/initsso');
|
||||||
|
};
|
61
packages/editor-ui/src/components/SSOLogin.vue
Normal file
61
packages/editor-ui/src/components/SSOLogin.vue
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Notification } from 'element-ui';
|
||||||
|
import { useSSOStore } from '@/stores/sso';
|
||||||
|
|
||||||
|
const ssoStore = useSSOStore();
|
||||||
|
|
||||||
|
const onSSOLogin = async () => {
|
||||||
|
try {
|
||||||
|
window.location.href = await ssoStore.getSSORedirectUrl();
|
||||||
|
} catch (error) {
|
||||||
|
Notification.error({
|
||||||
|
title: 'Error',
|
||||||
|
message: error.message,
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="ssoStore.showSsoLoginButton" :class="$style.ssoLogin">
|
||||||
|
<div :class="$style.divider">
|
||||||
|
<span>{{ $locale.baseText('sso.login.divider') }}</span>
|
||||||
|
</div>
|
||||||
|
<n8n-button
|
||||||
|
@click="onSSOLogin"
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
outline
|
||||||
|
:label="$locale.baseText('sso.login.button')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.ssoLogin {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
position: relative;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
padding: var(--spacing-xl) var(--spacing-l);
|
||||||
|
background: var(--color-foreground-xlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -448,6 +448,7 @@ export enum EnterpriseEditionFeature {
|
||||||
Sharing = 'sharing',
|
Sharing = 'sharing',
|
||||||
Ldap = 'ldap',
|
Ldap = 'ldap',
|
||||||
LogStreaming = 'logStreaming',
|
LogStreaming = 'logStreaming',
|
||||||
|
Saml = 'saml',
|
||||||
}
|
}
|
||||||
export const MAIN_NODE_PANEL_WIDTH = 360;
|
export const MAIN_NODE_PANEL_WIDTH = 360;
|
||||||
|
|
||||||
|
|
|
@ -1692,5 +1692,8 @@
|
||||||
"settings.ldap.form.pageSize.infoText": "Max number of records to return per page during synchronization. 0 for unlimited",
|
"settings.ldap.form.pageSize.infoText": "Max number of records to return per page during synchronization. 0 for unlimited",
|
||||||
"settings.ldap.form.searchTimeout.label": "Search Timeout (Seconds)",
|
"settings.ldap.form.searchTimeout.label": "Search Timeout (Seconds)",
|
||||||
"settings.ldap.form.searchTimeout.infoText": "The timeout value for queries to the AD/LDAP server. Increase if you are getting timeout errors caused by a slow AD/LDAP server",
|
"settings.ldap.form.searchTimeout.infoText": "The timeout value for queries to the AD/LDAP server. Increase if you are getting timeout errors caused by a slow AD/LDAP server",
|
||||||
"settings.ldap.section.synchronization.title": "Synchronization"
|
"settings.ldap.section.synchronization.title": "Synchronization",
|
||||||
|
|
||||||
|
"sso.login.divider": "or",
|
||||||
|
"sso.login.button": "Continue with SSO"
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ import WorkflowsView from '@/views/WorkflowsView.vue';
|
||||||
import { IPermissions } from './Interface';
|
import { IPermissions } from './Interface';
|
||||||
import { LOGIN_STATUS, ROLE } from '@/utils';
|
import { LOGIN_STATUS, ROLE } from '@/utils';
|
||||||
import { RouteConfigSingleView } from 'vue-router/types/router';
|
import { RouteConfigSingleView } from 'vue-router/types/router';
|
||||||
import { EnterpriseEditionFeature, VIEWS } from './constants';
|
import { VIEWS } from './constants';
|
||||||
import { useSettingsStore } from './stores/settings';
|
import { useSettingsStore } from './stores/settings';
|
||||||
import { useTemplatesStore } from './stores/templates';
|
import { useTemplatesStore } from './stores/templates';
|
||||||
import SettingsUsageAndPlanVue from './views/SettingsUsageAndPlan.vue';
|
import SettingsUsageAndPlanVue from './views/SettingsUsageAndPlan.vue';
|
||||||
|
|
|
@ -15,14 +15,15 @@ import {
|
||||||
VALUE_SURVEY_MODAL_KEY,
|
VALUE_SURVEY_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import {
|
import {
|
||||||
|
ILdapConfig,
|
||||||
ILogLevel,
|
ILogLevel,
|
||||||
IN8nPromptResponse,
|
IN8nPromptResponse,
|
||||||
IN8nPrompts,
|
IN8nPrompts,
|
||||||
IN8nUISettings,
|
IN8nUISettings,
|
||||||
IN8nValueSurveyData,
|
IN8nValueSurveyData,
|
||||||
ISettingsState,
|
ISettingsState,
|
||||||
|
UserManagementAuthenticationMethod,
|
||||||
WorkflowCallerPolicyDefaultOption,
|
WorkflowCallerPolicyDefaultOption,
|
||||||
ILdapConfig,
|
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { IDataObject, ITelemetrySettings } from 'n8n-workflow';
|
import { IDataObject, ITelemetrySettings } from 'n8n-workflow';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
@ -40,6 +41,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
showSetupOnFirstLoad: false,
|
showSetupOnFirstLoad: false,
|
||||||
smtpSetup: false,
|
smtpSetup: false,
|
||||||
|
authenticationMethod: UserManagementAuthenticationMethod.Email,
|
||||||
},
|
},
|
||||||
templatesEndpointHealthy: false,
|
templatesEndpointHealthy: false,
|
||||||
api: {
|
api: {
|
||||||
|
@ -169,13 +171,15 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||||
workflowCallerPolicyDefaultOption(): WorkflowCallerPolicyDefaultOption {
|
workflowCallerPolicyDefaultOption(): WorkflowCallerPolicyDefaultOption {
|
||||||
return this.settings.workflowCallerPolicyDefaultOption;
|
return this.settings.workflowCallerPolicyDefaultOption;
|
||||||
},
|
},
|
||||||
|
isDefaultAuthenticationSaml(): boolean {
|
||||||
|
return this.userManagement.authenticationMethod === UserManagementAuthenticationMethod.Saml;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setSettings(settings: IN8nUISettings): void {
|
setSettings(settings: IN8nUISettings): void {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.userManagement.enabled = settings.userManagement.enabled;
|
this.userManagement = settings.userManagement;
|
||||||
this.userManagement.showSetupOnFirstLoad = !!settings.userManagement.showSetupOnFirstLoad;
|
this.userManagement.showSetupOnFirstLoad = !!settings.userManagement.showSetupOnFirstLoad;
|
||||||
this.userManagement.smtpSetup = settings.userManagement.smtpSetup;
|
|
||||||
this.api = settings.publicApi;
|
this.api = settings.publicApi;
|
||||||
this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled;
|
this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled;
|
||||||
this.ldap.loginEnabled = settings.sso.ldap.loginEnabled;
|
this.ldap.loginEnabled = settings.sso.ldap.loginEnabled;
|
||||||
|
|
42
packages/editor-ui/src/stores/sso.ts
Normal file
42
packages/editor-ui/src/stores/sso.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { computed, reactive } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { EnterpriseEditionFeature } from '@/constants';
|
||||||
|
import { useRootStore } from '@/stores/n8nRootStore';
|
||||||
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
|
import { initSSO } from '@/api/sso';
|
||||||
|
|
||||||
|
export const useSSOStore = defineStore('sso', () => {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoading = computed(() => state.loading);
|
||||||
|
|
||||||
|
const setLoading = (loading: boolean) => {
|
||||||
|
state.loading = loading;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSamlLoginEnabled = computed(() => settingsStore.isSamlLoginEnabled);
|
||||||
|
const isEnterpriseSamlEnabled = computed(() =>
|
||||||
|
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Saml),
|
||||||
|
);
|
||||||
|
const isDefaultAuthenticationSaml = computed(() => settingsStore.isDefaultAuthenticationSaml);
|
||||||
|
const showSsoLoginButton = computed(
|
||||||
|
() =>
|
||||||
|
isSamlLoginEnabled.value &&
|
||||||
|
isEnterpriseSamlEnabled.value &&
|
||||||
|
isDefaultAuthenticationSaml.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSSORedirectUrl = () => initSSO(rootStore.getRestApiContext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
setLoading,
|
||||||
|
showSsoLoginButton,
|
||||||
|
getSSORedirectUrl,
|
||||||
|
};
|
||||||
|
});
|
|
@ -14,7 +14,9 @@
|
||||||
@secondaryClick="onSecondaryClick"
|
@secondaryClick="onSecondaryClick"
|
||||||
@submit="onSubmit"
|
@submit="onSubmit"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
/>
|
>
|
||||||
|
<SSOLogin v-if="withSso" />
|
||||||
|
</n8n-form-box>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -22,12 +24,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
import Logo from '../components/Logo.vue';
|
import Logo from '@/components/Logo.vue';
|
||||||
|
import SSOLogin from '@/components/SSOLogin.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'AuthView',
|
name: 'AuthView',
|
||||||
components: {
|
components: {
|
||||||
Logo,
|
Logo,
|
||||||
|
SSOLogin,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
form: {},
|
form: {},
|
||||||
|
@ -38,6 +42,10 @@ export default Vue.extend({
|
||||||
subtitle: {
|
subtitle: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
withSso: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onInput(e: { name: string; value: string }) {
|
onInput(e: { name: string; value: string }) {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<AuthView
|
<AuthView
|
||||||
:form="FORM_CONFIG"
|
:form="FORM_CONFIG"
|
||||||
:formLoading="loading"
|
:formLoading="loading"
|
||||||
|
:with-sso="true"
|
||||||
data-test-id="signin-form"
|
data-test-id="signin-form"
|
||||||
@submit="onSubmit"
|
@submit="onSubmit"
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in a new issue