This is an automated email from the ASF dual-hosted git repository.
alopresto pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/master by this push:
new fe68d43 NIFI-6280 - Broke out the matching for /access/knox/** and
/access/oidc/** to allow the Jetty security filters to be applied in the
/access/oidc/logout and /access/knox/logout cases. NIFI-6280 - Updated
terminology in JwtAuthenticationFilter to authentication instead of
authorization. Added stricter token parsing using an explicit regex pattern.
Added tests. NIFI-6280 - Updated terminology from Authorization to
Authentication. NIFI-6280 - Updated the access logout method [...]
fe68d43 is described below
commit fe68d43e1dfd00807ccbc43fd43bff73d49de26c
Author: thenatog <[email protected]>
AuthorDate: Thu May 16 13:19:47 2019 -0400
NIFI-6280 - Broke out the matching for /access/knox/** and /access/oidc/**
to allow the Jetty security filters to be applied in the /access/oidc/logout
and /access/knox/logout cases.
NIFI-6280 - Updated terminology in JwtAuthenticationFilter to
authentication instead of authorization. Added stricter token parsing using an
explicit regex pattern. Added tests.
NIFI-6280 - Updated terminology from Authorization to Authentication.
NIFI-6280 - Updated the access logout method to use getNiFiUserIdentity().
Updated javascript logout method to handle errors.
NIFI-6280 - Fixing checkstyle issues.
NIFI-6280 - Added some javadoc comments and logging. Renamed some variables
for clarity. Fixed handling of exception when JWT does not match expected
format.
NIFI-6280 - Cleaned up checkstyle, increased log severity level for logout
action, and cleaned up Groovy syntax in test.
This closes #3482.
Signed-off-by: Andy LoPresto <[email protected]>
---
.../apache/nifi/record/path/TestRecordPath.java | 27 ++--
.../src/main/resources/conf/logback.xml | 3 +
.../nifi/web/NiFiWebApiSecurityConfiguration.java | 7 +-
.../org/apache/nifi/web/api/AccessResource.java | 69 ++++----
.../accesscontrol/ITAccessTokenEndpoint.java | 1 -
.../web/security/NiFiAuthenticationFilter.java | 43 +++--
.../nifi/web/security/ProxiedEntitiesUtils.java | 18 ++-
.../web/security/jwt/JwtAuthenticationFilter.java | 28 +++-
.../apache/nifi/web/security/jwt/JwtService.java | 13 +-
.../jwt/JwtAuthenticationFilterTest.groovy | 180 +++++++++++++++++++++
.../nifi/web/security/jwt/JwtServiceTest.java | 3 +-
.../controllers/nf-ng-canvas-header-controller.js | 10 +-
12 files changed, 317 insertions(+), 85 deletions(-)
diff --git
a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
index 848886f..8714b33 100644
---
a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
+++
b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
@@ -17,16 +17,9 @@
package org.apache.nifi.record.path;
-import org.apache.nifi.record.path.exception.RecordPathException;
-import org.apache.nifi.serialization.SimpleRecordSchema;
-import org.apache.nifi.serialization.record.DataType;
-import org.apache.nifi.serialization.record.MapRecord;
-import org.apache.nifi.serialization.record.Record;
-import org.apache.nifi.serialization.record.RecordField;
-import org.apache.nifi.serialization.record.RecordFieldType;
-import org.apache.nifi.serialization.record.RecordSchema;
-import org.apache.nifi.serialization.record.util.DataTypeUtils;
-import org.junit.Test;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
@@ -42,10 +35,16 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import org.apache.nifi.record.path.exception.RecordPathException;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.serialization.record.util.DataTypeUtils;
+import org.junit.Test;
public class TestRecordPath {
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/logback.xml
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/logback.xml
index cf3af88..a16a49e 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/logback.xml
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/logback.xml
@@ -140,6 +140,9 @@
<logger name="org.apache.nifi.web.filter.RequestLogger" level="INFO"
additivity="false">
<appender-ref ref="USER_FILE"/>
</logger>
+ <logger name="org.apache.nifi.web.api.AccessResource" level="INFO"
additivity="false">
+ <appender-ref ref="USER_FILE"/>
+ </logger>
<!--
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 d7fd89b..647e6c8 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
@@ -50,7 +50,8 @@ import
org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
/**
- * NiFi Web Api Spring security
+ * NiFi Web Api Spring security. Applies the various NiFiAuthenticationFilter
servlet filters which will extract authentication
+ * credentials from API requests.
*/
@Configuration
@EnableWebSecurity
@@ -88,7 +89,9 @@ public class NiFiWebApiSecurityConfiguration extends
WebSecurityConfigurerAdapte
// the /access/download-token and /access/ui-extension-token endpoints
webSecurity
.ignoring()
- .antMatchers("/access", "/access/config", "/access/token",
"/access/kerberos", "/access/oidc/**", "/access/knox/**");
+ .antMatchers("/access", "/access/config", "/access/token",
"/access/kerberos",
+ "/access/oidc/exchange", "/access/oidc/callback",
"/access/oidc/request",
+ "/access/knox/callback", "/access/knox/request");
}
@Override
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 8796dce..8b632f8 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
@@ -29,6 +29,25 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
+import java.net.URI;
+import java.security.cert.X509Certificate;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletContext;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.authentication.AuthenticationResponse;
@@ -69,25 +88,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
-import javax.servlet.ServletContext;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.FormParam;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
-import java.net.URI;
-import java.security.cert.X509Certificate;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-
/**
* RESTful endpoint for managing access.
*/
@@ -103,6 +103,7 @@ public class AccessResource extends ApplicationResource {
private static final String OIDC_REQUEST_IDENTIFIER =
"oidc-request-identifier";
private static final String OIDC_ERROR_TITLE = "Unable to continue login
sequence";
+
private X509CertificateExtractor certificateExtractor;
private X509AuthenticationProvider x509AuthenticationProvider;
private X509PrincipalExtractor principalExtractor;
@@ -344,9 +345,6 @@ public class AccessResource extends ApplicationResource {
.build();
httpServletResponse.sendRedirect(logoutUri.toString());
}
-
- String authorizationHeader =
httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION);
- jwtService.logOut(authorizationHeader);
}
@GET
@@ -375,7 +373,7 @@ public class AccessResource extends ApplicationResource {
// build the authorization uri
final URI authorizationUri =
UriBuilder.fromUri(knoxService.getKnoxUrl())
- .queryParam("originalUrl", originalUri.toString())
+ .queryParam("originalUrl", originalUri)
.build();
// generate the response
@@ -416,7 +414,7 @@ public class AccessResource extends ApplicationResource {
)
public void knoxLogout(@Context HttpServletRequest httpServletRequest,
@Context HttpServletResponse httpServletResponse) throws Exception {
String redirectPath = generateResourceUri("..", "nifi", "login");
- httpServletResponse.sendRedirect(redirectPath.toString());
+ httpServletResponse.sendRedirect(redirectPath);
}
/**
@@ -747,7 +745,7 @@ public class AccessResource extends ApplicationResource {
return generateCreatedResponse(uri, token).build();
}
- @GET
+ @DELETE
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.WILDCARD)
@Path("/logout")
@@ -758,6 +756,7 @@ public class AccessResource extends ApplicationResource {
@ApiResponses(
value = {
@ApiResponse(code = 200, message = "User was logged out
successfully."),
+ @ApiResponse(code = 401, message = "Authentication token
provided was empty or not in the correct JWT format."),
@ApiResponse(code = 500, message = "Client failed to log
out."),
}
)
@@ -766,13 +765,19 @@ public class AccessResource extends ApplicationResource {
throw new IllegalStateException("User authentication/authorization
is only supported when running over HTTPS.");
}
- String authorizationHeader =
httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION);
- final String token =
StringUtils.substringAfterLast(authorizationHeader, " ");
- try {
- jwtService.logOut(token);
- return generateOkResponse().build();
- } catch (final JwtException e) {
- return Response.serverError().build();
+ String userIdentity = NiFiUserUtils.getNiFiUserIdentity();
+
+ if(userIdentity != null && !userIdentity.isEmpty()) {
+ try {
+ logger.info("Logging out user " + userIdentity);
+ jwtService.logOut(userIdentity);
+ return generateOkResponse().build();
+ } catch (final JwtException e) {
+ logger.error("Logout of user " + userIdentity + " failed due
to: " + e.getMessage());
+ return Response.serverError().build();
+ }
+ } else {
+ return Response.status(401, "Authentication token provided was
empty or not in the correct JWT format.").build();
}
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
index 406619f..0dc359f 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
@@ -410,7 +410,6 @@ public class ITAccessTokenEndpoint {
String logoutUrl = BASE_URL + "/access/logout";
Response response = TOKEN_USER.testCreateToken(accessTokenUrl, user,
password);
- Response responseA = TOKEN_USER.testCreateToken(accessTokenUrl,
"jack", password);
// ensure the request is successful
Assert.assertEquals(201, response.getStatus());
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java
index 030b19e..f07e43b 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java
@@ -38,7 +38,8 @@ import java.io.IOException;
import java.io.PrintWriter;
/**
- *
+ * The NiFiAuthenticationFilter abstract class defines the base methods for
NiFi's various existing and future
+ * authentication mechanisms. The subclassed filters are applied in
NiFiWebApiSecurityConfiguration.
*/
public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
@@ -74,16 +75,16 @@ public abstract class NiFiAuthenticationFilter extends
GenericFilterBean {
log.info(String.format("Attempting request for (%s) %s %s
(source ip: %s)", authenticationRequest.toString(), request.getMethod(),
request.getRequestURL().toString(),
request.getRemoteAddr()));
- // attempt to authorize the user
+ // attempt to authenticate the user
final Authentication authenticated =
authenticationManager.authenticate(authenticationRequest);
- successfulAuthorization(request, response, authenticated);
+ successfulAuthentication(request, response, authenticated);
}
} catch (final AuthenticationException ae) {
// invalid authentication - always error out
- unsuccessfulAuthorization(request, response, ae);
+ unsuccessfulAuthentication(request, response, ae);
return;
} catch (final Exception e) {
- log.error(String.format("Unable to authorize: %s",
e.getMessage()), e);
+ log.error(String.format("Unable to authenticate: %s",
e.getMessage()), e);
// set the response status
response.setContentType("text/plain");
@@ -91,7 +92,7 @@ public abstract class NiFiAuthenticationFilter extends
GenericFilterBean {
// other exception - always error out
PrintWriter out = response.getWriter();
- out.println(String.format("Failed to authorize request. Please
contact the system administrator."));
+ out.println(String.format("Failed to authenticate request. Please
contact the system administrator."));
return;
}
@@ -107,16 +108,32 @@ public abstract class NiFiAuthenticationFilter extends
GenericFilterBean {
*/
public abstract Authentication attemptAuthentication(HttpServletRequest
request);
- protected void successfulAuthorization(HttpServletRequest request,
HttpServletResponse response, Authentication authResult) {
+ /**
+ * If authentication was successful, apply the successful authentication
result to the security context and add
+ * proxy headers to the response if the request was made via a proxy.
+ *
+ * @param request The original client request that was successfully
authenticated.
+ * @param response Servlet response to the client containing the
successful authentication details.
+ * @param authResult The Authentication 'token'/object created by one of
the various NiFiAuthenticationFilter subclasses.
+ */
+ protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, Authentication authResult) {
log.info("Authentication success for " + authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
- ProxiedEntitiesUtils.successfulAuthorization(request, response,
authResult);
+ ProxiedEntitiesUtils.successfulAuthentication(request, response);
}
- protected void unsuccessfulAuthorization(HttpServletRequest request,
HttpServletResponse response, AuthenticationException ae) throws IOException {
+ /**
+ * If authentication was unsuccessful, update the response with the
appropriate status and give the reason for why
+ * the user was not able to be authenticated. Update the response with
proxy headers if the request was made via a proxy.
+ *
+ * @param request The original client request that failed to be
authenticated.
+ * @param response Servlet response to the client containing the
unsuccessful authentication attempt details.
+ * @param ae The related exception thrown and explanation for the
unsuccessful authentication attempt.
+ */
+ protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException ae) throws IOException {
// populate the response
- ProxiedEntitiesUtils.unsuccessfulAuthorization(request, response, ae);
+ ProxiedEntitiesUtils.unsuccessfulAuthentication(request, response, ae);
// set the response status
response.setContentType("text/plain");
@@ -132,11 +149,11 @@ public abstract class NiFiAuthenticationFilter extends
GenericFilterBean {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println(ae.getMessage());
} else if (ae instanceof AuthenticationServiceException) {
- log.error(String.format("Unable to authorize: %s",
ae.getMessage()), ae);
+ log.error(String.format("Unable to authenticate: %s",
ae.getMessage()), ae);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- out.println(String.format("Unable to authorize: %s",
ae.getMessage()));
+ out.println(String.format("Unable to authenticate: %s",
ae.getMessage()));
} else {
- log.error(String.format("Unable to authorize: %s",
ae.getMessage()), ae);
+ log.error(String.format("Unable to authenticate: %s",
ae.getMessage()), ae);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println("Access is denied.");
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ProxiedEntitiesUtils.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ProxiedEntitiesUtils.java
index 09f45bf..2c3c0ea 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ProxiedEntitiesUtils.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ProxiedEntitiesUtils.java
@@ -27,7 +27,6 @@ import org.apache.nifi.authorization.user.NiFiUser;
import org.apache.nifi.authorization.user.NiFiUserUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
@@ -148,13 +147,26 @@ public class ProxiedEntitiesUtils {
return StringUtils.join(proxyChain, "");
}
- public static void successfulAuthorization(HttpServletRequest request,
HttpServletResponse response, Authentication authResult) {
+ /**
+ * If a successfully authenticated request was made via a proxy, relevant
proxy headers will be added to the response.
+ *
+ * @param request The proxied client request that was successfully
authenticated.
+ * @param response A servlet response to the client containing the
successful authentication details.
+ */
+ public static void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response) {
if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
response.setHeader(PROXY_ENTITIES_ACCEPTED,
Boolean.TRUE.toString());
}
}
- public static void unsuccessfulAuthorization(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed) {
+ /**
+ * If an unauthenticated request was made via a proxy, add proxy headers
to explain why authentication failed.
+ *
+ * @param request The original client request that failed to be
authenticated.
+ * @param response Servlet response to the client containing the
unsuccessful authentication attempt details.
+ * @param failed The related exception thrown and explanation for the
unsuccessful authentication attempt.
+ */
+ public static void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed) {
if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
response.setHeader(PROXY_ENTITIES_DETAILS, failed.getMessage());
}
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 b237041..0d21a8a 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,13 +16,15 @@
*/
package org.apache.nifi.web.security.jwt;
-import org.apache.commons.lang3.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 javax.servlet.http.HttpServletRequest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
*/
@@ -30,8 +32,9 @@ 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";
- public static final String BEARER = "Bearer ";
+ private static final Pattern tokenPattern = Pattern.compile("^Bearer
(\\S*\\.\\S*\\.\\S*)$");
@Override
public Authentication attemptAuthentication(final HttpServletRequest
request) {
@@ -43,15 +46,30 @@ public class JwtAuthenticationFilter extends
NiFiAuthenticationFilter {
// 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 authorization = request.getHeader(AUTHORIZATION);
+ final String authorizationHeader = request.getHeader(AUTHORIZATION);
// if there is no authorization header, we don't know the user
- if (authorization == null || !StringUtils.startsWith(authorization,
BEARER)) {
+ if (authorizationHeader == null ||
!validJwtFormat(authorizationHeader)) {
return null;
} else {
// Extract the Base64 encoded token from the Authorization header
- final String token = StringUtils.substringAfterLast(authorization,
" ");
+ final String token = getTokenFromHeader(authorizationHeader);
return new JwtAuthenticationRequestToken(token,
request.getRemoteAddr());
}
}
+
+ private boolean validJwtFormat(String authenticationHeader) {
+ Matcher matcher = tokenPattern.matcher(authenticationHeader);
+ return matcher.matches();
+ }
+
+ private 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.");
+ }
+ }
+
}
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 63392a8..ecbfc67 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
@@ -32,7 +32,6 @@ import java.util.Calendar;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.admin.service.KeyService;
-import org.apache.nifi.authorization.user.NiFiUserUtils;
import org.apache.nifi.key.Key;
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
import org.slf4j.LoggerFactory;
@@ -170,17 +169,15 @@ public class JwtService {
}
}
- public void logOut(String authorizationHeader) {
- if (authorizationHeader == null || authorizationHeader.isEmpty()) {
- throw new JwtException("Log out failed: The required Authorization
header was not present in the request to log out user.");
+ public void logOut(String userIdentity) {
+ if (userIdentity == null || userIdentity.isEmpty()) {
+ throw new JwtException("Log out failed: The user identity was not
present in the request token to log out user.");
}
- String identity = NiFiUserUtils.getNiFiUserIdentity();
-
try {
- keyService.deleteKey(identity);
+ keyService.deleteKey(userIdentity);
} catch (Exception e) {
- logger.error("Unable to log out user: " + identity + ". Failed to
remove their token from database.");
+ logger.error("Unable to log out user: " + userIdentity + ". Failed
to remove their token from database.");
throw e;
}
}
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
new file mode 100644
index 0000000..dcabb0f
--- /dev/null
+++
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
@@ -0,0 +1,180 @@
+/*
+ * 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 368851e..1a1f95d 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
@@ -511,7 +511,7 @@ public class JwtServiceTest {
public void testLogoutWhenAuthTokenIsEmptyShouldThrowError() throws
Exception {
// Arrange
expectedException.expect(JwtException.class);
- expectedException.expectMessage("Log out failed: The required
Authorization header was not present in the request to log out user.");
+ expectedException.expectMessage("Log out failed: The user identity was
not present in the request token to log out user.");
// Act
jwtService.logOut(null);
@@ -520,5 +520,4 @@ public class JwtServiceTest {
// Should throw exception when authorization header is null
}
-
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-header-controller.js
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-header-controller.js
index 2f2cea7..c271348 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-header-controller.js
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-header-controller.js
@@ -118,12 +118,12 @@
this.logoutCtrl = {
logout: function () {
$.ajax({
- type: 'GET',
+ type: 'DELETE',
url: '../nifi-api/access/logout',
- dataType: 'json'
- })
- nfStorage.removeItem("jwt");
- window.location = '../nifi/logout';
+ }).done(function () {
+ nfStorage.removeItem("jwt");
+ window.location = '../nifi/logout';
+ }).fail(nfErrorHandler.handleAjaxError);
}
};
}