I have a problem with default behaviour in spring security with authorize requests provided with Java Config.
http
....
.authorizeRequests()
.antMatchers("/api/test/secured/*").authenticated()
When I do a call to for example /api/test/secured/user without login (with anonymous user), it returns 403 Forbidden. Is there an easy way to change status to 401 Unauthorized when anonymous user wants to get secured by authenticated() or #PreAuthorize resource?
As of Spring Boot 2 class Http401AuthenticationEntryPoint has been removed (see Spring Boot Issue 10725).
Instead of Http401AuthenticationEntryPoint use HttpStatusEntryPoint with HttpStatus.UNAUTHORIZED:
http.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
With spring security 4.x there is already a class for that
org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint
Spring boot also includes one
org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint
and both benefits that they require the developer to use spec compliant as 401 responses requires that header WWW-Authenticate must be set, example 401 response could be:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
error="invalid_token",
error_description="The access token expired"
So in your security configuration you define and autowire a bean of class
So for instance with spring boot app:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
#Bean
public Http401AuthenticationEntryPoint securityException401EntryPoint(){
return new Http401AuthenticationEntryPoint("Bearer realm=\"webrealm\"");
}
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login").anonymous()
.antMatchers("/").anonymous()
.antMatchers("/api/**").authenticated()
.and()
.csrf()
.disable()
.headers()
.frameOptions().disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.logout()
.permitAll()
.exceptionHandling().authenticationEntryPoint(securityException401EntryPoint());
}
the relevant line is:
.exceptionHandling().authenticationEntryPoint(securityException401EntryPoint());
I've got solution here:
http
.authenticationEntryPoint(authenticationEntryPoint)
AuthenticationEntryPoint source code:
#Component
public class Http401UnauthorizedEntryPoint implements AuthenticationEntryPoint {
private final Logger log = LoggerFactory.getLogger(Http401UnauthorizedEntryPoint.class);
/**
* Always returns a 401 error code to the client.
*/
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2) throws IOException,
ServletException {
log.debug("Pre-authenticated entry point called. Rejecting access");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied");
}
}
A simple approach in Spring Boot 2 using lambda expressions:
#Override
public void configure(HttpSecurity http) throws Exception {
http.
...
.exceptionHandling()
.authenticationEntryPoint((request, response, e) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write("{ \"error\": \"You are not authenticated.\" }");
})
...
}
You need to extend AuthenticationEntryPoint to do customization based upon the exceptions.
#ControllerAdvice
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
// 401
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed");
}
#ExceptionHandler (value = {AccessDeniedException.class})
public void commence(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
// 401
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Failed : " + accessDeniedException.getMessage());
}
}
Specify the above custom AuthenticationEntryPoint in your SecurityConfig like below:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity (prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint());
}
}
Who interested in mechanism of work. If you don't set http.exceptionHandling().authenticationEntryPoint() spring will use defaultAuthenticationEntryPoint() and method ExceptionHandlingConfigurer.createDefaultEntryPoint() will return new Http403ForbiddenEntryPoint()
So, just create Http401UnauthorizedEntryPoint(). Above answers how to do it, didn't duplicate it.
P.S. It's actual for Spring Security 5.2.5.RELEASE
Related
I have the following Spring Security configuration:
httpSecurity
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/**").fullyAuthenticated()
.and()
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
The authenticationTokenFilterBean() is applied even on endpoints that do not match /api/** expression. I also tried adding the following configuration code:
#Override
public void configure(WebSecurity webSecurity) {
webSecurity.ignoring().antMatchers("/some_endpoint");
}
but this still did not solve my problem. How can I tell Spring Security to apply filters only on endpoints that match the secured URI expression?
I have an application with the same requirement and to solve it I basically restricted Spring Security to a given ant match patter (using antMatcher) as follows:
http
.antMatcher("/api/**")
.authorizeRequests() //
.anyRequest().authenticated() //
.and()
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
You can read it as follows: for http only invoke these configurations on requests matching the ant pattern /api/** authorizing any request to authenticated users and add filter authenticationTokenFilterBean() before UsernamePasswordAuthenticationFilter. For all others requests this configuration has no effect.
GenericFilterBean has a following method :
/**
* Can be overridden in subclasses for custom filtering control,
* returning {#code true} to avoid filtering of the given request.
* <p>The default implementation always returns {#code false}.
* #param request current HTTP request
* #return whether the given request should <i>not</i> be filtered
* #throws ServletException in case of errors
*/
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return false;
}
So in your filter that extends GenericFilterBean you can override that method and implement logic to run the filter only on the routes that you would like.
My Requirement was to exclude the endpoint matching /api/auth/**, to achieve the same I have configured my WebSecurityConfig spring configuration component as follows:
/**
* The purpose of this method is to exclude the URL's specific to Login, Swagger UI and static files.
* Any URL that should be excluded from the Spring security chain should be added to the ignore list in this
* method only
*/
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/auth/**","/v2/api-docs",
"/configuration/ui",
"/swagger-resources",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js");
}
/**
* The purpose of this method is to define the HTTP configuration that defines how an HTTP request is
* going to be treated by the Spring Security chain. All the request URL's (excluding the URL's added
* in WebSecurity configuration ignore list) matching this configuration have to pass through the
* custom Spring security filter defined in this method
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
/**
* The purpose of this method is to create a new instance of JWTAuthenticationFilter
* and return the same from the method body. It must be ensured that this filter should
* not be configured as a Spring bean or registered into the Spring Application context
* failing which the below filter shall be registered as a default web filter, and thus
* all the URL's even the excluded ones shall be intercepted by the below filter
*/
public JWTAuthenticationFilter authenticationTokenFilterBean() {
return new JWTAuthenticationFilter();
}
We recently updated to Spring Boot 3.0.0 which uses Spring Security 6.0.0 and ran into a similar issue when a filter was applied to all requests, although the authorizeHttpRequests() was used with specific paths defined.
Turned out, if you want the HttpSecurity to be configured for a specific path, you need to use securityMatcher() at the beginning.
So it will be something like this:
private SecurityFilterChain configureFilterChain(HttpSecurity http, String pattern, String... roles) throws Exception {
return http
.securityMatcher(pattern)
.authorizeHttpRequests(auth -> auth.requestMatchers(AntPathRequestMatcher.antMatcher(pattern)).hasAnyRole(roles))
.addFilterBefore(new TokenFilter(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPointImpl())
.accessDeniedHandler(new AccessDeniedHandlerImpl())
.and()
.csrf().disable()
.build();
}
So in this case, TokenFilter will be applied to only requests which have this pattern.
If you use the
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
You can define in the constructor the specific path it will apply to:
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super("/api/**");
this.setAuthenticationManager(authenticationManager);
}
#Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return super.requiresAuthentication(request, response);
}
The requiresAuthentication method will be used to know if that endpoint needs authentication.
I think I've found a way to solve it. I have JwtTokenAuthenticationProcessingFilter which is an AbstractAuthenticationProcessingFilter. I want it to authenticate request if there is token in the head but do not block the request if failed. All you need is to rewrite the doFilter and invoke the chain.doFilter no matter what the authentication result is(invoking unsuccessfulAuthentication is optional). Here is part of my code.
public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
private final TokenExtractor tokenExtractor;
#Autowired
public JwtTokenAuthenticationProcessingFilter(TokenExtractor tokenExtractor, RequestMatcher matcher) {
super(matcher);
this.tokenExtractor = tokenExtractor;
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
boolean success = true;
Authentication authResult = null;
try {
authResult = this.attemptAuthentication(request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
success = false;
} catch (AuthenticationException var9) {
success = false;
}
if (success && null != authResult) {
this.successfulAuthentication(request, response, chain, authResult);
}
// Please ensure that chain.doFilter(request, response) is invoked upon successful authentication. You want
// processing of the request to advance to the next filter, because very last one filter
// FilterSecurityInterceptor#doFilter is responsible to actually invoke method in your controller that is
// handling requested API resource.
chain.doFilter(request, response);
}
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String tokenPayload = request.getHeader(WebSecurityConfig.AUTHENTICATION_HEADER_NAME);
RawAccessJwtToken token = new RawAccessJwtToken(tokenExtractor.extract(tokenPayload));
return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
}
}
Update at Apr 22
To register the filter, just add the following code to the WebSecurityConfig
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationProvider mJwtAuthenticationProvider;
#Autowired
public WebSecurityConfig(JwtAuthenticationProvider jwtAuthenticationProvider) {
this.mJwtAuthenticationProvider = jwtAuthenticationProvider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// When multiple authentication providers are defined, the providers will be queried in the order they’re
// declared.
auth.authenticationProvider(mJwtAuthenticationProvider);
}
}
In the code, I only revealed the critical part about adding the filter.
All this implementation was inspired by this site. Give credit to the author Vladimir Stankovic for his detail explanation.
I am wondering if there is a way to provide two separate types of authentication?
User should log, register, get user data for endpoints /login, /register, /user using basic auth. And when I call /api it should only be authenticated with JWT token provided in headers.
But when I call /api I get all data without any authentication. When user is logged and call /user, API gives JWT to access /api.
My code:
Configuration for basic auth:
#Configuration
#EnableWebSecurity
#Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable();
http
.authorizeRequests()
.antMatchers("/user").authenticated()
.antMatchers("/register").permitAll()
.and()
.formLogin().permitAll()
.defaultSuccessUrl("/user");
}
Configuration for JWT auth:
#Configuration
#Order(2)
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/api/**")
.addFilterAfter(new JWTAuthorizationFilter(),UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic().disable();
}
I had the same problem, I wanted basic Authentication for some endpoints and for some other I wanted other authentication methods. like yours. you wanna basic authentication for some of the endpoints (/login,/register, /user ) and JWT authentication for some other(/api/**).
I used some tutorials about multiple entry points in spring security but it didn't work.
So here is my solution (It worked)
Separate basic authentication from JWT authentication by creating a custom filter.
Add a prefix path for the endpoints that should be authenticated using basic authentication. like :
(/basic/login, /basic/register,/basic/user)
Create a new custom filter for /basic prefix (for /basic requests) and check basic authentication
#Component
public class BasicAuthenticationFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//Check for the requests that starts with /basic
if (httpServletRequest.getRequestURI().startsWith("/basic/")) {
try {
//Fetch Credential from authorization header
String authorization = httpServletRequest.getHeader("Authorization");
String base64Credentials = authorization.substring("Basic".length()).trim();
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
String credentials = new String(credDecoded, StandardCharsets.UTF_8);
final String username = credentials.split(":", 2)[0];
final String password = credentials.split(":", 2)[1];
//Check the username and password
if (username.equals("admin") && password.equals("admin")) {
//continue
chain.doFilter(request, response);
} else
throw new AuthenticationCredentialsNotFoundException("");
} catch (Exception e) {
throw new AuthenticationCredentialsNotFoundException("");
}
} else chain.doFilter(request, response);
}
}
Write main security configuration just for JWT and permit /basic URL
#Configuration
#EnableWebSecurity
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/basic/**").permitAll().and()
.csrf().disable()
.antMatcher("/api/**")
.addFilterAfter(new JWTAuthorizationFilter(),UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic().disable();
}
I have a small web application based on Spring MVC and Spring Security. I have difficulties setting my own AccessDeniedHandler that should redirect unauthorized users to my custom error page.
I use http.exceptionHandling().accessDeniedHandler(accessDeniedHandler) in my config class that extends WebSecurityConfigurerAdapter. The default AccessDeniedHandler keeps being invoked despite the setting (I debugged ExceptionTranslationFilter). As a result the container-defined error page is displayed instead of my custom one.
Do you have an idea what I am missing here? What could be the issue? Thank you kindly for your help.
An excerpt from my WebSecurityConfigurerAdapter super class:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/static/**", "/login/*", "/login").permitAll()
.antMatchers("/site/admin*").hasRole("ADMIN")
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
.usernameParameter("user-name")
.passwordParameter("password")
.defaultSuccessUrl("/site/welcome", true)
.loginProcessingUrl("/process-login")
.failureUrl("/login?login_error=1")
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.and().sessionManagement()
.invalidSessionUrl("/login")
.and().csrf()
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}
My custom AccessDeniedHandler implementation:
#Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static Logger LOG = Logger.getLogger(CustomAccessDeniedHandler.class);
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
LOG.warn(String.format("User [%s] attempted to access the protected URL [%s]!", authentication.getName(), request.getRequestURI()));
}
response.sendRedirect(request.getContextPath() + "/site/403");
}
}
I forgot to assign the autowired constructor parameter to a field! I am sorry for posting such a trivial problem here, but after I spent half a day looking for a solution, I was blind and I missed it...
public SpringSecurityConfiguration(
AccessDeniedHandler accessDeniedHandler, ...) {
this.accessDeniedHandler = accessDeniedHandler; // This line was missing.
...
}
I am trying to create an independent packageable jar with a custom annotation, which on inclusion in a controller mapping function (and taking userToken as input in header), returns a boolean whether the user is authenticated or now.
// Expected way of inclusion
public #ResponseBody boolean isAuthenticated(#Authenticator(#RequestHeader("userToken")) Boolean isUserAuthenticated) {
return isUserAuthenticated;
}
I know that this won't be the right syntax, since using this code gives the error that RequestMapping cannot be converted to String (and annotations only accept primitive values).
I am also open to other approaches, but it should have the flexibility to return authentication boolean only when needed and not through global interception.
Important: Please note #Authenticator comes from an independent package, imported in the current package through Maven. Would HTTPServletRequest pass in ConstraintValidator.
Use the spring security BasicAuthenticationFilter :
public class MyBasicAuthenticationFilter extends BasicAuthenticationFilter {
private AuthenticationManager authenticationManager;
public MyBasicAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
this.authenticationManager=authenticationManager;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// do you checks here
super.doFilterInternal(request, response, chain);
}
}
Then add this to your security config with something like:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
http.addFilterBefore(new MyBasicAuthenticationFilter(authenticationManager());
}
#Bean
public AuthenticationManager authenticationManager() {
return new MyAuthenticationManager();
}
I faced with a problem configuration Spring Security for single page application.
So, defualt config looks like
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
#Qualifier("customUserDetailsService")
UserDetailsService userDetailsService;
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/list").permitAll()
.antMatchers("/admin/**").access("hasRole('ADMIN')")
.and().formLogin().loginPage("/login").permitAll()
.usernameParameter("ssoId").passwordParameter("password")
.and().csrf()
.and().exceptionHandling().accessDeniedPage("/Access_Denied");
}
#Bean(name="authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
From the documentation for the methods for Login().loginPage("/login") says the it use for redirecting to the login page. For single page this configuration doesn't relevant.
How I should configure spring for single page application? I mean how to configure login, logout in controller and in configuration file.
Spring Lemon can be a complete example for this, but let me summarize the things below.
By default, when a user successfully logs in, Spring Security redirects him to the home page. When a login fails, or after a successful logout, the user is redirected back to the login page. Also, on trying to access URLs for which a user does not have sufficient rights, he is redirected to the login page.
As you say, this behavior won't suit for single page applications. Your API should instead send a 200 response along with the user data, or a 4xx response. This can be done by supplying your own handlers, like this:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
...
.successHandler(your authentication success handler object)
.failureHandler(your authentication failure handler object)
.and()
.logout()
...
.logoutSuccessHandler(your logout success handler object)
.and()
.exceptionHandling()
.authenticationEntryPoint(new Http403ForbiddenEntryPoint())
...
}
You will find many examples in the Internet on how to code these handler classes. For example, in the spring-lemon project, these are coded as below.
Authentication Success Handler
#Component
public class AuthenticationSuccessHandler
extends SimpleUrlAuthenticationSuccessHandler {
#Autowired
private ObjectMapper objectMapper;
#Autowired
private LemonService<?,?> lemonService;
#Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
AbstractUser<?,?> currentUser = lemonService.userForClient();
response.getOutputStream().print(
objectMapper.writeValueAsString(currentUser));
clearAuthenticationAttributes(request);
}
}
In summary, it returns a 200 response with the JSONified current-user in the response data.
Authentication Failure Handler
In fact, there is no need to code a class for the authentication failure handler - the SimpleUrlAuthenticationFailureHandler provided by Spring, if instantiated without any arguments, works as desired.
Logout Success Handler
public class LemonLogoutSuccessHandler
implements LogoutSuccessHandler {
#Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
}
For a detailed example, referring Spring Lemon's LemonWebSecurityConfig class and other classes in it's security packages of various modules can be helpful.