From 681d1b1ee7efb5eb6ace1bc9362fd676d6c3df07 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:20:13 -0400 Subject: [PATCH] Fixes saml login flow to work with anonymous auth (#1839) * Fixes anonymous auth flow to work with SAML Signed-off-by: Darshit Chanpura * Adds hardcoded credentials for anonymous user Signed-off-by: Darshit Chanpura * Updates basic auth header to be a config constant Signed-off-by: Darshit Chanpura * Removes unneeded usage of anonymous auth header constant Signed-off-by: Darshit Chanpura * Updates logic to display anonymous auth login button Signed-off-by: Darshit Chanpura * Adds test to check whether anonymous auth login button is displayed correctly Signed-off-by: Darshit Chanpura * Fixes integrationtests Signed-off-by: Darshit Chanpura * Adds integration tests for anonymous auth login with basic authorization header Signed-off-by: Darshit Chanpura * Generates random password for anonymous user Signed-off-by: Darshit Chanpura * Fixes lint errors Signed-off-by: Darshit Chanpura * Adds saml auth header to differentiate saml requests Signed-off-by: Darshit Chanpura * Fixes linter errors Signed-off-by: Darshit Chanpura * Fixes basic auth tests Signed-off-by: Darshit Chanpura * Removes console loggers Signed-off-by: Darshit Chanpura * Fixes lint error Signed-off-by: Darshit Chanpura * Addresses feedback Signed-off-by: Darshit Chanpura * Resolves #1840 Signed-off-by: Darshit Chanpura * Replace magic value with constant Signed-off-by: Darshit Chanpura * Renames query param and removes unused variables Signed-off-by: Darshit Chanpura * Uses enum instead of magic constant Signed-off-by: Darshit Chanpura * Extracts template function to a separate util file Signed-off-by: Darshit Chanpura * Renames test Signed-off-by: Darshit Chanpura * Removes unnecessary modifications required to solve this bug Signed-off-by: Darshit Chanpura * Fixes import Signed-off-by: Darshit Chanpura * Removes unused param Signed-off-by: Darshit Chanpura * Removes unused method param Signed-off-by: Darshit Chanpura * Removes incorrect method param Signed-off-by: Darshit Chanpura --------- Signed-off-by: Darshit Chanpura --- common/index.ts | 1 + public/apps/login/login-page.tsx | 14 +- .../__snapshots__/login-page.test.tsx.snap | 413 ++++++++++++++++++ public/apps/login/test/login-page.test.tsx | 56 ++- server/auth/types/basic/basic_auth.ts | 9 +- server/auth/types/multiple/multi_auth.ts | 10 +- server/auth/types/saml/routes.ts | 20 +- server/backend/opensearch_security_client.ts | 24 +- test/constant.ts | 5 - test/jest_integration/basic_auth.test.ts | 7 +- 10 files changed, 517 insertions(+), 42 deletions(-) diff --git a/common/index.ts b/common/index.ts index b5e6a475d..1a0eb3ff5 100644 --- a/common/index.ts +++ b/common/index.ts @@ -34,6 +34,7 @@ export const OPENID_AUTH_LOGIN_WITH_FRAGMENT = '/auth/openid/captureUrlFragment' export const SAML_AUTH_LOGIN = '/auth/saml/login'; export const SAML_AUTH_LOGIN_WITH_FRAGMENT = '/auth/saml/captureUrlFragment'; export const ANONYMOUS_AUTH_LOGIN = '/auth/anonymous'; +export const AUTH_TYPE_PARAM = 'auth_type'; export const OPENID_AUTH_LOGOUT = '/auth/openid/logout'; export const SAML_AUTH_LOGOUT = '/auth/saml/logout'; diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index a22a36dc7..1e5f43dd8 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -231,14 +231,14 @@ export function LoginPage(props: LoginPageDeps) { ); - if (authOpts.length > 1) { - if (props.config.auth.anonymous_auth_enabled) { - const anonymousConfig = props.config.ui[AuthType.ANONYMOUS].login; - formBody.push( - renderLoginButton(AuthType.ANONYMOUS, ANONYMOUS_AUTH_LOGIN, anonymousConfig) - ); - } + if (props.config.auth.anonymous_auth_enabled) { + const anonymousConfig = props.config.ui[AuthType.ANONYMOUS].login; + formBody.push( + renderLoginButton(AuthType.ANONYMOUS, ANONYMOUS_AUTH_LOGIN, anonymousConfig) + ); + } + if (authOpts.length > 1) { formBody.push(); formBody.push(); formBody.push(); diff --git a/public/apps/login/test/__snapshots__/login-page.test.tsx.snap b/public/apps/login/test/__snapshots__/login-page.test.tsx.snap index b8a1e1182..de432dcca 100644 --- a/public/apps/login/test/__snapshots__/login-page.test.tsx.snap +++ b/public/apps/login/test/__snapshots__/login-page.test.tsx.snap @@ -153,6 +153,419 @@ exports[`Login page renders renders with config value for multiauth 1`] = ` `; +exports[`Login page renders renders with config value for multiauth with anonymous auth enabled 1`] = ` + + + + + Title1 + + + + SubTitle1 + + + + + + } + value="" + /> + + + + } + type="password" + value="" + /> + + + + Log in + + + + + + + + + + + Button1 + + + + + Button2 + + + + +`; + +exports[`Login page renders renders with config value with anonymous auth enabled: string 1`] = ` + + + + + Title1 + + + + SubTitle1 + + + + + + } + value="" + /> + + + + } + type="password" + value="" + /> + + + + Log in + + + + + + + +`; + +exports[`Login page renders renders with config value with anonymous auth enabled: string array 1`] = ` + + + + + Title1 + + + + SubTitle1 + + + + + + } + value="" + /> + + + + } + type="password" + value="" + /> + + + + Log in + + + + + + + +`; + exports[`Login page renders renders with config value: string 1`] = ` { expect(component).toMatchSnapshot(); }); + it('renders with config value with anonymous auth enabled: string array', () => { + const config: ClientConfigType = { + ui: configUI, + auth: { + type: [AuthType.BASIC], + logout_url: API_AUTH_LOGOUT, + anonymous_auth_enabled: true, + }, + }; + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + it('renders with config value: string', () => { const config: ClientConfigType = { ui: configUI, @@ -166,12 +190,42 @@ describe('Login page', () => { expect(component).toMatchSnapshot(); }); + it('renders with config value with anonymous auth enabled: string', () => { + const config: ClientConfigType = { + ui: configUI, + auth: { + type: AuthType.BASIC, + logout_url: API_AUTH_LOGOUT, + anonymous_auth_enabled: true, + }, + }; + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + it('renders with config value for multiauth', () => { const config: ClientConfigType = { ui: configUI, auth: { - type: [AuthType.BASIC, 'openid', AuthType.SAML], + type: [AuthType.BASIC, AuthType.OPEN_ID, AuthType.SAML], + logout_url: API_AUTH_LOGOUT, + }, + }; + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders with config value for multiauth with anonymous auth enabled', () => { + const config: ClientConfigType = { + ui: configUI, + auth: { + type: [AuthType.BASIC, AuthType.OPEN_ID, AuthType.SAML], logout_url: API_AUTH_LOGOUT, + anonymous_auth_enabled: true, }, }; const component = shallow( diff --git a/server/auth/types/basic/basic_auth.ts b/server/auth/types/basic/basic_auth.ts index f21f86827..a9cfedb6c 100644 --- a/server/auth/types/basic/basic_auth.ts +++ b/server/auth/types/basic/basic_auth.ts @@ -28,9 +28,14 @@ import { SecurityPluginConfigType } from '../../..'; import { SecuritySessionCookie } from '../../../session/security_cookie'; import { BasicAuthRoutes } from './routes'; import { AuthenticationType } from '../authentication_type'; -import { LOGIN_PAGE_URI, ANONYMOUS_AUTH_LOGIN } from '../../../../common'; import { composeNextUrlQueryParam } from '../../../utils/next_url'; -import { AUTH_HEADER_NAME, AuthType, OPENDISTRO_SECURITY_ANONYMOUS } from '../../../../common'; +import { + LOGIN_PAGE_URI, + ANONYMOUS_AUTH_LOGIN, + AUTH_HEADER_NAME, + AuthType, + OPENDISTRO_SECURITY_ANONYMOUS, +} from '../../../../common'; export class BasicAuthentication extends AuthenticationType { public readonly type: string = AuthType.BASIC; diff --git a/server/auth/types/multiple/multi_auth.ts b/server/auth/types/multiple/multi_auth.ts index b190d9d03..b00b3d154 100644 --- a/server/auth/types/multiple/multi_auth.ts +++ b/server/auth/types/multiple/multi_auth.ts @@ -25,7 +25,7 @@ import { import { OpenSearchDashboardsResponse } from '../../../../../../src/core/server/http/router'; import { SecurityPluginConfigType } from '../../..'; import { AuthenticationType } from '../authentication_type'; -import { ANONYMOUS_AUTH_LOGIN, AuthType, LOGIN_PAGE_URI } from '../../../../common'; +import { AuthType, LOGIN_PAGE_URI } from '../../../../common'; import { composeNextUrlQueryParam } from '../../../utils/next_url'; import { MultiAuthRoutes } from './routes'; import { SecuritySessionCookie } from '../../../session/security_cookie'; @@ -166,14 +166,6 @@ export class MultipleAuthentication extends AuthenticationType { this.coreSetup.http.basePath.serverBasePath ); - if (this.config.auth.anonymous_auth_enabled) { - const redirectLocation = `${this.coreSetup.http.basePath.serverBasePath}${ANONYMOUS_AUTH_LOGIN}?${nextUrlParam}`; - return response.redirected({ - headers: { - location: `${redirectLocation}`, - }, - }); - } return response.redirected({ headers: { location: `${this.coreSetup.http.basePath.serverBasePath}${LOGIN_PAGE_URI}?${nextUrlParam}`, diff --git a/server/auth/types/saml/routes.ts b/server/auth/types/saml/routes.ts index 87605d65e..d14d0711b 100644 --- a/server/auth/types/saml/routes.ts +++ b/server/auth/types/saml/routes.ts @@ -131,11 +131,12 @@ export class SamlAuthRoutes { } try { - const credentials = await this.securityClient.authToken( + const credentials = await this.securityClient.authToken({ requestId, - request.body.SAMLResponse, - undefined - ); + samlResponse: request.body.SAMLResponse, + acsEndpoint: undefined, + authRequestType: AuthType.SAML, + }); const user = await this.securityClient.authenticateWithHeader( request, 'authorization', @@ -208,11 +209,12 @@ export class SamlAuthRoutes { async (context, request, response) => { const acsEndpoint = `${this.coreSetup.http.basePath.serverBasePath}/_opendistro/_security/saml/acs/idpinitiated`; try { - const credentials = await this.securityClient.authToken( - undefined, - request.body.SAMLResponse, - acsEndpoint - ); + const credentials = await this.securityClient.authToken({ + requestId: undefined, + samlResponse: request.body.SAMLResponse, + acsEndpoint, + authRequestType: AuthType.SAML, + }); const user = await this.securityClient.authenticateWithHeader( request, 'authorization', diff --git a/server/backend/opensearch_security_client.ts b/server/backend/opensearch_security_client.ts index 71a65d205..6f2d3439f 100755 --- a/server/backend/opensearch_security_client.ts +++ b/server/backend/opensearch_security_client.ts @@ -16,6 +16,7 @@ import { ILegacyClusterClient, OpenSearchDashboardsRequest } from '../../../../src/core/server'; import { User } from '../auth/user'; import { TenancyConfigSettings } from '../../public/apps/configuration/panels/tenancy-config/types'; +import { AUTH_TYPE_PARAM, AuthType } from '../../common'; export class SecurityClient { constructor(private readonly esClient: ILegacyClusterClient) {} @@ -182,7 +183,9 @@ export class SecurityClient { public async getSamlHeader(request: OpenSearchDashboardsRequest) { try { // response is expected to be an error - await this.esClient.asScoped(request).callAsCurrentUser('opensearch_security.authinfo'); + await this.esClient.asScoped(request).callAsCurrentUser('opensearch_security.authinfo', { + [AUTH_TYPE_PARAM]: AuthType.SAML, + }); } catch (error: any) { // the error looks like // wwwAuthenticateDirective: @@ -217,11 +220,12 @@ export class SecurityClient { throw new Error(`Invalid SAML configuration.`); } - public async authToken( - requestId: string | undefined, - samlResponse: any, - acsEndpoint: any | undefined = undefined - ) { + public async authToken({ + requestId, + samlResponse, + acsEndpoint = undefined, + authRequestType, + }: AuthTokenParams) { const body = { RequestId: requestId, SAMLResponse: samlResponse, @@ -230,6 +234,7 @@ export class SecurityClient { try { return await this.esClient.asScoped().callAsCurrentUser('opensearch_security.authtoken', { body, + [AUTH_TYPE_PARAM]: authRequestType, }); } catch (error: any) { console.log(error); @@ -237,3 +242,10 @@ export class SecurityClient { } } } + +interface AuthTokenParams { + requestId?: string; + samlResponse: any; + acsEndpoint?: any; + authRequestType?: string; +} diff --git a/test/constant.ts b/test/constant.ts index d4ab9e3ae..5dcb387e2 100644 --- a/test/constant.ts +++ b/test/constant.ts @@ -13,14 +13,9 @@ * permissions and limitations under the License. */ -import { version, opensearchDashboards } from '../package.json'; - export const OPENSEARCH_DASHBOARDS_SERVER_USER: string = 'kibanaserver'; export const OPENSEARCH_DASHBOARDS_SERVER_PASSWORD: string = 'kibanaserver'; -export const ELASTICSEARCH_VERSION: string = opensearchDashboards.version; -export const SECURITY_ES_PLUGIN_VERSION: string = version; - export const ADMIN_USER: string = 'admin'; export const ADMIN_PASSWORD: string = process.env.ADMIN_PASSWORD || 'admin'; const ADMIN_USER_PASS: string = `${ADMIN_USER}:${ADMIN_PASSWORD}`; diff --git a/test/jest_integration/basic_auth.test.ts b/test/jest_integration/basic_auth.test.ts index b4bbd3e55..cdb4a39d0 100644 --- a/test/jest_integration/basic_auth.test.ts +++ b/test/jest_integration/basic_auth.test.ts @@ -132,7 +132,6 @@ describe('start OpenSearch Dashboards server', () => { }); it('call authinfo API as admin', async () => { - const testUserCredentials = Buffer.from(ADMIN_CREDENTIALS); const response = await osdTestServer.request .get(root, '/api/v1/auth/authinfo') .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); @@ -142,7 +141,7 @@ describe('start OpenSearch Dashboards server', () => { it('call authinfo API without credentials', async () => { const response = await osdTestServer.request .get(root, '/api/v1/auth/authinfo') - .unset('Authorization'); + .unset(AUTHORIZATION_HEADER_NAME); expect(response.status).toEqual(401); }); @@ -209,7 +208,9 @@ describe('start OpenSearch Dashboards server', () => { }); it('enforce authentication on api/status route', async () => { - const response = await osdTestServer.request.get(root, '/api/status'); + const response = await osdTestServer.request + .get(root, '/api/status') + .unset(AUTHORIZATION_HEADER_NAME); expect(response.status).toEqual(401); });