Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enhance x509 authentication scheme to support client certificates (part 3) #2285

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
42ab95d
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Mar 28, 2022
69ec2e8
Merge branch 'master' into apiml/GH2198/enhance-x509
yelyzavetachebanova Mar 29, 2022
6ac2b51
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Apr 3, 2022
2a40e22
Merge branch 'master' into apiml/GH2198/enhance-x509
yelyzavetachebanova Apr 3, 2022
ea214de
merge with master branch
yelyzavetachebanova Apr 4, 2022
53cbf7f
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Apr 4, 2022
4d31660
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Apr 4, 2022
6f86a7d
Merge branch 'master' into apiml/GH2198/X509scheme-with-auth-failure-…
yelyzavetachebanova Apr 4, 2022
ec46886
Merge branch 'master' into apiml/GH2198/enhance-x509
yelyzavetachebanova Apr 4, 2022
19645d3
Merge branch 'apiml/GH2198/enhance-x509' into apiml/GH2198/X509scheme…
yelyzavetachebanova Apr 4, 2022
4dfed84
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Apr 5, 2022
8299225
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Apr 5, 2022
a692a2a
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Apr 5, 2022
aff7a6d
Merge branch 'master' into apiml/GH2198/X509scheme-with-auth-failure-…
yelyzavetachebanova Apr 5, 2022
aec2470
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Apr 7, 2022
ef719c8
Merge branch 'master' into apiml/GH2198/X509scheme-with-auth-failure-…
yelyzavetachebanova Apr 7, 2022
20ebd9c
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Apr 7, 2022
2bc7671
Merge branch 'master' into apiml/GH2198/X509scheme-with-auth-failure-…
yelyzavetachebanova Apr 8, 2022
ef7274f
feat: Enhance x509 authentication scheme to support client certificat…
yelyzavetachebanova Apr 8, 2022
8076b0c
Resolve PR review suggestions
yelyzavetachebanova Apr 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -23,6 +23,7 @@
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.message.core.MessageService;
import org.zowe.apiml.passticket.PassTicketService;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;

Expand Down Expand Up @@ -70,8 +71,8 @@ public Providers loginProviders(
*/
@Bean
@Qualifier("x509MFAuthSourceService")
public X509AuthSourceService getX509MFAuthSourceService(X509AbstractMapper mapper, TokenCreationService tokenCreationService, AuthenticationService authenticationService) {
return new X509AuthSourceService(mapper, tokenCreationService, authenticationService);
public X509AuthSourceService getX509MFAuthSourceService(X509AbstractMapper mapper, TokenCreationService tokenCreationService, AuthenticationService authenticationService, MessageService messageService) {
return new X509AuthSourceService(mapper, tokenCreationService, authenticationService, messageService);
}

/**
Expand All @@ -81,7 +82,7 @@ public X509AuthSourceService getX509MFAuthSourceService(X509AbstractMapper mappe
*/
@Bean
@Qualifier("x509CNAuthSourceService")
public X509AuthSourceService getX509CNAuthSourceService(TokenCreationService tokenCreationService, AuthenticationService authenticationService) {
return new X509CNAuthSourceService(new X509CommonNameUserMapper(), tokenCreationService, authenticationService);
public X509AuthSourceService getX509CNAuthSourceService(TokenCreationService tokenCreationService, AuthenticationService authenticationService, MessageService messageService) {
return new X509CNAuthSourceService(new X509CommonNameUserMapper(), tokenCreationService, authenticationService, messageService);
}
}
Expand Up @@ -9,23 +9,24 @@
*/
package org.zowe.apiml.gateway.security.service.schema;

import static org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService.X509_DEFAULT_EXPIRATION;

import com.netflix.appinfo.InstanceInfo;
import com.netflix.zuul.context.RequestContext;
import java.util.Optional;
import lombok.Getter;
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 java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService;
import org.zowe.apiml.gateway.security.service.schema.source.X509AuthSource;
import org.zowe.apiml.message.core.MessageService;

/**
* This schema adds requested information about client certificate. This information is added
Expand All @@ -35,11 +36,14 @@
@Slf4j
public class X509Scheme implements IAuthenticationScheme {
private final AuthSourceService authSourceService;
private final MessageService messageService;

public static final String AUTH_FAIL_HEADER = "X-Zowe-Auth-Failure";
public static final String ALL_HEADERS = "X-Certificate-Public,X-Certificate-DistinguishedName,X-Certificate-CommonName";

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

@Override
Expand All @@ -49,14 +53,34 @@ public AuthenticationScheme getScheme() {

@Override
public AuthenticationCommand createCommand(Authentication authentication, AuthSource authSource) {
final RequestContext context = RequestContext.getCurrentContext();
// Check for error in context to use it in header "X-Zowe-Auth-Failure"
if (context.containsKey(AUTH_FAIL_HEADER)) {
String errorHeaderValue = context.get(AUTH_FAIL_HEADER).toString();
// this command should expire immediately after creation because it is build based on missing/incorrect authentication
return new X509Command(System.currentTimeMillis(), errorHeaderValue);
}

String error;
X509AuthSource.Parsed parsedAuthSource = (X509AuthSource.Parsed) authSourceService.parse(authSource);

if (parsedAuthSource == null) {
error = this.messageService.createMessage("org.zowe.apiml.gateway.security.scheme.x509ParsingError", "Cannot parse provided authentication source").mapToLogMessage();
return new X509Command(null, error);
}

String[] headers;
if (StringUtils.isEmpty(authentication.getHeaders())) {
headers = ALL_HEADERS.split(",");
} else {
headers = authentication.getHeaders().split(",");
}
return new X509Command(headers);

final long defaultExpirationTime = System.currentTimeMillis() + X509_DEFAULT_EXPIRATION;
final long expirationTime = parsedAuthSource.getExpiration() != null ? parsedAuthSource.getExpiration().getTime() : defaultExpirationTime;
final long expireAt = Math.min(defaultExpirationTime, expirationTime);

return new X509Command(expireAt, headers, parsedAuthSource, null);
}

@Override
Expand All @@ -65,47 +89,59 @@ public Optional<AuthSource> getAuthSource() {
}

public class X509Command extends AuthenticationCommand {
private final Long expireAt;
private final String[] headers;
private final X509AuthSource.Parsed parsedAuthSource;
@Getter
private final String errorHeader;

public static final String PUBLIC_KEY = "X-Certificate-Public";
public static final String DISTINGUISHED_NAME = "X-Certificate-DistinguishedName";
public static final String COMMON_NAME = "X-Certificate-CommonName";

public X509Command(String[] headers) {
public X509Command(Long expireAt, String[] headers, X509AuthSource.Parsed parsedAuthSource, String errorHeader) {
this.expireAt = expireAt;
this.headers = headers;
this.parsedAuthSource = parsedAuthSource;
this.errorHeader = errorHeader;
}

public X509Command(Long expireAt, String errorHeader) {
this.expireAt = expireAt;
this.headers = new String[0];
this.parsedAuthSource = null;
this.errorHeader = errorHeader;
}

@Override
public void apply(InstanceInfo instanceInfo) {
final RequestContext context = RequestContext.getCurrentContext();
final AuthSource authSource = authSourceService.getAuthSourceFromRequest().orElse(null);
X509Certificate clientCertificate = authSource == null ? null : (X509Certificate) authSource.getRawSource();

if (clientCertificate != null) {
try {
setHeader(context, clientCertificate);
context.set(RoutingConstants.FORCE_CLIENT_WITH_APIML_CERT_KEY);
} catch (CertificateEncodingException e) {
log.error("Exception parsing certificate", e);
}
if (parsedAuthSource != null) {
setHeader(context, parsedAuthSource);
context.set(RoutingConstants.FORCE_CLIENT_WITH_APIML_CERT_KEY);
} else {
JwtCommand.setErrorHeader(context, errorHeader);
}
}

private void setHeader(RequestContext context, X509Certificate clientCert) throws CertificateEncodingException {
@Override
public boolean isExpired() {
if (expireAt == null) return false;

return System.currentTimeMillis() > expireAt;
}

private void setHeader(RequestContext context, X509AuthSource.Parsed parsedAuthSource) {
for (String header : headers) {
switch (header.trim()) {
case COMMON_NAME:
X509CommonNameUserMapper mapper = new X509CommonNameUserMapper();
String commonName = mapper.mapCertificateToMainframeUserId(clientCert);
context.addZuulRequestHeader(COMMON_NAME, commonName);
context.addZuulRequestHeader(COMMON_NAME, parsedAuthSource.getCommonName());
break;
case PUBLIC_KEY:
String encodedCert = Base64.getEncoder().encodeToString(clientCert.getEncoded());
context.addZuulRequestHeader(PUBLIC_KEY, encodedCert);
context.addZuulRequestHeader(PUBLIC_KEY, parsedAuthSource.getPublicKey());
break;
case DISTINGUISHED_NAME:
String distinguishedName = clientCert.getSubjectDN().toString();
context.addZuulRequestHeader(DISTINGUISHED_NAME, distinguishedName);
context.addZuulRequestHeader(DISTINGUISHED_NAME, parsedAuthSource.getDistinguishedName());
break;
default:
log.warn("Unsupported header specified in service metadata, " +
Expand Down
Expand Up @@ -15,6 +15,8 @@
* Interface represents main methods of service which gets the source of authentication and process it.
*/
public interface AuthSourceService {
// Default expiration time for client certificate 15 min
Long X509_DEFAULT_EXPIRATION = 15L * 60 * 1000;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this is the best place to have x509 related fields, and maybe it could be also configurable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be fixed in upcoming PR


/**
* Core method of the interface. Gets specific source of authentication from request and defines precedence
Expand Down
Expand Up @@ -17,6 +17,7 @@
import org.zowe.apiml.gateway.security.service.TokenCreationService;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSource.Origin;
import org.zowe.apiml.gateway.security.service.schema.source.X509AuthSource.Parsed;
import org.zowe.apiml.message.core.MessageService;
import org.zowe.apiml.security.common.error.AuthenticationTokenException;
import org.zowe.apiml.security.common.error.InvalidCertificateException;

Expand All @@ -34,9 +35,11 @@
@Slf4j
@RequiredArgsConstructor
public class X509AuthSourceService implements AuthSourceService {
public static final String AUTH_FAIL_HEADER = "X-Zowe-Auth-Failure";
private final X509AbstractMapper mapper;
private final TokenCreationService tokenService;
private final AuthenticationService authenticationService;
protected final MessageService messageService;

/**
* Gets client certificate from request.
Expand All @@ -49,15 +52,28 @@ public class X509AuthSourceService implements AuthSourceService {
@Override
public Optional<AuthSource> getAuthSourceFromRequest() {
final RequestContext context = RequestContext.getCurrentContext();

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;
}
clientCert = checkCertificate(clientCert);
return clientCert == null ? Optional.empty() : Optional.of(new X509AuthSource(clientCert));
}

/**
* Check that certificate from request is not null and valid; otherwise set error header and do not use certificate for authentication
* @param clientCert {@link X509Certificate} X509 client certificate.
* @return client certificate if it is valid, otherwise null
*/
protected X509Certificate checkCertificate(X509Certificate clientCert) {
if (clientCert == null) {
String error = this.messageService.createMessage("org.zowe.apiml.gateway.security.schema.missingAuthentication").mapToLogMessage();
storeErrorHeader(error);
} else {
// check that X509 certificate is valid client certificate (has correct extended key usage)
// if certificate is not valid - don't use it as a source of authentication
clientCert = isValid(clientCert) ? clientCert : null;
}
return clientCert;
}

/**
* Validates authentication source, check authentication source type and whether client certificate from the
* authentication source has the extended key usage set correctly.
Expand All @@ -80,10 +96,21 @@ public boolean isValid(AuthSource authSource) {
* @return true if client certificate is valid, false otherwise.
*/
protected boolean isValid(X509Certificate clientCert) {
if (clientCert == null) {
return false;
}

try {
return clientCert != null && mapper.isClientAuthCertificate(clientCert);
if (mapper.isClientAuthCertificate(clientCert)) {
return true;
} else {
String error = this.messageService.createMessage("org.zowe.apiml.gateway.security.scheme.x509ValidationError", "X509 certificate is missing the client certificate extended usage definition").mapToLogMessage();
storeErrorHeader(error);
return false;
}
} catch (Exception e) {
log.error("Error occurred while validation X509 certificate: " + e.getLocalizedMessage());
String error = this.messageService.createMessage("org.zowe.apiml.gateway.security.scheme.x509ValidationError", e.getLocalizedMessage()).mapToLogMessage();
storeErrorHeader(error);
return false;
}
}
Expand All @@ -105,7 +132,7 @@ public AuthSource.Parsed parse(AuthSource authSource) {
@Override
public String getLtpaToken(AuthSource authSource) {
String jwt = getJWT(authSource);
return authenticationService.getLtpaToken(jwt);
return jwt != null ? authenticationService.getLtpaToken(jwt) : null;
}

// Gets client certificate from request
Expand Down Expand Up @@ -155,4 +182,10 @@ public String getJWT(AuthSource authSource) {
}
return null;
}

// Method stores information about error into context to use it in header "X-Zowe-Auth-Failure"
protected void storeErrorHeader(String value) {
final RequestContext context = RequestContext.getCurrentContext();
context.put(AUTH_FAIL_HEADER, value);
}
}
Expand Up @@ -16,15 +16,16 @@
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.message.core.MessageService;

/**
* 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);
public X509CNAuthSourceService(X509CommonNameUserMapper mapper, TokenCreationService tokenService, AuthenticationService authenticationService, MessageService messageService) {
super(mapper, tokenService, authenticationService, messageService);
}

/**
Expand All @@ -46,9 +47,7 @@ public Optional<AuthSource> getAuthSourceFromRequest() {
// get certificate from standard attribute "javax.servlet.request.X509Certificate"
clientCert = super.getCertificateFromRequest(context.getRequest(), "javax.servlet.request.X509Certificate");
}
if (!isValid(clientCert)) {
clientCert = null;
}
clientCert = checkCertificate(clientCert);
return clientCert == null ? Optional.empty() : Optional.of(new X509AuthSource(clientCert));
}

Expand Down
14 changes: 14 additions & 0 deletions gateway-service/src/main/resources/gateway-log-messages.yml
Expand Up @@ -301,3 +301,17 @@ messages:
text: "Gateway service failed to obtain token."
reason: "Authentication request to get token failed."
action: "Contact your administrator."

- key: org.zowe.apiml.gateway.security.scheme.x509ParsingError
number: ZWEAG163
type: ERROR
text: "Error occurred while parsing X509 certificate."
reason: "%s"
action: "Configure your client to provide valid x509 certificate."

- key: org.zowe.apiml.gateway.security.scheme.x509ValidationError
number: ZWEAG164
type: ERROR
text: "Error occurred while validating X509 certificate. %s"
reason: "X509 certificate cannot be validated or the certificate cannot be used for client authentication."
action: "Configure your client to provide valid x509 certificate."