This is an automated email from the ASF dual-hosted git repository.
chaitalicod pushed a commit to branch atlas-2.6
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/atlas-2.6 by this push:
new daac4a1f2 ATLAS-5284: Support JWT authentication for Atlas (#655)
daac4a1f2 is described below
commit daac4a1f26792dd2765fd94cff8a08368977794d
Author: chaitalicod <[email protected]>
AuthorDate: Thu Jun 11 00:03:06 2026 +0530
ATLAS-5284: Support JWT authentication for Atlas (#655)
Co-authored-by: chaitali.borole <[email protected]>
(cherry picked from commit 9bcd37e27f13eaa177eff6d508bded8c1505b94d)
---
authn/pom.xml | 70 +++++
.../org/apache/atlas/authn/handler/AtlasAuth.java | 65 +++++
.../atlas/authn/handler/AtlasAuthHandler.java | 29 +++
.../handler/jwt/AtlasDefaultJwtAuthHandler.java | 86 ++++++
.../authn/handler/jwt/AtlasJwtAuthHandler.java | 290 +++++++++++++++++++++
.../java/org/apache/atlas/AtlasBaseClient.java | 48 +++-
.../token/retriever/JwTokenRetrieverDefault.java | 153 +++++++++++
.../atlas/token/retriever/TokenRetriever.java | 24 ++
pom.xml | 8 +
webapp/pom.xml | 7 +-
.../web/filters/AtlasAuthenticationFilter.java | 2 +-
.../atlas/web/filters/AtlasJwtAuthFilter.java | 119 +++++++++
.../atlas/web/filters/AtlasJwtAuthWrapper.java | 129 +++++++++
.../filters/AtlasKnoxSSOAuthenticationFilter.java | 1 +
.../atlas/web/security/AtlasSecurityConfig.java | 10 +-
.../atlas/web/filters/AtlasJwtAuthFilterTest.java | 97 +++++++
.../atlas/web/filters/AtlasJwtAuthWrapperTest.java | 108 ++++++++
.../web/security/AtlasSecurityConfigTest.java | 26 +-
18 files changed, 1258 insertions(+), 14 deletions(-)
diff --git a/authn/pom.xml b/authn/pom.xml
new file mode 100644
index 000000000..622ab32da
--- /dev/null
+++ b/authn/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.atlas</groupId>
+ <artifactId>apache-atlas</artifactId>
+ <version>3.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>atlas-authn</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Apache Atlas Authentication</name>
+ <description>JWT and authentication handler support for Apache
Atlas</description>
+
+ <properties>
+ <checkstyle.failOnViolation>true</checkstyle.failOnViolation>
+ <checkstyle.skip>false</checkstyle.skip>
+ </properties>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <version>${javax.servlet.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-configuration2</artifactId>
+ <version>${commons-conf2.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>${commons-lang3.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git a/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuth.java
b/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuth.java
new file mode 100644
index 000000000..db2dbb90e
--- /dev/null
+++ b/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuth.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.atlas.authn.handler;
+
+public class AtlasAuth {
+ public enum AuthType {
+ JWT_JWKS("JWT-JWKS");
+
+ private final String authType;
+
+ AuthType(String authType) {
+ this.authType = authType;
+ }
+ }
+
+ private String userName;
+ private AuthType type;
+ private boolean isAuthenticated;
+
+ public AtlasAuth(final String username, AuthType type) {
+ this.userName = username;
+ this.isAuthenticated = true;
+ this.type = type;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public AuthType getType() {
+ return type;
+ }
+
+ public void setType(AuthType type) {
+ this.type = type;
+ }
+
+ public boolean isAuthenticated() {
+ return isAuthenticated;
+ }
+
+ public void setAuthenticated(boolean authenticated) {
+ isAuthenticated = authenticated;
+ }
+}
diff --git
a/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuthHandler.java
b/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuthHandler.java
new file mode 100644
index 000000000..35fea06de
--- /dev/null
+++ b/authn/src/main/java/org/apache/atlas/authn/handler/AtlasAuthHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.atlas.authn.handler;
+
+import org.apache.commons.configuration2.Configuration;
+
+import javax.servlet.http.HttpServletRequest;
+
+public interface AtlasAuthHandler {
+ void initialize(Configuration config) throws Exception;
+
+ AtlasAuth authenticate(HttpServletRequest request);
+}
diff --git
a/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasDefaultJwtAuthHandler.java
b/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasDefaultJwtAuthHandler.java
new file mode 100644
index 000000000..981b44f5e
--- /dev/null
+++
b/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasDefaultJwtAuthHandler.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.atlas.authn.handler.jwt;
+
+import com.nimbusds.jose.proc.JWSKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
+import com.nimbusds.jwt.proc.DefaultJWTProcessor;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
+import org.apache.atlas.authn.handler.AtlasAuth;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+
+public class AtlasDefaultJwtAuthHandler extends AtlasJwtAuthHandler {
+ protected static final String AUTHORIZATION_HEADER = "Authorization";
+
+ @Override
+ public ConfigurableJWTProcessor<SecurityContext>
getJwtProcessor(JWSKeySelector<SecurityContext> keySelector) {
+ ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new
DefaultJWTProcessor<>();
+ JWTClaimsSetVerifier<SecurityContext> claimsVerifier = new
DefaultJWTClaimsVerifier<>();
+
+ jwtProcessor.setJWSKeySelector(keySelector);
+ jwtProcessor.setJWTClaimsSetVerifier(claimsVerifier);
+
+ return jwtProcessor;
+ }
+
+ @Override
+ public AtlasAuth authenticate(HttpServletRequest request) {
+ AtlasAuth atlasAuth = null;
+ String jwtAuthHeaderStr = getJwtAuthHeader(request);
+ String jwtCookieStr = StringUtils.isBlank(jwtAuthHeaderStr) ?
getJwtCookie(request) : null;
+
+ String username = authenticate(jwtAuthHeaderStr, jwtCookieStr);
+ if (username != null) {
+ atlasAuth = new AtlasAuth(username, AtlasAuth.AuthType.JWT_JWKS);
+ }
+ return atlasAuth;
+ }
+
+ public static boolean canAuthenticateRequest(final ServletRequest request)
{
+ HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+ String jwtAuthHeaderStr =
getJwtAuthHeader(httpServletRequest);
+ String jwtCookieStr =
StringUtils.isBlank(jwtAuthHeaderStr) ? getJwtCookie(httpServletRequest) : null;
+ return shouldProceedAuth(jwtAuthHeaderStr, jwtCookieStr);
+ }
+
+ public static String getJwtAuthHeader(final HttpServletRequest
httpServletRequest) {
+ return httpServletRequest.getHeader(AUTHORIZATION_HEADER);
+ }
+
+ public static String getJwtCookie(final HttpServletRequest
httpServletRequest) {
+ String jwtCookieStr = null;
+ Cookie[] cookies = httpServletRequest.getCookies();
+
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookieName.equals(cookie.getName())) {
+ jwtCookieStr = cookie.getName() + "=" + cookie.getValue();
+ break;
+ }
+ }
+ }
+ return jwtCookieStr;
+ }
+}
diff --git
a/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasJwtAuthHandler.java
b/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasJwtAuthHandler.java
new file mode 100644
index 000000000..0411d6e95
--- /dev/null
+++
b/authn/src/main/java/org/apache/atlas/authn/handler/jwt/AtlasJwtAuthHandler.java
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.atlas.authn.handler.jwt;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.jwk.source.RemoteJWKSet;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.proc.JWSKeySelector;
+import com.nimbusds.jose.proc.JWSVerificationKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import org.apache.atlas.authn.handler.AtlasAuthHandler;
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Date;
+import java.util.List;
+
+public abstract class AtlasJwtAuthHandler implements AtlasAuthHandler {
+ private static final Logger LOG =
LoggerFactory.getLogger(AtlasJwtAuthHandler.class);
+
+ private JWSVerifier verifier;
+ private String jwksProviderUrl;
+ public static final String KEY_PROVIDER_URL = "atlas.jwt.provider.url";
+ public static final String KEY_JWT_PUBLIC_KEY = "atlas.jwt.public-key";
+ public static final String KEY_JWT_COOKIE_NAME = "atlas.jwt.cookie-name";
+ public static final String KEY_JWT_AUDIENCES = "atlas.jwt.audiences";
+ public static final String JWT_AUTHZ_PREFIX = "Bearer ";
+
+ protected List<String> audiences;
+ protected JWKSource<SecurityContext> keySource;
+
+ protected static String cookieName = "hadoop-jwt";
+
+ @Override
+ public void initialize(final Configuration configuration) throws Exception
{
+ LOG.debug("===>>> AtlasJwtAuthHandler.initialize()");
+
+ jwksProviderUrl = configuration.getString(KEY_PROVIDER_URL);
+ if (!StringUtils.isBlank(jwksProviderUrl)) {
+ keySource = new RemoteJWKSet<>(new URL(jwksProviderUrl));
+ }
+
+ String pemPublicKey = configuration.getString(KEY_JWT_PUBLIC_KEY);
+
+ if (StringUtils.isNotBlank(pemPublicKey)) {
+ verifier = new RSASSAVerifier(parseJwtPublicKey(pemPublicKey));
+ } else if (StringUtils.isBlank(jwksProviderUrl)) {
+ throw new Exception("AtlasJwtAuthHandler: Mandatory configs
('atlas.jwt.provider.url' & 'atlas.jwt.public-key') are missing, must provide
at least one.");
+ }
+
+ String customCookieName = configuration.getString(KEY_JWT_COOKIE_NAME);
+ if (customCookieName != null) {
+ cookieName = customCookieName;
+ }
+
+ String audiencesStr = configuration.getString(KEY_JWT_AUDIENCES);
+ if (StringUtils.isNotBlank(audiencesStr)) {
+ audiences = Arrays.asList(audiencesStr.split(","));
+ }
+
+ LOG.debug("<<<=== AtlasJwtAuthHandler.initialize()");
+ }
+
+ protected String authenticate(final String jwtAuthHeader, final String
jwtCookie) {
+ LOG.debug("===>>> AtlasJwtAuthHandler.authenticate()");
+
+ if (shouldProceedAuth(jwtAuthHeader, jwtCookie)) {
+ String serializedJWT = getJWT(jwtAuthHeader, jwtCookie);
+
+ if (StringUtils.isNotBlank(serializedJWT)) {
+ try {
+ final SignedJWT jwtToken = SignedJWT.parse(serializedJWT);
+ boolean valid = validateToken(jwtToken);
+ if (valid) {
+ final String userName =
jwtToken.getJWTClaimsSet().getSubject();
+ LOG.info("JWT claims validated; issuing principal
user={}", userName);
+ return userName;
+ } else {
+ String sub = null;
+ try {
+ sub = jwtToken.getJWTClaimsSet().getSubject();
+ } catch (ParseException ignored) {
+ // ignore
+ }
+ LOG.warn("JWT validation failed (signature, audience,
or expiry). subject={}", sub);
+ }
+ } catch (ParseException pe) {
+ LOG.warn("Unable to parse the JWT token", pe);
+ }
+ } else {
+ LOG.warn("JWT token not found.");
+ }
+ }
+
+ LOG.debug("<<<=== AtlasJwtAuthHandler.authenticate()");
+
+ return null;
+ }
+
+ protected String getJWT(final String jwtAuthHeader, final String
jwtCookie) {
+ String serializedJWT = null;
+
+ if (StringUtils.isNotBlank(jwtAuthHeader) &&
jwtAuthHeader.startsWith(JWT_AUTHZ_PREFIX)) {
+ serializedJWT = jwtAuthHeader.substring(JWT_AUTHZ_PREFIX.length());
+ }
+
+ if (StringUtils.isBlank(serializedJWT) &&
StringUtils.isNotBlank(jwtCookie)) {
+ String[] cookie = jwtCookie.split("=");
+ if (cookieName.equals(cookie[0])) {
+ serializedJWT = cookie[1];
+ }
+ }
+
+ return serializedJWT;
+ }
+
+ protected boolean validateToken(final SignedJWT jwtToken) {
+ boolean expValid = validateExpiration(jwtToken);
+ boolean sigValid = false;
+ boolean audValid = false;
+
+ if (expValid) {
+ sigValid = validateSignature(jwtToken);
+
+ if (sigValid) {
+ audValid = validateAudiences(jwtToken);
+ }
+ }
+
+ LOG.debug("expValid={}, sigValid={}, audValid={}", expValid, sigValid,
audValid);
+
+ return sigValid && audValid && expValid;
+ }
+
+ protected boolean validateSignature(final SignedJWT jwtToken) {
+ boolean valid = false;
+
+ if (JWSObject.State.SIGNED == jwtToken.getState()) {
+ LOG.debug("JWT token is in a SIGNED state");
+
+ if (jwtToken.getSignature() != null) {
+ try {
+ if (StringUtils.isNotBlank(jwksProviderUrl)) {
+ JWSKeySelector<SecurityContext> keySelector
= new JWSVerificationKeySelector<>(jwtToken.getHeader().getAlgorithm(),
keySource);
+ ConfigurableJWTProcessor<SecurityContext> jwtProcessor
= getJwtProcessor(keySelector);
+
+ jwtProcessor.process(jwtToken, null);
+ valid = true;
+ LOG.debug("JWT token has been successfully verified.");
+ } else if (verifier != null) {
+ if (jwtToken.verify(verifier)) {
+ valid = true;
+ LOG.debug("JWT token has been successfully
verified.");
+ } else {
+ LOG.warn("JWT signature verification failed.");
+ }
+ } else {
+ LOG.warn("Cannot authenticate JWT token as neither
JWKS provider URL nor public key provided.");
+ }
+ } catch (JOSEException | BadJOSEException e) {
+ LOG.error("Error while validating signature.", e);
+ }
+ }
+ }
+
+ if (!valid) {
+ LOG.warn("Signature could not be verified.");
+ }
+
+ return valid;
+ }
+
+ private static RSAPublicKey parseJwtPublicKey(String pem) throws Exception
{
+ String trimmed = StringUtils.trimToEmpty(pem);
+
+ if (trimmed.contains("BEGIN CERTIFICATE")) {
+ CertificateFactory factory =
CertificateFactory.getInstance("X.509");
+ try (ByteArrayInputStream input = new
ByteArrayInputStream(trimmed.getBytes(StandardCharsets.UTF_8))) {
+ X509Certificate cert = (X509Certificate)
factory.generateCertificate(input);
+ PublicKey key = cert.getPublicKey();
+
+ if (key instanceof RSAPublicKey) {
+ return (RSAPublicKey) key;
+ }
+ }
+
+ throw new IllegalArgumentException("Certificate does not contain
an RSA public key");
+ }
+
+ String base64 = trimmed
+ .replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "")
+ .replaceAll("\\s", "");
+
+ byte[] decoded = Base64.getDecoder().decode(base64);
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ PublicKey key = kf.generatePublic(spec);
+
+ if (key instanceof RSAPublicKey) {
+ return (RSAPublicKey) key;
+ }
+
+ throw new IllegalArgumentException("Provided key is not an RSA public
key");
+ }
+
+ public abstract ConfigurableJWTProcessor<SecurityContext>
getJwtProcessor(JWSKeySelector<SecurityContext> keySelector);
+
+ protected boolean validateAudiences(final SignedJWT jwtToken) {
+ boolean valid = false;
+ try {
+ List<String> tokenAudienceList =
jwtToken.getJWTClaimsSet().getAudience();
+ if (audiences == null) {
+ valid = true;
+ } else {
+ for (String aud : tokenAudienceList) {
+ if (audiences.contains(aud)) {
+ LOG.debug("JWT token audience has been successfully
validated.");
+ valid = true;
+ break;
+ }
+ }
+ if (!valid) {
+ LOG.warn("JWT audience validation failed.");
+ }
+ }
+ } catch (ParseException pe) {
+ LOG.warn("Unable to parse the JWT token.", pe);
+ }
+ return valid;
+ }
+
+ protected boolean validateExpiration(final SignedJWT jwtToken) {
+ boolean valid = false;
+ try {
+ Date expires = jwtToken.getJWTClaimsSet().getExpirationTime();
+ if (expires == null || new Date().before(expires)) {
+ valid = true;
+ LOG.debug("JWT token expiration date has been successfully
validated.");
+ } else {
+ LOG.warn("JWT token provided is expired.");
+ }
+ } catch (ParseException pe) {
+ LOG.warn("Failed to validate JWT expiry.", pe);
+ }
+
+ return valid;
+ }
+
+ public static boolean shouldProceedAuth(final String authHeader, final
String jwtCookie) {
+ return (StringUtils.isNotBlank(authHeader) &&
authHeader.startsWith(JWT_AUTHZ_PREFIX))
+ || (StringUtils.isNotBlank(jwtCookie) &&
jwtCookie.startsWith(cookieName));
+ }
+}
diff --git a/client/common/src/main/java/org/apache/atlas/AtlasBaseClient.java
b/client/common/src/main/java/org/apache/atlas/AtlasBaseClient.java
index 2faa7c5e1..b6371d32f 100644
--- a/client/common/src/main/java/org/apache/atlas/AtlasBaseClient.java
+++ b/client/common/src/main/java/org/apache/atlas/AtlasBaseClient.java
@@ -43,6 +43,8 @@ import org.apache.atlas.model.impexp.AtlasImportResult;
import org.apache.atlas.model.impexp.AtlasServer;
import org.apache.atlas.model.metrics.AtlasMetrics;
import org.apache.atlas.security.SecureClientUtils;
+import org.apache.atlas.token.retriever.JwTokenRetrieverDefault;
+import org.apache.atlas.token.retriever.TokenRetriever;
import org.apache.atlas.type.AtlasType;
import org.apache.atlas.utils.AtlasJson;
import org.apache.atlas.utils.AuthenticationUtil;
@@ -69,8 +71,10 @@ import java.net.ConnectException;
import java.net.URI;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import static org.apache.atlas.security.SecurityProperties.TLS_ENABLED;
+import static
org.apache.atlas.token.retriever.JwTokenRetrieverDefault.JWT_SOURCE;
public abstract class AtlasBaseClient {
private static final Logger LOG =
LoggerFactory.getLogger(AtlasBaseClient.class);
@@ -109,6 +113,8 @@ public abstract class AtlasBaseClient {
private static final API EXPORT = new API(BASE_URI +
ADMIN_EXPORT, HttpMethod.POST, Response.Status.OK, MediaType.APPLICATION_JSON,
MediaType.APPLICATION_OCTET_STREAM);
private static final String IMPORT_REQUEST_PARAMTER = "request";
private static final String IMPORT_DATA_PARAMETER = "data";
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+ private static final String JWT_AUTHZ_PREFIX = "Bearer ";
protected WebResource service;
protected Configuration configuration;
@@ -117,6 +123,8 @@ public abstract class AtlasBaseClient {
private AtlasClientContext atlasClientContext;
private boolean retryEnabled;
private Cookie cookie;
+ private boolean useJwtAuth;
+ private TokenRetriever<String> tokenRetriever;
private SecureClientUtils clientUtils;
@@ -361,7 +369,7 @@ public abstract class AtlasBaseClient {
final URLConnectionClientHandler handler;
- if (isKerberosEnabled) {
+ if (isKerberosEnabled && !useJwtAuth) {
handler = clientUtils.getClientConnectionHandler(config,
configuration, doAsUser, ugi);
} else {
if (configuration.getBoolean(TLS_ENABLED, false)) {
@@ -457,6 +465,8 @@ public abstract class AtlasBaseClient {
requestBuilder.cookie(cookie);
}
+ handleJwt(requestBuilder);
+
clientResponse = requestBuilder.method(api.getMethod(),
ClientResponse.class, requestObject);
LOG.debug("HTTP Status : {}", clientResponse.getStatus());
@@ -537,11 +547,13 @@ public abstract class AtlasBaseClient {
}
void initializeState(Configuration configuration, String[] baseUrls,
UserGroupInformation ugi, String doAsUser) {
- this.configuration = configuration;
+ this.configuration = configuration;
+ useJwtAuth = isJwtSourceConfigured(configuration);
+ tokenRetriever = useJwtAuth ? getJwtTokenRetriever(configuration)
: null;
Client client = getClient(configuration, ugi, doAsUser);
- if ((!AuthenticationUtil.isKerberosAuthenticationEnabled()) &&
basicAuthUser != null && basicAuthPassword != null) {
+ if (!useJwtAuth &&
(!AuthenticationUtil.isKerberosAuthenticationEnabled()) && basicAuthUser !=
null && basicAuthPassword != null) {
final HTTPBasicAuthFilter authFilter = new
HTTPBasicAuthFilter(basicAuthUser, basicAuthPassword);
client.addFilter(authFilter);
@@ -553,6 +565,36 @@ public abstract class AtlasBaseClient {
service =
client.resource(UriBuilder.fromUri(activeServiceUrl).build());
}
+ private TokenRetriever<String> getJwtTokenRetriever(Configuration
configuration) {
+ return new JwTokenRetrieverDefault(configuration);
+ }
+
+ private boolean isJwtSourceConfigured(Configuration configuration) {
+ if (configuration == null) {
+ return false;
+ }
+
+ String jwtSource = configuration.getString(JWT_SOURCE, "");
+ return StringUtils.isNotBlank(jwtSource);
+ }
+
+ private void handleJwt(com.sun.jersey.api.client.WebResource.Builder
requestBuilder) {
+ if (!useJwtAuth) {
+ return;
+ }
+ if (tokenRetriever == null) {
+ LOG.warn("AtlasBaseClient.handleJwt(): tokenRetriever is null.
Skipping JWT header injection.");
+ return;
+ }
+
+ Optional<String> jwtOptional = tokenRetriever.retrieve();
+ if (jwtOptional.isPresent()) {
+ requestBuilder.header(AUTHORIZATION_HEADER, JWT_AUTHZ_PREFIX +
jwtOptional.get());
+ } else {
+ LOG.warn("AtlasBaseClient.handleJwt(): JWT token not available
from configured retriever. Authorization header not set.");
+ }
+ }
+
void sleepBetweenRetries() {
try {
Thread.sleep(getSleepBetweenRetriesMs());
diff --git
a/client/common/src/main/java/org/apache/atlas/token/retriever/JwTokenRetrieverDefault.java
b/client/common/src/main/java/org/apache/atlas/token/retriever/JwTokenRetrieverDefault.java
new file mode 100644
index 000000000..c9c858f7d
--- /dev/null
+++
b/client/common/src/main/java/org/apache/atlas/token/retriever/JwTokenRetrieverDefault.java
@@ -0,0 +1,153 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.atlas.token.retriever;
+
+import org.apache.atlas.security.SecurityUtil;
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Optional;
+
+public class JwTokenRetrieverDefault implements TokenRetriever<String> {
+ private static final Logger LOG =
LoggerFactory.getLogger(JwTokenRetrieverDefault.class);
+
+ public static final String JWT_SOURCE = "atlas.jwt.source";
+ public static final String JWT_ENV = "atlas.jwt.env";
+ public static final String JWT_FILE = "atlas.jwt.file";
+ public static final String JWT_CRED_FILE = "atlas.jwt.cred.file";
+ public static final String JWT_CRED_ALIAS = "atlas.jwt.cred.alias";
+
+ private static final String SOURCE_ENV = "env";
+ private static final String SOURCE_FILE = "file";
+ private static final String SOURCE_CRED = "cred";
+ private static final long CRED_CHECK_INTERVAL_MS = 60 * 1000;
+
+ private final String jwtSource;
+ private final String jwtEnvVar;
+ private final String jwtFilePath;
+ private final String jwtCredPathPropertyName;
+ private final String jwtCredFilePath;
+ private final String jwtCredAlias;
+ private final Configuration configuration;
+ private long jwtFileLastModified;
+ private long jwtCredFileLastCheckedAt;
+
+ private volatile Optional<String> cachedJwt = Optional.empty();
+
+ public JwTokenRetrieverDefault(Configuration config) {
+ configuration = config;
+ jwtSource =
StringUtils.trimToEmpty(config.getString(JWT_SOURCE, ""));
+ jwtEnvVar =
StringUtils.trimToEmpty(config.getString(JWT_ENV, ""));
+ jwtFilePath =
StringUtils.trimToEmpty(config.getString(JWT_FILE, ""));
+ jwtCredPathPropertyName = JWT_CRED_FILE;
+ jwtCredFilePath =
StringUtils.trimToEmpty(config.getString(jwtCredPathPropertyName, ""));
+ jwtCredAlias =
StringUtils.trimToEmpty(config.getString(JWT_CRED_ALIAS, ""));
+ }
+
+ @Override
+ public synchronized Optional<String> retrieve() {
+ String source = StringUtils.lowerCase(jwtSource);
+ switch (source) {
+ case SOURCE_ENV:
+ return getJwtFromEnv();
+ case SOURCE_FILE:
+ return getJwtFromFile();
+ case SOURCE_CRED:
+ return getJwtFromCredProvider();
+ default:
+ if (StringUtils.isNotBlank(source)) {
+ LOG.warn("JwTokenRetrieverDefault.retrieve(): unsupported
source='{}'", source);
+ }
+ return Optional.empty();
+ }
+ }
+
+ private Optional<String> getJwtFromEnv() {
+ if (StringUtils.isBlank(jwtEnvVar)) {
+ LOG.warn("JwTokenRetrieverDefault.getJwtFromEnv(): '{}' is not
configured.", JWT_ENV);
+ return Optional.empty();
+ }
+
+ String token = StringUtils.trimToEmpty(System.getenv(jwtEnvVar));
+ return StringUtils.isBlank(token) ? Optional.empty() :
Optional.of(token);
+ }
+
+ private Optional<String> getJwtFromFile() {
+ if (StringUtils.isBlank(jwtFilePath)) {
+ LOG.warn("JwTokenRetrieverDefault.getJwtFromFile(): '{}' is not
configured.", JWT_FILE);
+ return Optional.empty();
+ }
+
+ File jwtFile = new File(jwtFilePath);
+ if (!jwtFile.canRead()) {
+ return cachedJwt;
+ }
+
+ if (jwtFile.lastModified() == jwtFileLastModified &&
cachedJwt.isPresent()) {
+ return cachedJwt;
+ }
+
+ try (BufferedReader reader = new BufferedReader(new
FileReader(jwtFile))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (StringUtils.isNotBlank(line) && !line.startsWith("#")) {
+ cachedJwt = Optional.of(line.trim());
+ jwtFileLastModified = jwtFile.lastModified();
+ break;
+ }
+ }
+ } catch (IOException e) {
+ LOG.error("JwTokenRetrieverDefault.getJwtFromFile(): failed to
read JWT from file={}", jwtFilePath, e);
+ }
+
+ return cachedJwt;
+ }
+
+ private Optional<String> getJwtFromCredProvider() {
+ if (StringUtils.isBlank(jwtCredFilePath) ||
StringUtils.isBlank(jwtCredAlias)) {
+ LOG.warn("JwTokenRetrieverDefault.getJwtFromCredProvider(): '{}'
or '{}' is not configured.",
+ JWT_CRED_FILE, JWT_CRED_ALIAS);
+ return Optional.empty();
+ }
+
+ long now = System.currentTimeMillis();
+ if ((now - jwtCredFileLastCheckedAt) <= CRED_CHECK_INTERVAL_MS &&
cachedJwt.isPresent()) {
+ return cachedJwt;
+ }
+
+ try {
+ String token = StringUtils.trimToEmpty(
+ SecurityUtil.getPassword(configuration, jwtCredAlias,
jwtCredPathPropertyName));
+ if (StringUtils.isNotBlank(token)) {
+ cachedJwt = Optional.of(token);
+ }
+ } catch (Exception e) {
+ LOG.error("JwTokenRetrieverDefault.getJwtFromCredProvider():
failed to read JWT from credential provider.", e);
+ } finally {
+ jwtCredFileLastCheckedAt = now;
+ }
+
+ return cachedJwt;
+ }
+}
diff --git
a/client/common/src/main/java/org/apache/atlas/token/retriever/TokenRetriever.java
b/client/common/src/main/java/org/apache/atlas/token/retriever/TokenRetriever.java
new file mode 100644
index 000000000..cdcd84a42
--- /dev/null
+++
b/client/common/src/main/java/org/apache/atlas/token/retriever/TokenRetriever.java
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.atlas.token.retriever;
+
+import java.util.Optional;
+
+public interface TokenRetriever<T> {
+ Optional<T> retrieve();
+}
diff --git a/pom.xml b/pom.xml
index c5fa6b1f3..15bab027d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,7 @@
<module>addons/storm-bridge-shim</module>
<module>addons/trino-extractor</module>
<module>atlas-examples</module>
+ <module>authn</module>
<module>authorization</module>
<module>build-tools</module>
<module>client</module>
@@ -158,6 +159,7 @@
<maven-plugin-sortpom.version>3.0.1</maven-plugin-sortpom.version>
<maven-site-plugin.version>3.7</maven-site-plugin.version>
<netty.version>4.1.125.Final</netty.version>
+ <nimbus-jose-jwt.version>9.37.3</nimbus-jose-jwt.version>
<node-for-v1.version>v22.14.0</node-for-v1.version>
<node-for-v2.version>v12.16.0</node-for-v2.version>
<npm-for-v1.version>10.9.2</npm-for-v1.version>
@@ -262,6 +264,12 @@
</exclusions>
</dependency>
+ <dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ <version>${nimbus-jose-jwt.version}</version>
+ </dependency>
+
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
diff --git a/webapp/pom.xml b/webapp/pom.xml
index d0ce93d4d..a7b5f7837 100755
--- a/webapp/pom.xml
+++ b/webapp/pom.xml
@@ -66,7 +66,6 @@
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
- <version>9.37.3</version>
<scope>compile</scope>
<exclusions>
<exclusion>
@@ -136,6 +135,12 @@
<version>5.2.0</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.atlas</groupId>
+ <artifactId>atlas-authn</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.apache.atlas</groupId>
<artifactId>atlas-authorization</artifactId>
diff --git
a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationFilter.java
b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationFilter.java
index bc477f762..d72122aa6 100644
---
a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationFilter.java
+++
b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasAuthenticationFilter.java
@@ -427,7 +427,7 @@ public class AtlasAuthenticationFilter extends
AuthenticationFilter {
if (existingAuth == null) {
String authHeader = httpRequest.getHeader("Authorization");
- if (authHeader != null && authHeader.startsWith("Basic")) {
+ if (authHeader != null && (authHeader.startsWith("Basic") ||
authHeader.startsWith("Bearer"))) {
filterChain.doFilter(request, response);
} else if (isKerberos) {
doKerberosAuth(request, response, filterChain);
diff --git
a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthFilter.java
b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthFilter.java
new file mode 100644
index 000000000..c931b37a0
--- /dev/null
+++ b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthFilter.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.atlas.web.filters;
+
+import org.apache.atlas.ApplicationProperties;
+import org.apache.atlas.authn.handler.AtlasAuth;
+import org.apache.atlas.authn.handler.jwt.AtlasDefaultJwtAuthHandler;
+import org.apache.atlas.authn.handler.jwt.AtlasJwtAuthHandler;
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import
org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+@Lazy
+@Component
+public class AtlasJwtAuthFilter extends AtlasDefaultJwtAuthHandler implements
Filter {
+ private static final Logger LOG =
LoggerFactory.getLogger(AtlasJwtAuthFilter.class);
+ private static final String DEFAULT_ATLAS_ROLE = "ROLE_USER";
+
+ @PostConstruct
+ public void initialize() {
+ LOG.debug("===>>> AtlasJwtAuthFilter.initialize()");
+
+ try {
+ Configuration config = ApplicationProperties.get();
+ if
(StringUtils.isEmpty(config.getString(AtlasJwtAuthHandler.KEY_PROVIDER_URL))) {
+ config.setProperty(AtlasJwtAuthHandler.KEY_PROVIDER_URL,
+
config.getString(AtlasKnoxSSOAuthenticationFilter.JWT_AUTH_PROVIDER_URL));
+ }
+ config.setProperty(AtlasJwtAuthHandler.KEY_JWT_PUBLIC_KEY,
+
config.getString(AtlasKnoxSSOAuthenticationFilter.JWT_PUBLIC_KEY, ""));
+ config.setProperty(AtlasJwtAuthHandler.KEY_JWT_COOKIE_NAME,
+
config.getString(AtlasKnoxSSOAuthenticationFilter.JWT_COOKIE_NAME,
+
AtlasKnoxSSOAuthenticationFilter.JWT_COOKIE_NAME_DEFAULT));
+ config.setProperty(AtlasJwtAuthHandler.KEY_JWT_AUDIENCES,
+
config.getString(AtlasKnoxSSOAuthenticationFilter.JWT_AUDIENCES, ""));
+
+ super.initialize(config);
+ } catch (Exception e) {
+ LOG.error("Failed to initialize Atlas JWT Auth Filter.", e);
+ }
+
+ LOG.debug("<<<=== AtlasJwtAuthFilter.initialize()");
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) {
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse
servletResponse, FilterChain filterChain)
+ throws IOException, ServletException {
+ LOG.debug("===>>> AtlasJwtAuthFilter.doFilter()");
+
+ HttpServletRequest httpServletRequest = (HttpServletRequest)
servletRequest;
+ AtlasAuth atlasAuth =
authenticate(httpServletRequest);
+
+ if (atlasAuth != null) {
+ final List<GrantedAuthority> grantedAuths = Arrays.asList(new
SimpleGrantedAuthority(DEFAULT_ATLAS_ROLE));
+ final UserDetails principal = new
User(atlasAuth.getUserName(), "", grantedAuths);
+ final Authentication finalAuthentication = new
UsernamePasswordAuthenticationToken(principal, "", grantedAuths);
+ final WebAuthenticationDetails webDetails = new
WebAuthenticationDetails(httpServletRequest);
+ ((AbstractAuthenticationToken)
finalAuthentication).setDetails(webDetails);
+
SecurityContextHolder.getContext().setAuthentication(finalAuthentication);
+ }
+
+ Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
+ if (auth != null) {
+ LOG.debug("<<<=== AtlasJwtAuthFilter.doFilter() - user=[{}],
isUserAuthenticated=[{}]",
+ auth.getPrincipal(), auth.isAuthenticated());
+ } else {
+ LOG.warn("<<<=== AtlasJwtAuthFilter.doFilter() - Failed to
authenticate request using Atlas JWT authentication framework.");
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git
a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapper.java
b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapper.java
new file mode 100644
index 000000000..74680530b
--- /dev/null
+++ b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapper.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.atlas.web.filters;
+
+import org.apache.atlas.ApplicationProperties;
+import org.apache.atlas.AtlasException;
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+@Lazy
+@Component
+public class AtlasJwtAuthWrapper extends GenericFilterBean {
+ private static final Logger LOG =
LoggerFactory.getLogger(AtlasJwtAuthWrapper.class);
+
+ private String[] browserUserAgents = new String[] {""};
+
+ private final Configuration configuration;
+
+ @Lazy
+ @Autowired
+ AtlasJwtAuthFilter atlasJwtAuthFilter;
+
+ @Inject
+ public AtlasJwtAuthWrapper(Configuration configuration) {
+ this.configuration = configuration;
+ }
+
+ @PostConstruct
+ public void initialize() {
+ String defaultUserAgent =
configuration.getString(AtlasKnoxSSOAuthenticationFilter.DEFAULT_BROWSER_USERAGENT);
+ String userAgent =
configuration.getString(AtlasKnoxSSOAuthenticationFilter.BROWSER_USERAGENT);
+
+ if (StringUtils.isBlank(userAgent) &&
StringUtils.isNotBlank(defaultUserAgent)) {
+ userAgent = defaultUserAgent;
+ }
+
+ if (StringUtils.isNotBlank(userAgent)) {
+ browserUserAgents = userAgent.split(",");
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse
servletResponse, FilterChain filterChain)
+ throws IOException, ServletException {
+ LOG.debug("===>>> AtlasJwtAuthWrapper.doFilter({}, {}, {})",
servletRequest, servletResponse, filterChain);
+
+ Configuration configuration = null;
+ try {
+ configuration = ApplicationProperties.get();
+ } catch (AtlasException e) {
+ throw new RuntimeException(e);
+ }
+ boolean isProxyEnabled =
configuration.getBoolean("atlas.authentication.method.trustedproxy", false);
+ boolean useJwtAuthMechanism = servletRequest != null &&
!isRequestAuthenticated() &&
AtlasJwtAuthFilter.canAuthenticateRequest(servletRequest);
+ boolean ssoEnabled =
configuration.getBoolean("atlas.sso.knox.enabled", false);
+
+ if (!ssoEnabled && useJwtAuthMechanism && !isProxyEnabled) {
+ atlasJwtAuthFilter.doFilter(servletRequest, servletResponse,
filterChain);
+
+ if (!isRequestAuthenticated()) {
+ String userAgent = ((HttpServletRequest)
servletRequest).getHeader("User-Agent");
+ if (isBrowserAgent(userAgent)) {
+ LOG.debug("Redirecting to login page as request does not
have valid JWT auth details.");
+ ((HttpServletResponse)
servletResponse).sendRedirect("/login.jsp");
+ }
+ }
+ } else {
+ LOG.debug("<<<=== AtlasJwtAuthWrapper.doFilter() - Skipping JWT
auth.");
+ }
+ filterChain.doFilter(servletRequest, servletResponse);
+
+ LOG.debug("<<<=== AtlasJwtAuthWrapper.doFilter()");
+ }
+
+ protected boolean isBrowserAgent(String userAgent) {
+ boolean isBrowser = false;
+
+ if (browserUserAgents.length > 0 && StringUtils.isNotBlank(userAgent))
{
+ for (String ua : browserUserAgents) {
+ if (userAgent.toLowerCase().startsWith(ua.toLowerCase())) {
+ isBrowser = true;
+ break;
+ }
+ }
+ }
+
+ return isBrowser;
+ }
+
+ private boolean isRequestAuthenticated() {
+ Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
+ return auth != null && auth.isAuthenticated();
+ }
+}
diff --git
a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java
b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java
index c41f9d468..f2d43390b 100644
---
a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java
+++
b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java
@@ -82,6 +82,7 @@ public class AtlasKnoxSSOAuthenticationFilter implements
Filter {
public static final String JWT_COOKIE_NAME =
"atlas.sso.knox.cookiename";
public static final String JWT_ORIGINAL_URL_QUERY_PARAM =
"atlas.sso.knox.query.param.originalurl";
public static final String JWT_COOKIE_NAME_DEFAULT =
"hadoop-jwt";
+ public static final String JWT_AUDIENCES =
"atlas.sso.knox.audiences";
public static final String JWT_ORIGINAL_URL_QUERY_PARAM_DEFAULT =
"originalUrl";
public static final String DEFAULT_BROWSER_USERAGENT =
"Mozilla,Opera,Chrome";
public static final String PROXY_ATLAS_URL_PATH = "/atlas";
diff --git
a/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java
b/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java
index fbcdf3d7e..532f3319c 100644
---
a/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java
+++
b/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java
@@ -23,6 +23,7 @@ import org.apache.atlas.web.filters.AtlasAuthenticationFilter;
import org.apache.atlas.web.filters.AtlasCSRFPreventionFilter;
import org.apache.atlas.web.filters.AtlasDelegatingAuthenticationEntryPoint;
import org.apache.atlas.web.filters.AtlasHeaderPreAuthFilter;
+import org.apache.atlas.web.filters.AtlasJwtAuthWrapper;
import org.apache.atlas.web.filters.AtlasKnoxSSOAuthenticationFilter;
import org.apache.atlas.web.filters.HeadersUtil;
import org.apache.atlas.web.filters.StaleTransactionCleanupFilter;
@@ -99,6 +100,7 @@ public class AtlasSecurityConfig extends
WebSecurityConfigurerAdapter {
private final AtlasAuthenticationFilter atlasAuthenticationFilter;
private final AtlasCSRFPreventionFilter csrfPreventionFilter;
private final AtlasAuthenticationEntryPoint
atlasAuthenticationEntryPoint;
+ private final AtlasJwtAuthWrapper atlasJwtAuthWrapper;
// Our own Atlas filters need to be registered as well
private final Configuration configuration;
@@ -123,7 +125,8 @@ public class AtlasSecurityConfig extends
WebSecurityConfigurerAdapter {
AtlasAuthenticationEntryPoint atlasAuthenticationEntryPoint,
Configuration configuration,
StaleTransactionCleanupFilter staleTransactionCleanupFilter,
- ActiveServerFilter activeServerFilter) {
+ ActiveServerFilter activeServerFilter,
+ AtlasJwtAuthWrapper atlasJwtAuthWrapper) {
this.headerPreAuthFilter = headerPreAuthFilter;
this.ssoAuthenticationFilter = ssoAuthenticationFilter;
this.csrfPreventionFilter = atlasCSRFPreventionFilter;
@@ -135,6 +138,7 @@ public class AtlasSecurityConfig extends
WebSecurityConfigurerAdapter {
this.configuration = configuration;
this.staleTransactionCleanupFilter = staleTransactionCleanupFilter;
this.activeServerFilter = activeServerFilter;
+ this.atlasJwtAuthWrapper = atlasJwtAuthWrapper;
this.keycloakEnabled =
configuration.getBoolean(AtlasAuthenticationProvider.KEYCLOAK_AUTH_METHOD,
false);
}
@@ -242,8 +246,10 @@ public class AtlasSecurityConfig extends
WebSecurityConfigurerAdapter {
httpSecurity.addFilterAfter(activeServerFilter,
BasicAuthenticationFilter.class);
}
- httpSecurity.addFilterBefore(headerPreAuthFilter,
BasicAuthenticationFilter.class)
+ httpSecurity
+ .addFilterBefore(headerPreAuthFilter,
BasicAuthenticationFilter.class)
.addFilterAfter(ssoAuthenticationFilter,
AtlasHeaderPreAuthFilter.class)
+ .addFilterAfter(atlasJwtAuthWrapper,
AtlasKnoxSSOAuthenticationFilter.class)
.addFilterAfter(staleTransactionCleanupFilter,
BasicAuthenticationFilter.class)
.addFilterAfter(atlasAuthenticationFilter,
SecurityContextHolderAwareRequestFilter.class)
.addFilterAfter(csrfPreventionFilter,
AtlasAuthenticationFilter.class);
diff --git
a/webapp/src/test/java/org/apache/atlas/web/filters/AtlasJwtAuthFilterTest.java
b/webapp/src/test/java/org/apache/atlas/web/filters/AtlasJwtAuthFilterTest.java
new file mode 100644
index 000000000..503c7eafd
--- /dev/null
+++
b/webapp/src/test/java/org/apache/atlas/web/filters/AtlasJwtAuthFilterTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.atlas.web.filters;
+
+import org.apache.atlas.authn.handler.AtlasAuth;
+import org.mockito.Mockito;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class AtlasJwtAuthFilterTest {
+ @AfterMethod
+ public void cleanup() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ public void testInit_noopDoesNotThrow() throws Exception {
+ AtlasJwtAuthFilter filter = new AtlasJwtAuthFilter();
+ filter.init(mock(javax.servlet.FilterConfig.class));
+ }
+
+ @Test
+ public void testDestroy_noopDoesNotThrow() {
+ AtlasJwtAuthFilter filter = new AtlasJwtAuthFilter();
+ filter.destroy();
+ }
+
+ @Test
+ public void testDoFilter_setsAuthenticationWhenAuthenticateSucceeds()
throws Exception {
+ AtlasJwtAuthFilter filter = Mockito.spy(new AtlasJwtAuthFilter());
+
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ ServletResponse res = mock(ServletResponse.class);
+ FilterChain chain = mock(FilterChain.class);
+
+ AtlasAuth atlasAuth = new AtlasAuth("alice",
AtlasAuth.AuthType.JWT_JWKS);
+
+
doReturn(atlasAuth).when(filter).authenticate(any(HttpServletRequest.class));
+
+ filter.doFilter(req, res, chain);
+
+ Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
+ assertNotNull(auth);
+ assertTrue(auth.getPrincipal() instanceof User);
+ User user = (User) auth.getPrincipal();
+ assertEquals(user.getUsername(), "alice");
+ assertNotNull(auth.getAuthorities());
+ assertFalse(auth.getAuthorities().isEmpty());
+ }
+
+ @Test
+ public void
testDoFilter_leavesAuthenticationNullWhenAuthenticateReturnsNull() throws
Exception {
+ AtlasJwtAuthFilter filter = Mockito.spy(new AtlasJwtAuthFilter());
+
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ ServletResponse res = mock(ServletResponse.class);
+ FilterChain chain = mock(FilterChain.class);
+
+
doReturn(null).when(filter).authenticate(any(HttpServletRequest.class));
+
+ filter.doFilter(req, res, chain);
+
+ assertNull(SecurityContextHolder.getContext().getAuthentication());
+ }
+}
diff --git
a/webapp/src/test/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapperTest.java
b/webapp/src/test/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapperTest.java
new file mode 100644
index 000000000..d9d1d5a0d
--- /dev/null
+++
b/webapp/src/test/java/org/apache/atlas/web/filters/AtlasJwtAuthWrapperTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.atlas.web.filters;
+
+import org.apache.commons.configuration2.Configuration;
+import org.mockito.Mockito;
+import
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.Collections;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class AtlasJwtAuthWrapperTest {
+ @AfterMethod
+ public void tearDown() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ public void testDoFilter_redirectsToLoginForBrowserWhenNotAuthenticated()
throws Exception {
+ SecurityContextHolder.clearContext();
+
+ Configuration configuration = mock(Configuration.class);
+
when(configuration.getString(AtlasKnoxSSOAuthenticationFilter.DEFAULT_BROWSER_USERAGENT)).thenReturn("Mozilla,Opera,Chrome");
+
when(configuration.getString(AtlasKnoxSSOAuthenticationFilter.BROWSER_USERAGENT)).thenReturn(null);
+
+ AtlasJwtAuthWrapper wrapper = Mockito.spy(new
AtlasJwtAuthWrapper(configuration));
+ wrapper.initialize();
+
+ AtlasJwtAuthFilter jwtFilter = mock(AtlasJwtAuthFilter.class);
+ wrapper.atlasJwtAuthFilter = jwtFilter;
+
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ HttpServletResponse res = mock(HttpServletResponse.class);
+ FilterChain chain = mock(FilterChain.class);
+
+ when(req.getHeader("User-Agent")).thenReturn("Mozilla/5.0");
+ when(req.getHeader("Authorization")).thenReturn("Bearer sometoken");
+ doNothing().when(res).sendRedirect(anyString());
+
+ wrapper.doFilter(req, res, chain);
+
+ verify(jwtFilter, times(1)).doFilter(any(ServletRequest.class),
any(ServletResponse.class), any(FilterChain.class));
+ verify(res, atLeastOnce()).sendRedirect(anyString());
+ verify(chain, times(1)).doFilter(req, res);
+ }
+
+ @Test
+ public void testDoFilter_skipsJwtWhenAlreadyAuthenticated() throws
Exception {
+ User principal = new User("user", "", Collections.emptyList());
+ UsernamePasswordAuthenticationToken authentication =
+ new UsernamePasswordAuthenticationToken(principal, "",
principal.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ Configuration configuration = mock(Configuration.class);
+
when(configuration.getString(AtlasKnoxSSOAuthenticationFilter.DEFAULT_BROWSER_USERAGENT)).thenReturn("Mozilla,Opera,Chrome");
+
when(configuration.getString(AtlasKnoxSSOAuthenticationFilter.BROWSER_USERAGENT)).thenReturn(null);
+
+ AtlasJwtAuthWrapper wrapper = Mockito.spy(new
AtlasJwtAuthWrapper(configuration));
+ wrapper.initialize();
+
+ AtlasJwtAuthFilter jwtFilter = mock(AtlasJwtAuthFilter.class);
+ wrapper.atlasJwtAuthFilter = jwtFilter;
+
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ HttpServletResponse res = mock(HttpServletResponse.class);
+ FilterChain chain = mock(FilterChain.class);
+
+ wrapper.doFilter(req, res, chain);
+
+ verify(jwtFilter, never()).doFilter(any(ServletRequest.class),
any(ServletResponse.class), any(FilterChain.class));
+ verify(chain, times(1)).doFilter(req, res);
+ }
+}
diff --git
a/webapp/src/test/java/org/apache/atlas/web/security/AtlasSecurityConfigTest.java
b/webapp/src/test/java/org/apache/atlas/web/security/AtlasSecurityConfigTest.java
index 4828b4b92..cbf47cbf9 100644
---
a/webapp/src/test/java/org/apache/atlas/web/security/AtlasSecurityConfigTest.java
+++
b/webapp/src/test/java/org/apache/atlas/web/security/AtlasSecurityConfigTest.java
@@ -25,6 +25,7 @@ import org.apache.atlas.web.filters.AtlasAuthenticationFilter;
import org.apache.atlas.web.filters.AtlasCSRFPreventionFilter;
import org.apache.atlas.web.filters.AtlasDelegatingAuthenticationEntryPoint;
import org.apache.atlas.web.filters.AtlasHeaderPreAuthFilter;
+import org.apache.atlas.web.filters.AtlasJwtAuthWrapper;
import org.apache.atlas.web.filters.AtlasKnoxSSOAuthenticationFilter;
import org.apache.atlas.web.filters.StaleTransactionCleanupFilter;
import org.apache.commons.configuration2.Configuration;
@@ -132,6 +133,9 @@ public class AtlasSecurityConfigTest {
@Mock
private ActiveServerFilter mockActiveServerFilter;
+ @Mock
+ private AtlasJwtAuthWrapper mockAtlasJwtAuthWrapper;
+
@Mock
private KeycloakConfigResolver mockKeycloakConfigResolver;
@@ -174,7 +178,8 @@ public class AtlasSecurityConfigTest {
mockAtlasAuthenticationEntryPoint,
mockConfiguration,
mockStaleTransactionCleanupFilter,
- mockActiveServerFilter);
+ mockActiveServerFilter,
+ mockAtlasJwtAuthWrapper);
// Verify using reflection
assertFalse((Boolean) getPrivateField(atlasSecurityConfig,
"keycloakEnabled"));
@@ -201,7 +206,8 @@ public class AtlasSecurityConfigTest {
mockAtlasAuthenticationEntryPoint,
mockConfiguration,
mockStaleTransactionCleanupFilter,
- mockActiveServerFilter);
+ mockActiveServerFilter,
+ mockAtlasJwtAuthWrapper);
// Verify using reflection
assertTrue((Boolean) getPrivateField(atlasSecurityConfig,
"keycloakEnabled"));
@@ -362,7 +368,8 @@ public class AtlasSecurityConfigTest {
mockAtlasAuthenticationEntryPoint,
mockConfiguration,
mockStaleTransactionCleanupFilter,
- mockActiveServerFilter);
+ mockActiveServerFilter,
+ mockAtlasJwtAuthWrapper);
// Set up comprehensive HttpSecurity mocking first
setupHttpSecurityMocks();
@@ -447,7 +454,8 @@ public class AtlasSecurityConfigTest {
mockAtlasAuthenticationEntryPoint,
mockConfiguration,
mockStaleTransactionCleanupFilter,
- mockActiveServerFilter);
+ mockActiveServerFilter,
+ mockAtlasJwtAuthWrapper);
// Create fresh HttpSecurity mock for each test to avoid state
pollution
HttpSecurity freshHttpSecurity = mock(HttpSecurity.class);
@@ -490,6 +498,7 @@ public class AtlasSecurityConfigTest {
// Verify standard filters are always added
verify(freshHttpSecurity,
atLeastOnce()).addFilterBefore(eq(mockHeaderPreAuthFilter), any());
verify(freshHttpSecurity,
atLeastOnce()).addFilterAfter(eq(mockSsoAuthenticationFilter),
eq(AtlasHeaderPreAuthFilter.class));
+ verify(freshHttpSecurity,
atLeastOnce()).addFilterAfter(eq(mockAtlasJwtAuthWrapper),
eq(AtlasKnoxSSOAuthenticationFilter.class));
verify(freshHttpSecurity,
atLeastOnce()).addFilterAfter(eq(mockStaleTransactionCleanupFilter), any());
verify(freshHttpSecurity,
atLeastOnce()).addFilterAfter(eq(mockAtlasAuthenticationFilter), any());
verify(freshHttpSecurity,
atLeastOnce()).addFilterAfter(eq(mockCsrfPreventionFilter), any());
@@ -600,7 +609,8 @@ public class AtlasSecurityConfigTest {
mockAtlasAuthenticationEntryPoint,
mockConfiguration,
mockStaleTransactionCleanupFilter,
- mockActiveServerFilter);
+ mockActiveServerFilter,
+ mockAtlasJwtAuthWrapper);
setPrivateField(atlasSecurityConfig, "keycloakConfigFileResource",
mockKeycloakConfigFileResource);
@@ -831,7 +841,8 @@ public class AtlasSecurityConfigTest {
AtlasAuthenticationEntryPoint.class,
Configuration.class,
StaleTransactionCleanupFilter.class,
- ActiveServerFilter.class
+ ActiveServerFilter.class,
+ AtlasJwtAuthWrapper.class
).getAnnotation(Inject.class);
assertNotNull(injectAnnotation);
}
@@ -857,7 +868,8 @@ public class AtlasSecurityConfigTest {
mockAtlasAuthenticationEntryPoint,
mockConfiguration,
mockStaleTransactionCleanupFilter,
- mockActiveServerFilter);
+ mockActiveServerFilter,
+ mockAtlasJwtAuthWrapper);
// Set the keycloakConfigFileResource using reflection
setPrivateField(atlasSecurityConfig, "keycloakConfigFileResource",
mockKeycloakConfigFileResource);