Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
76ab2d7
add log signal attributes for invalid authentication; invalid authent…
nxhafa Apr 2, 2026
28510ab
add otel logs on routing failures
nxhafa Apr 2, 2026
22804b6
revert javadoc alignment
nxhafa Apr 2, 2026
0e202d1
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 9, 2026
8888ceb
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 10, 2026
b91e7fb
placeholder tests
Apr 15, 2026
2d6ec18
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 15, 2026
f1cd8e9
handling failed routing with jwt authentication
nxhafa Apr 17, 2026
24b61ff
test organization wip
Apr 17, 2026
dc382a3
Merge branch 'reboot/feat/otel-routing-invalidAuth' of https://github…
Apr 17, 2026
467e081
Merge branch 'v3.x.x' into reboot/feat/otel-routing-invalidAuth
pablocarle Apr 17, 2026
a642ed8
wip improve tests
Apr 20, 2026
5ef4079
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 20, 2026
e27b4ec
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 20, 2026
19ad2fb
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 21, 2026
bf1687b
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 21, 2026
f43ebbe
wip reorg
Apr 21, 2026
513f6bb
wip reorg
Apr 21, 2026
7d4cbe0
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 22, 2026
997a942
fix compilation
Apr 22, 2026
900423b
Merge branch 'v3.x.x' into reboot/feat/otel-routing-invalidAuth
pablocarle Apr 23, 2026
1c22a2e
wip org
Apr 23, 2026
e1136b4
tests reorganized passing
Apr 23, 2026
ed16d5e
wip unit tests
Apr 23, 2026
9ecdbd1
wip new tests
Apr 23, 2026
7f40a72
Merge branch 'v3.x.x' into reboot/feat/otel-routing-invalidAuth
nxhafa Apr 24, 2026
ed65774
change approach of setting error type in otelContext
nxhafa Apr 24, 2026
291a236
wip exception name
Apr 24, 2026
0d0b9a0
Merge branch 'reboot/feat/otel-routing-invalidAuth' of https://github…
Apr 24, 2026
25e358b
fixes for unit tests
Apr 24, 2026
ad0a65b
fix sslcontext (test) initialization
Apr 24, 2026
78d933b
add / fix passticket test
Apr 24, 2026
6887de0
safidt mock works, check other tests
Apr 24, 2026
8e771d4
wip acceptance tests (fix safidt tests)
Apr 27, 2026
c4e829a
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 27, 2026
1d8ad3e
wip
Apr 28, 2026
adcfe80
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 28, 2026
45e92d1
Merge branch 'v3.x.x' into reboot/feat/otel-routing-invalidAuth
pablocarle Apr 28, 2026
9acca0d
Merge branch 'reboot/feat/otel-routing-invalidAuth' of https://github…
Apr 28, 2026
9dc21a4
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-ro…
Apr 29, 2026
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 @@ -56,6 +56,10 @@ public synchronized static void reset() {
isInitialized.set(false);
}

public static boolean isInitialized() {
return isInitialized.get();
}

public synchronized static void prepareSslAuthentication(SslContextConfigurer providedConfigurer) throws Exception {

if (configurer.get() != null && !configurer.get().equals(providedConfigurer)) {
Expand Down Expand Up @@ -174,7 +178,7 @@ public synchronized static void prepareSslAuthentication(SslContextConfigurer pr

isInitialized.set(true);
}
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public static String createZoweJwtToken(String username, String domain, String l
return createToken(username, domain, ltpaToken, config, "APIML");
}

public static String createExpiredZoweJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) {
return createExpiredToken(username, domain, ltpaToken, config, "APIML");
}

public static String createZosmfJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) {
return createToken(username, domain, ltpaToken, config, "zOSMF");
}
Expand All @@ -55,6 +59,23 @@ public static String createToken(String username, String domain, String ltpaToke
.compact();
}

public static String createExpiredToken(String username, String domain, String ltpaToken, HttpsConfig config, String issuer) {
long now = System.currentTimeMillis();
long expiration = now - 200_000L;
Key jwtSecret = SecurityUtils.loadKey(config);

return Jwts.builder()
.subject(username)
.claim("dom", domain)
.claim("ltpa", ltpaToken)
.issuedAt(new Date(now))
.expiration(new Date(expiration))
.issuer(issuer)
.id(UUID.randomUUID().toString())
.signWith(jwtSecret)
.compact();
}

public static String createDummyJwtToken(String username, String issuer, long expiration) {
long now = System.currentTimeMillis();
return Jwts.builder()
Expand Down
122 changes: 85 additions & 37 deletions apiml/src/main/java/org/zowe/apiml/ZaasSchemeTransformApi.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.zowe.apiml.handler.FailedAuthenticationWebHandler;
import org.zowe.apiml.product.opentelemetry.OtelRequestContext;
import org.zowe.apiml.security.common.login.LoginFilter;
import org.zowe.apiml.security.common.login.LoginRequest;
import org.zowe.apiml.zaas.security.config.CompoundAuthProvider;
Expand All @@ -50,7 +51,7 @@
* </ol>
*
* <p>This filter is intended to be used on /login endpoints.</p>
*
* <p>
* Caution: Filter will read the body and make it available as a request attribute
*
* @see LoginRequest
Expand All @@ -72,6 +73,8 @@ public BasicLoginFilter(CompoundAuthProvider compoundAuthProvider, FailedAuthent
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
var hasBody = Optional.ofNullable(exchange.getAttribute(CachedBodyFilter.CACHED_BODY_ATTR)).isPresent();
var otelContext = OtelRequestContext.of(exchange);
otelContext.authMethod(OtelRequestContext.BASIC_AUTH_TYPE);
exchange.getAttributes().put(X509AuthFilter.SKIP_X509_AUTH_ATTR, hasBody);
return extractBasicAuth(exchange)
.map(this::useCredentials)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.zowe.apiml.message.api.ApiMessageView;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.product.opentelemetry.OtelRequestContext;
import org.zowe.apiml.security.common.error.AuthExceptionHandler;
import reactor.core.publisher.Mono;

Expand All @@ -48,7 +49,11 @@ public class FailedAuthenticationWebHandler implements ServerAuthenticationFailu
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
var exchange = webFilterExchange.getExchange();
var requestUri = exchange.getRequest().getURI().getPath();
var otelContext = OtelRequestContext.of(exchange);
log.debug("Unauthorized access to '{}' endpoint", requestUri);
otelContext.authenticationFailed();
otelContext.authErrorMessage(exception.getMessage());
otelContext.authErrorType(exception.getClass().getName());
var bufferFactory = new DefaultDataBufferFactory();
AtomicReference<DefaultDataBuffer> buffer = new AtomicReference<>();
BiConsumer<ApiMessageView, HttpStatus> consumer = (message, status) -> {
Expand All @@ -64,7 +69,7 @@ public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, A
buffer.set(bufferFactory.wrap(new byte[0]));
}
};
var addHeader = (BiConsumer<String, String>)(name, value) -> exchange.getResponse().getHeaders().add(name, value);
var addHeader = (BiConsumer<String, String>) (name, value) -> exchange.getResponse().getHeaders().add(name, value);
try {
handler.handleException(requestUri, consumer, addHeader, exception);
} catch (ServletException e) {
Expand Down
43 changes: 22 additions & 21 deletions apiml/src/test/java/org/zowe/apiml/ZaasSchemeTransformApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.web.server.ServerWebExchange;
import org.zowe.apiml.gateway.filters.RequestCredentials;
import org.zowe.apiml.message.core.MessageService;
import org.zowe.apiml.message.yaml.YamlMessageServiceInstance;
Expand Down Expand Up @@ -97,7 +98,7 @@ void thenReturnsExpectedTicket() throws PassTicketException {

when(passTicketService.generate("USER1", "app1")).thenReturn("ticket123");

StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertNotNull(result);
TicketResponse response = result.getBody();
assertNotNull(response);
Expand All @@ -112,7 +113,7 @@ void whenTicketGenerationFails_writeErrorHeader() throws PassTicketException {

when(passTicketService.generate("USER1", "app1")).thenThrow(new RuntimeException("boom"));

StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
}).verifyComplete();
}
Expand All @@ -126,7 +127,7 @@ void whenAuthSourceMissing_returnsMissingAuthError() {
RequestCredentials credentials = mockCredentials();
when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty());

StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertNotNull(result);
assertNull(result.getBody());
assertTrue(result.getHeaders().header("x-zowe-error").isEmpty());
Expand All @@ -141,7 +142,7 @@ void whenAuthSourceInvalid_writeErrorHeader() throws PassTicketException {
when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource));
when(authSourceService.isValid(authSource)).thenReturn(false);

StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand All @@ -150,7 +151,7 @@ void whenAuthSourceInvalid_writeErrorHeader() throws PassTicketException {
@Test
void whenApplicationNameIsMissing_inPassticket_thenReturnsError() {
when(credentials.getApplId()).thenReturn(null);
StepVerifier.create(transformApi.passticket(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.passticket(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand All @@ -168,7 +169,7 @@ class GivenSafIdtScheme {
void whenMissingAppId_returnsError() {
when(credentials.getApplId()).thenReturn(null);

StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand All @@ -191,7 +192,7 @@ void whenValidUser_returnsToken() throws PassTicketException {
when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1"))
.thenReturn("saf-idt");

StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertNotNull(result);
assertEquals("saf-idt", result.getBody().getToken());
}).verifyComplete();
Expand All @@ -209,7 +210,7 @@ void whenOidc_returnsDistributedId() throws PassTicketException {
when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1"))
.thenReturn("saf-idt");

StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertNotNull(result);
assertEquals("saf-idt", result.getBody().getToken());
assertNotNull(result.getBody().getDistributedIds());
Expand All @@ -224,7 +225,7 @@ void whenSafIdTokenCreationFails_returnsError() {
when(tokenCreationService.createSafIdTokenWithoutCredentials("USER1", "app1"))
.thenThrow(new RuntimeException("Simulated SAF IDT failure"));

StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals("Simulated SAF IDT failure", result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand All @@ -241,7 +242,7 @@ void whenAuthSourceInvalid_returnsError() {
when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.of(authSource));
when(authSourceService.isValid(authSource)).thenReturn(false);

StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand All @@ -251,7 +252,7 @@ void whenAuthSourceInvalid_returnsError() {
void whenApplicationNameIsMissing_inSafIdt_thenReturnsError() {
RequestCredentials credentials = mockCredentialsWithAppId(" "); // blank

StepVerifier.create(transformApi.safIdt(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.safIdt(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals("ApplicationName not provided.", result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand Down Expand Up @@ -283,7 +284,7 @@ void thenReturnsJwt() {

when(authSourceService.getJWT(authSource)).thenReturn("jwt-token");

StepVerifier.create(transformApi.zoweJwt(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.zoweJwt(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertNotNull(result);
ZaasTokenResponse response = result.getBody();
assertNotNull(response);
Expand All @@ -295,7 +296,7 @@ void thenReturnsJwt() {
void whenJwtRetrievalFails_returnsErrorResponse() {
when(authSourceService.getJWT(authSource)).thenThrow(new RuntimeException("boom"));

StepVerifier.create(transformApi.zoweJwt(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.zoweJwt(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals(INVALID_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand All @@ -307,7 +308,7 @@ void whenMissingAuthSource_returnsError() {

when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty());

StepVerifier.create(transformApi.zoweJwt(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.zoweJwt(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand Down Expand Up @@ -353,7 +354,7 @@ void whenValidAuthSource_returnsTokenResponse() throws ServiceNotFoundException
when(zosmfService.exchangeAuthenticationForZosmfToken(anyString(), eq(parsed)))
.thenReturn(mockResponse);

StepVerifier.create(transformApi.zosmf(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.zosmf(credentials, mock(ServerWebExchange.class))).assertNext(result -> {

assertNotNull(result);
assertEquals("zosmf-token", result.getBody().getToken());
Expand All @@ -365,7 +366,7 @@ void testZosmf_serviceThrowsException_returnsError() throws ServiceNotFoundExcep

when(zosmfService.exchangeAuthenticationForZosmfToken(any(), any()))
.thenThrow(new RuntimeException("Error returned from zosmf"));
StepVerifier.create(transformApi.zosmf(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.zosmf(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals("Error returned from zosmf", result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand All @@ -376,7 +377,7 @@ void testZosmf_serviceThrowsException_returnsError() throws ServiceNotFoundExcep
void whenAuthSourceMissing_returnsMissingAuthError() {

when(authSourceService.getAuthSourceFromRequest(any())).thenReturn(Optional.empty());
StepVerifier.create(transformApi.zosmf(credentials)).assertNext(result -> {
StepVerifier.create(transformApi.zosmf(credentials, mock(ServerWebExchange.class))).assertNext(result -> {
assertEquals(MISSING_AUTH_MSG, result.getHeaders().header(AUTH_FAIL_HEADER).get(0));
assertNull(result.getBody());
}).verifyComplete();
Expand Down Expand Up @@ -415,7 +416,7 @@ void setup() {
void giveOidcToken_whenPassticket_thenReturnUserIds() {
var requestCredentials = RequestCredentials.builder().applId("APPLID").build();

StepVerifier.create(transformApi.passticket(requestCredentials))
StepVerifier.create(transformApi.passticket(requestCredentials, mock(ServerWebExchange.class)))
.assertNext(response -> {
assertSame(DISTRIBUTED_IDS, response.getBody().getDistributedIds());
assertSame(USER_ID, response.getBody().getUserId());
Expand All @@ -426,7 +427,7 @@ void giveOidcToken_whenPassticket_thenReturnUserIds() {
void giveOidcToken_whenSafIdt_thenReturnUserIds() {
var requestCredentials = RequestCredentials.builder().applId("APPLID").build();

StepVerifier.create(transformApi.safIdt(requestCredentials))
StepVerifier.create(transformApi.safIdt(requestCredentials, mock(ServerWebExchange.class)))
.assertNext(response -> {
assertSame(DISTRIBUTED_IDS, response.getBody().getDistributedIds());
assertSame(USER_ID, response.getBody().getUserId());
Expand All @@ -438,7 +439,7 @@ void giveOidcToken_whenZosmf_thenReturnUserIds() throws ServiceNotFoundException
var requestCredentials = RequestCredentials.builder().build();
doReturn(ZaasTokenResponse.builder().distributedIds(DISTRIBUTED_IDS).userId(USER_ID).build()).when(zosmfService).exchangeAuthenticationForZosmfToken(any(), any());

StepVerifier.create(transformApi.zosmf(requestCredentials))
StepVerifier.create(transformApi.zosmf(requestCredentials, mock(ServerWebExchange.class)))
.assertNext(response -> {
assertSame(DISTRIBUTED_IDS, response.getBody().getDistributedIds());
assertSame(USER_ID, response.getBody().getUserId());
Expand All @@ -449,7 +450,7 @@ void giveOidcToken_whenZosmf_thenReturnUserIds() throws ServiceNotFoundException
void giveOidcToken_whenZoweJwt_thenReturnUserIds() {
var requestCredentials = RequestCredentials.builder().build();

StepVerifier.create(transformApi.zoweJwt(requestCredentials))
StepVerifier.create(transformApi.zoweJwt(requestCredentials, mock(ServerWebExchange.class)))
.assertNext(response -> {
assertSame(DISTRIBUTED_IDS, response.getBody().getDistributedIds());
assertSame(USER_ID, response.getBody().getUserId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.context.ApplicationEventPublisher;
import org.zowe.apiml.gateway.ApplicationRegistry;
import org.zowe.apiml.gateway.MockService;
import org.zowe.apiml.gateway.MockWebSocketService;

@AcceptanceTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
Expand Down Expand Up @@ -78,6 +79,15 @@ protected MockService.MockServiceBuilder mockService(String serviceId) {
.serviceId(serviceId);
}

protected MockWebSocketService.MockWsServiceBuilder mockServiceWs(String serviceId) {
return MockWebSocketService.wsBuilder()
.statusChangedListener(mockService -> {
applicationRegistry.update(mockService);
updateRoutingRules();
})
.serviceId(serviceId);
}

@AfterEach
void stopMocksWithTestScope() {
applicationRegistry.afterTest();
Expand Down
Loading
Loading