NIFI-9060 Refactored HTTP Cookie Path Handling

- Implemented ApplicationCookieService for adding and retrieving HTTP Cookies
- Added getCookieResourceUri() leveraging allowed proxy headers to support optional Cookie Paths
- Refactored Access Resources to use ApplicationCookieService for processing
- Changed __Host- prefix to __Secure- prefix for Bearer Token cookie to support Cookie Path processing
- Removed unnecessary jetty-http dependency from nifi-web-api
- Corrected NiFi path references in JavaScript to support prefixed paths

Signed-off-by: Nathan Gough <thenatog@gmail.com>

This closes #5329.
This commit is contained in:
exceptionfactory 2021-08-23 17:33:26 -05:00 committed by Nathan Gough
parent 5b7af511fc
commit 84dbf915a9
17 changed files with 572 additions and 187 deletions

View File

@ -32,8 +32,6 @@ import javax.ws.rs.core.UriBuilderException;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Stream;
/**
@ -43,8 +41,6 @@ public final class WebUtils {
private static final Logger logger = LoggerFactory.getLogger(WebUtils.class);
final static ReadWriteLock lock = new ReentrantReadWriteLock();
public static final String PROXY_SCHEME_HTTP_HEADER = "X-ProxyScheme";
public static final String PROXY_HOST_HTTP_HEADER = "X-ProxyHost";
public static final String PROXY_PORT_HTTP_HEADER = "X-ProxyPort";
@ -57,6 +53,8 @@ public final class WebUtils {
public static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
public static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix";
private static final String EMPTY = "";
private WebUtils() {
}
@ -207,7 +205,7 @@ public final class WebUtils {
return contextPath;
} catch (UriBuilderException e) {
logger.error("Error determining context path on " + jspDisplayName + ": " + e.getMessage());
return "";
return EMPTY;
}
}
@ -218,27 +216,18 @@ public final class WebUtils {
* @param request the HTTP request
* @return the provided context path or an empty string
*/
public static String determineContextPath(HttpServletRequest request) {
String contextPath = request.getContextPath();
String proxyContextPath = request.getHeader(PROXY_CONTEXT_PATH_HTTP_HEADER);
String forwardedContext = request.getHeader(FORWARDED_CONTEXT_HTTP_HEADER);
String prefix = request.getHeader(FORWARDED_PREFIX_HTTP_HEADER);
public static String determineContextPath(final HttpServletRequest request) {
final String proxyContextPath = request.getHeader(PROXY_CONTEXT_PATH_HTTP_HEADER);
final String forwardedContext = request.getHeader(FORWARDED_CONTEXT_HTTP_HEADER);
final String prefix = request.getHeader(FORWARDED_PREFIX_HTTP_HEADER);
logger.debug("Context path: " + contextPath);
String determinedContextPath = "";
// If a context path header is set, log each
String determinedContextPath = EMPTY;
if (anyNotBlank(proxyContextPath, forwardedContext, prefix)) {
logger.debug(String.format("On the request, the following context paths were parsed" +
" from headers:\n\t X-ProxyContextPath: %s\n\tX-Forwarded-Context: %s\n\tX-Forwarded-Prefix: %s",
proxyContextPath, forwardedContext, prefix));
// Implementing preferred order here: PCP, FC, FP
determinedContextPath = Stream.of(proxyContextPath, forwardedContext, prefix)
.filter(StringUtils::isNotBlank).findFirst().orElse("");
.filter(StringUtils::isNotBlank).findFirst().orElse(EMPTY);
}
logger.debug("Determined context path: " + determinedContextPath);
return determinedContextPath;
}
@ -366,7 +355,7 @@ public final class WebUtils {
portFromHostHeader = null;
}
if (StringUtils.isNotBlank(portFromHostHeader) && StringUtils.isNotBlank(portHeaderValue)) {
logger.warn(String.format("The proxied host header contained a port, but was overridden by the proxied port header"));
logger.warn("Forwarded Host Port [{}] replaced with Forwarded Port [{}]", portFromHostHeader, portHeaderValue);
}
port = StringUtils.isNotBlank(portHeaderValue) ? portHeaderValue : (StringUtils.isNotBlank(portFromHostHeader) ? portFromHostHeader : null);
return port;

View File

@ -432,11 +432,5 @@
<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>

View File

@ -34,6 +34,7 @@ import org.apache.nifi.authorization.user.NiFiUserDetails;
import org.apache.nifi.authorization.user.NiFiUserUtils;
import org.apache.nifi.authorization.util.IdentityMappingUtil;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.web.api.cookie.ApplicationCookieName;
import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
import org.apache.nifi.web.api.dto.AccessStatusDTO;
import org.apache.nifi.web.api.entity.AccessConfigurationEntity;
@ -42,7 +43,6 @@ 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.http.SecurityCookieName;
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
import org.apache.nifi.web.security.jwt.revocation.JwtLogoutListener;
import org.apache.nifi.web.security.kerberos.KerberosService;
@ -62,9 +62,7 @@ import org.springframework.security.oauth2.server.resource.BearerTokenAuthentica
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
@ -81,6 +79,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -96,7 +95,6 @@ public class AccessResource extends ApplicationResource {
private static final Logger logger = LoggerFactory.getLogger(AccessResource.class);
protected static final String AUTHENTICATION_NOT_ENABLED_MSG = "User authentication/authorization is only supported when running over HTTPS.";
static final String LOGOUT_REQUEST_IDENTIFIER = "nifi-logout-request-identifier";
private X509CertificateExtractor certificateExtractor;
private X509AuthenticationProvider x509AuthenticationProvider;
@ -259,7 +257,7 @@ public class AccessResource extends ApplicationResource {
accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
accessStatus.setMessage("Access Unknown: Authorization Header not found.");
// Remove Session Cookie when Authorization Header not found
removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName());
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.AUTHORIZATION_BEARER);
} else {
try {
// authenticate the token
@ -275,10 +273,7 @@ public class AccessResource extends ApplicationResource {
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
accessStatus.setMessage("You are already logged in.");
} catch (final AuthenticationException iae) {
if (WebUtils.getCookie(httpServletRequest, SecurityCookieName.AUTHORIZATION_BEARER.getName()) != null) {
removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName());
}
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.AUTHORIZATION_BEARER);
throw iae;
}
}
@ -343,7 +338,7 @@ public class AccessResource extends ApplicationResource {
@ApiResponse(code = 500, message = "Unable to create access token because an unexpected error occurred.")
}
)
public Response createAccessTokenFromTicket(@Context HttpServletRequest httpServletRequest) {
public Response createAccessTokenFromTicket(@Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse) {
// only support access tokens when communicating over HTTPS
if (!httpServletRequest.isSecure()) {
@ -379,7 +374,8 @@ public class AccessResource extends ApplicationResource {
final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, expiration, "KerberosService");
final String token = bearerTokenProvider.getBearerToken(loginAuthenticationToken);
final URI uri = URI.create(generateResourceUri("access", "kerberos"));
return generateTokenResponse(generateCreatedResponse(uri, token), token);
setBearerToken(httpServletResponse, token);
return generateCreatedResponse(uri, token).build();
} catch (final AuthenticationException e) {
throw new AccessDeniedException(e.getMessage(), e);
}
@ -414,9 +410,10 @@ public class AccessResource extends ApplicationResource {
}
)
public Response createAccessToken(
@Context HttpServletRequest httpServletRequest,
@FormParam("username") String username,
@FormParam("password") String password) {
@Context final HttpServletRequest httpServletRequest,
@Context final HttpServletResponse httpServletResponse,
@FormParam("username") final String username,
@FormParam("password") final String password) {
// only support access tokens when communicating over HTTPS
if (!httpServletRequest.isSecure()) {
@ -450,9 +447,10 @@ public class AccessResource extends ApplicationResource {
throw new AdministrationException(iae.getMessage(), iae);
}
final String token = bearerTokenProvider.getBearerToken(loginAuthenticationToken);
final String bearerToken = bearerTokenProvider.getBearerToken(loginAuthenticationToken);
final URI uri = URI.create(generateResourceUri("access", "token"));
return generateTokenResponse(generateCreatedResponse(uri, token), token);
setBearerToken(httpServletResponse, bearerToken);
return generateCreatedResponse(uri, bearerToken).build();
}
@DELETE
@ -484,7 +482,7 @@ public class AccessResource extends ApplicationResource {
try {
logger.info("Logout Started [{}]", mappedUserIdentity);
logger.debug("Removing Authorization Cookie [{}]", mappedUserIdentity);
removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName());
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.AUTHORIZATION_BEARER);
final String bearerToken = bearerTokenResolver.resolve(httpServletRequest);
jwtLogoutListener.logout(bearerToken);
@ -493,14 +491,7 @@ public class AccessResource extends ApplicationResource {
final LogoutRequest logoutRequest = new LogoutRequest(UUID.randomUUID().toString(), mappedUserIdentity);
logoutRequestManager.start(logoutRequest);
// generate a cookie to store the logout request identifier
final Cookie cookie = new Cookie(LOGOUT_REQUEST_IDENTIFIER, logoutRequest.getRequestIdentifier());
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(60);
cookie.setSecure(true);
httpServletResponse.addCookie(cookie);
applicationCookieService.addCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER, logoutRequest.getRequestIdentifier());
return generateOkResponse().build();
} catch (final LogoutException e) {
logger.error("Logout Failed Identity [{}]", mappedUserIdentity, e);
@ -538,22 +529,16 @@ public class AccessResource extends ApplicationResource {
LogoutRequest completeLogoutRequest(final HttpServletResponse httpServletResponse) {
LogoutRequest logoutRequest = null;
// check if a logout request identifier is present and if so complete the request
final Cookie cookie = WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER);
final String logoutRequestIdentifier = cookie == null ? null : cookie.getValue();
if (logoutRequestIdentifier != null) {
final Optional<String> cookieValue = getLogoutRequestIdentifier();
if (cookieValue.isPresent()) {
final String logoutRequestIdentifier = cookieValue.get();
logoutRequest = logoutRequestManager.complete(logoutRequestIdentifier);
}
if (logoutRequest == null) {
logger.warn("Logout Request [{}] not found", logoutRequestIdentifier);
} else {
logger.info("Logout Request [{}] Completed [{}]", logoutRequestIdentifier, logoutRequest.getMappedUserIdentity());
} else {
logger.warn("Logout Request Cookie [{}] not found", ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName());
}
// remove the cookie if it existed
removeLogoutRequestCookie(httpServletResponse);
return logoutRequest;
}
@ -578,8 +563,22 @@ public class AccessResource extends ApplicationResource {
return getNiFiUri() + "logout-complete";
}
void removeLogoutRequestCookie(final HttpServletResponse httpServletResponse) {
removeCookie(httpServletResponse, LOGOUT_REQUEST_IDENTIFIER);
/**
* Send Set-Cookie header to remove Logout Request Identifier cookie from client
*
* @param httpServletResponse HTTP Servlet Response
*/
protected void removeLogoutRequestCookie(final HttpServletResponse httpServletResponse) {
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
}
/**
* Get Logout Request Identifier from current HTTP Request Cookie header
*
* @return Optional Logout Request Identifier
*/
protected Optional<String> getLogoutRequestIdentifier() {
return applicationCookieService.getCookieValue(httpServletRequest, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
}
// setters

View File

@ -50,21 +50,20 @@ import org.apache.nifi.util.ComponentIdGenerator;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.NiFiServiceFacade;
import org.apache.nifi.web.Revision;
import org.apache.nifi.web.api.cookie.ApplicationCookieName;
import org.apache.nifi.web.api.cookie.ApplicationCookieService;
import org.apache.nifi.web.api.cookie.StandardApplicationCookieService;
import org.apache.nifi.web.api.dto.RevisionDTO;
import org.apache.nifi.web.api.entity.ComponentEntity;
import org.apache.nifi.web.api.entity.Entity;
import org.apache.nifi.web.api.entity.TransactionResultEntity;
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.http.SecurityCookieName;
import org.apache.nifi.web.security.util.CacheKey;
import org.apache.nifi.web.util.WebUtils;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.CacheControl;
@ -114,12 +113,13 @@ public abstract class ApplicationResource {
public static final String DISCONNECTED_NODE_ACKNOWLEDGED = "disconnectedNodeAcknowledged";
static final String LOGIN_ERROR_TITLE = "Unable to continue login sequence";
static final String LOGOUT_ERROR_TITLE = "Unable to continue logout sequence";
private static final int VALID_FOR_SESSION_ONLY = -1;
protected static final String NON_GUARANTEED_ENDPOINT = "Note: This endpoint is subject to change as NiFi and it's REST API evolve.";
private static final Logger logger = LoggerFactory.getLogger(ApplicationResource.class);
private static final String ROOT_PATH = "/";
public static final String NODEWISE = "false";
@Context
@ -128,6 +128,7 @@ public abstract class ApplicationResource {
@Context
protected UriInfo uriInfo;
protected ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
protected NiFiProperties properties;
private RequestReplicator requestReplicator;
private ClusterCoordinator clusterCoordinator;
@ -164,14 +165,23 @@ public abstract class ApplicationResource {
return uri.toString();
}
/**
* Get Resource URI used for Cookie Domain and Path properties
*
* @return Cookie Resource URI
*/
protected URI getCookieResourceUri() {
final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
return buildResourceUri(uriBuilder.replacePath(ROOT_PATH).build());
}
private URI buildResourceUri(final String... path) {
final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.segment(path);
URI uri = uriBuilder.build();
return buildResourceUri(uriBuilder.segment(path).build());
}
private URI buildResourceUri(final URI uri) {
try {
// check for proxy settings
final String scheme = getFirstHeaderValue(PROXY_SCHEME_HTTP_HEADER, FORWARDED_PROTO_HTTP_HEADER);
final String hostHeaderValue = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER);
final String portHeaderValue = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER);
@ -180,25 +190,20 @@ public abstract class ApplicationResource {
final String port = WebUtils.determineProxiedPort(hostHeaderValue, portHeaderValue);
// Catch header poisoning
String allowedContextPaths = properties.getAllowedContextPaths();
String resourcePath = WebUtils.getResourcePath(uri, httpServletRequest, allowedContextPaths);
final String allowedContextPaths = properties.getAllowedContextPaths();
final String resourcePath = WebUtils.getResourcePath(uri, httpServletRequest, allowedContextPaths);
// determine the port uri
int uriPort = uri.getPort();
if (port != null) {
if (StringUtils.isWhitespace(port)) {
uriPort = -1;
} else {
try {
uriPort = Integer.parseInt(port);
} catch (final NumberFormatException nfe) {
logger.warn(String.format("Unable to parse proxy port HTTP header '%s'. Using port from request URI '%s'.", port, uriPort));
}
if (StringUtils.isNumeric(port)) {
try {
uriPort = Integer.parseInt(port);
} catch (final NumberFormatException nfe) {
logger.warn("Parsing Proxy Port [{}] Failed: Using URI Port [{}]", port, uriPort);
}
}
// construct the URI
uri = new URI(
return new URI(
(StringUtils.isBlank(scheme)) ? uri.getScheme() : scheme,
uri.getUserInfo(),
(StringUtils.isBlank(host)) ? uri.getHost() : host,
@ -206,11 +211,9 @@ public abstract class ApplicationResource {
resourcePath,
uri.getQuery(),
uri.getFragment());
} catch (final URISyntaxException use) {
throw new UriBuilderException(use);
}
return uri;
}
/**
@ -314,7 +317,7 @@ public abstract class ApplicationResource {
}
protected MultivaluedMap<String, String> getRequestParameters() {
final MultivaluedMap<String, String> entity = new MultivaluedHashMap();
final MultivaluedMap<String, String> entity = new MultivaluedHashMap<>();
for (final Map.Entry<String, String[]> entry : httpServletRequest.getParameterMap().entrySet()) {
if (entry.getValue() == null) {
@ -330,7 +333,7 @@ public abstract class ApplicationResource {
}
protected Map<String, String> getHeaders() {
return getHeaders(new HashMap<String, String>());
return getHeaders(new HashMap<>());
}
protected Map<String, String> getHeaders(final Map<String, String> overriddenHeaders) {
@ -818,7 +821,7 @@ public abstract class ApplicationResource {
}
}
private final class Request<T extends Entity> {
private static final class Request<T extends Entity> {
final String userChain;
final String uri;
final Revision revision;
@ -1283,19 +1286,14 @@ public abstract class ApplicationResource {
}
protected 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(SecurityCookieName.AUTHORIZATION_BEARER.getName(), token, null, "/", VALID_FOR_SESSION_ONLY, true, true, null, 0, HttpCookie.SameSite.STRICT);
return builder.header(HttpHeader.SET_COOKIE.asString(), jwtCookie.getRFC6265SetCookie()).build();
}
protected void removeCookie(final HttpServletResponse httpServletResponse, final String cookieName) {
final Cookie cookie = new Cookie(cookieName, null);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(0);
cookie.setSecure(true);
httpServletResponse.addCookie(cookie);
/**
* Set Bearer Token as HTTP Session Cookie using standard Cookie Name
*
* @param response HTTP Servlet Response
* @param bearerToken JSON Web Token
*/
protected void setBearerToken(final HttpServletResponse response, final String bearerToken) {
applicationCookieService.addSessionCookie(getCookieResourceUri(), response, ApplicationCookieName.AUTHORIZATION_BEARER, bearerToken);
}
protected String getNiFiUri() {

View File

@ -39,17 +39,15 @@ import org.apache.http.message.BasicNameValuePair;
import org.apache.nifi.authentication.exception.AuthenticationNotSupportedException;
import org.apache.nifi.authorization.user.NiFiUserUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.http.SecurityCookieName;
import org.apache.nifi.web.api.cookie.ApplicationCookieName;
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
import org.apache.nifi.web.security.oidc.OIDCEndpoints;
import org.apache.nifi.web.security.oidc.OidcService;
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.WebUtils;
import javax.annotation.PreDestroy;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
@ -65,6 +63,7 @@ import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -78,7 +77,6 @@ import java.util.regex.Pattern;
public class OIDCAccessResource extends AccessResource {
private static final Logger logger = LoggerFactory.getLogger(OIDCAccessResource.class);
private static final String OIDC_REQUEST_IDENTIFIER = "oidc-request-identifier";
private static final String OIDC_ID_TOKEN_AUTHN_ERROR = "Unable to exchange authorization for ID token: ";
private static final String OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG = "OpenId Connect support is not configured";
private static final String REVOKE_ACCESS_TOKEN_LOGOUT = "oidc_access_token_logout";
@ -144,12 +142,12 @@ public class OIDCAccessResource extends AccessResource {
)
public void oidcCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
final AuthenticationResponse oidcResponse = parseOidcResponse(httpServletRequest, httpServletResponse, LOGGING_IN);
final Optional<String> requestIdentifier = getOidcRequestIdentifier();
final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
if (oidcResponse != null && oidcResponse.indicatesSuccess()) {
if (requestIdentifier.isPresent() && oidcResponse != null && oidcResponse.indicatesSuccess()) {
final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
final String oidcRequestIdentifier = requestIdentifier.get();
checkOidcState(httpServletResponse, oidcRequestIdentifier, successfulOidcResponse, LOGGING_IN);
try {
@ -210,8 +208,8 @@ public class OIDCAccessResource extends AccessResource {
return Response.status(Response.Status.CONFLICT).entity(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG).build();
}
final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
if (oidcRequestIdentifier == null) {
final Optional<String> requestIdentifier = getOidcRequestIdentifier();
if (!requestIdentifier.isPresent()) {
final String message = "The login request identifier was not found in the request. Unable to continue.";
logger.warn(message);
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
@ -221,12 +219,13 @@ public class OIDCAccessResource extends AccessResource {
removeOidcRequestCookie(httpServletResponse);
// get the jwt
final String jwt = oidcService.getJwt(oidcRequestIdentifier);
final String jwt = oidcService.getJwt(requestIdentifier.get());
if (jwt == null) {
throw new IllegalArgumentException("A JWT for this login request identifier could not be found. Unable to continue.");
}
return generateTokenResponse(generateOkResponse(jwt), jwt);
setBearerToken(httpServletResponse, jwt);
return generateOkResponse(jwt).build();
}
@GET
@ -247,7 +246,7 @@ public class OIDCAccessResource extends AccessResource {
}
final String mappedUserIdentity = NiFiUserUtils.getNiFiUserIdentity();
removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName());
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.AUTHORIZATION_BEARER);
logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
// Get the oidc discovery url
@ -291,13 +290,13 @@ public class OIDCAccessResource extends AccessResource {
)
public void oidcLogoutCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
final AuthenticationResponse oidcResponse = parseOidcResponse(httpServletRequest, httpServletResponse, !LOGGING_IN);
final Optional<String> requestIdentifier = getOidcRequestIdentifier();
final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
if (oidcResponse != null && oidcResponse.indicatesSuccess()) {
if (requestIdentifier.isPresent() && oidcResponse != null && oidcResponse.indicatesSuccess()) {
final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
// confirm state
final String oidcRequestIdentifier = requestIdentifier.get();
checkOidcState(httpServletResponse, oidcRequestIdentifier, successfulOidcResponse, false);
// Get the oidc discovery url
@ -404,31 +403,16 @@ public class OIDCAccessResource extends AccessResource {
* @return the authorization URI
*/
private URI oidcRequestAuthorizationCode(@Context HttpServletResponse httpServletResponse, String callback) {
final String oidcRequestIdentifier = UUID.randomUUID().toString();
// generate a cookie to associate this login sequence
final Cookie cookie = new Cookie(OIDC_REQUEST_IDENTIFIER, oidcRequestIdentifier);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(60);
cookie.setSecure(true);
httpServletResponse.addCookie(cookie);
// get the state for this request
applicationCookieService.addCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER, oidcRequestIdentifier);
final State state = oidcService.createState(oidcRequestIdentifier);
// build the authorization uri
final URI authorizationUri = UriBuilder.fromUri(oidcService.getAuthorizationEndpoint())
return UriBuilder.fromUri(oidcService.getAuthorizationEndpoint())
.queryParam("client_id", oidcService.getClientId())
.queryParam("response_type", "code")
.queryParam("scope", oidcService.getScope().toString())
.queryParam("state", state.getValue())
.queryParam("redirect_uri", callback)
.build();
// return Authorization URI
return authorizationUri;
}
private String determineLogoutMethod(String oidcDiscoveryUrl) {
@ -475,7 +459,7 @@ public class OIDCAccessResource extends AccessResource {
}
@PreDestroy
private final void closeClient() throws IOException {
public void closeClient() throws IOException {
httpClient.close();
}
@ -494,8 +478,8 @@ public class OIDCAccessResource extends AccessResource {
return null;
}
final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
if (oidcRequestIdentifier == null) {
final Optional<String> requestIdentifier = getOidcRequestIdentifier();
if (!requestIdentifier.isPresent()) {
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle,"The request identifier was " +
"not found in the request. Unable to continue.");
return null;
@ -522,16 +506,12 @@ public class OIDCAccessResource extends AccessResource {
// confirm state
final State state = successfulOidcResponse.getState();
if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) {
logger.error("The state value returned by the OpenId Connect Provider does not match the stored " +
"state. Unable to continue login/logout process.");
logger.error("OIDC Request [{}] State [{}] not valid", oidcRequestIdentifier, state);
// remove the oidc request cookie
removeOidcRequestCookie(httpServletResponse);
// forward to the error page
forwardToMessagePage(httpServletRequest, httpServletResponse, getForwardPageTitle(isLogin), "Purposed state does not match " +
"the stored state. Unable to continue login/logout process.");
return;
}
}
@ -552,7 +532,11 @@ public class OIDCAccessResource extends AccessResource {
}
private void removeOidcRequestCookie(final HttpServletResponse httpServletResponse) {
removeCookie(httpServletResponse, OIDC_REQUEST_IDENTIFIER);
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
}
private Optional<String> getOidcRequestIdentifier() {
return applicationCookieService.getCookieValue(httpServletRequest, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
}
public void setOidcService(OidcService oidcService) {

View File

@ -25,19 +25,17 @@ import org.apache.nifi.authorization.util.IdentityMapping;
import org.apache.nifi.authorization.util.IdentityMappingUtil;
import org.apache.nifi.idp.IdpType;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.api.cookie.ApplicationCookieName;
import org.apache.nifi.web.security.logout.LogoutRequest;
import org.apache.nifi.web.security.saml.SAMLCredentialStore;
import org.apache.nifi.web.security.saml.SAMLEndpoints;
import org.apache.nifi.web.security.saml.SAMLService;
import org.apache.nifi.web.security.saml.SAMLStateManager;
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
@ -53,6 +51,7 @@ import javax.ws.rs.core.UriInfo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@ -65,7 +64,6 @@ import java.util.stream.Collectors;
public class SAMLAccessResource extends AccessResource {
private static final Logger logger = LoggerFactory.getLogger(SAMLAccessResource.class);
private static final String SAML_REQUEST_IDENTIFIER = "saml-request-identifier";
private static final String SAML_METADATA_MEDIA_TYPE = "application/samlmetadata+xml";
private static final String LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND = "The logout request identifier was not found in the request. Unable to continue.";
private static final String LOGOUT_REQUEST_NOT_FOUND_FOR_GIVEN_IDENTIFIER = "No logout request was found for the given identifier. Unable to continue.";
@ -84,7 +82,7 @@ public class SAMLAccessResource extends AccessResource {
value = "Retrieves the service provider metadata.",
notes = NON_GUARANTEED_ENDPOINT
)
public Response samlMetadata(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
public Response samlMetadata(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
@ -122,12 +120,7 @@ public class SAMLAccessResource extends AccessResource {
final String samlRequestIdentifier = UUID.randomUUID().toString();
// generate a cookie to associate this login sequence
final Cookie cookie = new Cookie(SAML_REQUEST_IDENTIFIER, samlRequestIdentifier);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(60);
cookie.setSecure(true);
httpServletResponse.addCookie(cookie);
applicationCookieService.addCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.SAML_REQUEST_IDENTIFIER, samlRequestIdentifier);
// get the state for this request
final String relayState = samlStateManager.createState(samlRequestIdentifier);
@ -137,7 +130,6 @@ public class SAMLAccessResource extends AccessResource {
samlService.initiateLogin(httpServletRequest, httpServletResponse, relayState);
} catch (Exception e) {
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
return;
}
}
@ -184,8 +176,8 @@ public class SAMLAccessResource extends AccessResource {
initializeSamlServiceProvider();
// ensure the request has the cookie with the request id
final String samlRequestIdentifier = WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
if (samlRequestIdentifier == null) {
final Optional<String> requestIdentifier = getSamlRequestIdentifier();
if (!requestIdentifier.isPresent()) {
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "The login request identifier was not found in the request. Unable to continue.");
return;
}
@ -199,6 +191,7 @@ public class SAMLAccessResource extends AccessResource {
}
// ensure the RelayState value in the request matches the store state
final String samlRequestIdentifier = requestIdentifier.get();
if (!samlStateManager.isStateValid(samlRequestIdentifier, requestState)) {
logger.error("The RelayState value returned by the SAML IDP does not match the stored state. Unable to continue login process.");
removeSamlRequestCookie(httpServletResponse);
@ -258,7 +251,7 @@ public class SAMLAccessResource extends AccessResource {
notes = NON_GUARANTEED_ENDPOINT
)
public Response samlLoginExchange(@Context HttpServletRequest httpServletRequest,
@Context HttpServletResponse httpServletResponse) throws Exception {
@Context HttpServletResponse httpServletResponse) {
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
@ -271,30 +264,26 @@ public class SAMLAccessResource extends AccessResource {
return Response.status(Response.Status.CONFLICT).entity(SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED).build();
}
logger.info("Attempting to exchange SAML login request for a NiFi JWT...");
// ensure saml service provider is initialized
initializeSamlServiceProvider();
// ensure the request has the cookie with the request identifier
final String samlRequestIdentifier = WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
if (samlRequestIdentifier == null) {
final Optional<String> requestIdentifier = getSamlRequestIdentifier();
if (!requestIdentifier.isPresent()) {
final String message = "The login request identifier was not found in the request. Unable to continue.";
logger.warn(message);
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
}
// remove the saml request cookie
removeSamlRequestCookie(httpServletResponse);
// get the jwt
final String samlRequestIdentifier = requestIdentifier.get();
final String jwt = samlStateManager.getJwt(samlRequestIdentifier);
if (jwt == null) {
throw new IllegalArgumentException("A JWT for this login request identifier could not be found. Unable to continue.");
}
// generate the response
logger.info("SAML login exchange complete");
logger.info("SAML Login Request [{}] Completed", samlRequestIdentifier);
setBearerToken(httpServletResponse, jwt);
return generateOkResponse(jwt).build();
}
@ -312,13 +301,14 @@ public class SAMLAccessResource extends AccessResource {
assert(isSamlEnabled(httpServletRequest, httpServletResponse, !LOGGING_IN));
// ensure the logout request identifier is present
final String logoutRequestIdentifier = WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
if (StringUtils.isBlank(logoutRequestIdentifier)) {
final Optional<String> cookieValue = getLogoutRequestIdentifier();
if (!cookieValue.isPresent()) {
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
return;
}
// ensure there is a logout request in progress for the given identifier
final String logoutRequestIdentifier = cookieValue.get();
final LogoutRequest logoutRequest = logoutRequestManager.get(logoutRequestIdentifier);
if (logoutRequest == null) {
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_NOT_FOUND_FOR_GIVEN_IDENTIFIER);
@ -341,9 +331,8 @@ public class SAMLAccessResource extends AccessResource {
try {
logger.info("Initiating SAML Single Logout with IDP...");
samlService.initiateLogout(httpServletRequest, httpServletResponse, samlCredential);
} catch (Exception e) {
} catch (final Exception e) {
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
return;
}
}
@ -400,13 +389,14 @@ public class SAMLAccessResource extends AccessResource {
initializeSamlServiceProvider();
// ensure the logout request identifier is present
final String logoutRequestIdentifier = WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
if (StringUtils.isBlank(logoutRequestIdentifier)) {
final Optional<String> requestIdentifier = getLogoutRequestIdentifier();
if (!requestIdentifier.isPresent()) {
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
return;
}
// ensure there is a logout request in progress for the given identifier
final String logoutRequestIdentifier = requestIdentifier.get();
final LogoutRequest logoutRequest = logoutRequestManager.get(logoutRequestIdentifier);
if (logoutRequest == null) {
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_NOT_FOUND_FOR_GIVEN_IDENTIFIER);
@ -473,7 +463,7 @@ public class SAMLAccessResource extends AccessResource {
httpServletResponse.sendRedirect(getNiFiLogoutCompleteUri());
}
private void initializeSamlServiceProvider() throws MetadataProviderException {
private void initializeSamlServiceProvider() {
if (!samlService.isServiceProviderInitialized()) {
final String samlMetadataUri = generateResourceUri("saml", "metadata");
final String baseUri = samlMetadataUri.replace("/saml/metadata", "");
@ -490,7 +480,7 @@ public class SAMLAccessResource extends AccessResource {
}
private void removeSamlRequestCookie(final HttpServletResponse httpServletResponse) {
removeCookie(httpServletResponse, SAML_REQUEST_IDENTIFIER);
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.SAML_REQUEST_IDENTIFIER);
}
private boolean isSamlEnabled(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, boolean isLogin) throws Exception {
@ -514,6 +504,10 @@ public class SAMLAccessResource extends AccessResource {
return isLogin ? ApplicationResource.LOGIN_ERROR_TITLE : ApplicationResource.LOGOUT_ERROR_TITLE;
}
private Optional<String> getSamlRequestIdentifier() {
return applicationCookieService.getCookieValue(httpServletRequest, ApplicationCookieName.SAML_REQUEST_IDENTIFIER);
}
public void setSamlService(SAMLService samlService) {
this.samlService = samlService;
}

View File

@ -0,0 +1,42 @@
/*
* 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.api.cookie;
import org.apache.nifi.web.security.http.SecurityCookieName;
/**
* Application Cookie Names
*/
public enum ApplicationCookieName {
AUTHORIZATION_BEARER(SecurityCookieName.AUTHORIZATION_BEARER.getName()),
LOGOUT_REQUEST_IDENTIFIER("nifi-logout-request-identifier"),
OIDC_REQUEST_IDENTIFIER("nifi-oidc-request-identifier"),
SAML_REQUEST_IDENTIFIER("nifi-saml-request-identifier");
private final String cookieName;
ApplicationCookieName(final String cookieName) {
this.cookieName = cookieName;
}
public String getCookieName() {
return cookieName;
}
}

View File

@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.api.cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.util.Optional;
/**
* Application Cookie Service capable of generating and retrieving HTTP Cookies using standard properties
*/
public interface ApplicationCookieService {
/**
* Generate cookie with specified value
*
* @param resourceUri Resource URI containing path and domain
* @param response HTTP Servlet Response
* @param applicationCookieName Application Cookie Name to be added
* @param value Cookie value to be added
*/
void addCookie(URI resourceUri, HttpServletResponse response, ApplicationCookieName applicationCookieName, String value);
/**
* Generate cookie with session-based expiration and specified value
*
* @param resourceUri Resource URI containing path and domain
* @param response HTTP Servlet Response
* @param applicationCookieName Application Cookie Name
* @param value Cookie value to be added
*/
void addSessionCookie(URI resourceUri, HttpServletResponse response, ApplicationCookieName applicationCookieName, String value);
/**
* Get cookie value using specified name
*
* @param request HTTP Servlet Response
* @param applicationCookieName Application Cookie Name to be retrieved
* @return Optional Cookie Value
*/
Optional<String> getCookieValue(HttpServletRequest request, ApplicationCookieName applicationCookieName);
/**
* Generate cookie with an empty value instructing the client to remove the cookie
*
* @param resourceUri Resource URI containing path and domain
* @param response HTTP Servlet Response
* @param applicationCookieName Application Cookie Name to be removed
*/
void removeCookie(URI resourceUri, HttpServletResponse response, ApplicationCookieName applicationCookieName);
}

View File

@ -0,0 +1,134 @@
/*
* 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.api.cookie;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseCookie;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.HttpHeaders;
import java.net.URI;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
/**
* Standard implementation of Application Cookie Service using Spring Framework utilities
*/
public class StandardApplicationCookieService implements ApplicationCookieService {
private static final Duration MAX_AGE_SESSION = Duration.ofSeconds(-1);
private static final Duration MAX_AGE_REMOVE = Duration.ZERO;
private static final Duration MAX_AGE_STANDARD = Duration.ofSeconds(60);
private static final String DEFAULT_PATH = "/";
private static final String SAME_SITE_STRICT = "Strict";
private static final boolean SECURE_ENABLED = true;
private static final boolean HTTP_ONLY_ENABLED = true;
private static final Logger logger = LoggerFactory.getLogger(StandardApplicationCookieService.class);
/**
* Generate cookie with specified value
*
* @param resourceUri Resource URI containing path and domain
* @param response HTTP Servlet Response
* @param applicationCookieName Application Cookie Name to be added
* @param value Cookie value to be added
*/
@Override
public void addCookie(final URI resourceUri, final HttpServletResponse response, final ApplicationCookieName applicationCookieName, final String value) {
final ResponseCookie.ResponseCookieBuilder responseCookieBuilder = getCookieBuilder(resourceUri, applicationCookieName, value, MAX_AGE_STANDARD);
setResponseCookie(response, responseCookieBuilder.build());
logger.debug("Added Cookie [{}] URI [{}]", applicationCookieName.getCookieName(), resourceUri);
}
/**
* Generate cookie with session-based expiration and specified value as well as SameSite Strict property
*
* @param resourceUri Resource URI containing path and domain
* @param response HTTP Servlet Response
* @param applicationCookieName Application Cookie Name
* @param value Cookie value to be added
*/
@Override
public void addSessionCookie(final URI resourceUri, final HttpServletResponse response, final ApplicationCookieName applicationCookieName, final String value) {
final ResponseCookie.ResponseCookieBuilder responseCookieBuilder = getCookieBuilder(resourceUri, applicationCookieName, value, MAX_AGE_SESSION);
responseCookieBuilder.sameSite(SAME_SITE_STRICT);
setResponseCookie(response, responseCookieBuilder.build());
logger.debug("Added Session Cookie [{}] URI [{}]", applicationCookieName.getCookieName(), resourceUri);
}
/**
* Get cookie value using specified name
*
* @param request HTTP Servlet Response
* @param applicationCookieName Application Cookie Name to be retrieved
* @return Optional Cookie Value
*/
@Override
public Optional<String> getCookieValue(final HttpServletRequest request, final ApplicationCookieName applicationCookieName) {
final Cookie cookie = WebUtils.getCookie(request, applicationCookieName.getCookieName());
return cookie == null ? Optional.empty() : Optional.of(cookie.getValue());
}
/**
* Generate cookie with an empty value instructing the client to remove the cookie with a maximum age of 60 seconds
*
* @param resourceUri Resource URI containing path and domain
* @param response HTTP Servlet Response
* @param applicationCookieName Application Cookie Name to be removed
*/
@Override
public void removeCookie(final URI resourceUri, final HttpServletResponse response, final ApplicationCookieName applicationCookieName) {
Objects.requireNonNull(response, "Response required");
final ResponseCookie.ResponseCookieBuilder responseCookieBuilder = getCookieBuilder(resourceUri, applicationCookieName, StringUtils.EMPTY, MAX_AGE_REMOVE);
setResponseCookie(response, responseCookieBuilder.build());
logger.debug("Removed Cookie [{}] URI [{}]", applicationCookieName.getCookieName(), resourceUri);
}
private ResponseCookie.ResponseCookieBuilder getCookieBuilder(final URI resourceUri,
final ApplicationCookieName applicationCookieName,
final String value,
final Duration maxAge) {
Objects.requireNonNull(resourceUri, "Resource URI required");
Objects.requireNonNull(applicationCookieName, "Response Cookie Name required");
return ResponseCookie.from(applicationCookieName.getCookieName(), value)
.path(getCookiePath(resourceUri))
.domain(resourceUri.getHost())
.secure(SECURE_ENABLED)
.httpOnly(HTTP_ONLY_ENABLED)
.maxAge(maxAge);
}
private void setResponseCookie(final HttpServletResponse response, final ResponseCookie responseCookie) {
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());
}
private String getCookiePath(final URI resourceUri) {
return StringUtils.defaultIfBlank(resourceUri.getPath(), DEFAULT_PATH);
}
}

View File

@ -147,6 +147,7 @@ public class TestDataTransferResource {
final URI locationUri = new URI(locationUriStr);
doReturn(uriBuilder).when(uriInfo).getBaseUriBuilder();
doReturn(uriBuilder).when(uriBuilder).path(any(String.class));
doReturn(uriBuilder).when(uriBuilder).segment(any(String.class));
doReturn(locationUri).when(uriBuilder).build();
return uriInfo;
}

View File

@ -0,0 +1,184 @@
/*
* 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.api.cookie;
import org.apache.nifi.util.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.mock.web.MockCookie;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.HttpHeaders;
import java.net.URI;
import java.util.Optional;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class StandardApplicationCookieServiceTest {
private static final String DOMAIN = "localhost.localdomain";
private static final String RESOURCE_URI = String.format("https://%s", DOMAIN);
private static final String ROOT_PATH = "/";
private static final String CONTEXT_PATH = "/context";
private static final String CONTEXT_RESOURCE_URI = String.format("https://%s%s", DOMAIN, CONTEXT_PATH);
private static final int EXPECTED_MAX_AGE = 60;
private static final int SESSION_MAX_AGE = -1;
private static final int REMOVE_MAX_AGE = 0;
private static final String SAME_SITE_STRICT = "SameSite=Strict";
private static final String COOKIE_VALUE = UUID.randomUUID().toString();
private static final ApplicationCookieName COOKIE_NAME = ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER;
private URI resourceUri;
private URI contextResourceUri;
private StandardApplicationCookieService service;
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
@Captor
private ArgumentCaptor<String> cookieArgumentCaptor;
@Before
public void setService() {
service = new StandardApplicationCookieService();
resourceUri = URI.create(RESOURCE_URI);
contextResourceUri = URI.create(CONTEXT_RESOURCE_URI);
}
@Test
public void testAddCookie() {
service.addCookie(resourceUri, response, COOKIE_NAME, COOKIE_VALUE);
verify(response).addHeader(eq(HttpHeaders.SET_COOKIE), cookieArgumentCaptor.capture());
final String setCookieHeader = cookieArgumentCaptor.getValue();
assertAddCookieMatches(setCookieHeader, ROOT_PATH, EXPECTED_MAX_AGE);
}
@Test
public void testAddCookieContextPath() {
service.addCookie(contextResourceUri, response, COOKIE_NAME, COOKIE_VALUE);
verify(response).addHeader(eq(HttpHeaders.SET_COOKIE), cookieArgumentCaptor.capture());
final String setCookieHeader = cookieArgumentCaptor.getValue();
assertAddCookieMatches(setCookieHeader, CONTEXT_PATH, EXPECTED_MAX_AGE);
}
@Test
public void testAddSessionCookie() {
service.addSessionCookie(resourceUri, response, COOKIE_NAME, COOKIE_VALUE);
verify(response).addHeader(eq(HttpHeaders.SET_COOKIE), cookieArgumentCaptor.capture());
final String setCookieHeader = cookieArgumentCaptor.getValue();
assertAddCookieMatches(setCookieHeader, ROOT_PATH, SESSION_MAX_AGE);
assertTrue("SameSite not found", setCookieHeader.endsWith(SAME_SITE_STRICT));
}
@Test
public void testAddSessionCookieContextPath() {
service.addSessionCookie(contextResourceUri, response, COOKIE_NAME, COOKIE_VALUE);
verify(response).addHeader(eq(HttpHeaders.SET_COOKIE), cookieArgumentCaptor.capture());
final String setCookieHeader = cookieArgumentCaptor.getValue();
assertAddCookieMatches(setCookieHeader, CONTEXT_PATH, SESSION_MAX_AGE);
assertTrue("SameSite not found", setCookieHeader.endsWith(SAME_SITE_STRICT));
}
@Test
public void testGetCookieValue() {
final Cookie cookie = new Cookie(COOKIE_NAME.getCookieName(), COOKIE_VALUE);
when(request.getCookies()).thenReturn(new Cookie[]{cookie});
final Optional<String> cookieValue = service.getCookieValue(request, COOKIE_NAME);
assertTrue(cookieValue.isPresent());
assertEquals(COOKIE_VALUE, cookieValue.get());
}
@Test
public void testGetCookieValueEmpty() {
final Optional<String> cookieValue = service.getCookieValue(request, COOKIE_NAME);
assertFalse(cookieValue.isPresent());
}
@Test
public void testRemoveCookie() {
service.removeCookie(resourceUri, response, COOKIE_NAME);
verify(response).addHeader(eq(HttpHeaders.SET_COOKIE), cookieArgumentCaptor.capture());
final String setCookieHeader = cookieArgumentCaptor.getValue();
assertRemoveCookieMatches(setCookieHeader, ROOT_PATH);
}
@Test
public void testRemoveCookieContextPath() {
service.removeCookie(contextResourceUri, response, COOKIE_NAME);
verify(response).addHeader(eq(HttpHeaders.SET_COOKIE), cookieArgumentCaptor.capture());
final String setCookieHeader = cookieArgumentCaptor.getValue();
assertRemoveCookieMatches(setCookieHeader, CONTEXT_PATH);
}
private void assertAddCookieMatches(final String setCookieHeader, final String path, final long maxAge) {
final Cookie cookie = MockCookie.parse(setCookieHeader);
assertCookieMatches(setCookieHeader, cookie, path);
assertEquals(COOKIE_VALUE, cookie.getValue());
assertEquals(maxAge, cookie.getMaxAge());
}
private void assertRemoveCookieMatches(final String setCookieHeader, final String path) {
final Cookie cookie = MockCookie.parse(setCookieHeader);
assertCookieMatches(setCookieHeader, cookie, path);
assertEquals(StringUtils.EMPTY, cookie.getValue());
assertEquals(REMOVE_MAX_AGE, cookie.getMaxAge());
}
private void assertCookieMatches(final String setCookieHeader, final Cookie cookie, final String path) {
assertEquals("Cookie Name not matched", COOKIE_NAME.getCookieName(), cookie.getName());
assertEquals("Path not matched", path, cookie.getPath());
assertEquals("Domain not matched", DOMAIN, cookie.getDomain());
assertTrue("HTTP Only not matched", cookie.isHttpOnly());
assertTrue("Secure not matched", cookie.getSecure());
}
}

View File

@ -20,7 +20,8 @@ package org.apache.nifi.web.security.http;
* Enumeration of HTTP Cookie Names for Security
*/
public enum SecurityCookieName {
AUTHORIZATION_BEARER("__Host-Authorization-Bearer");
/** See IETF Cookie Prefixes Draft Section 3.1 related to Secure prefix handling */
AUTHORIZATION_BEARER("__Secure-Authorization-Bearer");
private String name;

View File

@ -40,7 +40,7 @@
}).on('mouseleave', function () {
$(this).removeClass('link-over');
}).on('click', function () {
window.location = '<%= contextPath %>/nifi';
window.location = '<%= contextPath %>/nifi/';
});
});
</script>

View File

@ -116,9 +116,9 @@
if (accessStatus.status === 'ACTIVE') {
// reload as appropriate - no need to schedule token refresh as the page is reloading
if (top !== window) {
parent.window.location = '/nifi';
parent.window.location = '../nifi/';
} else {
window.location = '/nifi';
window.location = '../nifi/';
}
} else {
$('#login-message-title').text('Unable to log in');

View File

@ -38,7 +38,7 @@
}).on('mouseleave', function () {
$(this).removeClass('link-over');
}).on('click', function () {
window.location = '../nifi';
window.location = '../nifi/';
});
});
}));

View File

@ -110,9 +110,9 @@
// handle home
$('#user-home').on('click', function () {
if (top !== window) {
parent.window.location = '../nifi';
parent.window.location = '../nifi/';
} else {
window.location = '../nifi';
window.location = '../nifi/';
}
});
});

View File

@ -58,7 +58,7 @@
headerText: 'Session Expired',
dialogContent: 'Your session has expired. Please press Ok to log in again.',
okHandler: function () {
window.location = '/nifi';
window.location = '../nifi/';
}
});
}