aboutsummaryrefslogtreecommitdiffstats
path: root/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilter.java
diff options
context:
space:
mode:
Diffstat (limited to 'jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilter.java')
-rw-r--r--jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilter.java95
1 files changed, 95 insertions, 0 deletions
diff --git a/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilter.java b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilter.java
new file mode 100644
index 00000000000..ad6c82138e1
--- /dev/null
+++ b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilter.java
@@ -0,0 +1,95 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter.security.athenz;
+
+import com.google.inject.Inject;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.http.filter.DiscFilterRequest;
+import com.yahoo.jdisc.http.filter.security.cors.CorsFilterConfig;
+import com.yahoo.jdisc.http.filter.security.cors.CorsRequestFilterBase;
+import com.yahoo.vespa.athenz.api.AthenzPrincipal;
+import com.yahoo.vespa.athenz.api.NToken;
+import com.yahoo.vespa.athenz.utils.AthenzIdentities;
+import com.yahoo.vespa.athenz.utils.ntoken.NTokenValidator;
+
+import java.nio.file.Paths;
+import java.security.cert.X509Certificate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+
+/**
+ * Authenticates Athenz principal, either through:
+ * 1. TLS client authentication (based on Athenz x509 identity certficiate).
+ * 2. The principal token (NToken) header.
+ * The TLS authentication is based on the following assumptions:
+ * - The underlying connector is configured with 'clientAuth' set to either WANT_AUTH or NEED_AUTH.
+ * - The trust store is configured with the Athenz CA certificates only.
+ *
+ * @author bjorncs
+ */
+public class AthenzPrincipalFilter extends CorsRequestFilterBase {
+
+ private final NTokenValidator validator;
+ private final String principalTokenHeader;
+
+ @Inject
+ public AthenzPrincipalFilter(AthenzPrincipalFilterConfig athenzPrincipalFilterConfig, CorsFilterConfig corsConfig) {
+ this(new NTokenValidator(Paths.get(athenzPrincipalFilterConfig.athenzConfFile())),
+ athenzPrincipalFilterConfig.principalHeaderName(),
+ new HashSet<>(corsConfig.allowedUrls()));
+ }
+
+ AthenzPrincipalFilter(NTokenValidator validator,
+ String principalTokenHeader,
+ Set<String> corsAllowedUrls) {
+ super(corsAllowedUrls);
+ this.validator = validator;
+ this.principalTokenHeader = principalTokenHeader;
+ }
+
+ @Override
+ public Optional<ErrorResponse> filterRequest(DiscFilterRequest request) {
+ try {
+ Optional<AthenzPrincipal> certificatePrincipal = getClientCertificate(request)
+ .map(AthenzIdentities::from)
+ .map(AthenzPrincipal::new);
+ Optional<AthenzPrincipal> nTokenPrincipal = getPrincipalToken(request, principalTokenHeader)
+ .map(validator::validate);
+
+ if (!certificatePrincipal.isPresent() && !nTokenPrincipal.isPresent()) {
+ String errorMessage = "Unable to authenticate Athenz identity. " +
+ "Either client certificate or principal token is required.";
+ return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, errorMessage));
+ }
+ if (certificatePrincipal.isPresent() && nTokenPrincipal.isPresent()
+ && !certificatePrincipal.get().getIdentity().equals(nTokenPrincipal.get().getIdentity())) {
+ String errorMessage = String.format(
+ "Identity in principal token does not match x509 CN: token-identity=%s, cert-identity=%s",
+ nTokenPrincipal.get().getIdentity().getFullName(),
+ certificatePrincipal.get().getIdentity().getFullName());
+ return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, errorMessage));
+ }
+ AthenzPrincipal principal = nTokenPrincipal.orElseGet(certificatePrincipal::get);
+ request.setUserPrincipal(principal);
+ request.setRemoteUser(principal.getName());
+ return Optional.empty();
+ } catch (Exception e) {
+ return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, e.getMessage()));
+ }
+ }
+
+ private static Optional<X509Certificate> getClientCertificate(DiscFilterRequest request) {
+ List<X509Certificate> chain = request.getClientCertificateChain();
+ if (chain.isEmpty()) return Optional.empty();
+ return Optional.of(chain.get(0));
+ }
+
+ private static Optional<NToken> getPrincipalToken(DiscFilterRequest request, String principalTokenHeaderName) {
+ return Optional.ofNullable(request.getHeader(principalTokenHeaderName))
+ .filter(token -> !token.isEmpty())
+ .map(NToken::new);
+ }
+
+}