diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/NoMainframeIdentityException.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/NoMainframeIdentityException.java index 41b1762f98..999e1f9da4 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/NoMainframeIdentityException.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/NoMainframeIdentityException.java @@ -10,13 +10,26 @@ package org.zowe.apiml.security.common.token; +import lombok.Getter; import org.springframework.security.core.AuthenticationException; /** * This exception is thrown in case the OIDC token is valid but distributed ID is not mapped to the mainframe ID. */ +@Getter public class NoMainframeIdentityException extends AuthenticationException { + + private final boolean validToken; + private final String token; + public NoMainframeIdentityException(String msg) { + this(msg, null, false); + } + + public NoMainframeIdentityException(String msg, String token, boolean validToken) { super(msg); + this.token = token; + this.validToken = validToken; } + } diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactory.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactory.java index a888604e8d..6dd50f9e33 100644 --- a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactory.java +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactory.java @@ -11,6 +11,7 @@ package org.zowe.apiml.cloudgatewayservice.filters; import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; @@ -66,12 +67,20 @@ protected WebClient.RequestHeadersSpec createRequest(ServiceInstance instance @Override @SuppressWarnings("squid:S2092") // the internal API cannot define generic more specifically protected Mono processResponse(ServerWebExchange exchange, GatewayFilterChain chain, ZaasTokenResponse response) { - ServerHttpRequest request; + ServerHttpRequest request = null; if (response.getToken() != null) { - request = exchange.getRequest().mutate().headers(headers -> - headers.add(HttpHeaders.COOKIE, new HttpCookie(response.getCookieName(), response.getToken()).toString()) - ).build(); - } else { + if (!StringUtils.isEmpty(response.getCookieName())) { + request = exchange.getRequest().mutate().headers(headers -> + headers.add(HttpHeaders.COOKIE, new HttpCookie(response.getCookieName(), response.getToken()).toString()) + ).build(); + } + if (!StringUtils.isEmpty(response.getHeaderName())) { + request = exchange.getRequest().mutate().headers(headers -> + headers.add(response.getHeaderName(), response.getToken()) + ).build(); + } + } + if (request == null) { request = updateHeadersForError(exchange, "Invalid or missing authentication"); } diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/TokenSchemeTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/TokenSchemeTest.java index eb2503d92c..9cb4af06fe 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/TokenSchemeTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/TokenSchemeTest.java @@ -41,7 +41,7 @@ public abstract class TokenSchemeTest { private static final String COOKIE_NAME = "token_cookie"; private static final String JWT = "jwt"; - private static final ZaasTokenResponse OK_RESPONSE = new ZaasTokenResponse(COOKIE_NAME, JWT); + private static final ZaasTokenResponse OK_RESPONSE = ZaasTokenResponse.builder().cookieName(COOKIE_NAME).token(JWT).build(); public abstract String getTokenEndpoint(); @@ -83,23 +83,23 @@ void createAllZaasServices() throws IOException { // on the beginning prepare all as zombie, each test will decide zaasError = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .responseCode(500) + .responseCode(500) .and().build(); zaasZombie = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .bodyJson(new ZaasTokenResponse()) + .bodyJson(new ZaasTokenResponse()) .and().build(); zaasOk = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .bodyJson(OK_RESPONSE) - .assertion(he -> assertEquals("service", he.getRequestHeaders().getFirst("x-service-id"))) + .bodyJson(OK_RESPONSE) + .assertion(he -> assertEquals("service", he.getRequestHeaders().getFirst("x-service-id"))) .and().build(); // south-bound service - alive for all tests service = mockService("service").scope(MockService.Scope.CLASS) .authenticationScheme(getAuthenticationScheme()) .addEndpoint("/service/test") - .assertion(he -> assertEquals(JWT, getCookie(he, COOKIE_NAME))) + .assertion(he -> assertEquals(JWT, getCookie(he, COOKIE_NAME))) .and().start(); } @@ -188,13 +188,13 @@ private String getServiceUrl() { void init() throws IOException { zaas = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .responseCode(401) + .responseCode(401) .and().start(); service = mockService("service").scope(MockService.Scope.CLASS) .authenticationScheme(getAuthenticationScheme()) .addEndpoint("/service/test") - .assertion(he -> assertNull(getCookie(he, COOKIE_NAME))) + .assertion(he -> assertNull(getCookie(he, COOKIE_NAME))) .and().start(); } @@ -229,15 +229,15 @@ private String getServiceUrl() { void init() throws IOException { zaas = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .responseCode(200) - .bodyJson(new ZaasTokenResponse()) + .responseCode(200) + .bodyJson(new ZaasTokenResponse()) .and().start(); service = mockService("service").scope(MockService.Scope.CLASS) .authenticationScheme(getAuthenticationScheme()) - .addEndpoint("/service/test") - .assertion(he -> assertNull(getCookie(he, COOKIE_NAME))) + .addEndpoint("/service/test") + .assertion(he -> assertNull(getCookie(he, COOKIE_NAME))) .and().start(); } @@ -258,9 +258,9 @@ void givenNoCredentials_whenCallingAService_thenDontPropagateCredentials() { void givenInvalidCredentials_whenCallingAService_thenDontPropagateCredentials() { given() .header(HttpHeaders.AUTHORIZATION, "Baerer nonSense") - .when() + .when() .get(getServiceUrl()) - .then() + .then() .statusCode(200); assertEquals(1, zaas.getCounter()); assertEquals(1, service.getCounter()); @@ -285,24 +285,24 @@ void init() throws IOException { MockService createZaas() throws IOException { return mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .bodyJson(OK_RESPONSE) - .assertion(he -> assertEquals("Bearer userJwt", he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) - - .assertion(he -> assertEquals("service", he.getRequestHeaders().getFirst("x-service-id"))) - - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("myheader"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-SAF-Token"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-Public"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-DistinguishedName"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-CommonName"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("Client-Cert"))) - - .assertion(he -> assertNull(getCookie(he, "mycookie"))) - .assertion(he -> assertEquals("pat", getCookie(he, "personalAccessToken"))) - .assertion(he -> assertEquals("jwt1", getCookie(he, "apimlAuthenticationToken"))) - .assertion(he -> assertEquals("jwt2", getCookie(he, "apimlAuthenticationToken.2"))) - .assertion(he -> assertNull(getCookie(he, "jwtToken"))) - .assertion(he -> assertNull(getCookie(he, "LtpaToken2"))) + .bodyJson(OK_RESPONSE) + .assertion(he -> assertEquals("Bearer userJwt", he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) + + .assertion(he -> assertEquals("service", he.getRequestHeaders().getFirst("x-service-id"))) + + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("myheader"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-SAF-Token"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-Public"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-DistinguishedName"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-CommonName"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("Client-Cert"))) + + .assertion(he -> assertNull(getCookie(he, "mycookie"))) + .assertion(he -> assertEquals("pat", getCookie(he, "personalAccessToken"))) + .assertion(he -> assertEquals("jwt1", getCookie(he, "apimlAuthenticationToken"))) + .assertion(he -> assertEquals("jwt2", getCookie(he, "apimlAuthenticationToken.2"))) + .assertion(he -> assertNull(getCookie(he, "jwtToken"))) + .assertion(he -> assertNull(getCookie(he, "LtpaToken2"))) .and().start(); } @@ -310,22 +310,22 @@ MockService createService() throws IOException { return mockService("service").scope(MockService.Scope.CLASS) .authenticationScheme(getAuthenticationScheme()) .addEndpoint("/service/test") - .assertion(he -> assertEquals(JWT, getCookie(he, COOKIE_NAME))) - - .assertion(he -> assertNull(he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("x-service-id"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-SAF-Token"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-Public"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-DistinguishedName"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-CommonName"))) - .assertion(he -> assertEquals("myvalue", he.getRequestHeaders().getFirst("myheader"))) - - .assertion(he -> assertNull(getCookie(he, "personalAccessToken"))) - .assertion(he -> assertNull(getCookie(he, "apimlAuthenticationToken"))) - .assertion(he -> assertNull(getCookie(he, "apimlAuthenticationToken.2"))) - .assertion(he -> assertNull(getCookie(he, "jwtToken"))) - .assertion(he -> assertNull(getCookie(he, "LtpaToken2"))) - .assertion(he -> assertEquals("mycookievalue", getCookie(he, "mycookie"))) + .assertion(he -> assertEquals(JWT, getCookie(he, COOKIE_NAME))) + + .assertion(he -> assertNull(he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("x-service-id"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-SAF-Token"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-Public"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-DistinguishedName"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-CommonName"))) + .assertion(he -> assertEquals("myvalue", he.getRequestHeaders().getFirst("myheader"))) + + .assertion(he -> assertNull(getCookie(he, "personalAccessToken"))) + .assertion(he -> assertNull(getCookie(he, "apimlAuthenticationToken"))) + .assertion(he -> assertNull(getCookie(he, "apimlAuthenticationToken.2"))) + .assertion(he -> assertNull(getCookie(he, "jwtToken"))) + .assertion(he -> assertNull(getCookie(he, "LtpaToken2"))) + .assertion(he -> assertEquals("mycookievalue", getCookie(he, "mycookie"))) .and().start(); } @@ -352,9 +352,9 @@ void givenMultipleHeaders_whenCallingAService_thenTheyAreResend() { .cookie("apimlAuthenticationToken.2", "jwt2") .cookie("jwtToken", "jwtToken") .cookie("LtpaToken2", "LtpaToken2") - .when() + .when() .get(getServiceUrl()) - .then() + .then() .statusCode(200); assertEquals(1, zaas.getCounter()); diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactoryTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactoryTest.java new file mode 100644 index 0000000000..5ab2a2a357 --- /dev/null +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactoryTest.java @@ -0,0 +1,99 @@ +/* + * 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.cloudgatewayservice.filters; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.zowe.apiml.constants.ApimlConstants; +import org.zowe.apiml.zaas.ZaasTokenResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +class TokenFilterFactoryTest { + + @Nested + class RequestUpdate { + + private MockServerHttpRequest testRequestMutation(ZaasTokenResponse response) { + MockServerHttpRequest request = MockServerHttpRequest.get("/url").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + new TokenFilterFactory(null, null, null) { + @Override + public String getEndpointUrl(ServiceInstance instance) { + return null; + } + }.processResponse(exchange, mock(GatewayFilterChain.class), response); + + return request; + } + + @Nested + class ValidResponse { + + @Test + void givenHeaderResponse_whenHandling_thenUpdateTheRequest() { + MockServerHttpRequest request = testRequestMutation(ZaasTokenResponse.builder() + .headerName("headerName") + .token("headerValue") + .build() + ); + assertEquals("headerValue", request.getHeaders().getFirst("headerName")); + } + + @Test + void givenCookieResponse_whenHandling_thenUpdateTheRequest() { + MockServerHttpRequest request = testRequestMutation(ZaasTokenResponse.builder() + .cookieName("cookieName") + .token("cookieValue") + .build() + ); + assertEquals("cookieName=\"cookieValue\"", request.getHeaders().getFirst("cookie")); + } + + } + + @Nested + class InvalidResponse { + + @Test + void givenEmptyResponse_whenHandling_thenNoUpdate() { + MockServerHttpRequest request = testRequestMutation(ZaasTokenResponse.builder() + .token("jwt") + .build() + ); + assertEquals(1, request.getHeaders().size()); + assertTrue(request.getHeaders().containsKey(ApimlConstants.AUTH_FAIL_HEADER)); + } + + @Test + void givenCookieAndHeaderInResponse_whenHandling_thenSetBoth() { + MockServerHttpRequest request = testRequestMutation(ZaasTokenResponse.builder() + .cookieName("cookie") + .headerName("header") + .token("jwt") + .build() + ); + assertEquals("jwt", request.getHeaders().getFirst("header")); + assertEquals("cookie=\"jwt\"", request.getHeaders().getFirst("cookie")); + } + + } + + } + +} \ No newline at end of file diff --git a/common-service-core/src/main/java/org/zowe/apiml/constants/ApimlConstants.java b/common-service-core/src/main/java/org/zowe/apiml/constants/ApimlConstants.java index 2e76dbeb59..c1112dcf5b 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/constants/ApimlConstants.java +++ b/common-service-core/src/main/java/org/zowe/apiml/constants/ApimlConstants.java @@ -26,5 +26,6 @@ private ApimlConstants() { public static final String AUTH_FAIL_HEADER = "X-Zowe-Auth-Failure"; public static final String HTTP_CLIENT_USE_CLIENT_CERTIFICATE = "apiml.useClientCert"; public static final String SAF_TOKEN_HEADER = "X-SAF-Token"; + public static final String HEADER_OIDC_TOKEN = "OIDC-token"; } diff --git a/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java b/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java index 21dd40ce65..f4ebf59a9e 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java +++ b/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java @@ -11,15 +11,18 @@ package org.zowe.apiml.zaas; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor +@Builder public class ZaasTokenResponse { private String cookieName; + private String headerName; private String token; } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilter.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilter.java index 24d73fcb31..5011479a51 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilter.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilter.java @@ -20,6 +20,7 @@ import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.security.service.ServiceAuthenticationServiceImpl; import org.zowe.apiml.gateway.security.service.schema.AuthenticationCommand; +import org.zowe.apiml.gateway.security.service.schema.OidcCommand; import org.zowe.apiml.gateway.security.service.schema.source.AuthSchemeException; import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService; @@ -83,6 +84,13 @@ public Object run() { if (authSource.isPresent() && !isSourceValidForCommand(authSource.get(), cmd)) { throw new AuthSchemeException("org.zowe.apiml.gateway.security.invalidAuthentication"); } + } catch (NoMainframeIdentityException noIdentityException) { + String error = this.messageService.createMessage("org.zowe.apiml.gateway.security.schema.x509.mappingFailed").mapToLogMessage(); + sendErrorMessage(error, context); + if (!noIdentityException.isValidToken()) { + return null; + } + cmd = new OidcCommand(noIdentityException.getToken()); } catch (TokenExpireException tee) { String error = this.messageService.createMessage("org.zowe.apiml.gateway.security.expiredToken").mapToLogMessage(); sendErrorMessage(error, context); @@ -91,10 +99,6 @@ public Object run() { String error = this.messageService.createMessage("org.zowe.apiml.gateway.security.invalidToken").mapToLogMessage(); sendErrorMessage(error, context); return null; - } catch (NoMainframeIdentityException noIdentityException) { - String error = this.messageService.createMessage("org.zowe.apiml.gateway.security.schema.x509.mappingFailed").mapToLogMessage(); - sendErrorMessage(error, context); - return null; } catch (AuthenticationException ae) { rejected = true; } catch (AuthSchemeException ase) { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/OidcCommand.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/OidcCommand.java new file mode 100644 index 0000000000..5f1718968a --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/OidcCommand.java @@ -0,0 +1,56 @@ +/* + * 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.security.service.schema; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.zuul.context.RequestContext; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHeaders; +import org.zowe.apiml.constants.ApimlConstants; +import org.zowe.apiml.util.CookieUtil; + +import java.util.HashSet; +import java.util.Set; + +import static org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper.IGNORED_HEADERS; +import static org.zowe.apiml.gateway.security.service.schema.JwtCommand.COOKIE_HEADER; +import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; + +@RequiredArgsConstructor +public class OidcCommand extends AuthenticationCommand { + + + private final String token; + + @Override + public void apply(InstanceInfo instanceInfo) { + RequestContext context = RequestContext.getCurrentContext(); + + String cookie = context.getRequest().getHeader(COOKIE_HEADER); + if (!StringUtils.isEmpty(cookie)) { + cookie = CookieUtil.removeCookie(cookie, COOKIE_AUTH_NAME); + if (StringUtils.isEmpty(cookie)) { + removeCookieFromRequest(context, COOKIE_HEADER); + } else { + context.addZuulRequestHeader(COOKIE_HEADER, cookie); + } + } + removeCookieFromRequest(context, HttpHeaders.AUTHORIZATION); + + context.addZuulRequestHeader(ApimlConstants.HEADER_OIDC_TOKEN, token); + } + + void removeCookieFromRequest(RequestContext context, String headerName) { + ((Set) context.computeIfAbsent(IGNORED_HEADERS, k -> new HashSet<>())).add(headerName.toLowerCase()); // NOSONAR + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceService.java index 38094f8fca..3a898c2d13 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceService.java @@ -109,7 +109,7 @@ private AuthSource.Parsed parseOIDCToken(OIDCAuthSource oidcAuthSource, Authenti String mappedUser = mapper.mapToMainframeUserId(oidcAuthSource); if (StringUtils.isEmpty(mappedUser)) { logger.log(MessageType.DEBUG, "No mainframe user id retrieved. Cancel parsing of OIDC token."); - throw new NoMainframeIdentityException("No mainframe identity found."); + throw new NoMainframeIdentityException("No mainframe identity found.", token, true); } logger.log(MessageType.DEBUG, "Parsing OIDC token."); QueryResponse response = authenticationService.parseJwtToken(token); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java index 41c657a336..210135a6ad 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java @@ -28,7 +28,12 @@ import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.http.*; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.security.authentication.AuthenticationServiceException; @@ -229,19 +234,19 @@ public String getZosmfRealm(String infoURIEndpoint) { public ZaasTokenResponse exchangeAuthenticationForZosmfToken(String token, AuthSource.Parsed authSource) throws ServiceNotFoundException { switch (authSource.getOrigin()) { case ZOSMF: - return new ZaasTokenResponse(ZosmfService.TokenType.JWT.getCookieName(), token); + return ZaasTokenResponse.builder().cookieName(JWT.getCookieName()).token(token).build(); case ZOWE: String ltpaToken = authenticationService.getLtpaToken(token); if (ltpaToken != null) { - return new ZaasTokenResponse(ZosmfService.TokenType.LTPA.getCookieName(), ltpaToken); + return ZaasTokenResponse.builder().cookieName(LTPA.getCookieName()).token(ltpaToken).build(); } default: Map zosmfTokens = tokenCreationService.createZosmfTokensWithoutCredentials(authSource.getUserId()); if (zosmfTokens.containsKey(JWT)) { - return new ZaasTokenResponse(ZosmfService.TokenType.JWT.getCookieName(), zosmfTokens.get(JWT)); + return ZaasTokenResponse.builder().cookieName(JWT.getCookieName()).token(zosmfTokens.get(JWT)).build(); } else if (zosmfTokens.containsKey(LTPA)) { - return new ZaasTokenResponse(ZosmfService.TokenType.LTPA.getCookieName(), zosmfTokens.get(LTPA)); + return ZaasTokenResponse.builder().cookieName(LTPA.getCookieName()).token(zosmfTokens.get(LTPA)).build(); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java index 15eae10852..d67151c139 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java @@ -16,7 +16,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.security.service.TokenCreationService; import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService; @@ -24,6 +30,7 @@ import org.zowe.apiml.gateway.security.ticket.ApplicationNameNotFoundException; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.ticket.TicketRequest; import org.zowe.apiml.ticket.TicketResponse; import org.zowe.apiml.zaas.ZaasTokenResponse; @@ -83,7 +90,26 @@ public ResponseEntity getZoweJwt(@RequestAttribute(AUTH_SOURC return ResponseEntity .status(HttpStatus.OK) - .body(new ZaasTokenResponse(COOKIE_AUTH_NAME, token)); + .body(ZaasTokenResponse.builder().cookieName(COOKIE_AUTH_NAME).token(token).build()); + + } + + /** + * Controller level exception handler for cases when NO mapping with mainframe ID exists. + * + * @param authSource credentials that will be used for authentication translation + * @param nmie exception thrown in case of missing user mapping + * @return status code OK, header name and value if OIDC token is valid, otherwise status code UNAUTHORIZED + */ + @ExceptionHandler(NoMainframeIdentityException.class) + public ResponseEntity handleNoMainframeIdException(@RequestAttribute(AUTH_SOURCE_ATTR) AuthSource authSource, NoMainframeIdentityException nmie) { + if (nmie.isValidToken() && authSource.getType() == AuthSource.AuthSourceType.OIDC) { + return ResponseEntity + .status(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(ZaasTokenResponse.builder().headerName(ApimlConstants.HEADER_OIDC_TOKEN).token(String.valueOf(authSource.getRawSource())).build()); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } @PostMapping(path = "safIdt", produces = MediaType.APPLICATION_JSON_VALUE) @@ -99,7 +125,7 @@ public ResponseEntity getSafIdToken(@RequestBody TicketReques String safIdToken = tokenCreationService.createSafIdTokenWithoutCredentials(authSourceParsed.getUserId(), applicationName); return ResponseEntity .status(HttpStatus.OK) - .body(new ZaasTokenResponse("", safIdToken)); + .body(ZaasTokenResponse.builder().token(safIdToken).build()); } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilterTest.java index aabcccfb5d..67fb2f7372 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -45,7 +45,11 @@ import java.util.Optional; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.*; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; @@ -235,6 +239,22 @@ void givenNoMappedDistributedId_thenCallThrough() { verify(RequestContext.getCurrentContext(), never()).setResponseStatusCode(401); } + @Test + void givenNoMappedDistributedIdAndInvalidToken_thenCallThrough() { + MessageTemplate messageTemplate = new MessageTemplate("key", "number", MessageType.ERROR, "text"); + Message message = Message.of("requestedKey", messageTemplate, new Object[0]); + doReturn(message).when(messageService).createMessage(anyString(), (Object) any()); + RequestContext.testSetCurrentContext(new RequestContext()); + doThrow(new NoMainframeIdentityException("User not found.", null, false)) + .when(serviceAuthenticationService) + .getAuthentication((String) null); + + assertNull(serviceAuthenticationFilter.run()); + + assertNotNull(RequestContext.getCurrentContext().getZuulRequestHeaders().get(ApimlConstants.AUTH_FAIL_HEADER.toLowerCase())); + assertEquals(ApimlConstants.AUTH_FAIL_HEADER, RequestContext.getCurrentContext().getZuulResponseHeaders().get(0).first()); + } + @ParameterizedTest @MethodSource("provideAuthSources") void givenInvalidAuthSource_whenAuthenticationException_thenReject(AuthSource authSource) { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/OidcCommandTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/OidcCommandTest.java new file mode 100644 index 0000000000..724c613a8d --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/OidcCommandTest.java @@ -0,0 +1,61 @@ +/* + * 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.security.service.schema; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.zuul.context.RequestContext; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletRequest; +import org.zowe.apiml.constants.ApimlConstants; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper.IGNORED_HEADERS; + +class OidcCommandTest { + + private static final String tokenValue = "tokenValue"; + + @Test + void givenTokenExistsAndMultipleCookies_thenSetAsHeaderAndRemoveAuthorization() { + RequestContext mockContext = new RequestContext(); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + RequestContext.testSetCurrentContext(mockContext); + mockContext.setRequest(mockRequest); + + mockRequest.addHeader(HttpHeaders.COOKIE, "cookieName=cookieValue; apimlAuthenticationToken=xyz"); + new OidcCommand(tokenValue).apply(mock(InstanceInfo.class)); + + assertEquals("cookieName=cookieValue", mockContext.getZuulRequestHeaders().get(HttpHeaders.COOKIE.toLowerCase())); + assertEquals(tokenValue, mockContext.getZuulRequestHeaders().get(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())); + assertTrue(((Set) mockContext.get(IGNORED_HEADERS)).contains(HttpHeaders.AUTHORIZATION.toLowerCase())); // NOSONAR + } + + @Test + void givenTokenExists_thenSetAsHeaderAndRemoveAuthorization() { + RequestContext mockContext = new RequestContext(); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + RequestContext.testSetCurrentContext(mockContext); + mockContext.setRequest(mockRequest); + + mockRequest.addHeader(HttpHeaders.COOKIE, "apimlAuthenticationToken=xyz"); + new OidcCommand(tokenValue).apply(mock(InstanceInfo.class)); + + assertEquals(tokenValue, mockContext.getZuulRequestHeaders().get(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())); + assertTrue(((Set) mockContext.get(IGNORED_HEADERS)).contains(HttpHeaders.AUTHORIZATION.toLowerCase())); // NOSONAR + assertTrue(((Set) mockContext.get(IGNORED_HEADERS)).contains(HttpHeaders.COOKIE.toLowerCase())); // NOSONAR + } + +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java index 882df9f76a..c8f52d6532 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java @@ -21,14 +21,21 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.security.service.TokenCreationService; import org.zowe.apiml.gateway.security.service.saf.SafIdtException; -import org.zowe.apiml.gateway.security.service.schema.source.*; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSchemeException; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService; +import org.zowe.apiml.gateway.security.service.schema.source.JwtAuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.OIDCAuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.ParsedTokenAuthSource; import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageService; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.TokenExpireException; import org.zowe.apiml.security.common.token.TokenNotValidException; import org.zowe.apiml.zaas.ZaasTokenResponse; @@ -36,7 +43,11 @@ import javax.management.ServiceNotFoundException; import java.util.Date; -import static org.apache.http.HttpStatus.*; +import static org.apache.http.HttpStatus.SC_BAD_REQUEST; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasSize; @@ -101,7 +112,7 @@ void setUp() { @Test void whenRequestZosmfToken_thenResponseOK() throws Exception { when(zosmfService.exchangeAuthenticationForZosmfToken(JWT_TOKEN, authParsedSource)) - .thenReturn(new ZaasTokenResponse(ZosmfService.TokenType.JWT.getCookieName(), JWT_TOKEN)); + .thenReturn(ZaasTokenResponse.builder().cookieName(ZosmfService.TokenType.JWT.getCookieName()).token(JWT_TOKEN).build()); mockMvc.perform(post(ZOSMF_TOKEN_URL) .requestAttr(AUTH_SOURCE_ATTR, authSource) @@ -234,6 +245,31 @@ void whenRequestingZoweTokensAndTokenExpireException_thenUnauthorized() throws E .andExpect(jsonPath("$.messages[0].messageContent", is("The token has expired"))); } + @Test + void whenRequestingZoweTokensAndUserMissingMapping_thenOkWithTokenInHeader() throws Exception { + authSource = new OIDCAuthSource(JWT_TOKEN); + when(authSourceService.getJWT(authSource)) + .thenThrow(new NoMainframeIdentityException("No user mappring", null, true)); + + mockMvc.perform(post(ZOWE_TOKEN_URL) + .requestAttr(AUTH_SOURCE_ATTR, authSource)) + .andExpect(status().is(SC_OK)) + .andExpect(jsonPath("$.token", is(JWT_TOKEN))) + .andExpect(jsonPath("$.headerName", is(ApimlConstants.HEADER_OIDC_TOKEN))); + + } + + @Test + void whenRequestingZoweTokensAndUserMissingMappingAndTokenIsInvalid_thenUnauthorized() throws Exception { + authSource = new OIDCAuthSource(JWT_TOKEN); + when(authSourceService.getJWT(authSource)) + .thenThrow(new NoMainframeIdentityException("No user mappring", null, false)); + + mockMvc.perform(post(ZOWE_TOKEN_URL) + .requestAttr(AUTH_SOURCE_ATTR, authSource)) + .andExpect(status().is(SC_UNAUTHORIZED)); + } + @Test void whenRequestingZoweTokensAndAuthSchemeException_thenUnauthorized() throws Exception { when(authSourceService.getJWT(authSource)) diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java index 5d764cd5aa..275822c1c6 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java @@ -147,8 +147,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", hasKey("authorization")) - .body("headers.authorization", startsWith("Bearer")) + .body("headers", not(hasKey("authorization"))) + .body("headers", hasKey(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())) .body("headers", not(hasKey("cookie"))); } @@ -178,8 +178,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) - .body("cookies", hasKey("apimlAuthenticationToken")) - .body("cookies.apimlAuthenticationToken", is(VALID_TOKEN_NO_MAPPING)); + .body("cookies", not(hasKey("apimlAuthenticationToken"))) + .body("cookies.apimlAuthenticationToken", is((String) null)); } } } @@ -212,8 +212,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", hasKey("authorization")) - .body("headers.authorization", startsWith("Bearer")) + .body("headers", not(hasKey("authorization"))) + .body("headers", hasKey(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())) .body("headers", not(hasKey("cookie"))); } @@ -242,7 +242,7 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", hasKey("cookie")) + .body("headers", not(hasKey("cookie"))) .body("cookies", not(hasKey("jwtToken"))); } } @@ -275,8 +275,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", hasKey("authorization")) - .body("headers.authorization", startsWith("Bearer")) + .body("headers", not(hasKey("authorization"))) + .body("headers", hasKey(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())) .body("headers", not(hasKey("x-saf-token"))); } @@ -337,8 +337,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", hasKey("authorization")) - .body("headers.authorization", startsWith("Bearer")); + .body("headers", not(hasKey("authorization"))) + .body("headers", hasKey(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())); } @Nested