Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.apache.commons.lang3.Strings;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
Expand Down Expand Up @@ -148,6 +149,9 @@
this.messageService = messageService;
}

@Value("${apiml.security.strictSchemeEnforcement:false}")
private boolean strictSchemeEnforcement;

@VisibleForTesting
AbstractAuthSchemeFactory() {
this(null, null, null);
Expand Down Expand Up @@ -212,6 +216,11 @@
* @return mutated request
*/
protected ServerHttpRequest cleanHeadersOnAuthFail(ServerWebExchange exchange, String errorMessage) {
String serviceId = (String) exchange.getAttribute("apiml.serviceId");
return cleanHeadersOnAuthFail(exchange, errorMessage, serviceId);
}

protected ServerHttpRequest cleanHeadersOnAuthFail(ServerWebExchange exchange, String errorMessage, String serviceId) {

Check failure on line 223 in gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactory.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZ70FoIvxbqz_cSV8NP6&open=AZ70FoIvxbqz_cSV8NP6&pullRequest=4743
var otelContext = OtelRequestContext.of(exchange);
otelContext.authenticationFailed();
otelContext.authErrorMessage(errorMessage);
Expand All @@ -221,6 +230,22 @@
// update original request - to remove all potential headers and cookies with credentials
Arrays.stream(CERTIFICATE_HEADERS).forEach(headers::remove);

// Strict scheme enforcement: strip Authorization: Basic when appropriate
if (strictSchemeEnforcement) {
AuthenticationScheme scheme = getAuthenticationScheme();
if (scheme != null && scheme != AuthenticationScheme.BYPASS) {
List<String> authValues = headers.get(HttpHeaders.AUTHORIZATION);
if (authValues != null) {
boolean hasBasic = authValues.stream()
.anyMatch(v -> v != null && v.regionMatches(true, 0, "Basic ", 0, 6));
if (hasBasic) {
headers.remove(HttpHeaders.AUTHORIZATION);
log.debug("Strict scheme enforcement: stripped Authorization: Basic for service {} (scheme: {})", serviceId, scheme);
}
}
}
}

// set error header in both side (request to the service, response to the user)
headers.add(ApimlConstants.AUTH_FAIL_HEADER, errorMessage);
exchange.getResponse().getHeaders().add(ApimlConstants.AUTH_FAIL_HEADER, errorMessage);
Expand Down Expand Up @@ -259,9 +284,12 @@
}

protected GatewayFilter createGatewayFilter(T config) {
return (exchange, chain) -> getAuthorizationResponseTransformer(exchange)
.apply(createRequestCredentials(exchange, config).build())
.flatMap(response -> processResponse(exchange, chain, response));
return (exchange, chain) -> {
exchange.getAttributes().put("apiml.serviceId", config.getServiceId());
return getAuthorizationResponseTransformer(exchange)
.apply(createRequestCredentials(exchange, config).build())
.flatMap(response -> processResponse(exchange, chain, response));
};
}

protected ServerHttpRequest addRequestHeader(ServerWebExchange exchange, String key, String value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ protected Mono<Void> processResponse(ServerWebExchange exchange, GatewayFilterCh
}
}
if (request == null) {
request = cleanHeadersOnAuthFail(exchange, failureHeader.orElse("Invalid or missing authentication"));
request = cleanHeadersOnAuthFail(exchange, failureHeader.orElse("Invalid or missing authentication"), (String) exchange.getAttribute("apiml.serviceId"));
exchange = exchange.mutate().request(request).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ protected Mono<Void> processResponse(ServerWebExchange exchange, GatewayFilterCh
}).build();
exchange.getResponse().getHeaders().add(ApimlConstants.AUTH_FAIL_HEADER, failureHeader);
} else {
request = cleanHeadersOnAuthFail(exchange, failureHeader);
request = cleanHeadersOnAuthFail(exchange, failureHeader, (String) exchange.getAttribute("apiml.serviceId"));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
OtelRequestContext.of(exchange).authMethod(authenticationScheme);

super.cleanHeadersOnAuthFail(exchange, config.getMessage());
super.cleanHeadersOnAuthFail(exchange, config.getMessage(), null);

return chain.filter(exchange);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
Expand All @@ -28,6 +29,7 @@
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.List;

import static org.zowe.apiml.constants.ApimlConstants.HTTP_CLIENT_USE_CLIENT_CERTIFICATE;

Expand All @@ -41,6 +43,9 @@

private final MessageService messageService;

@Value("${apiml.security.strictSchemeEnforcement:false}")
private boolean strictSchemeEnforcement;


public X509FilterFactory(MessageService messageService) {
super(Config.class);
Expand Down Expand Up @@ -74,8 +79,22 @@

private ServerHttpRequest updateHeadersForError(ServerWebExchange exchange) {
String headerValue = messageService.createMessage("org.zowe.apiml.gateway.security.schema.missingX509Authentication").mapToLogMessage();
ServerHttpRequest request = exchange.getRequest().mutate().header(ApimlConstants.AUTH_FAIL_HEADER, headerValue).build();
exchange.getResponse().getHeaders().add(ApimlConstants.AUTH_FAIL_HEADER, headerValue);
ServerHttpRequest request = exchange.getRequest().mutate().headers(headers -> {
// Strict scheme enforcement: strip Authorization: Basic
if (strictSchemeEnforcement) {
List<String> authValues = headers.get(HttpHeaders.AUTHORIZATION);
if (authValues != null) {
boolean hasBasic = authValues.stream()
.anyMatch(v -> v != null && v.regionMatches(true, 0, "Basic ", 0, 6));
if (hasBasic) {
headers.remove(HttpHeaders.AUTHORIZATION);
log.debug("Strict scheme enforcement: stripped Authorization: Basic for service (scheme: x509)");
}
}
}
headers.add(ApimlConstants.AUTH_FAIL_HEADER, headerValue);
exchange.getResponse().getHeaders().add(ApimlConstants.AUTH_FAIL_HEADER, headerValue);
}).build();

Check warning on line 97 in gateway-service/src/main/java/org/zowe/apiml/gateway/filters/X509FilterFactory.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Immediately return this expression instead of assigning it to the temporary variable "request".

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZ70FoMSxbqz_cSV8NP7&open=AZ70FoMSxbqz_cSV8NP7&pullRequest=4743
return request;
}

Expand Down
1 change: 1 addition & 0 deletions gateway-service/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ apiml:
externalUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}
security:
headersToBeCleared: X-Certificate-Public,X-Certificate-DistinguishedName,X-Certificate-CommonName
strictSchemeEnforcement: false
ssl:
nonStrictVerifySslCertificatesOfServices: false
rauditx:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.gateway.filters;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ServerWebExchange;
import org.zowe.apiml.auth.AuthenticationScheme;
import org.zowe.apiml.constants.ApimlConstants;
import org.zowe.apiml.product.opentelemetry.OtelRequestContext;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;

/**
* Tests for {@link AbstractAuthSchemeFactory#cleanHeadersOnAuthFail(ServerWebExchange, String)}
* focusing on the strict scheme enforcement feature.
*/
class AbstractAuthSchemeFactoryTest {

private static final String BASIC_AUTH_VALUE = "Basic dXNlcjpwYXNz";
private static final String BEARER_AUTH_VALUE = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.xxx";
private static final String ERROR_MESSAGE = "auth failed";

private OtelRequestContext otelContext;

Check warning on line 39 in gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractAuthSchemeFactoryTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused "otelContext" private field.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZ70FoMpxbqz_cSV8NQA&open=AZ70FoMpxbqz_cSV8NQA&pullRequest=4743

@BeforeEach
void setUpOtelContext() {
// OtelRequestContext has a static holder; just ensure it's initialized per test
otelContext = null;
}

/**
* Create an exchange with an Authorization header and setup OTEL context.
*/
private ServerWebExchange createExchange(String authorizationValue) {
MockServerHttpRequest request = MockServerHttpRequest.get("/test")
.header(HttpHeaders.AUTHORIZATION, authorizationValue)
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
otelContext = spy(OtelRequestContext.of(exchange));
exchange.getAttributes().put("apiml.serviceId", "test-service");
return exchange;
}

/**
* Create a spy of AbstractAuthSchemeFactory with the given scheme and enforcement setting.
*/
private AbstractAuthSchemeFactory<?, ?> createFactory(AuthenticationScheme scheme, boolean strictEnforcement) {
AbstractAuthSchemeFactory<?, ?> factory = spy(AbstractAuthSchemeFactory.class);
doReturn(scheme).when(factory).getAuthenticationScheme();
ReflectionTestUtils.setField(factory, "strictSchemeEnforcement", strictEnforcement);
return factory;
}

// ── Test 1: strictSchemeEnforcement=true + non-bypass scheme → Basic stripped ──

@Test
void givenStrictEnforcementAndNonBypassScheme_whenBasicAuthHeader_thenAuthorizationRemoved() {
ServerWebExchange exchange = createExchange(BASIC_AUTH_VALUE);
AbstractAuthSchemeFactory<?, ?> factory = createFactory(AuthenticationScheme.HTTP_BASIC_PASSTICKET, true);

ServerHttpRequest result = factory.cleanHeadersOnAuthFail(exchange, ERROR_MESSAGE);

assertNull(result.getHeaders().get(HttpHeaders.AUTHORIZATION),
"Authorization header should be removed under strict enforcement");
assertNotNull(result.getHeaders().get(ApimlConstants.AUTH_FAIL_HEADER),
"X-Zowe-Auth-Failure header should be set");
}

// ── Test 2: strictSchemeEnforcement=false (default) → Basic preserved ──

@Test
void givenStrictEnforcementDisabled_whenBasicAuthHeader_thenAuthorizationPreserved() {
ServerWebExchange exchange = createExchange(BASIC_AUTH_VALUE);
AbstractAuthSchemeFactory<?, ?> factory = createFactory(AuthenticationScheme.HTTP_BASIC_PASSTICKET, false);

ServerHttpRequest result = factory.cleanHeadersOnAuthFail(exchange, ERROR_MESSAGE);

assertNotNull(result.getHeaders().get(HttpHeaders.AUTHORIZATION),
"Authorization header should be preserved when strict enforcement is disabled");
assertEquals(BASIC_AUTH_VALUE, result.getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
}

// ── Test 3: scheme is BYPASS → Basic always preserved ──

@Test
void givenStrictEnforcementAndBypassScheme_whenBasicAuthHeader_thenAuthorizationPreserved() {
ServerWebExchange exchange = createExchange(BASIC_AUTH_VALUE);
AbstractAuthSchemeFactory<?, ?> factory = createFactory(AuthenticationScheme.BYPASS, true);

ServerHttpRequest result = factory.cleanHeadersOnAuthFail(exchange, ERROR_MESSAGE);

assertNotNull(result.getHeaders().get(HttpHeaders.AUTHORIZATION),
"Authorization header should be preserved for BYPASS scheme even with strict enforcement");
assertEquals(BASIC_AUTH_VALUE, result.getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
}

// ── Test 4: Authorization: Bearer → never stripped ──

@Test
void givenStrictEnforcementAndNonBypassScheme_whenBearerAuthHeader_thenAuthorizationPreserved() {
ServerWebExchange exchange = createExchange(BEARER_AUTH_VALUE);
AbstractAuthSchemeFactory<?, ?> factory = createFactory(AuthenticationScheme.ZOWE_JWT, true);

ServerHttpRequest result = factory.cleanHeadersOnAuthFail(exchange, ERROR_MESSAGE);

assertNotNull(result.getHeaders().get(HttpHeaders.AUTHORIZATION),
"Bearer Authorization should never be stripped");
assertEquals(BEARER_AUTH_VALUE, result.getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
}

// ── Test 5: x-zowe-auth-failure still set after stripping ──

@Test
void givenStrictEnforcement_whenBasicAuthHeaderStripped_thenAuthFailureHeaderSet() {
ServerWebExchange exchange = createExchange(BASIC_AUTH_VALUE);
AbstractAuthSchemeFactory<?, ?> factory = createFactory(AuthenticationScheme.HTTP_BASIC_PASSTICKET, true);

ServerHttpRequest result = factory.cleanHeadersOnAuthFail(exchange, ERROR_MESSAGE);

assertEquals(ERROR_MESSAGE, result.getHeaders().getFirst(ApimlConstants.AUTH_FAIL_HEADER),
"X-Zowe-Auth-Failure header should contain the error message");
}

// ── Test 6: scheme is null → no enforcement ──

@Test
void givenStrictEnforcementAndNullScheme_whenBasicAuthHeader_thenAuthorizationPreserved() {
ServerWebExchange exchange = createExchange(BASIC_AUTH_VALUE);
AbstractAuthSchemeFactory<?, ?> factory = createFactory(null, true);

ServerHttpRequest result = factory.cleanHeadersOnAuthFail(exchange, ERROR_MESSAGE);

assertNotNull(result.getHeaders().get(HttpHeaders.AUTHORIZATION),
"Authorization header should be preserved when scheme is null");
assertEquals(BASIC_AUTH_VALUE, result.getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
}

// ── Test 7: case-insensitive match ("basic " lowercase) ──

@Test
void givenStrictEnforcement_whenLowercaseBasicAuthHeader_thenAuthorizationRemoved() {
ServerWebExchange exchange = createExchange("basic dXNlcjpwYXNz");
AbstractAuthSchemeFactory<?, ?> factory = createFactory(AuthenticationScheme.HTTP_BASIC_PASSTICKET, true);

ServerHttpRequest result = factory.cleanHeadersOnAuthFail(exchange, ERROR_MESSAGE);

assertNull(result.getHeaders().get(HttpHeaders.AUTHORIZATION),
"Authorization header should be removed for case-insensitive 'basic ' match");
}

// ── Test 8: serviceId overload passes through ──

@Test
void givenServiceIdOverload_whenCleanHeadersOnAuthFail_thenBehaviorIdentical() {
ServerWebExchange exchange = createExchange(BASIC_AUTH_VALUE);
AbstractAuthSchemeFactory<?, ?> factory = createFactory(AuthenticationScheme.HTTP_BASIC_PASSTICKET, true);

ServerHttpRequest result = factory.cleanHeadersOnAuthFail(exchange, ERROR_MESSAGE, "test-service");

assertNull(result.getHeaders().get(HttpHeaders.AUTHORIZATION),
"Authorization header should be removed via 3-param overload");
assertEquals(ERROR_MESSAGE, result.getHeaders().getFirst(ApimlConstants.AUTH_FAIL_HEADER));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ void givenConfig_whenApply_thenSetAuthInformationWithoutErrorType() {
verify(otelContext).authErrorMessage(MESSAGE);

verify(otelContext).authMethod(AuthenticationScheme.SAF_IDT);
verify(underTest).cleanHeadersOnAuthFail(exchange, MESSAGE);
verify(underTest).cleanHeadersOnAuthFail(exchange, MESSAGE, null);
}

}
Loading
Loading