Removes deprecated platform security v1 routes (#203915)

## Summary

Removes the v1 routes deprecated in
https://github.com/elastic/kibana/pull/199656

Part of Kibana 9.0.0 readiness
https://github.com/elastic/kibana-team/issues/1190
This commit is contained in:
Jeramy Soucy 2025-01-07 18:38:23 -05:00 committed by GitHub
parent 95094b21e5
commit 9f4e851272
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 285 additions and 435 deletions

View file

@ -194,9 +194,7 @@ export class AuthenticationService {
}
const isAuthRoute = request.route.options.tags.includes(ROUTE_TAG_AUTH_FLOW);
const isLogoutRoute =
request.route.path === '/api/security/logout' ||
request.route.path === '/api/v1/security/logout';
const isLogoutRoute = request.route.path === '/api/security/logout';
// If users can eventually re-login we want to redirect them directly to the page they tried
// to access initially, but we only want to do that for routes that aren't part of the various

View file

@ -788,7 +788,7 @@ describe('SAMLAuthenticationProvider', () => {
method: 'POST',
path: '/_security/saml/prepare',
body: {
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/saml/callback',
},
});
@ -830,7 +830,7 @@ describe('SAMLAuthenticationProvider', () => {
method: 'POST',
path: '/_security/saml/prepare',
body: {
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/saml/callback',
},
});
@ -900,7 +900,7 @@ describe('SAMLAuthenticationProvider', () => {
method: 'POST',
path: '/_security/saml/prepare',
body: {
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/saml/callback',
},
});
});
@ -1003,7 +1003,7 @@ describe('SAMLAuthenticationProvider', () => {
method: 'POST',
path: '/_security/saml/prepare',
body: {
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/saml/callback',
},
});
});
@ -1294,7 +1294,7 @@ describe('SAMLAuthenticationProvider', () => {
path: '/_security/saml/invalidate',
body: {
query_string: 'SAMLRequest=xxx%20yyy',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/saml/callback',
},
});
});
@ -1408,7 +1408,7 @@ describe('SAMLAuthenticationProvider', () => {
path: '/_security/saml/invalidate',
body: {
query_string: 'SAMLRequest=xxx%20yyy',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/saml/callback',
},
});
});
@ -1430,7 +1430,7 @@ describe('SAMLAuthenticationProvider', () => {
path: '/_security/saml/invalidate',
body: {
query_string: 'SAMLRequest=xxx%20yyy',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml',
acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/saml/callback',
},
});
});

View file

@ -659,7 +659,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
private getACS() {
return `${this.options.getServerBaseURL()}${
this.options.basePath.serverBasePath
}/api/security/v1/saml`;
}/api/security/saml/callback`;
}
/**

View file

@ -78,6 +78,16 @@ describe('Common authentication routes', () => {
query: expect.any(Type),
params: undefined,
});
expect(routeConfig.security).toEqual({
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party IdPs',
},
authc: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party IdPs',
},
});
const queryValidator = (routeConfig.validate as any).query as Type<any>;
expect(queryValidator.validate({ someRandomField: 'some-random' })).toEqual({
@ -211,6 +221,17 @@ describe('Common authentication routes', () => {
query: undefined,
params: undefined,
});
expect(routeConfig.security).toEqual({
authz: {
enabled: false,
reason: `This route provides basic and token login capability, which is delegated to the internal authentication service`,
},
authc: {
enabled: false,
reason:
'This route is used for authentication - it does not require existing authentication',
},
});
const bodyValidator = (routeConfig.validate as any).body as Type<any>;
expect(

View file

@ -7,7 +7,6 @@
import type { TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { parseNextURL } from '@kbn/std';
import type { RouteDefinitionParams } from '..';
@ -34,130 +33,71 @@ export function defineCommonRoutes({
license,
logger,
buildFlavor,
docLinks,
}: RouteDefinitionParams) {
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
// For a serverless build, do not register deprecated versioned routes
for (const path of [
'/api/security/logout',
...(buildFlavor !== 'serverless' ? ['/api/security/v1/logout'] : []),
]) {
const isDeprecated = path === '/api/security/v1/logout';
router.get(
{
path,
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party IdPs',
},
authc: {
enabled: false,
reason:
'This route is used for authentication - it does not require existing authentication',
},
router.get(
{
path: '/api/security/logout',
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party IdPs',
},
// Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any
// set of query string parameters (e.g. SAML/OIDC logout request/response parameters).
validate: { query: schema.object({}, { unknowns: 'allow' }) },
options: {
access: 'public',
excludeFromOAS: true,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
...(isDeprecated && {
deprecated: {
documentationUrl: docLinks.links.security.deprecatedV1Endpoints,
severity: 'warning',
message: i18n.translate('xpack.security.deprecations.logoutRouteMessage', {
defaultMessage:
'The "{path}" URL is deprecated and will be removed in the next major version. Use "/api/security/logout" instead.',
values: { path },
}),
reason: {
type: 'migrate',
newApiMethod: 'GET',
newApiPath: '/api/security/logout',
},
},
}),
authc: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party IdPs',
},
},
async (context, request, response) => {
const serverBasePath = basePath.serverBasePath;
if (isDeprecated) {
logger.warn(
`The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version. Use "${serverBasePath}/api/security/logout" URL instead.`,
{ tags: ['deprecation'] }
);
}
if (!canRedirectRequest(request)) {
return response.badRequest({
body: 'Client should be able to process redirect response.',
});
}
try {
const deauthenticationResult = await getAuthenticationService().logout(request);
if (deauthenticationResult.failed()) {
return response.customError(wrapIntoCustomErrorResponse(deauthenticationResult.error));
}
return response.redirected({
headers: { location: deauthenticationResult.redirectURL || `${serverBasePath}/` },
});
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
// Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any
// set of query string parameters (e.g. SAML/OIDC logout request/response parameters).
validate: { query: schema.object({}, { unknowns: 'allow' }) },
options: {
access: 'public',
excludeFromOAS: true,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
},
},
async (context, request, response) => {
const serverBasePath = basePath.serverBasePath;
if (!canRedirectRequest(request)) {
return response.badRequest({
body: 'Client should be able to process redirect response.',
});
}
);
}
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
// For a serverless build, do not register deprecated versioned routes
for (const path of [
'/internal/security/me',
...(buildFlavor !== 'serverless' ? ['/api/security/v1/me'] : []),
]) {
const isDeprecated = path === '/api/security/v1/me';
router.get(
{
path,
security: {
authz: {
enabled: false,
reason: `This route delegates authorization to Core's security service; there must be an authenticated user for this route to return information`,
},
},
validate: false,
options: {
access: isDeprecated ? 'public' : 'internal',
...(isDeprecated && {
deprecated: {
documentationUrl: docLinks.links.security.deprecatedV1Endpoints,
severity: 'warning',
message: i18n.translate('xpack.security.deprecations.meRouteMessage', {
defaultMessage:
'The "{path}" endpoint is deprecated and will be removed in the next major version.',
values: { path },
}),
reason: { type: 'remove' },
},
}),
try {
const deauthenticationResult = await getAuthenticationService().logout(request);
if (deauthenticationResult.failed()) {
return response.customError(wrapIntoCustomErrorResponse(deauthenticationResult.error));
}
return response.redirected({
headers: { location: deauthenticationResult.redirectURL || `${serverBasePath}/` },
});
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
}
);
router.get(
{
path: '/internal/security/me',
security: {
authz: {
enabled: false,
reason: `This route delegates authorization to Core's security service; there must be an authenticated user for this route to return information`,
},
},
createLicensedRouteHandler(async (context, request, response) => {
if (isDeprecated) {
logger.warn(
`The "${basePath.serverBasePath}${path}" endpoint is deprecated and will be removed in the next major version.`,
{ tags: ['deprecation'] }
);
}
const { security: coreSecurity } = await context.core;
return response.ok({ body: coreSecurity.authc.getCurrentUser()! });
})
);
}
validate: false,
options: {
access: 'internal',
},
},
createLicensedRouteHandler(async (context, request, response) => {
const { security: coreSecurity } = await context.core;
return response.ok({ body: coreSecurity.authc.getCurrentUser()! });
})
);
const basicParamsSchema = schema.object({
username: schema.string({ minLength: 1 }),

View file

@ -22,63 +22,44 @@ import { ROUTE_TAG_AUTH_FLOW, ROUTE_TAG_CAN_REDIRECT } from '../tags';
export function defineOIDCRoutes({
router,
httpResources,
logger,
getAuthenticationService,
basePath,
docLinks,
}: RouteDefinitionParams) {
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
for (const path of ['/api/security/oidc/implicit', '/api/security/v1/oidc/implicit']) {
const isDeprecated = path === '/api/security/v1/oidc/implicit';
/**
* The route should be configured as a redirect URI in OP when OpenID Connect implicit flow
* is used, so that we can extract authentication response from URL fragment and send it to
* the `/api/security/oidc/callback` route.
*/
httpResources.register(
{
path,
validate: false,
options: {
authRequired: false,
excludeFromOAS: true,
...(isDeprecated && {
deprecated: {
documentationUrl: docLinks.links.security.deprecatedV1Endpoints,
severity: 'warning',
message: i18n.translate('xpack.security.deprecations.oidcImplicitRouteMessage', {
defaultMessage:
'The "{path}" URL is deprecated and will be removed in the next major version. Use "/api/security/oidc/implicit" instead.',
values: { path },
}),
reason: {
type: 'migrate',
newApiMethod: 'GET',
newApiPath: '/api/security/oidc/implicit',
},
},
}),
/**
* The route should be configured as a redirect URI in OP when OpenID Connect implicit flow
* is used, so that we can extract authentication response from URL fragment and send it to
* the `/api/security/oidc/callback` route.
*/
httpResources.register(
{
path: '/api/security/oidc/implicit',
validate: false,
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
authc: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
},
(context, request, response) => {
const serverBasePath = basePath.serverBasePath;
if (isDeprecated) {
logger.warn(
`The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version. Use "${serverBasePath}/api/security/oidc/implicit" URL instead.`,
{ tags: ['deprecation'] }
);
}
return response.renderHtml({
body: `
options: {
excludeFromOAS: true,
},
},
(context, request, response) => {
const serverBasePath = basePath.serverBasePath;
return response.renderHtml({
body: `
<!DOCTYPE html>
<title>Kibana OpenID Connect Login</title>
<link rel="icon" href="data:,">
<script src="${serverBasePath}/internal/security/oidc/implicit.js"></script>
`,
});
}
);
}
});
}
);
/**
* The route that accompanies `/api/security/oidc/implicit` and renders a JavaScript snippet
@ -89,7 +70,17 @@ export function defineOIDCRoutes({
{
path: '/internal/security/oidc/implicit.js',
validate: false,
options: { authRequired: false, excludeFromOAS: true },
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
authc: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
},
options: { excludeFromOAS: true },
},
(context, request, response) => {
const serverBasePath = basePath.serverBasePath;
@ -103,183 +94,117 @@ export function defineOIDCRoutes({
}
);
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
for (const path of ['/api/security/oidc/callback', '/api/security/v1/oidc']) {
const isDeprecated = path === '/api/security/v1/oidc';
router.get(
{
path,
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
router.get(
{
path: '/api/security/oidc/callback',
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
validate: {
query: schema.object(
{
authenticationResponseURI: schema.maybe(schema.uri()),
code: schema.maybe(schema.string()),
error: schema.maybe(schema.string()),
error_description: schema.maybe(schema.string()),
error_uri: schema.maybe(schema.uri()),
iss: schema.maybe(schema.uri({ scheme: ['https'] })),
login_hint: schema.maybe(schema.string()),
target_link_uri: schema.maybe(schema.uri()),
state: schema.maybe(schema.string()),
},
// The client MUST ignore unrecognized response parameters according to
// https://openid.net/specs/openid-connect-core-1_0.html#AuthResponseValidation and
// https://tools.ietf.org/html/rfc6749#section-4.1.2.
{ unknowns: 'allow' }
),
},
options: {
access: 'public',
excludeFromOAS: true,
authRequired: false,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
...(isDeprecated && {
deprecated: {
documentationUrl: docLinks.links.security.deprecatedV1Endpoints,
severity: 'warning',
message: i18n.translate('xpack.security.deprecations.oidcCallbackRouteMessage', {
defaultMessage:
'The "{path}" URL is deprecated and will be removed in the next major version. Use "/api/security/oidc/callback" instead.',
values: { path },
}),
reason: {
type: 'migrate',
newApiMethod: 'GET',
newApiPath: '/api/security/oidc/callback',
},
},
}),
authc: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
},
createLicensedRouteHandler(async (context, request, response) => {
const serverBasePath = basePath.serverBasePath;
// An HTTP GET request with a query parameter named `authenticationResponseURI` that includes URL fragment OpenID
// Connect Provider sent during implicit authentication flow to the Kibana own proxy page that extracted that URL
// fragment and put it into `authenticationResponseURI` query string parameter for this endpoint. See more details
// at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth
let loginAttempt: ProviderLoginAttempt | undefined;
if (request.query.authenticationResponseURI) {
loginAttempt = {
type: OIDCLogin.LoginWithImplicitFlow,
authenticationResponseURI: request.query.authenticationResponseURI,
};
} else if (request.query.code || request.query.error) {
if (isDeprecated) {
logger.warn(
`The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version. Use "${serverBasePath}/api/security/oidc/callback" URL instead.`,
{ tags: ['deprecation'] }
);
}
// An HTTP GET request with a query parameter named `code` (or `error`) as the response to a successful (or
// failed) authentication from an OpenID Connect Provider during authorization code authentication flow.
// See more details at https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth.
loginAttempt = {
type: OIDCLogin.LoginWithAuthorizationCodeFlow,
// We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway.
authenticationResponseURI: request.url.pathname + request.url.search,
};
} else if (request.query.iss) {
logger.warn(
`The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version. Use "${serverBasePath}/api/security/oidc/initiate_login" URL for Third-Party Initiated login instead.`,
{ tags: ['deprecation'] }
);
// An HTTP GET request with a query parameter named `iss` as part of a 3rd party initiated authentication.
// See more details at https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin
loginAttempt = {
type: OIDCLogin.LoginInitiatedBy3rdParty,
iss: request.query.iss,
loginHint: request.query.login_hint,
};
}
if (!loginAttempt) {
return response.badRequest({
body: 'Unrecognized login attempt.',
});
}
return performOIDCLogin(request, response, loginAttempt);
})
);
}
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
for (const path of ['/api/security/oidc/initiate_login', '/api/security/v1/oidc']) {
const isDeprecated = path === '/api/security/v1/oidc';
/**
* An HTTP POST request with the payload parameter named `iss` as part of a 3rd party initiated authentication.
* See more details at https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin
*/
router.post(
{
path,
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
validate: {
query: schema.object(
{
authenticationResponseURI: schema.maybe(schema.uri()),
code: schema.maybe(schema.string()),
error: schema.maybe(schema.string()),
error_description: schema.maybe(schema.string()),
error_uri: schema.maybe(schema.uri()),
state: schema.maybe(schema.string()),
},
},
validate: {
body: schema.object(
{
iss: schema.uri({ scheme: ['https'] }),
login_hint: schema.maybe(schema.string()),
target_link_uri: schema.maybe(schema.uri()),
},
// Other parameters MAY be sent, if defined by extensions. Any parameters used that are not understood MUST
// be ignored by the Client according to https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin.
{ unknowns: 'allow' }
),
},
options: {
access: 'public',
excludeFromOAS: true,
authRequired: false,
xsrfRequired: false,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
...(isDeprecated && {
deprecated: {
documentationUrl: docLinks.links.security.deprecatedV1Endpoints,
severity: 'warning',
message: i18n.translate('xpack.security.deprecations.oidcInitiateRouteMessage', {
defaultMessage:
'The "{path}" URL is deprecated and will be removed in the next major version. Use "/api/security/oidc/initiate_login" instead.',
values: { path },
}),
reason: {
type: 'migrate',
newApiMethod: 'POST',
newApiPath: '/api/security/oidc/initiate_login',
},
},
}),
},
// The client MUST ignore unrecognized response parameters according to
// https://openid.net/specs/openid-connect-core-1_0.html#AuthResponseValidation and
// https://tools.ietf.org/html/rfc6749#section-4.1.2.
{ unknowns: 'allow' }
),
},
createLicensedRouteHandler(async (context, request, response) => {
const serverBasePath = basePath.serverBasePath;
if (isDeprecated) {
logger.warn(
`The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version. Use "${serverBasePath}/api/security/oidc/initiate_login" URL for Third-Party Initiated login instead.`,
{ tags: ['deprecation'] }
);
}
options: {
access: 'public',
excludeFromOAS: true,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
},
},
createLicensedRouteHandler(async (context, request, response) => {
// An HTTP GET request with a query parameter named `authenticationResponseURI` that includes URL fragment OpenID
// Connect Provider sent during implicit authentication flow to the Kibana own proxy page that extracted that URL
// fragment and put it into `authenticationResponseURI` query string parameter for this endpoint. See more details
// at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth
let loginAttempt: ProviderLoginAttempt | undefined;
if (request.query.authenticationResponseURI) {
loginAttempt = {
type: OIDCLogin.LoginWithImplicitFlow,
authenticationResponseURI: request.query.authenticationResponseURI,
};
} else if (request.query.code || request.query.error) {
// An HTTP GET request with a query parameter named `code` (or `error`) as the response to a successful (or
// failed) authentication from an OpenID Connect Provider during authorization code authentication flow.
// See more details at https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth.
loginAttempt = {
type: OIDCLogin.LoginWithAuthorizationCodeFlow,
// We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway.
authenticationResponseURI: request.url.pathname + request.url.search,
};
}
return performOIDCLogin(request, response, {
type: OIDCLogin.LoginInitiatedBy3rdParty,
iss: request.body.iss,
loginHint: request.body.login_hint,
if (!loginAttempt) {
return response.badRequest({
body: 'Unrecognized login attempt.',
});
})
);
}
}
return performOIDCLogin(request, response, loginAttempt);
})
);
/**
* An HTTP POST request with the payload parameter named `iss` as part of a 3rd party initiated authentication.
* See more details at https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin
*/
router.post(
{
path: '/api/security/oidc/initiate_login',
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
authc: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
},
validate: {
body: schema.object(
{
iss: schema.uri({ scheme: ['https'] }),
login_hint: schema.maybe(schema.string()),
target_link_uri: schema.maybe(schema.uri()),
},
// Other parameters MAY be sent, if defined by extensions. Any parameters used that are not understood MUST
// be ignored by the Client according to https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin.
{ unknowns: 'allow' }
),
},
options: {
access: 'public',
excludeFromOAS: true,
xsrfRequired: false,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
},
},
createLicensedRouteHandler(async (context, request, response) => {
return performOIDCLogin(request, response, {
type: OIDCLogin.LoginInitiatedBy3rdParty,
iss: request.body.iss,
loginHint: request.body.login_hint,
});
})
);
/**
* An HTTP GET request with the query string parameter named `iss` as part of a 3rd party initiated authentication.
@ -293,6 +218,10 @@ export function defineOIDCRoutes({
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
authc: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party OIDC providers',
},
},
validate: {
query: schema.object(
@ -309,7 +238,6 @@ export function defineOIDCRoutes({
options: {
access: 'public',
excludeFromOAS: true,
authRequired: false,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
},
},

View file

@ -43,19 +43,9 @@ describe('SAML authentication routes', () => {
routeHandler = acsRouteHandler;
});
it('additionally registers BWC route', () => {
expect(
router.post.mock.calls.find(([{ path }]) => path === '/api/security/saml/callback')
).toBeDefined();
expect(
router.post.mock.calls.find(([{ path }]) => path === '/api/security/v1/saml')
).toBeDefined();
});
it('correctly defines route.', () => {
expect(routeConfig.options).toEqual({
access: 'public',
authRequired: false,
excludeFromOAS: true,
xsrfRequired: false,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
@ -65,6 +55,16 @@ describe('SAML authentication routes', () => {
query: undefined,
params: undefined,
});
expect(routeConfig.security).toEqual({
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party SAML providers',
},
authc: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party SAML providers',
},
});
const bodyValidator = (routeConfig.validate as any).body as Type<any>;
expect(bodyValidator.validate({ SAMLResponse: 'saml-response' })).toEqual({

View file

@ -6,7 +6,6 @@
*/
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import type { RouteDefinitionParams } from '..';
import { SAMLAuthenticationProvider, SAMLLogin } from '../../authentication';
@ -15,87 +14,51 @@ import { ROUTE_TAG_AUTH_FLOW, ROUTE_TAG_CAN_REDIRECT } from '../tags';
/**
* Defines routes required for SAML authentication.
*/
export function defineSAMLRoutes({
router,
getAuthenticationService,
basePath,
logger,
buildFlavor,
docLinks,
}: RouteDefinitionParams) {
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
// For a serverless build, do not register deprecated versioned routes
for (const path of [
'/api/security/saml/callback',
...(buildFlavor !== 'serverless' ? ['/api/security/v1/saml'] : []),
]) {
const isDeprecated = path === '/api/security/v1/saml';
router.post(
{
path,
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party SAML providers',
},
export function defineSAMLRoutes({ router, getAuthenticationService }: RouteDefinitionParams) {
router.post(
{
path: '/api/security/saml/callback',
security: {
authz: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party SAML providers',
},
validate: {
body: schema.object(
{ SAMLResponse: schema.string(), RelayState: schema.maybe(schema.string()) },
{ unknowns: 'ignore' }
),
},
options: {
access: 'public',
excludeFromOAS: true,
authRequired: false,
xsrfRequired: false,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
...(isDeprecated && {
deprecated: {
documentationUrl: docLinks.links.security.deprecatedV1Endpoints,
severity: 'warning',
message: i18n.translate('xpack.security.deprecations.samlPostRouteMessage', {
defaultMessage:
'The "{path}" URL is deprecated and will be removed in the next major version. Use "/api/security/saml/callback" instead.',
values: { path },
}),
reason: {
type: 'migrate',
newApiMethod: 'POST',
newApiPath: '/api/security/saml/callback',
},
},
}),
authc: {
enabled: false,
reason: 'This route must remain accessible to 3rd-party SAML providers',
},
},
async (context, request, response) => {
if (isDeprecated) {
const serverBasePath = basePath.serverBasePath;
logger.warn(
// When authenticating using SAML we _expect_ to redirect to the SAML Identity provider.
`The "${serverBasePath}${path}" URL is deprecated and might stop working in a future release. Use "${serverBasePath}/api/security/saml/callback" URL instead.`
);
}
validate: {
body: schema.object(
{ SAMLResponse: schema.string(), RelayState: schema.maybe(schema.string()) },
{ unknowns: 'ignore' }
),
},
options: {
access: 'public',
excludeFromOAS: true,
xsrfRequired: false,
tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
},
},
async (context, request, response) => {
// When authenticating using SAML we _expect_ to redirect to the Kibana target location.
const authenticationResult = await getAuthenticationService().login(request, {
provider: { type: SAMLAuthenticationProvider.type },
value: {
type: SAMLLogin.LoginWithSAMLResponse,
samlResponse: request.body.SAMLResponse,
relayState: request.body.RelayState,
},
});
// When authenticating using SAML we _expect_ to redirect to the Kibana target location.
const authenticationResult = await getAuthenticationService().login(request, {
provider: { type: SAMLAuthenticationProvider.type },
value: {
type: SAMLLogin.LoginWithSAMLResponse,
samlResponse: request.body.SAMLResponse,
relayState: request.body.RelayState,
},
if (authenticationResult.redirected()) {
return response.redirected({
headers: { location: authenticationResult.redirectURL! },
});
if (authenticationResult.redirected()) {
return response.redirected({
headers: { location: authenticationResult.redirectURL! },
});
}
return response.unauthorized({ body: authenticationResult.error });
}
);
}
return response.unauthorized({ body: authenticationResult.error });
}
);
}