mirror of https://github.com/apache/nifi.git
NIFI-12487 Added CSRF Protection to Registry (#8136)
- Added CSRF Token Repository based on existing implementation - Updated Angular Request Interceptor to read cookie and send Request-Token Header
This commit is contained in:
parent
49bbc38b6b
commit
2794193608
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.apache.nifi.registry.web.security;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.nifi.registry.security.authorization.Authorizer;
|
||||
import org.apache.nifi.registry.security.authorization.resource.ResourceType;
|
||||
import org.apache.nifi.registry.security.identity.IdentityMapper;
|
||||
|
@ -23,6 +24,11 @@ import org.apache.nifi.registry.service.AuthorizationService;
|
|||
import org.apache.nifi.registry.web.security.authentication.AnonymousIdentityFilter;
|
||||
import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider;
|
||||
import org.apache.nifi.registry.web.security.authentication.IdentityFilter;
|
||||
import org.apache.nifi.registry.web.security.authentication.csrf.CsrfCookieFilter;
|
||||
import org.apache.nifi.registry.web.security.authentication.csrf.CsrfCookieName;
|
||||
import org.apache.nifi.registry.web.security.authentication.csrf.CsrfRequestMatcher;
|
||||
import org.apache.nifi.registry.web.security.authentication.csrf.StandardCookieCsrfTokenRepository;
|
||||
import org.apache.nifi.registry.web.security.authentication.csrf.StandardCsrfTokenRequestAttributeHandler;
|
||||
import org.apache.nifi.registry.web.security.authentication.jwt.JwtIdentityProvider;
|
||||
import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityAuthenticationProvider;
|
||||
import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityProvider;
|
||||
|
@ -32,6 +38,7 @@ import org.slf4j.LoggerFactory;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
|
@ -41,10 +48,15 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
|||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.csrf.CsrfException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
|
||||
|
||||
|
@ -81,7 +93,12 @@ public class NiFiRegistrySecurityConfig {
|
|||
// Add Resource Authorization after Spring Security but before Jersey Resources
|
||||
.addFilterAfter(resourceAuthorizationFilter(), AuthorizationFilter.class)
|
||||
.anonymous(anonymous -> anonymous.authenticationFilter(new AnonymousIdentityFilter()))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.csrf(csrf -> {
|
||||
csrf.requireCsrfProtectionMatcher(new CsrfRequestMatcher());
|
||||
csrf.csrfTokenRepository(new StandardCookieCsrfTokenRepository());
|
||||
csrf.csrfTokenRequestHandler(new StandardCsrfTokenRequestAttributeHandler());
|
||||
})
|
||||
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
|
||||
.logout(AbstractHttpConfigurer::disable)
|
||||
.rememberMe(AbstractHttpConfigurer::disable)
|
||||
.requestCache(AbstractHttpConfigurer::disable)
|
||||
|
@ -107,6 +124,7 @@ public class NiFiRegistrySecurityConfig {
|
|||
)
|
||||
.exceptionHandling(exceptionHandling -> exceptionHandling
|
||||
.authenticationEntryPoint(http401AuthenticationEntryPoint())
|
||||
.accessDeniedHandler(new StandardAccessDeniedHandler())
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
@ -158,4 +176,26 @@ public class NiFiRegistrySecurityConfig {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static class StandardAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(final HttpServletRequest request, final HttpServletResponse response, final AccessDeniedException accessDeniedException) throws IOException {
|
||||
final String message;
|
||||
final int status;
|
||||
if (accessDeniedException instanceof CsrfException) {
|
||||
status = HttpServletResponse.SC_FORBIDDEN;
|
||||
message = "Access Denied: CSRF Header and Cookie not matched";
|
||||
logger.info("Access Denied: CSRF Header [{}] not matched: {}", CsrfCookieName.REQUEST_TOKEN.getCookieName(), accessDeniedException.toString());
|
||||
} else {
|
||||
status = HttpServletResponse.SC_UNAUTHORIZED;
|
||||
message = "Access Denied";
|
||||
logger.debug(message, accessDeniedException);
|
||||
}
|
||||
|
||||
response.setStatus(status);
|
||||
response.setContentType("text/plain");
|
||||
response.getWriter().println(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.registry.web.security.authentication.csrf;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Cross-Site Request Forgery Cookie Filter based on Spring Security recommendations for Single-Page Applications
|
||||
*/
|
||||
public class CsrfCookieFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
|
||||
// Get CSRF Token set from Request Attribute Handler
|
||||
final CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||
// Invoking CsrfToken.getToken() causes deferred tokens to be loaded and set as response headers
|
||||
csrfToken.getToken();
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
|
@ -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.registry.web.security.authentication.csrf;
|
||||
|
||||
/**
|
||||
* Cross-Site Request Forgery Mitigation Cookie Names
|
||||
*/
|
||||
public enum CsrfCookieName {
|
||||
/** Cross-Site Request Forgery mitigation token requires Strict Same Site handling */
|
||||
REQUEST_TOKEN("__Secure-Request-Token");
|
||||
|
||||
private final String cookieName;
|
||||
|
||||
CsrfCookieName(final String cookieName) {
|
||||
this.cookieName = cookieName;
|
||||
}
|
||||
|
||||
public String getCookieName() {
|
||||
return cookieName;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.registry.web.security.authentication.csrf;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import static org.springframework.web.util.WebUtils.getCookie;
|
||||
|
||||
/**
|
||||
* Cross-Site Request Forgery Mitigation Request Matcher
|
||||
*/
|
||||
public class CsrfRequestMatcher implements RequestMatcher {
|
||||
|
||||
@Override
|
||||
public boolean matches(final HttpServletRequest request) {
|
||||
final boolean matches;
|
||||
|
||||
if (CsrfFilter.DEFAULT_CSRF_MATCHER.matches(request)) {
|
||||
// Presence of Request Token Cookie requires invoking the CsrfFilter
|
||||
final Cookie requestTokenCookie = getCookie(request, CsrfCookieName.REQUEST_TOKEN.getCookieName());
|
||||
matches = requestTokenCookie != null;
|
||||
} else {
|
||||
matches = false;
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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.registry.web.security.authentication.csrf;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Standard implementation of CSRF Token Repository using stateless Spring Security double-submit cookie strategy
|
||||
*/
|
||||
public class StandardCookieCsrfTokenRepository implements CsrfTokenRepository {
|
||||
private static final String REQUEST_HEADER = "Request-Token";
|
||||
|
||||
private static final String REQUEST_PARAMETER = "requestToken";
|
||||
|
||||
private static final Duration MAX_AGE_SESSION = Duration.ofSeconds(-1);
|
||||
|
||||
private static final Duration MAX_AGE_REMOVE = Duration.ZERO;
|
||||
|
||||
private static final String EMPTY_VALUE = "";
|
||||
|
||||
private static final String SAME_SITE_POLICY_STRICT = "Strict";
|
||||
|
||||
private static final String ROOT_PATH = "/";
|
||||
|
||||
private static final boolean SECURE_ENABLED = true;
|
||||
|
||||
/**
|
||||
* Generate CSRF Token or return current Token when present in HTTP Servlet Request Cookie header
|
||||
*
|
||||
* @param httpServletRequest HTTP Servlet Request
|
||||
* @return CSRF Token
|
||||
*/
|
||||
@Override
|
||||
public CsrfToken generateToken(final HttpServletRequest httpServletRequest) {
|
||||
CsrfToken csrfToken = loadToken(httpServletRequest);
|
||||
if (csrfToken == null) {
|
||||
csrfToken = getCsrfToken(generateRandomToken());
|
||||
}
|
||||
return csrfToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save CSRF Token in HTTP Servlet Response using defaults that allow JavaScript read for session cookies
|
||||
*
|
||||
* @param csrfToken CSRF Token to be saved or null indicated the token should be removed
|
||||
* @param httpServletRequest HTTP Servlet Request
|
||||
* @param httpServletResponse HTTP Servlet Response
|
||||
*/
|
||||
@Override
|
||||
public void saveToken(final CsrfToken csrfToken, final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse) {
|
||||
if (csrfToken == null) {
|
||||
final ResponseCookie responseCookie = getResponseCookie(EMPTY_VALUE, MAX_AGE_REMOVE);
|
||||
httpServletResponse.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());
|
||||
} else {
|
||||
final String token = csrfToken.getToken();
|
||||
final ResponseCookie responseCookie = getResponseCookie(token, MAX_AGE_SESSION);
|
||||
httpServletResponse.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load CSRF Token from HTTP Servlet Request Cookie header
|
||||
*
|
||||
* @param httpServletRequest HTTP Servlet Request
|
||||
* @return CSRF Token or null when Cookie header not found
|
||||
*/
|
||||
@Override
|
||||
public CsrfToken loadToken(final HttpServletRequest httpServletRequest) {
|
||||
final Cookie cookie = WebUtils.getCookie(httpServletRequest, CsrfCookieName.REQUEST_TOKEN.getCookieName());
|
||||
final String token = cookie == null ? null : cookie.getValue();
|
||||
return StringUtils.hasLength(token) ? getCsrfToken(token) : null;
|
||||
}
|
||||
|
||||
private CsrfToken getCsrfToken(final String token) {
|
||||
return new DefaultCsrfToken(REQUEST_HEADER, REQUEST_PARAMETER, token);
|
||||
}
|
||||
|
||||
private String generateRandomToken() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private ResponseCookie getResponseCookie(final String cookieValue, final Duration maxAge) {
|
||||
return ResponseCookie.from(CsrfCookieName.REQUEST_TOKEN.getCookieName(), cookieValue)
|
||||
.sameSite(SAME_SITE_POLICY_STRICT)
|
||||
.secure(SECURE_ENABLED)
|
||||
.path(ROOT_PATH)
|
||||
.maxAge(maxAge)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.registry.web.security.authentication.csrf;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Cross-Site Request Forgery Mitigation Token Handler implementation supporting resolution using Request Header
|
||||
*/
|
||||
public class StandardCsrfTokenRequestAttributeHandler extends CsrfTokenRequestAttributeHandler {
|
||||
private final XorCsrfTokenRequestAttributeHandler handler = new XorCsrfTokenRequestAttributeHandler();
|
||||
|
||||
/**
|
||||
* Handle Request using standard Spring Security implementation
|
||||
*
|
||||
* @param request HTTP Servlet Request being handled
|
||||
* @param response HTTP Servlet Response being handled
|
||||
* @param csrfTokenSupplier Supplier for CSRF Token
|
||||
*/
|
||||
@Override
|
||||
public void handle(final HttpServletRequest request, final HttpServletResponse response, final Supplier<CsrfToken> csrfTokenSupplier) {
|
||||
this.handler.handle(request, response, csrfTokenSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve CSRF Token Value from HTTP Request Header
|
||||
*
|
||||
* @param request HTTP Servlet Request being processed
|
||||
* @param csrfToken CSRF Token created from a CSRF Token Repository
|
||||
* @return Token Value from Request Header or null when not found
|
||||
*/
|
||||
@Override
|
||||
public String resolveCsrfTokenValue(final HttpServletRequest request, final CsrfToken csrfToken) {
|
||||
final String headerTokenValue = request.getHeader(csrfToken.getHeaderName());
|
||||
|
||||
final String resolvedToken;
|
||||
if (StringUtils.hasText(headerTokenValue)) {
|
||||
resolvedToken = super.resolveCsrfTokenValue(request, csrfToken);
|
||||
} else {
|
||||
resolvedToken = null;
|
||||
}
|
||||
|
||||
return resolvedToken;
|
||||
}
|
||||
}
|
|
@ -25,12 +25,13 @@ import NfStorage from 'services/nf-storage.service';
|
|||
*/
|
||||
function NfRegistryTokenInterceptor(nfStorage) {
|
||||
this.nfStorage = nfStorage;
|
||||
this.requestTokenPattern = /Request-Token=([^;]+)/;
|
||||
}
|
||||
NfRegistryTokenInterceptor.prototype = {
|
||||
constructor: NfRegistryTokenInterceptor,
|
||||
|
||||
/**
|
||||
* Injects the authorization token into the request headers.
|
||||
* Injects the authorization token and request token cookie value into the request headers.
|
||||
*
|
||||
* @param request angular HttpRequest.
|
||||
* @param next angular HttpHandler.
|
||||
|
@ -41,6 +42,11 @@ NfRegistryTokenInterceptor.prototype = {
|
|||
if (token) {
|
||||
request = request.clone({headers: request.headers.set('Authorization', 'Bearer ' + token)});
|
||||
}
|
||||
var requestTokenMatcher = this.requestTokenPattern.exec(document.cookie);
|
||||
if (requestTokenMatcher) {
|
||||
var requestToken = requestTokenMatcher[1];
|
||||
request = request.clone({headers: request.headers.set('Request-Token', requestToken)});
|
||||
}
|
||||
return next.handle(request);
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue