mirror of https://github.com/apache/nifi.git
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 <alopresto@apache.org>
This commit is contained in:
parent
8a50cb10b2
commit
fe68d43e1d
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
<!--
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue