Skip to content
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
1 change: 1 addition & 0 deletions packages/kbn-mock-idp-plugin/public/login_page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const LoginPage = ({ config }: { config: ConfigType }) => {
full_name: values.full_name,
email: sanitizeEmail(values.full_name),
roles: [values.role],
url: window.location.href,
});
},
});
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-mock-idp-plugin/public/role_switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export const RoleSwitcher = () => {
full_name: currentUserState.value!.full_name,
email: currentUserState.value!.email,
roles: [role],
url: window.location.href,
});
setIsOpen(false);
},
Expand Down
8 changes: 8 additions & 0 deletions packages/kbn-mock-idp-plugin/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@kbn/es';
import type { ServerlessProductTier } from '@kbn/es/src/utils';
import { createSAMLResponse, MOCK_IDP_LOGIN_PATH, MOCK_IDP_LOGOUT_PATH } from '@kbn/mock-idp-utils';
import { getSAMLRequestId } from '@kbn/mock-idp-utils/src/utils';

import type { ConfigType } from './config';

Expand All @@ -35,6 +36,7 @@ const createSAMLResponseSchema = schema.object({
full_name: schema.maybe(schema.nullable(schema.string())),
email: schema.maybe(schema.nullable(schema.string())),
roles: schema.arrayOf(schema.string()),
url: schema.string(),
});

// BOOKMARK - List of Kibana project types
Expand Down Expand Up @@ -154,6 +156,11 @@ export const plugin: PluginInitializer<void, void, PluginSetupDependencies> = as
: {};

try {
const requestId = await getSAMLRequestId(request.body.url);
if (requestId) {
logger.info(`Sending SAML response for request ID: ${requestId}`);
}

return response.ok({
body: {
SAMLResponse: await createSAMLResponse({
Expand All @@ -162,6 +169,7 @@ export const plugin: PluginInitializer<void, void, PluginSetupDependencies> = as
full_name: request.body.full_name ?? undefined,
email: request.body.email ?? undefined,
roles: request.body.roles,
...(requestId ? { authnRequestId: requestId } : {}),
...serverlessOptions,
}),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ export {
createSAMLResponse,
ensureSAMLRoleMapping,
generateCosmosDBApiRequestHeaders,
getSAMLRequestId,
} from './utils';
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ import {
} from './constants';
import { decodeWithChecksum } from './jwt-codecs/encoder-checksum';
import { removePrefixEssuDev } from './jwt-codecs/encoder-prefix';
import { createMockIdpMetadata, createSAMLResponse, ensureSAMLRoleMapping } from './utils';
import {
createMockIdpMetadata,
createSAMLResponse,
ensureSAMLRoleMapping,
getSAMLRequestId,
} from './utils';

describe('mock-idp-utils', () => {
describe('createMockIdpMetadata', () => {
Expand Down Expand Up @@ -411,4 +416,25 @@ describe('mock-idp-utils', () => {
await expect(ensureSAMLRoleMapping(mockClient)).rejects.toThrow('Elasticsearch error');
});
});

describe('getSAMLReuestId', () => {
it('should extract SAMLRequest ID from URL', async () => {
const url =
'http://localhost:5601/mock_idp/login?SAMLRequest=fZJvT8IwEMa%2FSnPvYVsnExqGQYmRxD8Epi98Q0q5SWPXzl6H8u2dggYT4tvePc9z97sOLz4qw7boSTubQ9KNgaFVbq3tSw6PxXWnDxejIcnK8FqMm7Cxc3xrkAJrhZbEvpJD461wkjQJKyskEZRYjO9uBe%2FGovYuOOUMsDER%2BtBGXTlLTYV%2BgX6rFT7Ob3PYhFCLKDJOSbNxFEQvi5NI1joiVI3XYRd9pUVt2aykegU2aefQVobv2U%2FLK6del3pdt%2B8v2gKbTnJYxhivB6XkPDtL0wT755hgT2a9lA9kkpWDslTIy7TfthM1OLUUpA058JhnnTjpJL0iyQTvi5R3z1P%2BDGx2WPFS2z26%2F3is9k0kbopi1pk9LApgTz8naBvgAFx8p%2Ftj0v8byx%2B8MDpJYxgd%2B%2F6e9b41mk5mzmi1Y2Nj3PuVRxkwh1IaQohGB%2BHfHzD6BA%3D%3D';
const requestId = await getSAMLRequestId(url);
expect(requestId).toEqual('_0e0d9fa2264331e87e1e5a65329a16f9ffce2f38');
});

it('should return undefined if SAMLRequest parameter is missing', async () => {
const noParamUrl = 'http://localhost:5601/mock_idp/login';
const requestId = await getSAMLRequestId(noParamUrl);
expect(requestId).toBeUndefined();
});

it('should return undefined if SAMLRequest parameter is invalid', async () => {
const invalidUrl = 'http://localhost:5601/mock_idp/login?SAMLRequest=YmxhaCBibGFoIGJsYWg=';
const requestId = await getSAMLRequestId(invalidUrl);
expect(requestId).toBeUndefined();
});
});
});
28 changes: 28 additions & 0 deletions src/platform/packages/private/kbn-mock-idp-utils/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
import type { Client } from '@elastic/elasticsearch';
import { createHmac, randomBytes, X509Certificate } from 'crypto';
import { readFile } from 'fs/promises';
import Url from 'url';
import { promisify } from 'util';
import { SignedXml } from 'xml-crypto';
import { parseString } from 'xml2js';
import zlib from 'zlib';

import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';

Expand Down Expand Up @@ -406,3 +410,27 @@ function wrapSignedJwt(signedJwt: string): string {
const accessTokenEncodedWithChecksum = encodeWithChecksum(signedJwt);
return prefixWithEssuDev(accessTokenEncodedWithChecksum);
}

const inflateRawAsync = promisify(zlib.inflateRaw);
const parseStringAsync = promisify(parseString);

export async function getSAMLRequestId(requestUrl: string): Promise<string | undefined> {
const samlRequest = Url.parse(requestUrl, true /* parseQueryString */).query.SAMLRequest;

let requestId: string | undefined;

if (samlRequest) {
try {
const inflatedSAMLRequest = (await inflateRawAsync(
Buffer.from(samlRequest as string, 'base64')
)) as Buffer;

const parsedSAMLRequest = (await parseStringAsync(inflatedSAMLRequest.toString())) as any;
requestId = parsedSAMLRequest['saml2p:AuthnRequest'].$.ID as string;
} catch (e) {
return undefined;
}
}

return requestId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('SAMLAuthenticationProvider', () => {
refresh_token: 'some-refresh-token',
realm: 'test-realm',
authentication: mockUser,
in_response_to: mockSAMLSet1.requestId,
});

await expect(
Expand Down Expand Up @@ -105,6 +106,7 @@ describe('SAMLAuthenticationProvider', () => {
refresh_token: 'some-refresh-token',
realm: 'test-realm',
authentication: mockUser,
in_response_to: mockSAMLSet1.requestId,
});

provider = new SAMLAuthenticationProvider(mockOptions, {
Expand Down Expand Up @@ -204,6 +206,7 @@ describe('SAMLAuthenticationProvider', () => {
refresh_token: 'user-initiated-login-refresh-token',
realm: 'test-realm',
authentication: mockUser,
in_response_to: mockSAMLSet1.requestId,
});

await expect(
Expand Down Expand Up @@ -249,6 +252,7 @@ describe('SAMLAuthenticationProvider', () => {
refresh_token: 'user-initiated-login-refresh-token',
realm: 'test-realm',
authentication: mockUser,
in_response_to: mockSAMLSet1.requestId,
});

provider = new SAMLAuthenticationProvider(mockOptions, {
Expand Down Expand Up @@ -368,6 +372,7 @@ describe('SAMLAuthenticationProvider', () => {
refresh_token: 'some-refresh-token',
realm: 'test-realm',
authentication: mockUser,
in_response_to: mockSamlResponses.set25.requestId,
});

const requestIdMap: Record<string, { redirectURL: string }> = {};
Expand Down Expand Up @@ -1746,6 +1751,7 @@ describe('SAMLAuthenticationProvider', () => {
refresh_token: 'essu_dev_some-refresh-token',
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
authentication: mockUser,
in_response_to: mockSAMLSet1.requestId,
});
mockOptions.uiam?.getUserProfileGrant.mockReturnValue({
type: 'uiamAccessToken',
Expand Down Expand Up @@ -1984,6 +1990,7 @@ describe('SAMLAuthenticationProvider', () => {
refresh_token: 'x_essu_dev_some-refresh-token',
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
authentication: mockUser,
in_response_to: mockSAMLSet1.requestId,
});
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { errors } from '@elastic/elasticsearch';
import Boom from '@hapi/boom';

import type { KibanaRequest } from '@kbn/core/server';
Expand Down Expand Up @@ -412,6 +413,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
refresh_token: string;
realm: string;
authentication: AuthenticationInfo;
in_response_to?: string;
};
try {
// This operation should be performed on behalf of the user with a privilege that normal
Expand All @@ -428,10 +430,24 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
},
})) as any;
} catch (err) {
let inResponseToRequestId;
if (err instanceof errors.ResponseError) {
const body = (err as errors.ResponseError).meta.body as
| { error: Record<string, string> }
| undefined;
inResponseToRequestId =
body?.error?.['security.saml.unsolicited_in_response_to'] ?? undefined;
}

this.logger.error(
`Failed to log in with SAML response, ${
!isIdPInitiatedLogin ? `current requestIds: ${stateRequestIds}, ` : ''
} error: ${getDetailedErrorMessage(err)}`
[
'Failed to log in with SAML response',
inResponseToRequestId
? `SP-initiated, unsolicited InResponseTo: ${inResponseToRequestId}`
: 'IDP-initiated',
state ? `current requestIds: [${stateRequestIds}]` : 'no state',
getDetailedErrorMessage(err),
].join(', ')
);

// Since we don't know upfront what realm is targeted by the Identity Provider initiated login
Expand Down Expand Up @@ -469,7 +485,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
let remainingRequestIdMap = stateRequestIdMap;

if (!isIdPInitiatedLogin) {
const inResponseToRequestId = this.parseRequestIdFromSAMLResponse(samlResponse);
const inResponseToRequestId = result.in_response_to;
this.logger.debug(`Login was performed with requestId: ${inResponseToRequestId}`);

if (stateRequestIds.length && inResponseToRequestId) {
Expand Down Expand Up @@ -507,16 +523,8 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
);
}

private parseRequestIdFromSAMLResponse(samlResponse: string): string | null {
const samlResponseBuffer = Buffer.from(samlResponse, 'base64');
const samlResponseString = samlResponseBuffer.toString('utf-8');
const inResponseToRequestIdMatch = samlResponseString.match(/InResponseTo="([a-z0-9_]*)"/);

return inResponseToRequestIdMatch ? inResponseToRequestIdMatch[1] : null;
}

private updateRemainingRequestIds(
requestIdToRemove: string | null,
requestIdToRemove: string | undefined,
remainingRequestIds: Record<RequestId, { redirectURL: string }>
): [boolean, Record<RequestId, { redirectURL: string }>] {
if (requestIdToRemove) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ project:
dependsOn:
- '@kbn/core'
- '@kbn/dev-utils'
- '@kbn/mock-idp-utils'
tags:
- plugin
- prod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
import crypto from 'crypto';
import fs from 'fs';
import { stringify } from 'query-string';
import url from 'url';
import zlib from 'zlib';
import { promisify } from 'util';
import { parseString } from 'xml2js';
import { SignedXml } from 'xml-crypto';
import { KBN_KEY_PATH } from '@kbn/dev-utils';
import { CLOUD_USER_ID } from '../constants';
Expand All @@ -23,25 +21,13 @@ import { CLOUD_USER_ID } from '../constants';
* http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf.
*/

const inflateRawAsync = promisify(zlib.inflateRaw);
const deflateRawAsync = promisify(zlib.deflateRaw);
const parseStringAsync = promisify(parseString);

const signingKey = fs.readFileSync(KBN_KEY_PATH);
const signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
const canonicalizationAlgorithm = 'http://www.w3.org/2001/10/xml-exc-c14n#';

export async function getSAMLRequestId(urlWithSAMLRequestId: string) {
const inflatedSAMLRequest = (await inflateRawAsync(
Buffer.from(
url.parse(urlWithSAMLRequestId, true /* parseQueryString */).query.SAMLRequest as string,
'base64'
)
)) as Buffer;

const parsedSAMLRequest = (await parseStringAsync(inflatedSAMLRequest.toString())) as any;
return parsedSAMLRequest['saml2p:AuthnRequest'].$.ID;
}
export { getSAMLRequestId } from '@kbn/mock-idp-utils/src/utils';

export async function getSAMLResponse({
destination,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"kbn_references": [
"@kbn/core",
"@kbn/dev-utils",
"@kbn/mock-idp-utils",
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ project:
sourceRoot: x-pack/platform/test/security_api_integration/packages/helpers
dependsOn:
- '@kbn/dev-utils'
- '@kbn/mock-idp-utils'
tags:
- shared-common
- package
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
import crypto from 'crypto';
import fs from 'fs';
import { stringify } from 'query-string';
import url from 'url';
import { promisify } from 'util';
import { SignedXml } from 'xml-crypto';
import { parseString } from 'xml2js';
import zlib from 'zlib';

import { KBN_KEY_PATH } from '@kbn/dev-utils';
Expand All @@ -23,25 +21,13 @@ import { KBN_KEY_PATH } from '@kbn/dev-utils';
* http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf.
*/

const inflateRawAsync = promisify(zlib.inflateRaw);
const deflateRawAsync = promisify(zlib.deflateRaw);
const parseStringAsync = promisify(parseString);

const signingKey = fs.readFileSync(KBN_KEY_PATH);
const signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
const canonicalizationAlgorithm = 'http://www.w3.org/2001/10/xml-exc-c14n#';

export async function getSAMLRequestId(urlWithSAMLRequestId: string) {
const inflatedSAMLRequest = (await inflateRawAsync(
Buffer.from(
url.parse(urlWithSAMLRequestId, true /* parseQueryString */).query.SAMLRequest as string,
'base64'
)
)) as Buffer;

const parsedSAMLRequest = (await parseStringAsync(inflatedSAMLRequest.toString())) as any;
return parsedSAMLRequest['saml2p:AuthnRequest'].$.ID;
}
export { getSAMLRequestId } from '@kbn/mock-idp-utils/src/utils';

export async function getSAMLResponse({
destination,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
],
"kbn_references": [
"@kbn/dev-utils",
"@kbn/mock-idp-utils",
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export default function ({ getService }: FtrProviderContext) {

describe('finishing handshake', () => {
let handshakeCookie: Cookie;
let samlRequestId: string;
let samlRequestId: string | undefined;

beforeEach(async () => {
const handshakeResponse = await supertest
Expand Down
Loading