feat(core): Add SAML XML validation (#5600)
* 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 * adds saml XML validation * add comment * protect test endpoint * improve ignoreSSL and some cleanup * fix wrong schema used * remove console.log
This commit is contained in:
committed by
GitHub
parent
ddfa16cf27
commit
ca66ec8f4d
@@ -10,7 +10,7 @@ import { isSsoJustInTimeProvisioningEnabled } from '../ssoHelpers';
|
||||
import type { SamlPreferences } from './types/samlPreferences';
|
||||
import { SAML_PREFERENCES_DB_KEY } from './constants';
|
||||
import type { IdentityProviderInstance } from 'samlify';
|
||||
import { IdentityProvider } from 'samlify';
|
||||
import { IdentityProvider, setSchemaValidator } from 'samlify';
|
||||
import {
|
||||
createUserFromSamlAttributes,
|
||||
getMappedSamlAttributesFromFlowResult,
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
} from './samlHelpers';
|
||||
import type { Settings } from '../../databases/entities/Settings';
|
||||
import axios from 'axios';
|
||||
import https from 'https';
|
||||
import type { SamlLoginBinding } from './types';
|
||||
import type { BindingContext, PostBindingContext } from 'samlify/types/src/entity';
|
||||
import { validateMetadata, validateResponse } from './samlValidator';
|
||||
|
||||
export class SamlService {
|
||||
private static instance: SamlService;
|
||||
@@ -46,30 +48,14 @@ export class SamlService {
|
||||
this._attributeMapping = mapping;
|
||||
}
|
||||
|
||||
private _metadata = '';
|
||||
private metadata = '';
|
||||
|
||||
private metadataUrl = '';
|
||||
|
||||
private ignoreSSL = false;
|
||||
|
||||
private loginBinding: SamlLoginBinding = 'post';
|
||||
|
||||
public get metadata(): string {
|
||||
return this._metadata;
|
||||
}
|
||||
|
||||
public set metadata(metadata: string) {
|
||||
this._metadata = metadata;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.loadFromDbAndApplySamlPreferences()
|
||||
.then(() => {
|
||||
LoggerProxy.debug('Initializing SAML service');
|
||||
})
|
||||
.catch(() => {
|
||||
LoggerProxy.error('Error initializing SAML service');
|
||||
});
|
||||
}
|
||||
|
||||
static getInstance(): SamlService {
|
||||
if (!SamlService.instance) {
|
||||
SamlService.instance = new SamlService();
|
||||
@@ -79,6 +65,15 @@ export class SamlService {
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.loadFromDbAndApplySamlPreferences();
|
||||
setSchemaValidator({
|
||||
validate: async (response: string) => {
|
||||
const valid = await validateResponse(response);
|
||||
if (!valid) {
|
||||
return Promise.reject(new Error('Invalid SAML response'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getIdentityProviderInstance(forceRecreate = false): IdentityProviderInstance {
|
||||
@@ -125,7 +120,6 @@ export class SamlService {
|
||||
'post',
|
||||
) as PostBindingContext;
|
||||
//TODO:SAML: debug logging
|
||||
|
||||
LoggerProxy.debug(loginRequest.context);
|
||||
return loginRequest;
|
||||
}
|
||||
@@ -188,6 +182,7 @@ export class SamlService {
|
||||
mapping: this.attributeMapping,
|
||||
metadata: this.metadata,
|
||||
metadataUrl: this.metadataUrl,
|
||||
ignoreSSL: this.ignoreSSL,
|
||||
loginBinding: this.loginBinding,
|
||||
loginEnabled: isSamlLoginEnabled(),
|
||||
loginLabel: getSamlLoginLabel(),
|
||||
@@ -198,12 +193,19 @@ export class SamlService {
|
||||
this.loginBinding = prefs.loginBinding ?? this.loginBinding;
|
||||
this.metadata = prefs.metadata ?? this.metadata;
|
||||
this.attributeMapping = prefs.mapping ?? this.attributeMapping;
|
||||
this.ignoreSSL = prefs.ignoreSSL ?? this.ignoreSSL;
|
||||
if (prefs.metadataUrl) {
|
||||
this.metadataUrl = prefs.metadataUrl;
|
||||
const fetchedMetadata = await this.fetchMetadataFromUrl();
|
||||
if (fetchedMetadata) {
|
||||
this.metadata = fetchedMetadata;
|
||||
}
|
||||
} else if (prefs.metadata) {
|
||||
const validationResult = await validateMetadata(prefs.metadata);
|
||||
if (!validationResult) {
|
||||
throw new Error('Invalid SAML metadata');
|
||||
}
|
||||
this.metadata = prefs.metadata;
|
||||
}
|
||||
setSamlLoginEnabled(prefs.loginEnabled ?? isSamlLoginEnabled());
|
||||
setSamlLoginLabel(prefs.loginLabel ?? getSamlLoginLabel());
|
||||
@@ -248,18 +250,24 @@ export class SamlService {
|
||||
|
||||
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;
|
||||
// TODO:SAML: this will not work once axios is upgraded to > 1.2.0 (see checkServerIdentity)
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: !this.ignoreSSL,
|
||||
});
|
||||
const response = await axios.get(this.metadataUrl, { httpsAgent: agent });
|
||||
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.');
|
||||
const validationResult = await validateMetadata(xml);
|
||||
if (!validationResult) {
|
||||
throw new BadRequestError(
|
||||
`Data received from ${this.metadataUrl} is not valid SAML metadata.`,
|
||||
);
|
||||
}
|
||||
return xml;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new BadRequestError('SAML Metadata URL is invalid or response is .');
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new BadRequestError(`Error fetching SAML Metadata from ${this.metadataUrl}: ${error}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -298,10 +306,14 @@ export class SamlService {
|
||||
|
||||
async testSamlConnection(): Promise<boolean> {
|
||||
try {
|
||||
// TODO:SAML: this will not work once axios is upgraded to > 1.2.0 (see checkServerIdentity)
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: !this.ignoreSSL,
|
||||
});
|
||||
const requestContext = this.getLoginRequestUrl();
|
||||
if (!requestContext) return false;
|
||||
if (requestContext.binding === 'redirect') {
|
||||
const fetchResult = await axios.get(requestContext.context.context);
|
||||
const fetchResult = await axios.get(requestContext.context.context, { httpsAgent: agent });
|
||||
if (fetchResult.status !== 200) {
|
||||
LoggerProxy.debug('SAML: Error while testing SAML connection.');
|
||||
return false;
|
||||
@@ -319,6 +331,7 @@ export class SamlService {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'Content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
httpsAgent: agent,
|
||||
});
|
||||
if (fetchResult.status !== 200) {
|
||||
LoggerProxy.debug('SAML: Error while testing SAML connection.');
|
||||
|
||||
Reference in New Issue
Block a user