Skip to content

Commit

Permalink
feat: Enhance x509 authentication scheme to support client certificat…
Browse files Browse the repository at this point in the history
…es (part 2) (#2260)

* feat: Enhance x509 authentication scheme to support client certificates (part 1)

move the logic which gets authentication source from request to scheme

* feat: Enhance x509 authentication scheme to support client certificates (part 2)

- validate extended key usage for X509 certificate in getAuthSourceFromRequest() method;
- use AuthSourceService in X509Scheme.

Signed-off-by: Yelyzaveta Chebanova <[email protected]>

* merge with master branch

Signed-off-by: Yelyzaveta Chebanova <[email protected]>

* feat: Enhance x509 authentication scheme to support client certificates (part 2)

-remove unnecessary usage of Serializable

Signed-off-by: Yelyzaveta Chebanova <[email protected]>

* feat: Enhance x509 authentication scheme to support client certificates (part 2)

- cleanup

Signed-off-by: Yelyzaveta Chebanova <[email protected]>
  • Loading branch information
yelyzavetachebanova authored Apr 6, 2022
1 parent d5d6f93 commit d888a11
Show file tree
Hide file tree
Showing 13 changed files with 532 additions and 190 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.zowe.apiml.gateway.security.login.Providers;
import org.zowe.apiml.gateway.security.login.x509.X509AbstractMapper;
import org.zowe.apiml.gateway.security.login.x509.X509CommonNameUserMapper;
import org.zowe.apiml.gateway.security.service.AuthenticationService;
import org.zowe.apiml.gateway.security.service.TokenCreationService;
import org.zowe.apiml.gateway.security.service.schema.source.X509AuthSourceService;
import org.zowe.apiml.gateway.security.service.schema.source.X509CNAuthSourceService;
import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService;
import org.zowe.apiml.passticket.PassTicketService;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
Expand Down Expand Up @@ -71,4 +73,15 @@ public Providers loginProviders(
public X509AuthSourceService getX509MFAuthSourceService(X509AbstractMapper mapper, TokenCreationService tokenCreationService, AuthenticationService authenticationService) {
return new X509AuthSourceService(mapper, tokenCreationService, authenticationService);
}

/**
* Implementation of AuthSourceService interface which uses client certificate as an authentication source.
* This bean does not perform the mapping between common name from the client certificate and the mainframe user ID.
* It treats client name from certificate as user ID and uses X509CommonNameUserMapper for validation.
*/
@Bean
@Qualifier("x509CNAuthSourceService")
public X509AuthSourceService getX509CNAuthSourceService(TokenCreationService tokenCreationService, AuthenticationService authenticationService) {
return new X509CNAuthSourceService(new X509CommonNameUserMapper(), tokenCreationService, authenticationService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import org.springframework.stereotype.Service;
import org.zowe.apiml.auth.Authentication;
import org.zowe.apiml.auth.AuthenticationScheme;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSource;

import java.util.EnumMap;
import java.util.List;
Expand All @@ -30,11 +30,7 @@ public class AuthenticationSchemeFactory {
private final IAuthenticationScheme defaultScheme;
private final Map<AuthenticationScheme, IAuthenticationScheme> map;

private final AuthSourceService authSourceService;

public AuthenticationSchemeFactory(@Autowired AuthSourceService authSourceService, @Autowired List<IAuthenticationScheme> schemes) {
this.authSourceService = authSourceService;

public AuthenticationSchemeFactory(@Autowired List<IAuthenticationScheme> schemes) {
map = new EnumMap<>(AuthenticationScheme.class);

IAuthenticationScheme foundDefaultScheme = null;
Expand Down Expand Up @@ -82,8 +78,8 @@ public AuthenticationCommand getAuthenticationCommand(Authentication authenticat
} else {
scheme = getSchema(authentication.getScheme());
}

return scheme.createCommand(authentication, authSourceService.getAuthSourceFromRequest().orElse(null));
final AuthSource authSource = scheme.getAuthSource().orElse(null);
return scheme.createCommand(authentication, authSource);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@

import com.netflix.appinfo.InstanceInfo;
import com.netflix.zuul.context.RequestContext;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.zowe.apiml.auth.Authentication;
import org.zowe.apiml.auth.AuthenticationScheme;
import org.zowe.apiml.gateway.security.login.x509.X509CommonNameUserMapper;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSource;

import javax.servlet.http.HttpServletRequest;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService;

/**
* This schema adds requested information about client certificate. This information is added
Expand All @@ -31,9 +34,13 @@
@Component
@Slf4j
public class X509Scheme implements IAuthenticationScheme {
private final AuthSourceService authSourceService;

public static final String ALL_HEADERS = "X-Certificate-Public,X-Certificate-DistinguishedName,X-Certificate-CommonName";

public X509Scheme(@Autowired @Qualifier("x509CNAuthSourceService") AuthSourceService authSourceService) {
this.authSourceService = authSourceService;
}

@Override
public AuthenticationScheme getScheme() {
Expand All @@ -52,7 +59,12 @@ public AuthenticationCommand createCommand(Authentication authentication, AuthSo

}

public static class X509Command extends AuthenticationCommand {
@Override
public Optional<AuthSource> getAuthSource() {
return authSourceService.getAuthSourceFromRequest();
}

public class X509Command extends AuthenticationCommand {
private final String[] headers;

public static final String PUBLIC_KEY = "X-Certificate-Public";
Expand All @@ -66,8 +78,8 @@ public X509Command(String[] headers) {
@Override
public void apply(InstanceInfo instanceInfo) {
final RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
X509Certificate clientCertificate = getCertificateFromRequest(request);
final AuthSource authSource = authSourceService.getAuthSourceFromRequest().orElse(null);
X509Certificate clientCertificate = authSource == null ? null : (X509Certificate) authSource.getRawSource();

if (clientCertificate != null) {
try {
Expand All @@ -79,22 +91,6 @@ public void apply(InstanceInfo instanceInfo) {
}
}

private X509Certificate getCertificateFromRequest(HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute("client.auth.X509Certificate");
X509Certificate clientCert = getOne(certs);
if (clientCert == null) {
certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
return getOne(certs);
}
return clientCert;
}

private X509Certificate getOne(X509Certificate[] certs) {
if (certs != null && certs.length > 0) {
return certs[0];
} else return null;
}

private void setHeader(RequestContext context, X509Certificate clientCert) throws CertificateEncodingException {
for (String header : headers) {
switch (header.trim()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ public class X509AuthSourceService implements AuthSourceService {
public Optional<AuthSource> getAuthSourceFromRequest() {
final RequestContext context = RequestContext.getCurrentContext();

X509Certificate clientCert = getCertificateFromRequest(context.getRequest());
X509Certificate clientCert = getCertificateFromRequest(context.getRequest(), "client.auth.X509Certificate");
// check that X509 certificate is valid client certificate (has correct extended key usage)
if (!isValid(clientCert)) {
clientCert = null;
}
return clientCert == null ? Optional.empty() : Optional.of(new X509AuthSource(clientCert));
}

Expand All @@ -64,17 +68,26 @@ public Optional<AuthSource> getAuthSourceFromRequest() {
public boolean isValid(AuthSource authSource) {
if (authSource instanceof X509AuthSource) {
X509Certificate clientCert = (X509Certificate) authSource.getRawSource();
if (clientCert == null) {
return false;
}
if (!mapper.isClientAuthCertificate(clientCert)) {
throw new InvalidCertificateException("X509 certificate does contain the client certificate extended usage definition.");
}
return true;
return isValid(clientCert);
}
return false;
}

/**
* Validates X509 certificate, checks that certificate is not null and has the extended key usage set correctly.
*
* @param clientCert {@link X509Certificate} X509 client certificate.
* @return true if client certificate is valid, false otherwise.
*/
protected boolean isValid(X509Certificate clientCert) {
try {
return clientCert != null && mapper.isClientAuthCertificate(clientCert);
} catch (Exception e) {
log.error("Error occurred while validation X509 certificate: " + e.getLocalizedMessage());
return false;
}
}

/**
* Parse client certificate from authentication source.
*
Expand All @@ -96,12 +109,12 @@ public String getLtpaToken(AuthSource authSource) {
}

// Gets client certificate from request
private X509Certificate getCertificateFromRequest(HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute("client.auth.X509Certificate");
protected X509Certificate getCertificateFromRequest(HttpServletRequest request, String attributeName) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(attributeName);
return getOne(certs);
}

private X509Certificate getOne(X509Certificate[] certs) {
protected X509Certificate getOne(X509Certificate[] certs) {
if (certs != null && certs.length > 0) {
return certs[0];
} else return null;
Expand All @@ -123,7 +136,7 @@ private Parsed parseClientCert(X509Certificate clientCert, X509AbstractMapper ma
Origin.X509, encodedCert, distinguishedName);
} catch (CertificateEncodingException e) {
log.error("Exception parsing certificate", e);
return null;
throw new InvalidCertificateException("Exception parsing certificate. " + e.getLocalizedMessage());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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.source;

import com.netflix.zuul.context.RequestContext;
import java.security.cert.X509Certificate;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.zowe.apiml.gateway.security.login.x509.X509CommonNameUserMapper;
import org.zowe.apiml.gateway.security.service.AuthenticationService;
import org.zowe.apiml.gateway.security.service.TokenCreationService;

/**
* Custom implementation of AuthSourceService interface which uses client certificate as an authentication source.
* This implementation uses instance of {@link X509CommonNameUserMapper} for validation and parsing of the client certificate.
*/
@Slf4j
public class X509CNAuthSourceService extends X509AuthSourceService {
public X509CNAuthSourceService(X509CommonNameUserMapper mapper, TokenCreationService tokenService, AuthenticationService authenticationService) {
super(mapper, tokenService, authenticationService);
}

/**
* Gets client certificate from request.
* <p>
* First try to get certificate from custom attribute "client.auth.X509Certificate".
* If certificate not found - try to get it from standard attribute "javax.servlet.request.X509Certificate".
* In case of multiple certificates only the first one will be used.
* <p>
* @return Optional<AuthSource> with client certificate of Optional.empty()
*/
@Override
public Optional<AuthSource> getAuthSourceFromRequest() {
final RequestContext context = RequestContext.getCurrentContext();

// get certificate from custom attribute "client.auth.X509Certificate"
X509Certificate clientCert = super.getCertificateFromRequest(context.getRequest(), "client.auth.X509Certificate");
if (clientCert == null) {
// get certificate from standard attribute "javax.servlet.request.X509Certificate"
clientCert = super.getCertificateFromRequest(context.getRequest(), "javax.servlet.request.X509Certificate");
}
if (!isValid(clientCert)) {
clientCert = null;
}
return clientCert == null ? Optional.empty() : Optional.of(new X509AuthSource(clientCert));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

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;
Expand Down Expand Up @@ -152,7 +153,26 @@ void givenInvalidJwtToken() {
}
}

@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 {
Expand Down
Loading

0 comments on commit d888a11

Please sign in to comment.