From aae6867b9d20af0e5d1c8c1b5f66a37db7e696e5 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 20 Mar 2020 12:52:09 +0100 Subject: [PATCH] Review#3: add initial set of Login Selector API integration tests. --- .../security/server/routes/views/login.ts | 3 - x-pack/scripts/functional_tests.js | 1 + .../apis/security/kerberos_login.ts | 10 +- .../fixtures/kerberos_tools.ts | 13 + .../apis/index.ts | 14 + .../apis/login_selector.ts | 505 ++++++++++++++++++ .../login_selector_api_integration/config.ts | 141 +++++ .../ftr_provider_context.d.ts | 11 + .../services.ts | 14 + .../apis/security/pki_auth.ts | 1 - x-pack/test/pki_api_integration/config.ts | 1 - x-pack/test/saml_api_integration/config.ts | 2 +- .../fixtures/idp_metadata.xml | 2 +- .../fixtures/idp_metadata_2.xml | 41 ++ .../fixtures/saml_tools.ts | 17 +- 15 files changed, 762 insertions(+), 14 deletions(-) create mode 100644 x-pack/test/kerberos_api_integration/fixtures/kerberos_tools.ts create mode 100644 x-pack/test/login_selector_api_integration/apis/index.ts create mode 100644 x-pack/test/login_selector_api_integration/apis/login_selector.ts create mode 100644 x-pack/test/login_selector_api_integration/config.ts create mode 100644 x-pack/test/login_selector_api_integration/ftr_provider_context.d.ts create mode 100644 x-pack/test/login_selector_api_integration/services.ts create mode 100644 x-pack/test/saml_api_integration/fixtures/idp_metadata_2.xml diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index f37411c301dbd..4cabd4337971c 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -37,9 +37,6 @@ export function defineLoginRoutes({ async (context, request, response) => { // Default to true if license isn't available or it can't be resolved for some reason. const shouldShowLogin = license.isEnabled() ? license.getFeatures().showLogin : true; - - // Authentication flow isn't triggered automatically for this route, so we should explicitly - // check whether user has an active session already. const isUserAlreadyLoggedIn = request.auth.isAuthenticated; if (isUserAlreadyLoggedIn || !shouldShowLogin) { logger.debug('User is already authenticated, redirecting...'); diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index c1f8047c8a5cc..65e22713b778c 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -25,6 +25,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/oidc_api_integration/config.ts'), require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'), require.resolve('../test/pki_api_integration/config.ts'), + require.resolve('../test/login_selector_api_integration/config.ts'), require.resolve('../test/spaces_api_integration/spaces_only/config.ts'), require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial.ts'), require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic.ts'), diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index b561c9ea47513..81999826adbb1 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -8,10 +8,14 @@ import expect from '@kbn/expect'; import request, { Cookie } from 'request'; import { delay } from 'bluebird'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { + getMutualAuthenticationResponseToken, + getSPNEGOToken, +} from '../../fixtures/kerberos_tools'; export default function({ getService }: FtrProviderContext) { - const spnegoToken = - 'YIIChwYGKwYBBQUCoIICezCCAnegDTALBgkqhkiG9xIBAgKiggJkBIICYGCCAlwGCSqGSIb3EgECAgEAboICSzCCAkegAwIBBaEDAgEOogcDBQAAAAAAo4IBW2GCAVcwggFToAMCAQWhERsPVEVTVC5FTEFTVElDLkNPohwwGqADAgEDoRMwERsESFRUUBsJbG9jYWxob3N0o4IBGTCCARWgAwIBEqEDAgECooIBBwSCAQNBN2a1Rso+KEJsDwICYLCt7ACLzdlbhEZF5YNsehO109b/WiZR1VTK6kCQyDdBdQFefyvV8EiC35mz7XnTb239nWz6xBGbdmtjSfF0XzpXKbL/zGzLEKkEXQuqFLPUN6qEJXsh0OoNdj9OWwmTr93FVyugs1hO/E5wjlAe2SDYpBN6uZICXu6dFg9nLQKkb/XgbgKM7ZZvgA/UElWDgHav4nPO1VWppCCLKHqXTRnvpr/AsxeON4qeJLaukxBigfIaJlLFMNQal5H7MyXa0j3Y1sckbURnWoBt6r4XE7c8F8cz0rYoGwoCO+Cs5tNutKY6XcsAFbLh59hjgIkhVBhhyTeypIHSMIHPoAMCARKigccEgcSsXqIRAcHfZivrbHfsnvbFgmzmnrKVPFNtJ9Hl23KunCsNW49nP4VF2dEf9n12prDaIguJDV5LPHpTew9rmCj1GCahKJ9bJbRKIgImLFd+nelm3E2zxRqAhrgM1469oDg0ksE3+5lJBuJlVEECMp0F/gxvEiL7DhasICqw+FOJ/jD9QUYvg+E6BIxWgZyPszaxerzBBszAhIF1rxCHRRL1KLjskNeJlBhH77DkAO6AEmsYGdsgEq7b7uCov9PKPiiPAuFF'; + const spnegoToken = getSPNEGOToken(); + const supertest = getService('supertestWithoutAuth'); const config = getService('config'); @@ -105,7 +109,7 @@ export default function({ getService }: FtrProviderContext) { // Verify that mutual authentication works. expect(response.headers['www-authenticate']).to.be( - 'Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg==' + `Negotiate ${getMutualAuthenticationResponseToken()}` ); const cookies = response.headers['set-cookie']; diff --git a/x-pack/test/kerberos_api_integration/fixtures/kerberos_tools.ts b/x-pack/test/kerberos_api_integration/fixtures/kerberos_tools.ts new file mode 100644 index 0000000000000..2fed5d475cd5c --- /dev/null +++ b/x-pack/test/kerberos_api_integration/fixtures/kerberos_tools.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function getSPNEGOToken() { + return 'YIIChwYGKwYBBQUCoIICezCCAnegDTALBgkqhkiG9xIBAgKiggJkBIICYGCCAlwGCSqGSIb3EgECAgEAboICSzCCAkegAwIBBaEDAgEOogcDBQAAAAAAo4IBW2GCAVcwggFToAMCAQWhERsPVEVTVC5FTEFTVElDLkNPohwwGqADAgEDoRMwERsESFRUUBsJbG9jYWxob3N0o4IBGTCCARWgAwIBEqEDAgECooIBBwSCAQNBN2a1Rso+KEJsDwICYLCt7ACLzdlbhEZF5YNsehO109b/WiZR1VTK6kCQyDdBdQFefyvV8EiC35mz7XnTb239nWz6xBGbdmtjSfF0XzpXKbL/zGzLEKkEXQuqFLPUN6qEJXsh0OoNdj9OWwmTr93FVyugs1hO/E5wjlAe2SDYpBN6uZICXu6dFg9nLQKkb/XgbgKM7ZZvgA/UElWDgHav4nPO1VWppCCLKHqXTRnvpr/AsxeON4qeJLaukxBigfIaJlLFMNQal5H7MyXa0j3Y1sckbURnWoBt6r4XE7c8F8cz0rYoGwoCO+Cs5tNutKY6XcsAFbLh59hjgIkhVBhhyTeypIHSMIHPoAMCARKigccEgcSsXqIRAcHfZivrbHfsnvbFgmzmnrKVPFNtJ9Hl23KunCsNW49nP4VF2dEf9n12prDaIguJDV5LPHpTew9rmCj1GCahKJ9bJbRKIgImLFd+nelm3E2zxRqAhrgM1469oDg0ksE3+5lJBuJlVEECMp0F/gxvEiL7DhasICqw+FOJ/jD9QUYvg+E6BIxWgZyPszaxerzBBszAhIF1rxCHRRL1KLjskNeJlBhH77DkAO6AEmsYGdsgEq7b7uCov9PKPiiPAuFF'; +} + +export function getMutualAuthenticationResponseToken() { + return 'oRQwEqADCgEAoQsGCSqGSIb3EgECAg=='; +} diff --git a/x-pack/test/login_selector_api_integration/apis/index.ts b/x-pack/test/login_selector_api_integration/apis/index.ts new file mode 100644 index 0000000000000..35f83733a7105 --- /dev/null +++ b/x-pack/test/login_selector_api_integration/apis/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('apis', function() { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./login_selector')); + }); +} diff --git a/x-pack/test/login_selector_api_integration/apis/login_selector.ts b/x-pack/test/login_selector_api_integration/apis/login_selector.ts new file mode 100644 index 0000000000000..489f0265da57a --- /dev/null +++ b/x-pack/test/login_selector_api_integration/apis/login_selector.ts @@ -0,0 +1,505 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import request, { Cookie } from 'request'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import url from 'url'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import expect from '@kbn/expect'; +import { getStateAndNonce } from '../../oidc_api_integration/fixtures/oidc_tools'; +import { + getMutualAuthenticationResponseToken, + getSPNEGOToken, +} from '../../kerberos_api_integration/fixtures/kerberos_tools'; +import { getSAMLRequestId, getSAMLResponse } from '../../saml_api_integration/fixtures/saml_tools'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const randomness = getService('randomness'); + const supertest = getService('supertestWithoutAuth'); + const config = getService('config'); + + const kibanaServerConfig = config.get('servers.kibana'); + const validUsername = kibanaServerConfig.username; + const validPassword = kibanaServerConfig.password; + + const CA_CERT = readFileSync(CA_CERT_PATH); + const CLIENT_CERT = readFileSync( + resolve(__dirname, '../../pki_api_integration/fixtures/first_client.p12') + ); + + async function checkSessionCookie(sessionCookie: Cookie, username: string, providerName: string) { + expect(sessionCookie.key).to.be('sid'); + expect(sessionCookie.value).to.not.be.empty(); + expect(sessionCookie.path).to.be('/'); + expect(sessionCookie.httpOnly).to.be(true); + + const apiResponse = await supertest + .get('/internal/security/me') + .ca(CA_CERT) + .pfx(CLIENT_CERT) + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .expect(200); + + expect(apiResponse.body).to.only.have.keys([ + 'username', + 'full_name', + 'email', + 'roles', + 'metadata', + 'enabled', + 'authentication_realm', + 'lookup_realm', + 'authentication_provider', + ]); + + expect(apiResponse.body.username).to.be(username); + expect(apiResponse.body.authentication_provider).to.be(providerName); + } + + describe('Login Selector', () => { + it('should redirect user to a login selector', async () => { + const response = await supertest + .get('/abc/xyz/handshake?one=two three') + .ca(CA_CERT) + .expect(302); + expect(response.headers['set-cookie']).to.be(undefined); + expect(response.headers.location).to.be( + '/login?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three' + ); + }); + + it('should allow access to login selector with intermediate authentication cookie', async () => { + const handshakeResponse = await supertest + .post('/internal/security/login_with') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ providerType: 'saml', providerName: 'saml1', currentURL: 'https://kibana.com/' }) + .expect(200); + + // The cookie that includes some state of the in-progress authentication, that doesn't allow + // to fully authenticate user yet. + const intermediateAuthCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!; + + await supertest + .get('/login') + .ca(CA_CERT) + .set('Cookie', intermediateAuthCookie.cookieString()) + .expect(200); + }); + + describe('SAML', () => { + function createSAMLResponse(options = {}) { + return getSAMLResponse({ + destination: `http://localhost:${kibanaServerConfig.port}/api/security/saml/callback`, + sessionIndex: String(randomness.naturalNumber()), + ...options, + }); + } + + it('should be able to log in via IdP initiated login for any configured realm', async () => { + for (const providerName of ['saml1', 'saml2']) { + const authenticationResponse = await supertest + .post('/api/security/saml/callback') + .ca(CA_CERT) + .send({ + SAMLResponse: await createSAMLResponse({ + issuer: `http://www.elastic.co/${providerName}`, + }), + }) + .expect(302); + + // User should be redirected to the base URL. + expect(authenticationResponse.headers.location).to.be('/'); + + const cookies = authenticationResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + + await checkSessionCookie(request.cookie(cookies[0])!, 'a@b.c', providerName); + } + }); + + it('should be able to log in via IdP initiated login even if session with other provider type exists', async () => { + const basicAuthenticationResponse = await supertest + .post('/internal/security/login') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ username: validUsername, password: validPassword }) + .expect(204); + + const basicSessionCookie = request.cookie( + basicAuthenticationResponse.headers['set-cookie'][0] + )!; + await checkSessionCookie(basicSessionCookie, 'elastic', 'basic1'); + + for (const providerName of ['saml1', 'saml2']) { + const authenticationResponse = await supertest + .post('/api/security/saml/callback') + .ca(CA_CERT) + .set('Cookie', basicSessionCookie.cookieString()) + .send({ + SAMLResponse: await createSAMLResponse({ + issuer: `http://www.elastic.co/${providerName}`, + }), + }) + .expect(302); + + // It should be `/overwritten_session` instead of `/` once it's generalized. + expect(authenticationResponse.headers.location).to.be('/'); + + const cookies = authenticationResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + + await checkSessionCookie(request.cookie(cookies[0])!, 'a@b.c', providerName); + } + }); + + it('should be able to log in via IdP initiated login even if session with other SAML provider exists', async () => { + // First login with `saml1`. + const saml1AuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .ca(CA_CERT) + .send({ + SAMLResponse: await createSAMLResponse({ issuer: `http://www.elastic.co/saml1` }), + }) + .expect(302); + + const saml1SessionCookie = request.cookie( + saml1AuthenticationResponse.headers['set-cookie'][0] + )!; + await checkSessionCookie(saml1SessionCookie, 'a@b.c', 'saml1'); + + // And now try to login with `saml2`. + const saml2AuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .ca(CA_CERT) + .set('Cookie', saml1SessionCookie.cookieString()) + .send({ + SAMLResponse: await createSAMLResponse({ issuer: `http://www.elastic.co/saml2` }), + }) + .expect(302); + + // It should be `/overwritten_session` instead of `/` once it's generalized. + expect(saml2AuthenticationResponse.headers.location).to.be('/'); + + const saml2SessionCookie = request.cookie( + saml2AuthenticationResponse.headers['set-cookie'][0] + )!; + await checkSessionCookie(saml2SessionCookie, 'a@b.c', 'saml2'); + }); + + // Ideally we should be able to abandon intermediate session and let user log in, but for the + // time being we cannot distinguish errors coming from Elasticsearch for the case when SAML + // response just doesn't correspond to request ID we have in intermediate cookie and the case + // when something else has happened. + it('should fail for IdP initiated login if intermediate session with other SAML provider exists', async () => { + // First start authentication flow with `saml1`. + const saml1HandshakeResponse = await supertest + .post('/internal/security/login_with') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'saml', + providerName: 'saml1', + currentURL: 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three#/workpad', + }) + .expect(200); + + expect( + saml1HandshakeResponse.body.location.startsWith(`https://elastic.co/sso/saml`) + ).to.be(true); + + const saml1HandshakeCookie = request.cookie( + saml1HandshakeResponse.headers['set-cookie'][0] + )!; + + // And now try to login with `saml2`. + await supertest + .post('/api/security/saml/callback') + .ca(CA_CERT) + .set('Cookie', saml1HandshakeCookie.cookieString()) + .send({ + SAMLResponse: await createSAMLResponse({ issuer: `http://www.elastic.co/saml2` }), + }) + .expect(401); + }); + + it('should be able to log in via SP initiated login with any configured realm', async () => { + for (const providerName of ['saml1', 'saml2']) { + const handshakeResponse = await supertest + .post('/internal/security/login_with') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'saml', + providerName, + currentURL: + 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three#/workpad', + }) + .expect(200); + + expect(handshakeResponse.body.location.startsWith(`https://elastic.co/sso/saml`)).to.be( + true + ); + + const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!; + const samlRequestId = await getSAMLRequestId(handshakeResponse.body.location); + + const authenticationResponse = await supertest + .post('/api/security/saml/callback') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .set('Cookie', handshakeCookie.cookieString()) + .send({ + SAMLResponse: await createSAMLResponse({ + inResponseTo: samlRequestId, + issuer: `http://www.elastic.co/${providerName}`, + }), + }) + .expect(302); + + // User should be redirected to the URL that initiated handshake. + expect(authenticationResponse.headers.location).to.be( + '/abc/xyz/handshake?one=two three#/workpad' + ); + + const cookies = authenticationResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + + await checkSessionCookie(request.cookie(cookies[0])!, 'a@b.c', providerName); + } + }); + + // Ideally we should be able to abandon intermediate session and let user log in, but for the + // time being we cannot distinguish errors coming from Elasticsearch for the case when SAML + // response just doesn't correspond to request ID we have in intermediate cookie and the case + // when something else has happened. + it('should be able to log in via SP initiated login even if intermediate session with other SAML provider exists', async () => { + // First start authentication flow with `saml1`. + const saml1HandshakeResponse = await supertest + .post('/internal/security/login_with') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'saml', + providerName: 'saml1', + currentURL: 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three#/saml1', + }) + .expect(200); + + expect( + saml1HandshakeResponse.body.location.startsWith(`https://elastic.co/sso/saml`) + ).to.be(true); + + const saml1HandshakeCookie = request.cookie( + saml1HandshakeResponse.headers['set-cookie'][0] + )!; + + // And now try to login with `saml2`. + const saml2HandshakeResponse = await supertest + .post('/internal/security/login_with') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .set('Cookie', saml1HandshakeCookie.cookieString()) + .send({ + providerType: 'saml', + providerName: 'saml2', + currentURL: 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three#/saml2', + }) + .expect(200); + + expect( + saml2HandshakeResponse.body.location.startsWith(`https://elastic.co/sso/saml`) + ).to.be(true); + + const saml2HandshakeCookie = request.cookie( + saml2HandshakeResponse.headers['set-cookie'][0] + )!; + + const saml2AuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .ca(CA_CERT) + .set('Cookie', saml2HandshakeCookie.cookieString()) + .send({ + SAMLResponse: await createSAMLResponse({ issuer: `http://www.elastic.co/saml2` }), + }) + .expect(302); + + expect(saml2AuthenticationResponse.headers.location).to.be( + '/abc/xyz/handshake?one=two three#/saml2' + ); + + const saml2SessionCookie = request.cookie( + saml2AuthenticationResponse.headers['set-cookie'][0] + )!; + await checkSessionCookie(saml2SessionCookie, 'a@b.c', 'saml2'); + }); + }); + + describe('Kerberos', () => { + it('should be able to log in from Login Selector', async () => { + const spnegoResponse = await supertest + .post('/internal/security/login_with') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'kerberos', + providerName: 'kerberos1', + currentURL: 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three#/workpad', + }) + .expect(401); + + expect(spnegoResponse.headers['set-cookie']).to.be(undefined); + expect(spnegoResponse.headers['www-authenticate']).to.be('Negotiate'); + + const authenticationResponse = await supertest + .post('/internal/security/login_with') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .set('Authorization', `Negotiate ${getSPNEGOToken()}`) + .send({ + providerType: 'kerberos', + providerName: 'kerberos1', + currentURL: 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three#/workpad', + }) + .expect(200); + + // Verify that mutual authentication works. + expect(authenticationResponse.headers['www-authenticate']).to.be( + `Negotiate ${getMutualAuthenticationResponseToken()}` + ); + + const cookies = authenticationResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + + await checkSessionCookie( + request.cookie(cookies[0])!, + 'tester@TEST.ELASTIC.CO', + 'kerberos1' + ); + }); + }); + + describe('OpenID Connect', () => { + it('should be able to log in via IdP initiated login', async () => { + const handshakeResponse = await supertest + .get('/api/security/oidc/initiate_login?iss=https://test-op.elastic.co') + .ca(CA_CERT) + .expect(302); + const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!; + + // Set the nonce in our mock OIDC Provider so that it can generate the ID Tokens + const { state, nonce } = getStateAndNonce(handshakeResponse.headers.location); + await supertest + .post('/api/oidc_provider/setup') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ nonce }) + .expect(200); + + const authenticationResponse = await supertest + .get(`/api/security/oidc/callback?code=code2&state=${state}`) + .ca(CA_CERT) + .set('Cookie', handshakeCookie.cookieString()) + .expect(302); + + // User should be redirected to the base URL. + expect(authenticationResponse.headers.location).to.be('/'); + + const cookies = authenticationResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + + await checkSessionCookie(request.cookie(cookies[0])!, 'user2', 'oidc1'); + }); + + it('should be able to log in via SP initiated login', async () => { + const handshakeResponse = await supertest + .post('/internal/security/login_with') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'oidc', + providerName: 'oidc1', + currentURL: 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three', + }) + .expect(200); + + const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!; + const redirectURL = url.parse(handshakeResponse.body.location, true /* parseQueryString */); + expect( + handshakeResponse.body.location.startsWith( + `https://test-op.elastic.co/oauth2/v1/authorize` + ) + ).to.be(true); + + expect(redirectURL.query.scope).to.not.be.empty(); + expect(redirectURL.query.response_type).to.not.be.empty(); + expect(redirectURL.query.client_id).to.not.be.empty(); + expect(redirectURL.query.redirect_uri).to.not.be.empty(); + expect(redirectURL.query.state).to.not.be.empty(); + expect(redirectURL.query.nonce).to.not.be.empty(); + + // Set the nonce in our mock OIDC Provider so that it can generate the ID Tokens + const { state, nonce } = redirectURL.query; + await supertest + .post('/api/oidc_provider/setup') + .ca(CA_CERT) + .set('kbn-xsrf', 'xxx') + .send({ nonce }) + .expect(200); + + const authenticationResponse = await supertest + .get(`/api/security/oidc/callback?code=code1&state=${state}`) + .ca(CA_CERT) + .set('Cookie', handshakeCookie.cookieString()) + .expect(302); + + // User should be redirected to the URL that initiated handshake. + expect(authenticationResponse.headers.location).to.be('/abc/xyz/handshake?one=two three'); + + const cookies = authenticationResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + + await checkSessionCookie(request.cookie(cookies[0])!, 'user1', 'oidc1'); + }); + }); + + describe('PKI', () => { + it('should redirect user to a login selector even if client provides certificate', async () => { + const response = await supertest + .get('/abc/xyz/handshake?one=two three') + .ca(CA_CERT) + .pfx(CLIENT_CERT) + .expect(302); + expect(response.headers['set-cookie']).to.be(undefined); + expect(response.headers.location).to.be( + '/login?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three' + ); + }); + + it('should be able to log in from Login Selector', async () => { + const authenticationResponse = await supertest + .post('/internal/security/login_with') + .ca(CA_CERT) + .pfx(CLIENT_CERT) + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'pki', + providerName: 'pki1', + currentURL: 'https://kibana.com/login?next=/abc/xyz/handshake?one=two%20three#/workpad', + }) + .expect(200); + + const cookies = authenticationResponse.headers['set-cookie']; + expect(cookies).to.have.length(1); + + await checkSessionCookie(request.cookie(cookies[0])!, 'first_client', 'pki1'); + }); + }); + }); +} diff --git a/x-pack/test/login_selector_api_integration/config.ts b/x-pack/test/login_selector_api_integration/config.ts new file mode 100644 index 0000000000000..6ca9d19b74c17 --- /dev/null +++ b/x-pack/test/login_selector_api_integration/config.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const kibanaAPITestsConfig = await readConfigFile( + require.resolve('../../../test/api_integration/config.js') + ); + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port'); + + const kerberosKeytabPath = resolve(__dirname, '../kerberos_api_integration/fixtures/krb5.keytab'); + const kerberosConfigPath = resolve(__dirname, '../kerberos_api_integration/fixtures/krb5.conf'); + + const oidcJWKSPath = resolve(__dirname, '../oidc_api_integration/fixtures/jwks.json'); + const oidcIdPPlugin = resolve(__dirname, '../oidc_api_integration/fixtures/oidc_provider'); + + const pkiKibanaCAPath = resolve(__dirname, '../pki_api_integration/fixtures/kibana_ca.crt'); + + const saml1IdPMetadataPath = resolve( + __dirname, + '../saml_api_integration/fixtures/idp_metadata.xml' + ); + const saml2IdPMetadataPath = resolve( + __dirname, + '../saml_api_integration/fixtures/idp_metadata_2.xml' + ); + + const servers = { + ...xPackAPITestsConfig.get('servers'), + elasticsearch: { + ...xPackAPITestsConfig.get('servers.elasticsearch'), + protocol: 'https', + }, + kibana: { + ...xPackAPITestsConfig.get('servers.kibana'), + protocol: 'https', + }, + }; + + return { + testFiles: [require.resolve('./apis')], + servers, + security: { disableTestUser: true }, + services: { + randomness: kibanaAPITestsConfig.get('services.randomness'), + legacyEs: kibanaAPITestsConfig.get('services.legacyEs'), + supertestWithoutAuth: xPackAPITestsConfig.get('services.supertestWithoutAuth'), + }, + junit: { + reportName: 'X-Pack Login Selector API Integration Tests', + }, + + esTestCluster: { + ...xPackAPITestsConfig.get('esTestCluster'), + ssl: true, + serverArgs: [ + ...xPackAPITestsConfig.get('esTestCluster.serverArgs'), + 'xpack.security.authc.token.enabled=true', + 'xpack.security.authc.token.timeout=15s', + 'xpack.security.http.ssl.client_authentication=optional', + 'xpack.security.http.ssl.verification_mode=certificate', + 'xpack.security.authc.realms.native.native1.order=0', + 'xpack.security.authc.realms.kerberos.kerb1.order=1', + `xpack.security.authc.realms.kerberos.kerb1.keytab.path=${kerberosKeytabPath}`, + 'xpack.security.authc.realms.pki.pki1.order=2', + 'xpack.security.authc.realms.pki.pki1.delegation.enabled=true', + `xpack.security.authc.realms.pki.pki1.certificate_authorities=${CA_CERT_PATH}`, + 'xpack.security.authc.realms.saml.saml1.order=3', + `xpack.security.authc.realms.saml.saml1.idp.metadata.path=${saml1IdPMetadataPath}`, + 'xpack.security.authc.realms.saml.saml1.idp.entity_id=http://www.elastic.co/saml1', + `xpack.security.authc.realms.saml.saml1.sp.entity_id=http://localhost:${kibanaPort}`, + `xpack.security.authc.realms.saml.saml1.sp.logout=http://localhost:${kibanaPort}/logout`, + `xpack.security.authc.realms.saml.saml1.sp.acs=http://localhost:${kibanaPort}/api/security/saml/callback`, + 'xpack.security.authc.realms.saml.saml1.attributes.principal=urn:oid:0.0.7', + 'xpack.security.authc.realms.oidc.oidc1.order=4', + `xpack.security.authc.realms.oidc.oidc1.rp.client_id=0oa8sqpov3TxMWJOt356`, + `xpack.security.authc.realms.oidc.oidc1.rp.client_secret=0oa8sqpov3TxMWJOt356`, + `xpack.security.authc.realms.oidc.oidc1.rp.response_type=code`, + `xpack.security.authc.realms.oidc.oidc1.rp.redirect_uri=https://localhost:${kibanaPort}/api/security/oidc/callback`, + `xpack.security.authc.realms.oidc.oidc1.op.authorization_endpoint=https://test-op.elastic.co/oauth2/v1/authorize`, + `xpack.security.authc.realms.oidc.oidc1.op.endsession_endpoint=https://test-op.elastic.co/oauth2/v1/endsession`, + `xpack.security.authc.realms.oidc.oidc1.op.token_endpoint=https://localhost:${kibanaPort}/api/oidc_provider/token_endpoint`, + `xpack.security.authc.realms.oidc.oidc1.op.userinfo_endpoint=https://localhost:${kibanaPort}/api/oidc_provider/userinfo_endpoint`, + `xpack.security.authc.realms.oidc.oidc1.op.issuer=https://test-op.elastic.co`, + `xpack.security.authc.realms.oidc.oidc1.op.jwkset_path=${oidcJWKSPath}`, + `xpack.security.authc.realms.oidc.oidc1.claims.principal=sub`, + `xpack.security.authc.realms.oidc.oidc1.ssl.certificate_authorities=${CA_CERT_PATH}`, + 'xpack.security.authc.realms.saml.saml2.order=5', + `xpack.security.authc.realms.saml.saml2.idp.metadata.path=${saml2IdPMetadataPath}`, + 'xpack.security.authc.realms.saml.saml2.idp.entity_id=http://www.elastic.co/saml2', + `xpack.security.authc.realms.saml.saml2.sp.entity_id=http://localhost:${kibanaPort}`, + `xpack.security.authc.realms.saml.saml2.sp.logout=http://localhost:${kibanaPort}/logout`, + `xpack.security.authc.realms.saml.saml2.sp.acs=http://localhost:${kibanaPort}/api/security/saml/callback`, + 'xpack.security.authc.realms.saml.saml2.attributes.principal=urn:oid:0.0.7', + ], + serverEnvVars: { + // We're going to use the same TGT multiple times and during a short period of time, so we + // have to disable replay cache so that ES doesn't complain about that. + ES_JAVA_OPTS: `-Djava.security.krb5.conf=${kerberosConfigPath} -Dsun.security.krb5.rcache=none`, + }, + }, + + kbnTestServer: { + ...xPackAPITestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${oidcIdPPlugin}`, + '--optimize.enabled=false', + '--server.ssl.enabled=true', + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, + `--server.ssl.certificateAuthorities=${JSON.stringify([CA_CERT_PATH, pkiKibanaCAPath])}`, + `--server.ssl.clientAuthentication=optional`, + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + `--xpack.security.authc.providers=${JSON.stringify({ + basic: { basic1: { order: 0 } }, + kerberos: { kerberos1: { order: 4 } }, + pki: { pki1: { order: 2 } }, + oidc: { oidc1: { order: 3, realm: 'oidc1' } }, + saml: { + saml1: { order: 1, realm: 'saml1' }, + saml2: { order: 5, realm: 'saml2', maxRedirectURLSize: '100b' }, + }, + })}`, + '--server.xsrf.whitelist', + JSON.stringify([ + '/api/oidc_provider/token_endpoint', + '/api/oidc_provider/userinfo_endpoint', + ]), + ], + }, + }; +} diff --git a/x-pack/test/login_selector_api_integration/ftr_provider_context.d.ts b/x-pack/test/login_selector_api_integration/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..e3add3748f56d --- /dev/null +++ b/x-pack/test/login_selector_api_integration/ftr_provider_context.d.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/login_selector_api_integration/services.ts b/x-pack/test/login_selector_api_integration/services.ts new file mode 100644 index 0000000000000..8bb2dae90bf59 --- /dev/null +++ b/x-pack/test/login_selector_api_integration/services.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { services as commonServices } from '../common/services'; +import { services as apiIntegrationServices } from '../api_integration/services'; + +export const services = { + ...commonServices, + randomness: apiIntegrationServices.randomness, + supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth, +}; diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index fe772a3b1d460..ac16335b7f466 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -9,7 +9,6 @@ import request, { Cookie } from 'request'; import { delay } from 'bluebird'; import { readFileSync } from 'fs'; import { resolve } from 'path'; -// @ts-ignore import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/pki_api_integration/config.ts b/x-pack/test/pki_api_integration/config.ts index 21ae1b40efa16..8177e4aa1afba 100644 --- a/x-pack/test/pki_api_integration/config.ts +++ b/x-pack/test/pki_api_integration/config.ts @@ -6,7 +6,6 @@ import { resolve } from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; -// @ts-ignore import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { services } from './services'; diff --git a/x-pack/test/saml_api_integration/config.ts b/x-pack/test/saml_api_integration/config.ts index 502d34d4c9e5d..0580c28555d16 100644 --- a/x-pack/test/saml_api_integration/config.ts +++ b/x-pack/test/saml_api_integration/config.ts @@ -37,7 +37,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { 'xpack.security.authc.token.timeout=15s', 'xpack.security.authc.realms.saml.saml1.order=0', `xpack.security.authc.realms.saml.saml1.idp.metadata.path=${idpPath}`, - 'xpack.security.authc.realms.saml.saml1.idp.entity_id=http://www.elastic.co', + 'xpack.security.authc.realms.saml.saml1.idp.entity_id=http://www.elastic.co/saml1', `xpack.security.authc.realms.saml.saml1.sp.entity_id=http://localhost:${kibanaPort}`, `xpack.security.authc.realms.saml.saml1.sp.logout=http://localhost:${kibanaPort}/logout`, `xpack.security.authc.realms.saml.saml1.sp.acs=http://localhost:${kibanaPort}/api/security/saml/callback`, diff --git a/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml b/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml index a890fe812987b..57b9e824c9d53 100644 --- a/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml +++ b/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml @@ -1,6 +1,6 @@ + entityID="http://www.elastic.co/saml1"> diff --git a/x-pack/test/saml_api_integration/fixtures/idp_metadata_2.xml b/x-pack/test/saml_api_integration/fixtures/idp_metadata_2.xml new file mode 100644 index 0000000000000..ff67779d7732c --- /dev/null +++ b/x-pack/test/saml_api_integration/fixtures/idp_metadata_2.xml @@ -0,0 +1,41 @@ + + + + + + + + MIIDOTCCAiGgAwIBAgIVANNWkg9lzNiLqNkMFhFKHcXyaZmqMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDM0MloYDzIwNjkxMjE0MTcwMzQyWjARMQ8w +DQYDVQQDEwZraWJhbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQ +wYYbQtbRBKJ4uNZc2+IgRU+7NNL21ZebQlEIMgK7jAqOMrsW2b5DATz41Fd+GQFU +FUYYjwo+PQj6sJHshOJo/gNb32HrydvMI7YPvevkszkuEGCfXxQ3Dw2RTACLgD0Q +OCkwHvn3TMf0loloV/ePGWaZDYZaXi3a5DdWi/HFFoJysgF0JV2f6XyKhJkGaEfJ +s9pWX269zH/XQvGNx4BEimJpYB8h4JnDYPFIiQdqj+sl2b+kS1hH9kL5gBAMXjFU +vcNnX+PmyTjyJrGo75k0ku+spBf1bMwuQt3uSmM+TQIXkvFDmS0DOVESrpA5EC1T +BUGRz6o/I88Xx4Mud771AgMBAAGjYzBhMB0GA1UdDgQWBBQLB1Eo23M3Ss8MsFaz +V+Twcb3PmDAfBgNVHSMEGDAWgBQa7SYOe8NGcF00EbwPHA91YCsHSTAUBgNVHREE +DTALgglsb2NhbGhvc3QwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAnEl/ +z5IElIjvkK4AgMPrNcRlvIGDt2orEik7b6Jsq6/RiJQ7cSsYTZf7xbqyxNsUOTxv ++frj47MEN448H2nRvUxH29YR3XygV5aEwADSAhwaQWn0QfWTCZbJTmSoNEDtDOzX +TGDlAoCD9s9Xz9S1JpxY4H+WWRZrBSDM6SC1c6CzuEeZRuScNAjYD5mh2v6fOlSy +b8xJWSg0AFlJPCa3ZsA2SKbNqI0uNfJTnkXRm88Z2NHcgtlADbOLKauWfCrpgsCk +cZgo6yAYkOM148h/8wGla1eX+iE1R72NUABGydu8MSQKvc0emWJkGsC1/KqPlf/O +eOUsdwn1yDKHRxDHyA== + + + + + + + + + + diff --git a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts index bbe0df7ff3a2c..a924d0964c245 100644 --- a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts +++ b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts @@ -45,14 +45,21 @@ export async function getSAMLResponse({ inResponseTo, sessionIndex, username = 'a@b.c', -}: { destination?: string; inResponseTo?: string; sessionIndex?: string; username?: string } = {}) { + issuer = 'http://www.elastic.co/saml1', +}: { + destination?: string; + inResponseTo?: string; + sessionIndex?: string; + username?: string; + issuer?: string; +} = {}) { const issueInstant = new Date().toISOString(); const notOnOrAfter = new Date(Date.now() + 3600 * 1000).toISOString(); const samlAssertionTemplateXML = ` - http://www.elastic.co + ${issuer} a@b.c @@ -99,7 +106,7 @@ export async function getSAMLResponse({ ${inResponseTo ? `InResponseTo="${inResponseTo}"` : ''} Version="2.0" IssueInstant="${issueInstant}" Destination="${destination}"> - http://www.elastic.co + ${issuer} ${signature.getSignedXml()} @@ -111,9 +118,11 @@ export async function getSAMLResponse({ export async function getLogoutRequest({ destination, sessionIndex, + issuer = 'http://www.elastic.co/saml1', }: { destination: string; sessionIndex: string; + issuer?: string; }) { const issueInstant = new Date().toISOString(); const logoutRequestTemplateXML = ` @@ -121,7 +130,7 @@ export async function getLogoutRequest({ Destination="${destination}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> - http://www.elastic.co + ${issuer} a@b.c ${sessionIndex}