From ec661f8847e090c3293bb018c08b51625232cbf5 Mon Sep 17 00:00:00 2001 From: Simple-Analysis <74850112+Simple-Analysis@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:08:55 -0700 Subject: [PATCH] Added client certificate options to support mutual TLS for OpenID endpoint (#1650) * Added client certificate options to support mutual TLS --------- Signed-off-by: Calvin Harrison <74850112+Simple-Analysis@users.noreply.github.com> Signed-off-by: Simple-Analysis <74850112+Simple-Analysis@users.noreply.github.com> Co-authored-by: Peter Nied --- server/auth/types/openid/openid_auth.test.ts | 94 ++++++++++++++++++- server/auth/types/openid/openid_auth.ts | 41 +++++++- server/index.ts | 4 + server/utils/object_properties_defined.ts | 24 +++++ test/certs/cert.pem | 35 +++++++ test/certs/keyStore.p12 | Bin 0 -> 4333 bytes test/certs/private-key.pem | 52 ++++++++++ 7 files changed, 243 insertions(+), 7 deletions(-) create mode 100644 server/utils/object_properties_defined.ts create mode 100644 test/certs/cert.pem create mode 100644 test/certs/keyStore.p12 create mode 100644 test/certs/private-key.pem diff --git a/server/auth/types/openid/openid_auth.test.ts b/server/auth/types/openid/openid_auth.test.ts index f998d844f..55f730481 100644 --- a/server/auth/types/openid/openid_auth.test.ts +++ b/server/auth/types/openid/openid_auth.test.ts @@ -21,20 +21,27 @@ import { OpenIdAuthentication } from './openid_auth'; import { SecurityPluginConfigType } from '../../../index'; import { SecuritySessionCookie } from '../../../session/security_cookie'; import { deflateValue } from '../../../utils/compression'; +import { getObjectProperties } from '../../../utils/object_properties_defined'; import { IRouter, CoreSetup, ILegacyClusterClient, - Logger, SessionStorageFactory, } from '../../../../../../src/core/server'; +interface Logger { + debug(message: string): void; + info(message: string): void; + warn(message: string): void; + error(message: string): void; + fatal(message: string): void; +} + describe('test OpenId authHeaderValue', () => { let router: IRouter; let core: CoreSetup; let esClient: ILegacyClusterClient; let sessionStorageFactory: SessionStorageFactory; - let logger: Logger; // Consistent with auth_handler_factory.test.ts beforeEach(() => {}); @@ -50,6 +57,14 @@ describe('test OpenId authHeaderValue', () => { }, } as unknown) as SecurityPluginConfigType; + const logger = { + debug: (message: string) => {}, + info: (message: string) => {}, + warn: (message: string) => {}, + error: (message: string) => {}, + fatal: (message: string) => {}, + }; + test('make sure that cookies with authHeaderValue are still valid', async () => { const openIdAuthentication = new OpenIdAuthentication( config, @@ -117,4 +132,79 @@ describe('test OpenId authHeaderValue', () => { expect(headers).toEqual(expectedHeaders); }); + + test('Make sure that wreckClient can be configured with mTLS', async () => { + const customConfig = { + openid: { + certificate: 'test/certs/cert.pem', + private_key: 'test/certs/private-key.pem', + header: 'authorization', + scope: [], + }, + }; + + const openidConfig = (customConfig as unknown) as SecurityPluginConfigType; + + const openIdAuthentication = new OpenIdAuthentication( + openidConfig, + sessionStorageFactory, + router, + esClient, + core, + logger + ); + + const wreckHttpsOptions = openIdAuthentication.getWreckHttpsOptions(); + + console.log( + '============= PEM =============', + '\n\n', + getObjectProperties(customConfig.openid, 'OpenID'), + '\n\n', + getObjectProperties(wreckHttpsOptions, 'wreckHttpsOptions') + ); + + expect(wreckHttpsOptions.key).toBeDefined(); + expect(wreckHttpsOptions.cert).toBeDefined(); + expect(wreckHttpsOptions.pfx).toBeUndefined(); + }); + + test('Ensure private key and certificate are not exposed when using PFX certificate', async () => { + const customConfig = { + openid: { + pfx: 'test/certs/keyStore.p12', + certificate: 'test/certs/cert.pem', + private_key: 'test/certs/private-key.pem', + passphrase: '', + header: 'authorization', + scope: [], + }, + }; + + const openidConfig = (customConfig as unknown) as SecurityPluginConfigType; + + const openIdAuthentication = new OpenIdAuthentication( + openidConfig, + sessionStorageFactory, + router, + esClient, + core, + logger + ); + + const wreckHttpsOptions = openIdAuthentication.getWreckHttpsOptions(); + + console.log( + '============= PFX =============', + '\n\n', + getObjectProperties(customConfig.openid, 'OpenID'), + '\n\n', + getObjectProperties(wreckHttpsOptions, 'wreckHttpsOptions') + ); + + expect(wreckHttpsOptions.pfx).toBeDefined(); + expect(wreckHttpsOptions.key).toBeUndefined(); + expect(wreckHttpsOptions.cert).toBeUndefined(); + expect(wreckHttpsOptions.passphrase).toBeUndefined(); + }); }); diff --git a/server/auth/types/openid/openid_auth.ts b/server/auth/types/openid/openid_auth.ts index accabb7c1..747cb84eb 100644 --- a/server/auth/types/openid/openid_auth.ts +++ b/server/auth/types/openid/openid_auth.ts @@ -36,6 +36,7 @@ import { OpenIdAuthRoutes } from './routes'; import { AuthenticationType } from '../authentication_type'; import { callTokenEndpoint } from './helper'; import { composeNextUrlQueryParam } from '../../../utils/next_url'; +import { getObjectProperties } from '../../../utils/object_properties_defined'; import { getExpirationDate } from './helper'; import { AuthType, OPENID_AUTH_LOGIN } from '../../../../common'; import { @@ -55,6 +56,10 @@ export interface OpenIdAuthConfig { export interface WreckHttpsOptions { ca?: string | Buffer | Array; + cert?: string | Buffer | Array; + key?: string | Buffer | Array; + passphrase?: string; + pfx?: string | Buffer | Array; checkServerIdentity?: (host: string, cert: PeerCertificate) => Error | undefined; } @@ -65,6 +70,7 @@ export class OpenIdAuthentication extends AuthenticationType { private authHeaderName: string; private openIdConnectUrl: string; private wreckClient: typeof wreck; + private wreckHttpsOption: WreckHttpsOptions = {}; constructor( config: SecurityPluginConfigType, @@ -119,21 +125,42 @@ export class OpenIdAuthentication extends AuthenticationType { } private createWreckClient(): typeof wreck { - const wreckHttpsOption: WreckHttpsOptions = {}; if (this.config.openid?.root_ca) { - wreckHttpsOption.ca = [fs.readFileSync(this.config.openid.root_ca)]; + this.wreckHttpsOption.ca = [fs.readFileSync(this.config.openid.root_ca)]; + this.logger.debug(`Using CA Cert: ${this.config.openid.root_ca}`); + } + if (this.config.openid?.pfx) { + // Use PFX or PKCS12 if provided + this.logger.debug(`Using PFX or PKCS12: ${this.config.openid.pfx}`); + this.wreckHttpsOption.pfx = [fs.readFileSync(this.config.openid.pfx)]; + } else if (this.config.openid?.certificate && this.config.openid?.private_key) { + // Use 'certificate' and 'private_key' if provided + this.logger.debug(`Using Certificate: ${this.config.openid.certificate}`); + this.logger.debug(`Using Private Key: ${this.config.openid.private_key}`); + this.wreckHttpsOption.cert = [fs.readFileSync(this.config.openid.certificate)]; + this.wreckHttpsOption.key = [fs.readFileSync(this.config.openid.private_key)]; + } else { + this.logger.debug( + `Client certificates not provided. Mutual TLS will not be used to obtain endpoints.` + ); + } + // Check if passphrase is provided, use it for 'pfx' and 'key' + if (this.config.openid?.passphrase !== '') { + this.logger.debug(`Passphrase not provided for private key and/or pfx.`); + this.wreckHttpsOption.passphrase = this.config.openid?.passphrase; } if (this.config.openid?.verify_hostnames === false) { this.logger.debug(`openId auth 'verify_hostnames' option is off.`); - wreckHttpsOption.checkServerIdentity = (host: string, cert: PeerCertificate) => { + this.wreckHttpsOption.checkServerIdentity = (host: string, cert: PeerCertificate) => { return undefined; }; } - if (Object.keys(wreckHttpsOption).length > 0) { + this.logger.info(getObjectProperties(this.wreckHttpsOption, 'WreckHttpsOptions')); + if (Object.keys(this.wreckHttpsOption).length > 0) { return wreck.defaults({ agents: { http: new HTTP.Agent(), - https: new HTTPS.Agent(wreckHttpsOption), + https: new HTTPS.Agent(this.wreckHttpsOption), httpsAllowUnauthorized: new HTTPS.Agent({ rejectUnauthorized: false, }), @@ -144,6 +171,10 @@ export class OpenIdAuthentication extends AuthenticationType { } } + getWreckHttpsOptions(): WreckHttpsOptions { + return this.wreckHttpsOption; + } + createExtraStorage() { // @ts-ignore const hapiServer: Server = this.sessionStorageFactory.asScoped({}).server; diff --git a/server/index.ts b/server/index.ts index c1af7fa16..915da0315 100644 --- a/server/index.ts +++ b/server/index.ts @@ -178,6 +178,10 @@ export const configSchema = schema.object({ base_redirect_url: schema.string({ defaultValue: '' }), logout_url: schema.string({ defaultValue: '' }), root_ca: schema.string({ defaultValue: '' }), + certificate: schema.string({ defaultValue: '' }), + private_key: schema.string({ defaultValue: '' }), + passphrase: schema.string({ defaultValue: '' }), + pfx: schema.string({ defaultValue: '' }), verify_hostnames: schema.boolean({ defaultValue: true }), refresh_tokens: schema.boolean({ defaultValue: true }), trust_dynamic_headers: schema.boolean({ defaultValue: false }), diff --git a/server/utils/object_properties_defined.ts b/server/utils/object_properties_defined.ts new file mode 100644 index 000000000..fc1337146 --- /dev/null +++ b/server/utils/object_properties_defined.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +export function getObjectProperties(obj: Record, objName: string): string { + const objSummary: string[] = []; + + for (const [key, value] of Object.entries(obj)) { + objSummary.push(`${key}: ${value !== undefined ? 'Defined' : 'Not Defined'}`); + } + + return `${objName} properties:\n${objSummary.map((option) => ` ${option}`).join('\n')}`; +} diff --git a/test/certs/cert.pem b/test/certs/cert.pem new file mode 100644 index 000000000..33f8b236d --- /dev/null +++ b/test/certs/cert.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGJzCCBA+gAwIBAgIUMlm6Xg1wnOLi9gRLy3v4jF5U2JcwDQYJKoZIhvcNAQEL +BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFjAUBgNVBAMMDVRlc3RlciBNY1Vu +aXQxQzBBBgkqhkiG9w0BCQEWNHRlc3Rlci5tY3VuaXRAb3BlbnNlYXJjaGRhc2hi +b2FyZHNzZWN1cml0eXBsdWdpbi5jb20wHhcNMjMxMTIzMDg1NzQwWhcNMjQxMTIy +MDg1NzQwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAf +BgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAwwNVGVzdGVy +IE1jVW5pdDFDMEEGCSqGSIb3DQEJARY0dGVzdGVyLm1jdW5pdEBvcGVuc2VhcmNo +ZGFzaGJvYXJkc3NlY3VyaXR5cGx1Z2luLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBALLtHnXJyc7t50o2AlhzpaoZP81l80BYfEGf8wolNrlMXzJ7 +M32X7hG5quSdqlurUSS1L9hkl7Taqbq4fsiGrZX/s+8nkYDRfnaCU6nFH5gvwKcC +bPJyVCYKhuYL0qqKWjek+orknr3P6a7J6Db+eg6HuXlPFLl//JNwYrTBYtaymtaq +ek4mNyxUXlFq/4DXWDCe6DjMhdxdA56vB/3yF4qIdbKjhXuyIsNvMOLEsJe7c7tA ++7E1595vOX+jERSASDn7qA310tc9/NImobHz1fFBD3wL/WWMjfbRPLPC1LSM18EY +o3Cn9mDvHxXy/GHYq9AN5P7esQ0WmIA3AAZR0mUIDOGTG2kFIGlo9sSRBJ+Ygbf7 +7cCA9WE8cqAsEgajT9ZQRxYtGFJyK/M8mSldS+k0TMyWWR0wUeEdknGaTnfYx8lX +LcUFjWM6lTdJam4lereX7qkoJlxCadHPDpW+DIE55dOR9PVvVJtAOeMVZtMe7veo +QW4AMZFjV87rQKdzmQYEu1hLWyDOl46QA9U8nZhYc9A5TGGMDUwhvscoNQ7DjUt/ +O29IL/n9wpaa3hA/K3adg6hmSL57HBR4OxPRfNLT4c77zEyTTqpK9v0m5UGcKJyF +m7gQpG9MAdCjCA1JfzWXLLkA/Wsz7OVppYmhH7+dawwLKBAHC1bUo2iWx38TAgMB +AAGjUzBRMB0GA1UdDgQWBBQIkBuvFnIXXVsQ4xsWy5/OIoV0uDAfBgNVHSMEGDAW +gBQIkBuvFnIXXVsQ4xsWy5/OIoV0uDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4ICAQBRgxLAsPfEiG2rYplRCw/NMfI10EyiiWl0711LiIw3C47eQiC/ +sK7YXWsl4UTRtMyuK7kPTJf3g9e8VDHGyUZfqtBK2+ZYZSV49DWjb2ihWyK7TT1p +bvIW64gSWVWi6J5WOjBbHnlqaggjfNxo76VRaWP2e14m0B5QsnNYvG9LqK++27IL +ESZhb+Vd3NmVlcWIae1CXT8rdsAI3MR30aNDLnK2/YStfvDdgPGKC9VDqoBSywos +FPTVL+eqGAie9xDcSPpCpqCqGzPQk/Si25b9rUUWfxNyMomyVYeHTaZbT6XralKH +6ZtZwi8nuKw9/TKnMH2fAoBk9ZEq47ididzrPTZKDk5MDicpwWwX2JJbilcqWOGQ +0qeJsZk4zCNzLRiX8jjd0kLCfV0KcqRp2Skr0bAlO/KU8cvP2+IV92PnsB5ZD+yi +BmurX0tUT8DAusq3JLzJiBrngWpfUR0uQRzzJqHK8vtVOq+4BnxC6YEgnL5MIKCL +FDdQnE9G/t4N+Bot631Zfhm4KDSB9ycodAsdMujBco6GJdWbSueZB8YdGcBx47ig +1u1yDRqETXRyCU2PITxHbtBlgCkeNPRxlulz18WJLTlAPBhiZrS8GTqo7FjlE78N +GhgJ37+mzqK68J/PRVXpF3jLl0jhSEyUtgLTH/+y33WERhF6BG5cnYOlNA== +-----END CERTIFICATE----- diff --git a/test/certs/keyStore.p12 b/test/certs/keyStore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..ca99176c6b06422af2233c5aa36f1e4dcae449af GIT binary patch literal 4333 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 bbk%CodkTaJdi?|lMO>vHj=(qZ0s;sCFmpR9 literal 0 HcmV?d00001 diff --git a/test/certs/private-key.pem b/test/certs/private-key.pem new file mode 100644 index 000000000..ba3c59928 --- /dev/null +++ b/test/certs/private-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +3Q5ijNV6CUa+Ka7Qg7FWESHFemt1gQ== +-----END PRIVATE KEY-----