Skip to content

Commit

Permalink
feat: Move OIDC access token from cookie to special header (#3513)
Browse files Browse the repository at this point in the history
* POC

Signed-off-by: Pavel Jares <[email protected]>

* fix

Signed-off-by: Pavel Jares <[email protected]>

* replace old constructors

Signed-off-by: achmelo <[email protected]>

* update IT

Signed-off-by: achmelo <[email protected]>

* fix

Signed-off-by: Pavel Jares <[email protected]>

* update IT

Signed-off-by: achmelo <[email protected]>

* fix IT

Signed-off-by: Pavel Jares <[email protected]>

* exception handler for no MF ID, unit test

Signed-off-by: achmelo <[email protected]>

* unit tests for request modification

Signed-off-by: Pavel Jares <[email protected]>

* license

Signed-off-by: achmelo <[email protected]>

* minor changes

Signed-off-by: Pavel Jares <[email protected]>

* lowercase header

Signed-off-by: achmelo <[email protected]>

* remove import

Signed-off-by: achmelo <[email protected]>

* remove authorization header from httpservletrequest

Signed-off-by: achmelo <[email protected]>

* test no ID and invalid token

Signed-off-by: achmelo <[email protected]>

* ignore cookies if auth cookie only remains

Signed-off-by: achmelo <[email protected]>

* expect no cookie in request

Signed-off-by: achmelo <[email protected]>

* fix sonar

Signed-off-by: Pavel Jares <[email protected]>

---------

Signed-off-by: Pavel Jares <[email protected]>
Signed-off-by: achmelo <[email protected]>
Co-authored-by: achmelo <[email protected]>
(cherry picked from commit 6248308)
  • Loading branch information
pj892031 authored and achmelo committed Apr 23, 2024
1 parent 1634f7f commit f47ed01
Show file tree
Hide file tree
Showing 15 changed files with 416 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,12 +67,20 @@ protected WebClient.RequestHeadersSpec<?> createRequest(ServiceInstance instance
@Override
@SuppressWarnings("squid:S2092") // the internal API cannot define generic more specifically
protected Mono<Void> 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");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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();
}

Expand All @@ -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());
Expand All @@ -285,47 +285,47 @@ 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();
}

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();
}

Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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"));
}

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
Loading

0 comments on commit f47ed01

Please sign in to comment.