feat(core): Add SAML post and test endpoints (#5595)
* consolidate SSO settings * update saml settings * fix type error * limit user changes when saml is enabled * add test * add toggle endpoint and fetch metadata * rename enabled param * add handling of POST saml login request * add config test endpoint
This commit is contained in:
committed by
GitHub
parent
b5179597f3
commit
523fa71705
@@ -2,7 +2,7 @@ import type express from 'express';
|
||||
import * as Db from '@/Db';
|
||||
import type { User } from '@/databases/entities/User';
|
||||
import { jsonParse, LoggerProxy } from 'n8n-workflow';
|
||||
import { AuthError } from '@/ResponseHelper';
|
||||
import { AuthError, BadRequestError } from '@/ResponseHelper';
|
||||
import { getServiceProviderInstance } from './serviceProvider.ee';
|
||||
import type { SamlUserAttributes } from './types/samlUserAttributes';
|
||||
import type { SamlAttributeMapping } from './types/samlAttributeMapping';
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
setSamlLoginLabel,
|
||||
updateUserFromSamlAttributes,
|
||||
} from './samlHelpers';
|
||||
import type { Settings } from '../../databases/entities/Settings';
|
||||
import axios from 'axios';
|
||||
import type { SamlLoginBinding } from './types';
|
||||
import type { BindingContext, PostBindingContext } from 'samlify/types/src/entity';
|
||||
|
||||
export class SamlService {
|
||||
private static instance: SamlService;
|
||||
@@ -44,6 +48,10 @@ export class SamlService {
|
||||
|
||||
private _metadata = '';
|
||||
|
||||
private metadataUrl = '';
|
||||
|
||||
private loginBinding: SamlLoginBinding = 'post';
|
||||
|
||||
public get metadata(): string {
|
||||
return this._metadata;
|
||||
}
|
||||
@@ -53,7 +61,7 @@ export class SamlService {
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.loadSamlPreferences()
|
||||
this.loadFromDbAndApplySamlPreferences()
|
||||
.then(() => {
|
||||
LoggerProxy.debug('Initializing SAML service');
|
||||
})
|
||||
@@ -70,7 +78,7 @@ export class SamlService {
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.loadSamlPreferences();
|
||||
await this.loadFromDbAndApplySamlPreferences();
|
||||
}
|
||||
|
||||
getIdentityProviderInstance(forceRecreate = false): IdentityProviderInstance {
|
||||
@@ -83,19 +91,48 @@ export class SamlService {
|
||||
return this.identityProviderInstance;
|
||||
}
|
||||
|
||||
getRedirectLoginRequestUrl(): string {
|
||||
getLoginRequestUrl(binding?: SamlLoginBinding): {
|
||||
binding: SamlLoginBinding;
|
||||
context: BindingContext | PostBindingContext;
|
||||
} {
|
||||
if (binding === undefined) binding = this.loginBinding;
|
||||
if (binding === 'post') {
|
||||
return {
|
||||
binding,
|
||||
context: this.getPostLoginRequestUrl(),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
binding,
|
||||
context: this.getRedirectLoginRequestUrl(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getRedirectLoginRequestUrl(): BindingContext {
|
||||
const loginRequest = getServiceProviderInstance().createLoginRequest(
|
||||
this.getIdentityProviderInstance(),
|
||||
'redirect',
|
||||
);
|
||||
//TODO:SAML: debug logging
|
||||
LoggerProxy.debug(loginRequest.context);
|
||||
return loginRequest.context;
|
||||
return loginRequest;
|
||||
}
|
||||
|
||||
private getPostLoginRequestUrl(): PostBindingContext {
|
||||
const loginRequest = getServiceProviderInstance().createLoginRequest(
|
||||
this.getIdentityProviderInstance(),
|
||||
'post',
|
||||
) as PostBindingContext;
|
||||
//TODO:SAML: debug logging
|
||||
|
||||
LoggerProxy.debug(loginRequest.context);
|
||||
return loginRequest;
|
||||
}
|
||||
|
||||
async handleSamlLogin(
|
||||
req: express.Request,
|
||||
binding: 'post' | 'redirect',
|
||||
binding: SamlLoginBinding,
|
||||
): Promise<
|
||||
| {
|
||||
authenticatedUser: User | undefined;
|
||||
@@ -150,21 +187,32 @@ export class SamlService {
|
||||
return {
|
||||
mapping: this.attributeMapping,
|
||||
metadata: this.metadata,
|
||||
metadataUrl: this.metadataUrl,
|
||||
loginBinding: this.loginBinding,
|
||||
loginEnabled: isSamlLoginEnabled(),
|
||||
loginLabel: getSamlLoginLabel(),
|
||||
};
|
||||
}
|
||||
|
||||
async setSamlPreferences(prefs: SamlPreferences): Promise<void> {
|
||||
this.attributeMapping = prefs.mapping;
|
||||
this.metadata = prefs.metadata;
|
||||
setSamlLoginEnabled(prefs.loginEnabled);
|
||||
setSamlLoginLabel(prefs.loginLabel);
|
||||
async setSamlPreferences(prefs: SamlPreferences): Promise<SamlPreferences | undefined> {
|
||||
this.loginBinding = prefs.loginBinding ?? this.loginBinding;
|
||||
this.metadata = prefs.metadata ?? this.metadata;
|
||||
this.attributeMapping = prefs.mapping ?? this.attributeMapping;
|
||||
if (prefs.metadataUrl) {
|
||||
this.metadataUrl = prefs.metadataUrl;
|
||||
const fetchedMetadata = await this.fetchMetadataFromUrl();
|
||||
if (fetchedMetadata) {
|
||||
this.metadata = fetchedMetadata;
|
||||
}
|
||||
}
|
||||
setSamlLoginEnabled(prefs.loginEnabled ?? isSamlLoginEnabled());
|
||||
setSamlLoginLabel(prefs.loginLabel ?? getSamlLoginLabel());
|
||||
this.getIdentityProviderInstance(true);
|
||||
await this.saveSamlPreferences();
|
||||
const result = await this.saveSamlPreferencesToDb();
|
||||
return result;
|
||||
}
|
||||
|
||||
async loadSamlPreferences(): Promise<SamlPreferences | undefined> {
|
||||
async loadFromDbAndApplySamlPreferences(): Promise<SamlPreferences | undefined> {
|
||||
const samlPreferences = await Db.collections.Settings.findOne({
|
||||
where: { key: SAML_PREFERENCES_DB_KEY },
|
||||
});
|
||||
@@ -178,26 +226,47 @@ export class SamlService {
|
||||
return;
|
||||
}
|
||||
|
||||
async saveSamlPreferences(): Promise<void> {
|
||||
async saveSamlPreferencesToDb(): Promise<SamlPreferences | undefined> {
|
||||
const samlPreferences = await Db.collections.Settings.findOne({
|
||||
where: { key: SAML_PREFERENCES_DB_KEY },
|
||||
});
|
||||
const settingsValue = JSON.stringify(this.getSamlPreferences());
|
||||
let result: Settings;
|
||||
if (samlPreferences) {
|
||||
samlPreferences.value = settingsValue;
|
||||
await Db.collections.Settings.save(samlPreferences);
|
||||
result = await Db.collections.Settings.save(samlPreferences);
|
||||
} else {
|
||||
await Db.collections.Settings.save({
|
||||
result = await Db.collections.Settings.save({
|
||||
key: SAML_PREFERENCES_DB_KEY,
|
||||
value: settingsValue,
|
||||
loadOnStartup: true,
|
||||
});
|
||||
}
|
||||
if (result) return jsonParse<SamlPreferences>(result.value);
|
||||
return;
|
||||
}
|
||||
|
||||
async fetchMetadataFromUrl(): Promise<string | undefined> {
|
||||
try {
|
||||
const prevRejectStatus = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
const response = await axios.get(this.metadataUrl);
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = prevRejectStatus;
|
||||
if (response.status === 200 && response.data) {
|
||||
const xml = (await response.data) as string;
|
||||
// TODO: SAML: validate XML
|
||||
// throw new BadRequestError('Received XML is not valid SAML metadata.');
|
||||
return xml;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new BadRequestError('SAML Metadata URL is invalid or response is .');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async getAttributesFromLoginResponse(
|
||||
req: express.Request,
|
||||
binding: 'post' | 'redirect',
|
||||
binding: SamlLoginBinding,
|
||||
): Promise<SamlUserAttributes> {
|
||||
let parsedSamlResponse;
|
||||
try {
|
||||
@@ -226,4 +295,41 @@ export class SamlService {
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
async testSamlConnection(): Promise<boolean> {
|
||||
try {
|
||||
const requestContext = this.getLoginRequestUrl();
|
||||
if (!requestContext) return false;
|
||||
if (requestContext.binding === 'redirect') {
|
||||
const fetchResult = await axios.get(requestContext.context.context);
|
||||
if (fetchResult.status !== 200) {
|
||||
LoggerProxy.debug('SAML: Error while testing SAML connection.');
|
||||
return false;
|
||||
}
|
||||
} else if (requestContext.binding === 'post') {
|
||||
const context = requestContext.context as PostBindingContext;
|
||||
const endpoint = context.entityEndpoint;
|
||||
const params = new URLSearchParams();
|
||||
params.append(context.type, context.context);
|
||||
if (context.relayState) {
|
||||
params.append('RelayState', context.relayState);
|
||||
}
|
||||
const fetchResult = await axios.post(endpoint, params, {
|
||||
headers: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'Content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
if (fetchResult.status !== 200) {
|
||||
LoggerProxy.debug('SAML: Error while testing SAML connection.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
LoggerProxy.debug('SAML: Error while testing SAML connection: ', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user