Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] Added client certificate options to support mutual TLS for OpenID endpoint #1683

Merged
merged 1 commit into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions server/auth/types/openid/openid_auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SecuritySessionCookie>;
let logger: Logger;

// Consistent with auth_handler_factory.test.ts
beforeEach(() => {});
Expand All @@ -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,
Expand Down Expand Up @@ -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();
});
});
41 changes: 36 additions & 5 deletions server/auth/types/openid/openid_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -55,6 +56,10 @@ export interface OpenIdAuthConfig {

export interface WreckHttpsOptions {
ca?: string | Buffer | Array<string | Buffer>;
cert?: string | Buffer | Array<string | Buffer>;
key?: string | Buffer | Array<string | Buffer>;
passphrase?: string;
pfx?: string | Buffer | Array<string | Buffer>;
checkServerIdentity?: (host: string, cert: PeerCertificate) => Error | undefined;
}

Expand All @@ -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,
Expand Down Expand Up @@ -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,
}),
Expand All @@ -144,6 +171,10 @@ export class OpenIdAuthentication extends AuthenticationType {
}
}

getWreckHttpsOptions(): WreckHttpsOptions {
return this.wreckHttpsOption;
}

createExtraStorage() {
// @ts-ignore
const hapiServer: Server = this.sessionStorageFactory.asScoped({}).server;
Expand Down
4 changes: 4 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down
24 changes: 24 additions & 0 deletions server/utils/object_properties_defined.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>, 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')}`;
}
35 changes: 35 additions & 0 deletions test/certs/cert.pem
Original file line number Diff line number Diff line change
@@ -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-----
Binary file added test/certs/keyStore.p12
Binary file not shown.
52 changes: 52 additions & 0 deletions test/certs/private-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
3Q5ijNV6CUa+Ka7Qg7FWESHFemt1gQ==
-----END PRIVATE KEY-----