mirror of https://github.com/apache/nifi.git
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 <exceptionfactory@apache.org>
This commit is contained in:
parent
3963f66dff
commit
1090a9748a
|
@ -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, "; "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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 JWT from Authorization header or cookie value
|
||||
final String headerToken = bearerTokenResolver.resolve(request);
|
||||
|
||||
// get the principal out of the user token
|
||||
final String authorizationHeader = request.getHeader(AUTHORIZATION);
|
||||
|
||||
// if there is no authorization header, we don't know the user
|
||||
if (authorizationHeader == null || !validJwtFormat(authorizationHeader)) {
|
||||
if (StringUtils.isNotBlank(headerToken)) {
|
||||
return new JwtAuthenticationRequestToken(headerToken, request.getRemoteAddr());
|
||||
} else {
|
||||
return null;
|
||||
} 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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 org.apache.nifi.util.StringUtils;
|
||||
import org.apache.nifi.web.security.InvalidAuthenticationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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 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";
|
||||
public static final String JWT_COOKIE_NAME = "__Host-Authorization-Bearer";
|
||||
|
||||
@Override
|
||||
public String resolve(HttpServletRequest request) {
|
||||
final String authorizationHeader = request.getHeader(AUTHORIZATION);
|
||||
final Cookie cookieHeader = WebUtils.getCookie(request, JWT_COOKIE_NAME);
|
||||
|
||||
if (StringUtils.isNotBlank(authorizationHeader) && validAuthorizationHeaderFormat(authorizationHeader)) {
|
||||
return getTokenFromHeader(authorizationHeader);
|
||||
} else if(cookieHeader != null && validJwtFormat(cookieHeader.getValue())) {
|
||||
return cookieHeader.getValue();
|
||||
} else {
|
||||
logger.debug("Authorization header was not present or not in a valid format.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -598,38 +598,6 @@ public class JwtServiceTest {
|
|||
// Should throw exception when user is not found
|
||||
}
|
||||
|
||||
@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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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')) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue