Skip to content

Commit

Permalink
feat: exchange client certificate for SAF IDT (#2455)
Browse files Browse the repository at this point in the history
* resolve conflicts

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

* fix: Use valid localca.cer (#2384)

* Replace localca.cer

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

* fix

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

* revert

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

* use certificate for dev instance

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

* remove unused imports

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

Co-authored-by: Petr Weinfurt <[email protected]>
Co-authored-by: Andrea Tabone <[email protected]>
  • Loading branch information
3 people authored and CarsonCook committed Jun 28, 2022
1 parent 4051303 commit 295024d
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 134 deletions.
2 changes: 2 additions & 0 deletions api-catalog-ui/frontend/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=3
REACT_APP_STATUS_UPDATE_DEBOUNCE=300
REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000
REACT_APP_GATEWAY_URL=
SSL_CRT_FILE=../../keystore/localhost/localhost.pem
SSL_KEY_FILE=../../keystore/localhost/localhost.keystore.key
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,30 @@

import com.netflix.appinfo.InstanceInfo;
import com.netflix.zuul.context.RequestContext;

import java.util.Arrays;
import java.util.Date;
import java.util.Optional;
import io.jsonwebtoken.Claims;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.zowe.apiml.auth.Authentication;
import org.zowe.apiml.auth.AuthenticationScheme;
import org.zowe.apiml.gateway.security.service.PassTicketException;
import org.zowe.apiml.gateway.security.service.saf.SafIdtAuthException;
import org.zowe.apiml.gateway.security.service.saf.SafIdtException;
import org.zowe.apiml.gateway.security.service.saf.SafIdtProvider;
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.passticket.IRRPassTicketGenerationException;
import org.zowe.apiml.passticket.PassTicketService;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.token.TokenExpireException;
import org.zowe.apiml.security.common.token.TokenNotValidException;
import org.zowe.apiml.util.CookieUtil;

import io.jsonwebtoken.Claims;

import javax.annotation.PostConstruct;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.Date;
import java.util.Optional;

import static org.zowe.apiml.gateway.security.service.JwtUtils.getJwtClaims;

Expand All @@ -54,12 +52,6 @@ public class SafIdtScheme implements IAuthenticationScheme {

@Value("${apiml.security.saf.defaultIdtExpiration:10}")
int defaultIdtExpiration;
private String cookieName;

@PostConstruct
public void initCookieName() {
cookieName = authConfigurationProperties.getCookieProperties().getCookieName();
}

@Override
public AuthenticationScheme getScheme() {
Expand All @@ -68,73 +60,103 @@ public AuthenticationScheme getScheme() {

@Override
public AuthenticationCommand createCommand(Authentication authentication, AuthSource authSource) {
final AuthSource.Parsed parsedAuthSource = authSourceService.parse(authSource);

if (parsedAuthSource == null) {
return AuthenticationCommand.EMPTY;
// check the authentication source
if (authSource == null || authSource.getRawSource() == null) {
throw new AuthSchemeException("org.zowe.apiml.gateway.security.schema.missingAuthentication");
}
// parse the authentication source
AuthSource.Parsed parsedAuthSource;
try {
parsedAuthSource = authSourceService.parse(authSource);
if (parsedAuthSource == null) {
throw new IllegalStateException("Error occurred while parsing authenticationSource");
}
} catch (TokenNotValidException e) {
throw new AuthSchemeException("org.zowe.apiml.gateway.security.invalidToken");
} catch (TokenExpireException e) {
throw new AuthSchemeException("org.zowe.apiml.gateway.security.expiredToken");
}

final String userId = parsedAuthSource.getUserId();
final String applId = authentication.getApplid();
String safIdentityToken;
long expireAt;

String applId = getApplId(authentication);
safIdentityToken = generateSafIdentityToken(parsedAuthSource, applId);
expireAt = getSafIdtExpiration(safIdentityToken);

return new SafIdtCommand(safIdentityToken, expireAt);
}

@Override
public Optional<AuthSource> getAuthSource() {
return authSourceService.getAuthSourceFromRequest();
}

private String getApplId(Authentication authentication) {
String applId = authentication == null ? null : authentication.getApplid();
if (applId == null) {
throw new PassTicketException(
"Applid is required. Check the configuration of service"
);
throw new AuthSchemeException("org.zowe.apiml.gateway.security.scheme.missingApplid");
}
return applId;
}

private String generateSafIdentityToken(@NotNull AuthSource.Parsed parsedAuthSource, @NotNull String applId) {
String safIdentityToken;

String userId = parsedAuthSource.getUserId();
if (userId == null) {
throw new AuthSchemeException("org.zowe.apiml.gateway.security.schema.x509.mappingFailed");
}

char[] passTicket = "".toCharArray();
try {
char[] passTicket = passTicketService.generate(userId, applId).toCharArray();
try {
safIdentityToken = safIdtProvider.generate(userId, passTicket, applId);
} finally {
Arrays.fill(passTicket, (char) 0);
}
passTicket = passTicketService.generate(userId, applId).toCharArray();
safIdentityToken = safIdtProvider.generate(userId, passTicket, applId);
} catch (IRRPassTicketGenerationException e) {
throw new PassTicketException(
String.format("Could not generate PassTicket for user ID '%s' and APPLID '%s'", userId, applId), e
);
throw new AuthSchemeException("org.zowe.apiml.security.ticket.generateFailed", e.getMessage());
} catch (SafIdtException | SafIdtAuthException e) {
throw new AuthSchemeException("org.zowe.apiml.security.idt.failed", e.getMessage());
} finally {
Arrays.fill(passTicket, (char) 0);
}
return safIdentityToken;
}

private long getSafIdtExpiration(String safIdentityToken) {
Date expirationTime;
try {
Claims claims = getJwtClaims(safIdentityToken);
Date expirationDate = claims.getExpiration();
if (expirationDate == null) {
expirationDate = DateUtils.addMinutes(new Date(), defaultIdtExpiration);
expirationTime = claims.getExpiration();
if (expirationTime == null) {
expirationTime = DateUtils.addMinutes(new Date(), defaultIdtExpiration);
}

return new SafIdtCommand(safIdentityToken, cookieName, expirationDate.getTime());
} catch (TokenNotValidException | TokenExpireException e) {
throw new SafIdtException("Unable to parse Identity Token", e);
} catch (TokenNotValidException e) {
throw new AuthSchemeException("org.zowe.apiml.gateway.security.invalidToken");
} catch (TokenExpireException e) {
throw new AuthSchemeException("org.zowe.apiml.gateway.security.expiredToken");
}
}

@Override
public Optional<AuthSource> getAuthSource() {
return authSourceService.getAuthSourceFromRequest();
return expirationTime.getTime();
}

@RequiredArgsConstructor
public class SafIdtCommand extends AuthenticationCommand {
private static final long serialVersionUID = 8213192949049438897L;

@Getter
private final String safIdentityToken;
private final String cookieName;
@Getter
private final Long expireAt;

private static final String COOKIE_HEADER = "cookie";
private static final String SAF_TOKEN_HEADER = "X-SAF-Token";
protected static final String SAF_TOKEN_HEADER = "X-SAF-Token";

@Override
public void apply(InstanceInfo instanceInfo) {
final RequestContext context = RequestContext.getCurrentContext();
context.addZuulRequestHeader(SAF_TOKEN_HEADER, safIdentityToken);
context.addZuulRequestHeader(COOKIE_HEADER,
CookieUtil.removeCookie(
context.getZuulRequestHeaders().get(COOKIE_HEADER),
cookieName
)
);
if (safIdentityToken != null) {
final RequestContext context = RequestContext.getCurrentContext();
// add header with SafIdt token to request and remove APIML token from Cookie if exists
context.addZuulRequestHeader(SAF_TOKEN_HEADER, safIdentityToken);
JwtCommand.removeCookie(context, authConfigurationProperties.getCookieProperties().getCookieName());
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.jsonwebtoken.security.Keys;
import io.restassured.http.Cookie;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.http.Header;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpUriRequest;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -24,27 +25,33 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestTemplate;
import org.zowe.apiml.acceptance.common.AcceptanceTest;
import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices;
import org.zowe.apiml.acceptance.netflix.MetadataBuilder;
import org.zowe.apiml.gateway.security.service.saf.SafRestAuthenticationService;
import org.zowe.apiml.util.config.SslContext;
import org.zowe.apiml.util.config.SslContextConfigurer;

import java.io.IOException;
import java.util.Date;
import org.zowe.apiml.util.config.SslContext;

import static io.restassured.RestAssured.given;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.*;
import static org.zowe.apiml.gateway.filters.pre.ServiceAuthenticationFilter.AUTH_FAIL_HEADER;

/**
* This test verifies that the token/client certificate was exchanged. The input is a valid apimlJwtToken/client certificate.
* The output to be tested is the saf idt token.
*/
@AcceptanceTest
@TestPropertySource(properties = {"spring.profiles.active=debug", "apiml.security.x509.externalMapperUrl="})
class SafIdtSchemeTest extends AcceptanceTestWithTwoServices {
@Value("${server.ssl.keyStorePassword:password}")
private char[] keystorePassword;
Expand Down Expand Up @@ -135,47 +142,35 @@ void prepareService() throws IOException {
}

@Test
void givenInvalidJwtToken() {
void givenInvalidJwtToken() throws IOException {
Cookie withInvalidToken = new Cookie.Builder("apimlAuthenticationToken=invalidValue").build();

//@formatter:off
given()
.cookie(withInvalidToken)
.when()
.when()
.get(basePath + serviceWithDefaultConfiguration.getPath())
.then()
.statusCode(is(HttpStatus.SC_OK));
//@formatter:on

ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
verify(mockClient, times(1)).execute(captor.capture());

verify(mockTemplate, times(0))
.exchange(any(), eq(HttpMethod.POST), any(), eq(SafRestAuthenticationService.Token.class));
.exchange(any(), eq(HttpMethod.POST), any(), eq(SafRestAuthenticationService.Token.class));

Header zoweAuthFailureHeader = captor.getValue().getFirstHeader(AUTH_FAIL_HEADER);
assertNotNull(zoweAuthFailureHeader);
assertEquals("ZWEAG102E Token is not valid", zoweAuthFailureHeader.getValue());
}
}
}

@Nested
class GivenServerCertificate {
@Test
void thenSafheaderInRequestHeaders() throws IOException {
applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId());
mockValid200HttpResponse();

given()
.config(SslContext.apimlRootCert)
.when()
.get(basePath + serviceWithDefaultConfiguration.getPath())
.then()
.statusCode(is(HttpStatus.SC_OK));

ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
verify(mockClient, times(1)).execute(captor.capture());

assertThat(captor.getValue().getHeaders("X-SAF-Token").length, is(0));
}
}
/*
@Nested
class GivenClientCertificate {
String resultSafToken;

@BeforeEach
void setUp() throws Exception {
SslContextConfigurer configurer = new SslContextConfigurer(keystorePassword, clientKeystore, keystore);
Expand All @@ -188,45 +183,63 @@ void setUp() throws Exception {
applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId());

reset(mockClient);

resultSafToken = Jwts.builder()
.setExpiration(DateUtils.addMinutes(new Date(), 10))
.signWith(Keys.secretKeyFor(SignatureAlgorithm.HS256))
.compact();

ResponseEntity<SafRestAuthenticationService.Token> response = mock(ResponseEntity.class);
when(mockTemplate.exchange(any(), eq(HttpMethod.POST), any(), eq(SafRestAuthenticationService.Token.class)))
.thenReturn(response);
SafRestAuthenticationService.Token responseBody =
new SafRestAuthenticationService.Token(resultSafToken, "applid");
when(response.getBody()).thenReturn(responseBody);

mockValid200HttpResponse();
}

@Nested
class WhenClientAuthInExtendedKeyUsage {
// TODO: add checks for transformation once X509 -> SafIdt is implemented
@Test
@Ignore
void thenOk() throws IOException {
mockValid200HttpResponse();
void thenValidSafIdTokenProvided() throws IOException {
given()
.config(SslContext.clientCertUser)
.when()
.get(basePath + serviceWithDefaultConfiguration.getPath())
.then()
.statusCode(is(HttpStatus.SC_OK));

ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
verify(mockClient, times(1)).execute(captor.capture());
assertHeaderWithValue(captor.getValue(), "X-SAF-Token", resultSafToken);
assertThat(captor.getValue().getHeaders(AUTH_FAIL_HEADER).length, is(0));
}
}

/**
* When client certificate from request does not have extended key usage set correctly and can't be used for
* client authentication then request fails with response code 400 - BAD REQUEST
* /
* client authentication then request will continue with X-Zowe-Auth-Failure header only.
*/
@Nested
class WhenNoClientAuthInExtendedKeyUsage {
@Test
@Ignore
void thenBadRequest() {
void thenNoSafIdTokenProvided() throws IOException {

given()
.config(SslContext.apimlRootCert)
.when()
.get(basePath + serviceWithDefaultConfiguration.getPath())
.then()
.statusCode(is(HttpStatus.SC_BAD_REQUEST));
.statusCode(is(HttpStatus.SC_OK));

ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
verify(mockClient, times(1)).execute(captor.capture());
assertThat(captor.getValue().getHeaders("X-SAF-Token").length, is(0));
assertHeaderWithValue(captor.getValue(), AUTH_FAIL_HEADER, "ZWEAG165E X509 certificate is missing the client certificate extended usage definition");
}
}
}
*/

private void assertHeaderWithValue(HttpUriRequest request, String header, String value) {
assertThat(request.getHeaders(header).length, is(1));
Expand Down
Loading

0 comments on commit 295024d

Please sign in to comment.