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:
Csaba Tuncsik 2023-03-17 21:07:08 +01:00 committed by GitHub
parent e0ea97af8d
commit 6916628a9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 144 additions and 10 deletions

View file

@ -137,9 +137,9 @@ export class SamlController {
const result = this.samlService.getLoginRequestUrl();
if (result?.binding === 'redirect') {
// forced client side redirect through the use of a javascript redirect
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 res.status(301).send(result.context.context);
// return res.send(getInitSSOPostView(result.context));
// Return the redirect URL directly
return res.send(result.context.context);
} else if (result?.binding === 'post') {
return res.send(getInitSSOFormView(result.context as PostBindingContext));
} else {

View file

@ -33,6 +33,7 @@
{{ redirectText }}
</n8n-link>
</div>
<slot></slot>
</div>
</template>

View file

@ -623,10 +623,17 @@ export interface IN8nPromptResponse {
updated: boolean;
}
export enum UserManagementAuthenticationMethod {
Email = 'email',
Ldap = 'ldap',
Saml = 'saml',
}
export interface IUserManagementConfig {
enabled: boolean;
showSetupOnFirstLoad?: boolean;
smtpSetup: boolean;
authenticationMethod: UserManagementAuthenticationMethod;
}
export interface IPermissionGroup {

View 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');
};

View 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>

View file

@ -448,6 +448,7 @@ export enum EnterpriseEditionFeature {
Sharing = 'sharing',
Ldap = 'ldap',
LogStreaming = 'logStreaming',
Saml = 'saml',
}
export const MAIN_NODE_PANEL_WIDTH = 360;

View file

@ -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.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.section.synchronization.title": "Synchronization"
"settings.ldap.section.synchronization.title": "Synchronization",
"sso.login.divider": "or",
"sso.login.button": "Continue with SSO"
}

View file

@ -31,7 +31,7 @@ import WorkflowsView from '@/views/WorkflowsView.vue';
import { IPermissions } from './Interface';
import { LOGIN_STATUS, ROLE } from '@/utils';
import { RouteConfigSingleView } from 'vue-router/types/router';
import { EnterpriseEditionFeature, VIEWS } from './constants';
import { VIEWS } from './constants';
import { useSettingsStore } from './stores/settings';
import { useTemplatesStore } from './stores/templates';
import SettingsUsageAndPlanVue from './views/SettingsUsageAndPlan.vue';

View file

@ -15,14 +15,15 @@ import {
VALUE_SURVEY_MODAL_KEY,
} from '@/constants';
import {
ILdapConfig,
ILogLevel,
IN8nPromptResponse,
IN8nPrompts,
IN8nUISettings,
IN8nValueSurveyData,
ISettingsState,
UserManagementAuthenticationMethod,
WorkflowCallerPolicyDefaultOption,
ILdapConfig,
} from '@/Interface';
import { IDataObject, ITelemetrySettings } from 'n8n-workflow';
import { defineStore } from 'pinia';
@ -40,6 +41,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
enabled: false,
showSetupOnFirstLoad: false,
smtpSetup: false,
authenticationMethod: UserManagementAuthenticationMethod.Email,
},
templatesEndpointHealthy: false,
api: {
@ -169,13 +171,15 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
workflowCallerPolicyDefaultOption(): WorkflowCallerPolicyDefaultOption {
return this.settings.workflowCallerPolicyDefaultOption;
},
isDefaultAuthenticationSaml(): boolean {
return this.userManagement.authenticationMethod === UserManagementAuthenticationMethod.Saml;
},
},
actions: {
setSettings(settings: IN8nUISettings): void {
this.settings = settings;
this.userManagement.enabled = settings.userManagement.enabled;
this.userManagement = settings.userManagement;
this.userManagement.showSetupOnFirstLoad = !!settings.userManagement.showSetupOnFirstLoad;
this.userManagement.smtpSetup = settings.userManagement.smtpSetup;
this.api = settings.publicApi;
this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled;
this.ldap.loginEnabled = settings.sso.ldap.loginEnabled;

View 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,
};
});

View file

@ -14,7 +14,9 @@
@secondaryClick="onSecondaryClick"
@submit="onSubmit"
@input="onInput"
/>
>
<SSOLogin v-if="withSso" />
</n8n-form-box>
</div>
</div>
</template>
@ -22,12 +24,14 @@
<script lang="ts">
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({
name: 'AuthView',
components: {
Logo,
SSOLogin,
},
props: {
form: {},
@ -38,6 +42,10 @@ export default Vue.extend({
subtitle: {
type: String,
},
withSso: {
type: Boolean,
default: false,
},
},
methods: {
onInput(e: { name: string; value: string }) {

View file

@ -2,6 +2,7 @@
<AuthView
:form="FORM_CONFIG"
:formLoading="loading"
:with-sso="true"
data-test-id="signin-form"
@submit="onSubmit"
/>