This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 1090a97 NIFI-7870 Resolved access to extension resources when using
JWT
1090a97 is described below
commit 1090a9748a8ce5fe21d5a79d85a46858f1288f85
Author: Nathan Gough <[email protected]>
AuthorDate: Mon Feb 22 11:28:01 2021 -0500
NIFI-7870 Resolved access to extension resources when using JWT
- Added SameSite Session Cookie __Host-Authorization-Bearer for sending JWT
- Configured Spring Security CSRF Filter comparing Authorization header and
Cookie JWT
- Implemented BearerTokenResolver for retrieving JWT
This closes #4988
Signed-off-by: David Handermann <[email protected]>
---
.../replication/ThreadPoolRequestReplicator.java | 36 +++--
.../nifi-framework/nifi-web/nifi-web-api/pom.xml | 6 +
.../apache/nifi/web/NiFiCsrfTokenRepository.java | 91 +++++++++++
.../nifi/web/NiFiWebApiSecurityConfiguration.java | 19 ++-
.../org/apache/nifi/web/api/AccessResource.java | 95 +++++------
.../apache/nifi/web/security/LogoutException.java | 35 ++++
.../nifi/web/security/jwt/BearerTokenResolver.java | 30 ++++
.../web/security/jwt/JwtAuthenticationFilter.java | 41 +----
.../apache/nifi/web/security/jwt/JwtService.java | 13 +-
...ionFilter.java => NiFiBearerTokenResolver.java} | 57 +++----
.../jwt/JwtAuthenticationFilterTest.groovy | 180 ---------------------
.../nifi/web/security/jwt/JwtServiceTest.java | 32 ----
.../security/jwt/NiFiBearerTokenResolverTest.java | 124 ++++++++++++++
.../webapp/js/nf/canvas/nf-canvas-error-handler.js | 2 +-
.../src/main/webapp/js/nf/login/nf-login.js | 13 +-
.../nifi-web-ui/src/main/webapp/js/nf/nf-common.js | 8 +-
16 files changed, 413 insertions(+), 369 deletions(-)
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
index 49383fd..6665095 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
@@ -40,7 +40,7 @@ import org.apache.nifi.reporting.Severity;
import org.apache.nifi.util.ComponentIdGenerator;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
-import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
+import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -244,25 +244,13 @@ public class ThreadPoolRequestReplicator implements
RequestReplicator {
// remove the access token if present, since the user is already
authenticated... authorization
// will happen when the request is replicated using the proxy chain
above
- headers.remove(JwtAuthenticationFilter.AUTHORIZATION);
+ headers.remove(NiFiBearerTokenResolver.AUTHORIZATION);
// if knox sso cookie name is set, remove any authentication cookie
since this user is already authenticated
// and will be included in the proxied entities chain above...
authorization will happen when the
// request is replicated
- final String knoxCookieName = nifiProperties.getKnoxCookieName();
- if (headers.containsKey("Cookie") &&
StringUtils.isNotBlank(knoxCookieName)) {
- final String rawCookies = headers.get("Cookie");
- final String[] rawCookieParts = rawCookies.split(";");
- final Set<String> filteredCookieParts =
Stream.of(rawCookieParts).map(String::trim).filter(cookie ->
!cookie.startsWith(knoxCookieName + "=")).collect(Collectors.toSet());
-
- // if that was the only cookie, remove it
- if (filteredCookieParts.isEmpty()) {
- headers.remove("Cookie");
- } else {
- // otherwise rebuild the cookies without the knox token
- headers.put("Cookie", StringUtils.join(filteredCookieParts, ";
"));
- }
- }
+ removeCookie(headers, nifiProperties.getKnoxCookieName());
+ removeCookie(headers, NiFiBearerTokenResolver.JWT_COOKIE_NAME);
// remove the host header
headers.remove("Host");
@@ -869,4 +857,20 @@ public class ThreadPoolRequestReplicator implements
RequestReplicator {
expiredRequestIds.forEach(id -> onResponseConsumed(id));
return responseMap.size();
}
+
+ private void removeCookie(Map<String, String> headers, final String
cookieName) {
+ if (headers.containsKey("Cookie") &&
StringUtils.isNotBlank(cookieName)) {
+ final String rawCookies = headers.get("Cookie");
+ final String[] rawCookieParts = rawCookies.split(";");
+ final Set<String> filteredCookieParts =
Stream.of(rawCookieParts).map(String::trim).filter(cookie ->
!cookie.startsWith(cookieName + "=")).collect(Collectors.toSet());
+
+ // if that was the only cookie, remove it
+ if (filteredCookieParts.isEmpty()) {
+ headers.remove("Cookie");
+ } else {
+ // otherwise rebuild the cookies without the knox token
+ headers.put("Cookie", StringUtils.join(filteredCookieParts, ";
"));
+ }
+ }
+ }
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
index f2e01dc..865681e 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
@@ -436,5 +436,11 @@
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-http</artifactId>
+ <version>${jetty.version}</version>
+ <scope>compile</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiCsrfTokenRepository.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiCsrfTokenRepository.java
new file mode 100644
index 0000000..1c4fc4c
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiCsrfTokenRepository.java
@@ -0,0 +1,91 @@
+/*
+ * 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.nifi.web;
+
+
+import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.security.web.csrf.DefaultCsrfToken;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A {@link CsrfTokenRepository} implementation for NiFi that matches the NiFi
Cookie JWT against the
+ * Authorization header JWT to protect against CSRF. If the request is an
idempotent method type, then only the Cookie
+ * is required to be present - this allows authenticating access to static
resources using a Cookie. If the request is a non-idempotent
+ * method, NiFi requires the Authorization header (eg. for POST requests).
+ */
+public final class NiFiCsrfTokenRepository implements CsrfTokenRepository {
+
+ private static String EMPTY = "empty";
+ private CookieCsrfTokenRepository cookieRepository;
+
+ public NiFiCsrfTokenRepository() {
+ cookieRepository = new CookieCsrfTokenRepository();
+ }
+
+ @Override
+ public CsrfToken generateToken(HttpServletRequest request) {
+ // Return an empty value CsrfToken - it will not be saved to the
response as our CSRF token is added elsewhere
+ return new DefaultCsrfToken(EMPTY, EMPTY, EMPTY);
+ }
+
+ @Override
+ public void saveToken(CsrfToken token, HttpServletRequest request,
+ HttpServletResponse response) {
+ // Do nothing - we don't need to add new CSRF tokens to the response
+ }
+
+ @Override
+ public CsrfToken loadToken(HttpServletRequest request) {
+ CsrfToken cookie = cookieRepository.loadToken(request);
+ // We add the Bearer string here in order to match the Authorization
header on comparison in CsrfFilter
+ return cookie != null ? new DefaultCsrfToken(cookie.getHeaderName(),
cookie.getParameterName(), String.format("Bearer %s", cookie.getToken())) :
null;
+ }
+
+ /**
+ * Sets the name of the HTTP request parameter that should be used to
provide a token.
+ *
+ * @param parameterName the name of the HTTP request parameter that should
be used to
+ * provide a token
+ */
+ public void setParameterName(String parameterName) {
+ cookieRepository.setParameterName(parameterName);
+ }
+
+ /**
+ * Sets the name of the HTTP header that should be used to provide the
token.
+ *
+ * @param headerName the name of the HTTP header that should be used to
provide the
+ * token
+ */
+ public void setHeaderName(String headerName) {
+ cookieRepository.setHeaderName(headerName);
+ }
+
+ /**
+ * Sets the name of the cookie that the expected CSRF token is saved to
and read from.
+ *
+ * @param cookieName the name of the cookie that the expected CSRF token
is saved to
+ * and read from
+ */
+ public void setCookieName(String cookieName) {
+ cookieRepository.setCookieName(cookieName);
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
index 7403185..cf1e525 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
@@ -16,12 +16,12 @@
*/
package org.apache.nifi.web;
-import java.util.Arrays;
import org.apache.nifi.util.NiFiProperties;
import
org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter;
import
org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider;
import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
import org.apache.nifi.web.security.jwt.JwtAuthenticationProvider;
+import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
import org.apache.nifi.web.security.knox.KnoxAuthenticationFilter;
import org.apache.nifi.web.security.knox.KnoxAuthenticationProvider;
import org.apache.nifi.web.security.otp.OtpAuthenticationFilter;
@@ -46,10 +46,15 @@ import
org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.config.http.SessionCreationPolicy;
import
org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.security.web.csrf.CsrfFilter;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import
org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import java.util.Arrays;
+
/**
* NiFi Web Api Spring security. Applies the various NiFiAuthenticationFilter
servlet filters which will extract authentication
* credentials from API requests.
@@ -116,14 +121,16 @@ public class NiFiWebApiSecurityConfiguration extends
WebSecurityConfigurerAdapte
@Override
protected void configure(HttpSecurity http) throws Exception {
+ NiFiCsrfTokenRepository csrfRepository = new NiFiCsrfTokenRepository();
+ csrfRepository.setHeaderName(NiFiBearerTokenResolver.AUTHORIZATION);
+ csrfRepository.setCookieName(NiFiBearerTokenResolver.JWT_COOKIE_NAME);
+
http
.cors().and()
.rememberMe().disable()
- .authorizeRequests()
- .anyRequest().fullyAuthenticated()
- .and()
- .sessionManagement()
- .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+ .authorizeRequests().anyRequest().fullyAuthenticated().and()
+
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
+ .csrf().requireCsrfProtectionMatcher(new
AndRequestMatcher(CsrfFilter.DEFAULT_CSRF_MATCHER, new
RequestHeaderRequestMatcher("Cookie"))).csrfTokenRepository(csrfRepository);
// x509
http.addFilterBefore(x509FilterBean(),
AnonymousAuthenticationFilter.class);
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
index bf4a792..0221485 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
@@ -60,12 +60,13 @@ import org.apache.nifi.web.api.dto.AccessStatusDTO;
import org.apache.nifi.web.api.entity.AccessConfigurationEntity;
import org.apache.nifi.web.api.entity.AccessStatusEntity;
import org.apache.nifi.web.security.InvalidAuthenticationException;
+import org.apache.nifi.web.security.LogoutException;
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.UntrustedProxyException;
-import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
import org.apache.nifi.web.security.jwt.JwtAuthenticationProvider;
import org.apache.nifi.web.security.jwt.JwtAuthenticationRequestToken;
import org.apache.nifi.web.security.jwt.JwtService;
+import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
import org.apache.nifi.web.security.kerberos.KerberosService;
import org.apache.nifi.web.security.knox.KnoxService;
import org.apache.nifi.web.security.logout.LogoutRequest;
@@ -82,6 +83,8 @@ import
org.apache.nifi.web.security.token.OtpAuthenticationToken;
import org.apache.nifi.web.security.x509.X509AuthenticationProvider;
import org.apache.nifi.web.security.x509.X509AuthenticationRequestToken;
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.HttpHeader;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -90,6 +93,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml.SAMLCredential;
import
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.web.util.WebUtils;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
@@ -106,6 +110,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
@@ -143,6 +148,7 @@ public class AccessResource extends ApplicationResource {
private static final Pattern REVOKE_ACCESS_TOKEN_LOGOUT_FORMAT =
Pattern.compile("(\\.google\\.com)");
private static final Pattern ID_TOKEN_LOGOUT_FORMAT =
Pattern.compile("(\\.okta)");
private static final int msTimeout = 30_000;
+ private static final int VALID_FOR_SESSION_ONLY = -1;
private static final String SAML_REQUEST_IDENTIFIER =
"saml-request-identifier";
private static final String SAML_METADATA_MEDIA_TYPE =
"application/samlmetadata+xml";
@@ -342,7 +348,7 @@ public class AccessResource extends ApplicationResource {
initializeSamlServiceProvider();
// ensure the request has the cookie with the request id
- final String samlRequestIdentifier =
getCookieValue(httpServletRequest.getCookies(), SAML_REQUEST_IDENTIFIER);
+ final String samlRequestIdentifier =
WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
if (samlRequestIdentifier == null) {
forwardToLoginMessagePage(httpServletRequest, httpServletResponse,
"The login request identifier was not found in the request. Unable to
continue.");
return;
@@ -435,7 +441,7 @@ public class AccessResource extends ApplicationResource {
initializeSamlServiceProvider();
// ensure the request has the cookie with the request identifier
- final String samlRequestIdentifier =
getCookieValue(httpServletRequest.getCookies(), SAML_REQUEST_IDENTIFIER);
+ final String samlRequestIdentifier =
WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
if (samlRequestIdentifier == null) {
final String message = "The login request identifier was not found
in the request. Unable to continue.";
logger.warn(message);
@@ -479,7 +485,7 @@ public class AccessResource extends ApplicationResource {
}
// ensure the logout request identifier is present
- final String logoutRequestIdentifier =
getCookieValue(httpServletRequest.getCookies(), LOGOUT_REQUEST_IDENTIFIER);
+ final String logoutRequestIdentifier =
WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
if (StringUtils.isBlank(logoutRequestIdentifier)) {
forwardToLogoutMessagePage(httpServletRequest,
httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
return;
@@ -585,7 +591,7 @@ public class AccessResource extends ApplicationResource {
initializeSamlServiceProvider();
// ensure the logout request identifier is present
- final String logoutRequestIdentifier =
getCookieValue(httpServletRequest.getCookies(), LOGOUT_REQUEST_IDENTIFIER);
+ final String logoutRequestIdentifier =
WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
if (StringUtils.isBlank(logoutRequestIdentifier)) {
forwardToLogoutMessagePage(httpServletRequest,
httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
return;
@@ -732,7 +738,7 @@ public class AccessResource extends ApplicationResource {
return;
}
- final String oidcRequestIdentifier =
getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+ final String oidcRequestIdentifier =
WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
if (oidcRequestIdentifier == null) {
forwardToLoginMessagePage(httpServletRequest, httpServletResponse,
"The login request identifier was " +
"not found in the request. Unable to continue.");
@@ -785,7 +791,6 @@ public class AccessResource extends ApplicationResource {
// store the NiFi token
oidcService.storeJwt(oidcRequestIdentifier, nifiJwt);
-
} catch (final Exception e) {
logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
@@ -831,7 +836,7 @@ public class AccessResource extends ApplicationResource {
return
Response.status(Response.Status.CONFLICT).entity(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG).build();
}
- final String oidcRequestIdentifier =
getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+ final String oidcRequestIdentifier =
WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
if (oidcRequestIdentifier == null) {
final String message = "The login request identifier was not found
in the request. Unable to continue.";
logger.warn(message);
@@ -847,8 +852,7 @@ public class AccessResource extends ApplicationResource {
throw new IllegalArgumentException("A JWT for this login request
identifier could not be found. Unable to continue.");
}
- // generate the response
- return generateOkResponse(jwt).build();
+ return generateTokenResponse(generateOkResponse(jwt), jwt);
}
@GET
@@ -868,6 +872,10 @@ public class AccessResource extends ApplicationResource {
throw new
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
}
+ final String mappedUserIdentity = NiFiUserUtils.getNiFiUserIdentity();
+ removeCookie(httpServletResponse,
NiFiBearerTokenResolver.JWT_COOKIE_NAME);
+ logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
+
// Get the oidc discovery url
String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
@@ -920,7 +928,7 @@ public class AccessResource extends ApplicationResource {
return;
}
- final String oidcRequestIdentifier =
getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+ final String oidcRequestIdentifier =
WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
if (oidcRequestIdentifier == null) {
forwardToLogoutMessagePage(httpServletRequest,
httpServletResponse, "The login request identifier was " +
"not found in the request. Unable to continue.");
@@ -1164,8 +1172,8 @@ public class AccessResource extends ApplicationResource {
// if there is not certificate, consider a token
if (certificates == null) {
- // look for an authorization token
- final String authorization =
httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION);
+ // look for an authorization token in header or cookie
+ final String authorization = new
NiFiBearerTokenResolver().resolve(httpServletRequest);
// if there is no authorization header, we don't know the user
if (authorization == null) {
@@ -1173,10 +1181,8 @@ public class AccessResource extends ApplicationResource {
accessStatus.setMessage("No credentials supplied, unknown
user.");
} else {
try {
- // Extract the Base64 encoded token from the
Authorization header
- final String token =
StringUtils.substringAfterLast(authorization, " ");
-
- final JwtAuthenticationRequestToken jwtRequest = new
JwtAuthenticationRequestToken(token, httpServletRequest.getRemoteAddr());
+ // authenticate the token
+ final JwtAuthenticationRequestToken jwtRequest = new
JwtAuthenticationRequestToken(authorization,
httpServletRequest.getRemoteAddr());
final NiFiAuthenticationToken authenticationResponse =
(NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(jwtRequest);
final NiFiUser nifiUser = ((NiFiUserDetails)
authenticationResponse.getDetails()).getNiFiUser();
@@ -1328,7 +1334,7 @@ public class AccessResource extends ApplicationResource {
value = "Creates a token for accessing the REST API via Kerberos
ticket exchange / SPNEGO negotiation",
notes = "The token returned is formatted as a JSON Web Token
(JWT). The token is base64 encoded and comprised of three parts. The header, " +
"the body, and the signature. The expiration of the token
is a contained within the body. The token can be used in the Authorization
header " +
- "in the format 'Authorization: Bearer <token>'.",
+ "in the format 'Authorization: Bearer <token>'. It is also
stored in the browser as a cookie.",
response = String.class
)
@ApiResponses(
@@ -1383,7 +1389,7 @@ public class AccessResource extends ApplicationResource {
// build the response
final URI uri = URI.create(generateResourceUri("access",
"kerberos"));
- return generateCreatedResponse(uri, token).build();
+ return generateTokenResponse(generateCreatedResponse(uri,
token), token);
} catch (final AuthenticationException e) {
throw new AccessDeniedException(e.getMessage(), e);
}
@@ -1391,12 +1397,12 @@ public class AccessResource extends ApplicationResource
{
}
/**
- * Creates a token for accessing the REST API via username/password.
+ * Creates a token for accessing the REST API via username/password stored
as a cookie in the browser.
*
* @param httpServletRequest the servlet request
* @param username the username
* @param password the password
- * @return A JWT (string)
+ * @return A JWT (string) in a cookie and as the body
*/
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@@ -1405,8 +1411,8 @@ public class AccessResource extends ApplicationResource {
@ApiOperation(
value = "Creates a token for accessing the REST API via
username/password",
notes = "The token returned is formatted as a JSON Web Token
(JWT). The token is base64 encoded and comprised of three parts. The header, " +
- "the body, and the signature. The expiration of the token
is a contained within the body. The token can be used in the Authorization
header " +
- "in the format 'Authorization: Bearer <token>'.",
+ "the body, and the signature. The expiration of the token
is a contained within the body. It is stored in the browser as a cookie, but
also returned in" +
+ "the response body to be stored/used by third party client
scripts.",
response = String.class
)
@ApiResponses(
@@ -1459,7 +1465,7 @@ public class AccessResource extends ApplicationResource {
// build the response
final URI uri = URI.create(generateResourceUri("access", "token"));
- return generateCreatedResponse(uri, token).build();
+ return generateTokenResponse(generateCreatedResponse(uri, token),
token);
}
@DELETE
@@ -1490,8 +1496,9 @@ public class AccessResource extends ApplicationResource {
try {
logger.info("Logging out " + mappedUserIdentity);
-
jwtService.logOutUsingAuthHeader(httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION));
- logger.info("Successfully invalidated JWT for " +
mappedUserIdentity);
+ logOutUser(httpServletRequest);
+ removeCookie(httpServletResponse,
NiFiBearerTokenResolver.JWT_COOKIE_NAME);
+ logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
// create a LogoutRequest and tell the LogoutRequestManager about
it for later retrieval
final LogoutRequest logoutRequest = new
LogoutRequest(UUID.randomUUID().toString(), mappedUserIdentity);
@@ -1507,7 +1514,10 @@ public class AccessResource extends ApplicationResource {
return generateOkResponse().build();
} catch (final JwtException e) {
- logger.error("Logout of user " + mappedUserIdentity + " failed due
to: " + e.getMessage(), e);
+ logger.error("JWT processing failed for [{}], due to: ",
mappedUserIdentity, e.getMessage(), e);
+ return Response.serverError().build();
+ } catch (final LogoutException e) {
+ logger.error("Logout failed for user [{}] due to: ",
mappedUserIdentity, e.getMessage(), e);
return Response.serverError().build();
}
}
@@ -1543,7 +1553,7 @@ public class AccessResource extends ApplicationResource {
LogoutRequest logoutRequest = null;
// check if a logout request identifier is present and if so complete
the request
- final String logoutRequestIdentifier =
getCookieValue(httpServletRequest.getCookies(), LOGOUT_REQUEST_IDENTIFIER);
+ final String logoutRequestIdentifier =
WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
if (logoutRequestIdentifier != null) {
logoutRequest =
logoutRequestManager.complete(logoutRequestIdentifier);
}
@@ -1577,25 +1587,6 @@ public class AccessResource extends ApplicationResource {
return proposedTokenExpiration;
}
- /**
- * Gets the value of a cookie matching the specified name. If no cookie
with that name exists, null is returned.
- *
- * @param cookies the cookies
- * @param name the name of the cookie
- * @return the value of the corresponding cookie, or null if the cookie
does not exist
- */
- private String getCookieValue(final Cookie[] cookies, final String name) {
- if (cookies != null) {
- for (final Cookie cookie : cookies) {
- if (name.equals(cookie.getName())) {
- return cookie.getValue();
- }
- }
- }
-
- return null;
- }
-
private String getOidcCallback() {
return generateResourceUri("access", "oidc", "callback");
}
@@ -1812,4 +1803,14 @@ public class AccessResource extends ApplicationResource {
this.logoutRequestManager = logoutRequestManager;
}
+ private void logOutUser(HttpServletRequest httpServletRequest) {
+ final String jwt = new
NiFiBearerTokenResolver().resolve(httpServletRequest);
+ jwtService.logOut(jwt);
+ }
+
+ private Response generateTokenResponse(ResponseBuilder builder, String
token) {
+ // currently there is no way to use javax.servlet-api to set
SameSite=Strict, so we do this using Jetty
+ HttpCookie jwtCookie = new
HttpCookie(NiFiBearerTokenResolver.JWT_COOKIE_NAME, token, null, "/",
VALID_FOR_SESSION_ONLY, true, true, null, 0, HttpCookie.SameSite.STRICT);
+ return builder.header(HttpHeader.SET_COOKIE.asString(),
jwtCookie.getRFC6265SetCookie()).build();
+ }
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/LogoutException.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/LogoutException.java
new file mode 100644
index 0000000..4af1191
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/LogoutException.java
@@ -0,0 +1,35 @@
+/*
+ * 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.nifi.web.security;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * Thrown if the authentication of a given request is invalid. For instance,
+ * an expired certificate or token.
+ */
+public class LogoutException extends AuthenticationException {
+
+ public LogoutException(String msg) {
+ super(msg);
+ }
+
+ public LogoutException(String msg, Throwable t) {
+ super(msg, t);
+ }
+
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/BearerTokenResolver.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/BearerTokenResolver.java
new file mode 100644
index 0000000..caf8789
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/BearerTokenResolver.java
@@ -0,0 +1,30 @@
+/*
+ * 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.nifi.web.security.jwt;
+
+import javax.servlet.http.HttpServletRequest;
+
+public interface BearerTokenResolver {
+ /**
+ * Resolve any
+ * <a href="https://tools.ietf.org/html/rfc6750#section-1.2"
target="_blank">Bearer
+ * Token</a> value from the request.
+ * @param request the request
+ * @return the Bearer Token value or {@code null} if none found
+ */
+ String resolve(HttpServletRequest request);
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
index 9a25ff3..6ac2401 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
@@ -16,60 +16,35 @@
*/
package org.apache.nifi.web.security.jwt;
-import org.apache.nifi.web.security.InvalidAuthenticationException;
+import org.apache.nifi.util.StringUtils;
import org.apache.nifi.web.security.NiFiAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import javax.servlet.http.HttpServletRequest;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-/**
- */
public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
private static final Logger logger =
LoggerFactory.getLogger(JwtAuthenticationFilter.class);
// The Authorization header contains authentication credentials
- public static final String AUTHORIZATION = "Authorization";
- private static final Pattern tokenPattern = Pattern.compile("^Bearer
(\\S*\\.\\S*\\.\\S*)$");
+ private static NiFiBearerTokenResolver bearerTokenResolver = new
NiFiBearerTokenResolver();
@Override
public Authentication attemptAuthentication(final HttpServletRequest
request) {
- // only support jwt login when running securely
+ // Only support JWT login when running securely
if (!request.isSecure()) {
return null;
}
- // TODO: Refactor request header extraction logic to shared utility as
it is duplicated in AccessResource
-
- // get the principal out of the user token
- final String authorizationHeader = request.getHeader(AUTHORIZATION);
+ // Get JWT from Authorization header or cookie value
+ final String headerToken = bearerTokenResolver.resolve(request);
- // if there is no authorization header, we don't know the user
- if (authorizationHeader == null ||
!validJwtFormat(authorizationHeader)) {
- return null;
+ if (StringUtils.isNotBlank(headerToken)) {
+ return new JwtAuthenticationRequestToken(headerToken,
request.getRemoteAddr());
} else {
- // Extract the Base64 encoded token from the Authorization header
- final String token = getTokenFromHeader(authorizationHeader);
- return new JwtAuthenticationRequestToken(token,
request.getRemoteAddr());
- }
- }
-
- private boolean validJwtFormat(String authenticationHeader) {
- Matcher matcher = tokenPattern.matcher(authenticationHeader);
- return matcher.matches();
- }
-
- public static String getTokenFromHeader(String authenticationHeader) {
- Matcher matcher = tokenPattern.matcher(authenticationHeader);
- if(matcher.matches()) {
- return matcher.group(1);
- } else {
- throw new InvalidAuthenticationException("JWT did not match
expected pattern.");
+ return null;
}
}
-
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
index 886ae46..d235d49 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
@@ -31,6 +31,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.admin.service.KeyService;
import org.apache.nifi.key.Key;
+import org.apache.nifi.web.security.LogoutException;
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
import org.slf4j.LoggerFactory;
@@ -186,7 +187,7 @@ public class JwtService {
* @throws JwtException if there is a problem with the token input
* @throws Exception if there is an issue logging the user out
*/
- public void logOut(String token) {
+ public void logOut(String token) throws LogoutException {
Jws<Claims> claims = parseTokenFromBase64EncodedString(token);
// Get the key ID from the claims
@@ -199,13 +200,9 @@ public class JwtService {
try {
keyService.deleteKey(keyId);
} catch (Exception e) {
- logger.error("The key with key ID: " + keyId + " failed to be
removed from the user database.");
- throw e;
+ final String errorMessage = String.format("The key with key ID: %s
failed to be removed from the user database.", keyId);
+ logger.error(errorMessage);
+ throw new LogoutException(errorMessage);
}
}
-
- public void logOutUsingAuthHeader(String authorizationHeader) {
- String base64EncodedToken =
JwtAuthenticationFilter.getTokenFromHeader(authorizationHeader);
- logOut(base64EncodedToken);
- }
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolver.java
similarity index 50%
copy from
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
copy to
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolver.java
index 9a25ff3..c936c4d 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolver.java
@@ -16,60 +16,55 @@
*/
package org.apache.nifi.web.security.jwt;
+import org.apache.nifi.util.StringUtils;
import org.apache.nifi.web.security.InvalidAuthenticationException;
-import org.apache.nifi.web.security.NiFiAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.security.core.Authentication;
+import org.springframework.web.util.WebUtils;
+import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-/**
- */
-public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
-
- private static final Logger logger =
LoggerFactory.getLogger(JwtAuthenticationFilter.class);
-
- // The Authorization header contains authentication credentials
+public class NiFiBearerTokenResolver implements BearerTokenResolver {
+ private static final Logger logger =
LoggerFactory.getLogger(NiFiBearerTokenResolver.class);
+ private static final Pattern BEARER_HEADER_PATTERN =
Pattern.compile("^Bearer (\\S*\\.\\S*\\.\\S*){1}$");
+ private static final Pattern JWT_PATTERN =
Pattern.compile("^(\\S*\\.\\S*\\.\\S*)$");
public static final String AUTHORIZATION = "Authorization";
- private static final Pattern tokenPattern = Pattern.compile("^Bearer
(\\S*\\.\\S*\\.\\S*)$");
+ public static final String JWT_COOKIE_NAME = "__Host-Authorization-Bearer";
@Override
- public Authentication attemptAuthentication(final HttpServletRequest
request) {
- // only support jwt login when running securely
- if (!request.isSecure()) {
- return null;
- }
-
- // TODO: Refactor request header extraction logic to shared utility as
it is duplicated in AccessResource
-
- // get the principal out of the user token
+ public String resolve(HttpServletRequest request) {
final String authorizationHeader = request.getHeader(AUTHORIZATION);
+ final Cookie cookieHeader = WebUtils.getCookie(request,
JWT_COOKIE_NAME);
- // if there is no authorization header, we don't know the user
- if (authorizationHeader == null ||
!validJwtFormat(authorizationHeader)) {
- return null;
+ if (StringUtils.isNotBlank(authorizationHeader) &&
validAuthorizationHeaderFormat(authorizationHeader)) {
+ return getTokenFromHeader(authorizationHeader);
+ } else if(cookieHeader != null &&
validJwtFormat(cookieHeader.getValue())) {
+ return cookieHeader.getValue();
} else {
- // Extract the Base64 encoded token from the Authorization header
- final String token = getTokenFromHeader(authorizationHeader);
- return new JwtAuthenticationRequestToken(token,
request.getRemoteAddr());
+ logger.debug("Authorization header was not present or not in a
valid format.");
+ return null;
}
}
- private boolean validJwtFormat(String authenticationHeader) {
- Matcher matcher = tokenPattern.matcher(authenticationHeader);
+ private boolean validAuthorizationHeaderFormat(String authorizationHeader)
{
+ Matcher matcher = BEARER_HEADER_PATTERN.matcher(authorizationHeader);
+ return matcher.matches();
+ }
+
+ private boolean validJwtFormat(String jwt) {
+ Matcher matcher = JWT_PATTERN.matcher(jwt);
return matcher.matches();
}
- public static String getTokenFromHeader(String authenticationHeader) {
- Matcher matcher = tokenPattern.matcher(authenticationHeader);
- if(matcher.matches()) {
+ private String getTokenFromHeader(String authenticationHeader) {
+ Matcher matcher = BEARER_HEADER_PATTERN.matcher(authenticationHeader);
+ if (matcher.matches()) {
return matcher.group(1);
} else {
throw new InvalidAuthenticationException("JWT did not match
expected pattern.");
}
}
-
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/jwt/JwtAuthenticationFilterTest.groovy
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/jwt/JwtAuthenticationFilterTest.groovy
deleted file mode 100644
index dcabb0f..0000000
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/jwt/JwtAuthenticationFilterTest.groovy
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * 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.nifi.web.security.jwt
-
-import groovy.json.JsonOutput
-import org.apache.nifi.web.security.InvalidAuthenticationException
-import org.junit.After
-import org.junit.AfterClass
-import org.junit.Before
-import org.junit.BeforeClass
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.ExpectedException
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4.class)
-class JwtAuthenticationFilterTest extends GroovyTestCase {
-
- public static String jwtString
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none()
-
- @BeforeClass
- static void setUpOnce() throws Exception {
- final String ALG_HEADER = "{\"alg\":\"HS256\"}"
- final int EXPIRATION_SECONDS = 500
- Calendar now = Calendar.getInstance()
- final long currentTime = (long) (now.getTimeInMillis() / 1000.0)
- final long TOKEN_ISSUED_AT = currentTime
- final long TOKEN_EXPIRATION_SECONDS = currentTime + EXPIRATION_SECONDS
-
- // Generate a token that we will add a valid signature from a
different token
- // Always use LinkedHashMap to enforce order of the keys because the
signature depends on order
- final String EXPECTED_PAYLOAD =
- JsonOutput.toJson(
- sub:'unknownuser',
- iss:'MockIdentityProvider',
- aud:'MockIdentityProvider',
- preferred_username:'unknownuser',
- kid:1,
- exp:TOKEN_EXPIRATION_SECONDS,
- iat:TOKEN_ISSUED_AT)
-
- // Set up our JWT string with a test token
- jwtString = JwtServiceTest.generateHS256Token(ALG_HEADER,
EXPECTED_PAYLOAD, true, true)
-
- }
-
- @AfterClass
- static void tearDownOnce() throws Exception {
-
- }
-
- @Before
- void setUp() throws Exception {
-
- }
-
- @After
- void tearDown() throws Exception {
-
- }
-
- @Test
- void testValidAuthenticationHeaderString() {
- // Arrange
- String authenticationHeader = "Bearer " + jwtString
-
- // Act
- boolean isValidHeader = new
JwtAuthenticationFilter().validJwtFormat(authenticationHeader)
-
- // Assert
- assertTrue(isValidHeader)
- }
-
- @Test
- void testMissingBearer() {
- // Arrange
-
- // Act
- boolean isValidHeader = new
JwtAuthenticationFilter().validJwtFormat(jwtString)
-
- // Assert
- assertFalse(isValidHeader)
- }
-
- @Test
- void testExtraCharactersAtBeginningOfToken() {
- // Arrange
- String authenticationHeader = "xBearer " + jwtString
-
- // Act
- boolean isValidToken = new
JwtAuthenticationFilter().validJwtFormat(authenticationHeader)
-
- // Assert
- assertFalse(isValidToken)
- }
-
- @Test
- void testBadTokenFormat() {
- // Arrange
- String[] tokenStrings = jwtString.split("\\.")
- String badToken = "Bearer " + tokenStrings[1] + tokenStrings[2]
-
- // Act
- boolean isValidToken = new
JwtAuthenticationFilter().validJwtFormat(badToken)
-
- // Assert
- assertFalse(isValidToken)
- }
-
- @Test
- void testMultipleTokenInvalid() {
- // Arrange
- String authenticationHeader = "Bearer " + jwtString
- authenticationHeader = authenticationHeader + " " +
authenticationHeader
-
- // Act
- boolean isValidToken = new
JwtAuthenticationFilter().validJwtFormat(authenticationHeader)
-
- // Assert
- assertFalse(isValidToken)
- }
-
- @Test
- void testExtractToken() {
- // Arrange
- String authenticationHeader = "Bearer " + jwtString
-
- // Act
- String extractedToken = new
JwtAuthenticationFilter().getTokenFromHeader(authenticationHeader)
-
- // Assert
- assertEquals(jwtString, extractedToken)
- }
-
- @Test
- void testMultipleTokenDottedInvalid() {
- // Arrange
- String authenticationHeader = "Bearer " + jwtString
- authenticationHeader = authenticationHeader + "." +
authenticationHeader
-
- // Act
- boolean isValidToken = new
JwtAuthenticationFilter().validJwtFormat(authenticationHeader)
-
- // Assert
- assertFalse(isValidToken)
- }
-
- @Test
- void testMultipleTokenNotExtracted() {
- // Arrange
- expectedException.expect(InvalidAuthenticationException.class)
- expectedException.expectMessage("JWT did not match expected pattern.")
- String authenticationHeader = "Bearer " + jwtString
- authenticationHeader = authenticationHeader + " " +
authenticationHeader
-
- // Act
- String token = new
JwtAuthenticationFilter().getTokenFromHeader(authenticationHeader)
-
- // Assert
- // Expect InvalidAuthenticationException
- }
-}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
index eb1f5ee..513eb0c 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
@@ -599,38 +599,6 @@ public class JwtServiceTest {
}
@Test
- public void testShouldLogOutUserUsingAuthHeader() throws Exception {
- // Arrange
- expectedException.expect(JwtException.class);
- expectedException.expectMessage("Unable to validate the access
token.");
-
- // Token expires in 60 seconds
- final int EXPIRATION_MILLIS = 60000;
- LoginAuthenticationToken loginAuthenticationToken = new
LoginAuthenticationToken(DEFAULT_IDENTITY,
- EXPIRATION_MILLIS,
- "MockIdentityProvider");
- logger.info("Generating token for " + loginAuthenticationToken);
-
- // Act
- String token =
jwtService.generateSignedToken(loginAuthenticationToken);
-
- logger.info("Generated JWT: " + token);
- logger.info("Validating token...");
- String authID = jwtService.getAuthenticationFromToken(token);
- assertEquals(DEFAULT_IDENTITY, authID);
- logger.info("Token was valid");
- logger.info("Logging out user: " + authID);
- String header = "Bearer " + token;
- jwtService.logOutUsingAuthHeader(header);
- logger.info("Logged out user: " + authID);
- logger.info("Checking that token is now invalid...");
- jwtService.getAuthenticationFromToken(token);
-
- // Assert
- // Should throw exception when user is not found
- }
-
- @Test
public void testLogoutWhenAuthTokenIsEmptyShouldThrowError() throws
Exception {
// Arrange
expectedException.expect(JwtException.class);
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolverTest.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolverTest.java
new file mode 100644
index 0000000..e14c7ed
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolverTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.nifi.web.security.jwt;
+
+import groovy.json.JsonOutput;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class NiFiBearerTokenResolverTest {
+
+ public static String jwtString;
+
+ @Mock
+ private static HttpServletRequest request;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ final String ALG_HEADER = "{\"alg\":\"HS256\"}";
+ final int EXPIRATION_SECONDS = 500;
+ Calendar now = Calendar.getInstance();
+ final long currentTime = (long) (now.getTimeInMillis() / 1000.0);
+ final long TOKEN_ISSUED_AT = currentTime;
+ final long TOKEN_EXPIRATION_SECONDS = currentTime + EXPIRATION_SECONDS;
+
+ Map<String, String> hashMap = new HashMap<String, String>() {{
+ put("sub", "unknownuser");
+ put("iss", "MockIdentityProvider");
+ put("aud", "MockIdentityProvider");
+ put("preferred_username", "unknownuser");
+ put("kid", String.valueOf(1));
+ put("exp", String.valueOf(TOKEN_EXPIRATION_SECONDS));
+ put("iat", String.valueOf(TOKEN_ISSUED_AT));
+ }};
+
+ // Generate a token that we will add a valid signature from a
different token
+ // Always use LinkedHashMap to enforce order of the keys because the
signature depends on order
+ final String EXPECTED_PAYLOAD = JsonOutput.toJson(hashMap);
+
+ // Set up our JWT string with a test token
+ jwtString = JwtServiceTest.generateHS256Token(ALG_HEADER,
EXPECTED_PAYLOAD, true, true);
+
+ request = mock(HttpServletRequest.class);
+ }
+
+ @Test
+ public void testValidAuthenticationHeaderString() {
+ String authenticationHeader = "Bearer " + jwtString;
+
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
+ String isValidHeader = new NiFiBearerTokenResolver().resolve(request);
+
+ assertEquals(jwtString, isValidHeader);
+ }
+
+ @Test
+ public void testMissingBearer() {
+ String authenticationHeader = jwtString;
+
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
+ String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
+
+ assertNull(resolvedToken);
+ }
+
+ @Test
+ public void testExtraCharactersAtBeginningOfToken() {
+ String authenticationHeader = "xBearer " + jwtString;
+
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
+ String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
+
+ assertNull(resolvedToken);
+ }
+
+ @Test
+ public void testBadTokenFormat() {
+ String[] tokenStrings = jwtString.split("\\.");
+
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(String.valueOf("Bearer
" + tokenStrings[1] + tokenStrings[2]));
+ String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
+
+ assertNull(resolvedToken);
+ }
+
+ @Test
+ public void testMultipleTokenInvalid() {
+ String authenticationHeader = "Bearer " + jwtString;
+
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(String.format("%s
%s", authenticationHeader, authenticationHeader));
+ String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
+
+ assertNull(resolvedToken);
+ }
+
+ @Test
+ public void testExtractToken() {
+ String authenticationHeader = "Bearer " + jwtString;
+
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
+ String extractedToken = new NiFiBearerTokenResolver().resolve(request);
+
+ assertEquals(jwtString, extractedToken);
+ }
+}
\ No newline at end of file
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-error-handler.js
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-error-handler.js
index ad4305f..17464c8 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-error-handler.js
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-error-handler.js
@@ -45,7 +45,7 @@
// In case no further requests will be successful based on the status,
// the canvas is disabled, and the message pane is shown.
if ($('#message-pane').is(':visible')) {
- nfCommon.showLogoutLink();
+ nfCommon.updateLogoutLink();
// hide the splash screen if required
if ($('#splash').is(':visible')) {
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
index 3c1b95f..72cd6c2 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
@@ -99,7 +99,7 @@
'password': $('#password').val()
}
}).done(function (jwt) {
- // get the payload and store the token with the appropirate
expiration
+ // Get the payload and store the token with the appropriate
expiration. JWT is also stored automatically in a cookie.
var token = nfCommon.getJwtPayload(jwt);
var expiration = parseInt(token['exp'], 10) *
nfCommon.MILLIS_PER_SECOND;
nfStorage.setItem('jwt', jwt, expiration);
@@ -112,9 +112,6 @@
}).done(function (response) {
var accessStatus = response.accessStatus;
- // update the logout link appropriately
- showLogoutLink();
-
// update according to the access status
if (accessStatus.status === 'ACTIVE') {
// reload as appropriate - no need to schedule token
refresh as the page is reloading
@@ -155,10 +152,6 @@
});
};
- var showLogoutLink = function () {
- nfCommon.showLogoutLink();
- };
-
var nfLogin = {
/**
* Initializes the login page.
@@ -166,9 +159,7 @@
init: function () {
nfStorage.init();
- if (nfStorage.getItem('jwt') !== null) {
- showLogoutLink();
- }
+ nfCommon.updateLogoutLink();
// supporting logging in via enter press
$('#username, #password').on('keyup', function (e) {
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
index be6b370..2b79896 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
@@ -852,11 +852,11 @@
/**
* Shows the logout link if appropriate.
*/
- showLogoutLink: function () {
- if (nfStorage.getItem('jwt') === null) {
- $('#user-logout-container').css('display', 'none');
- } else {
+ updateLogoutLink: function () {
+ if (nfStorage.getItem('jwt') !== null) {
$('#user-logout-container').css('display', 'block');
+ } else {
+ $('#user-logout-container').css('display', 'none');
}
},