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.
Related
I employed this spring boot SAML sample project to my existing project. But I noticed that the redirect url will be automatically set as the url where I get redirected to the log in page. I am wondering is there anyway for me to change this redirect url to a different one? The base url that will trigger redirection to the auth page is a http link but I need to redirect to a https link after logging in. (Now I am not able to move forward to the page where I can input credentials. I am blocked by the invalid url error.) For now I tried create a custom LoginSuccessHandler that implements AuthenticationSuccessHandler .
#Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
redirectStrategy.sendRedirect(request, response,"/testing");
}
}
And I had this in the saml config
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.authenticationEntryPoint(samlEntryPoint());
http
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(samlFilter(), CsrfFilter.class);
http
.authorizeRequests()
.antMatchers("/api/**").permitAll()
.anyRequest().authenticated().and().formLogin().successHandler(successHandler);
http
.logout()
.disable(); // The logout procedure is already handled by SAML filters.
}
But this doesn't work. The redirect url does't get changed to what I specified ("/testing"). While using Inteli J debugging mode, it seems that this custom class is not being executed. So I am suspecting I had the wrong way calling this class. Then I also tried provided this LoginSuccessHandler class to samlWebSSOProcessingFilter, which also made no difference.
#Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setFilterProcessesUrl("/spring-security-saml2-sample");
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
return samlWebSSOProcessingFilter;
}
You need to do the following changes to redirect to desired URL.
First inject the dependency LoginSuccessHandler in the WebSecurityConfig file
#Autowired
private LoginSuccessHandler loginSuccessHandler;
and then update the SAMLProcessingFilter as mentioned below
// Processing filter for WebSSO profile messages
#Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOProcessingFilter;
}
then do the following change in the LoginSuccessHandler
#Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// Add your server URL below.
String redirectUri = "https://{server}/testing";
response.sendRedirect(redirectUri);
}
}
We have a legacy Spring application (A) (that is not using spring-boot) that handles authentication and writes the session to Redis using spring-session (the data in Redis is stored as XML).
We now want to introduce a new application (B), using spring-boot 2.2.6.RELEASE and spring-session Corn-RC1, that should be useable if a user has signed into (A) with ROLE_ADMIN. I.e. this can be regarded as a very crude way of doing single sign on. A user should never be able to authenticate in B (it'd like to disable authentication if possible), it should only check that an existing user is authenticated in the session repository (redis) and has ROLE_ADMIN. Both A and B will be located under the same domain so cookies will be propagated by the browser. I've tried various different ways of getting this to work, for example:
#Configuration
#EnableWebSecurity
class ServiceBSpringSecurityConfig : WebSecurityConfigurerAdapter() {
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.formLogin()
.and()
.httpBasic().disable()
}
}
but this will show the default login screen:
I've also tried removing this part entirely:
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
but then it'll generate a default user and password and it does not seem to call the configure method (or the configuration doesn't work regardless).
How can I solve this?
What you need is to disable formLogin and httBasic on Application B and add a filter before spring's authentication filter AnonymousAuthenticationFilter or UsernamePasswordAuthenticationFilter. In the custom filter you will extract the cookie/header/token from the request object and based on that reach out to the redis cache for session details. This filter would then validate the session and create object of type org.springframework.security.core.Authentication and set that in the current SpringSecurityContext.
Below is the sudo code for this;
ServiceBSpringSecurityConfig
#Configuration
#EnableWebSecurity
public class ServiceBSpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(authEntryPoint()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.httpBasic().disabled().and()
.formLogin().disabled().and()
.authorizeRequests().anyRequest().hasRole("ADMIN")
http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public AuthTokenFilter authTokenFilter() {
return new AuthTokenFilter();
}
#Bean
public AuthEntryPoint authEntryPoint() {
return new AuthEntryPoint()
}
}
AuthEntryPoint
public class AuthEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPoint.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// Very generic authEntryPoint which simply returns unauthorized
// Could implement additional functionality of forwarding the Application A login-page
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
AuthTokenFilter
public class AuthTokenFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// extract some sort of token or cookie value from request
token = request.getHeader("Token");
if (token != null) {
// Validate the token by retrieving session from redis cache
// Create org.springframework.security.core.Authentication from the token
Authentication auth = authFactory.getAuthentication(token);
// Set the spring security context with the auth
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
// Do something if token not present at all
}
// Continue to to filter chain
filterChain.doFilter(request, response);
}
}
As mentioned this is sudo code so some adjustment might be required. However the general gist of token based auth remains the same.
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.
...
}
How can I get my custom ResponseEntityExceptionHandler or OAuth2ExceptionRenderer to handle Exceptions raised by Spring security on a pure resource server?
We implemented a
#ControllerAdvice
#RestController
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
so whenever there is an error on the resource server we want it to answer with
{
"message": "...",
"type": "...",
"status": 400
}
The resource server uses the application.properties setting:
security.oauth2.resource.userInfoUri: http://localhost:9999/auth/user
to authenticate and authorize a request against our auth server.
However any spring security error will always bypass our exception handler at
#ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<Map<String, Object>> handleInvalidTokenException(InvalidTokenException e) {
return createErrorResponseAndLog(e, 401);
}
and produce either
{
"timestamp": "2016-12-14T10:40:34.122Z",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/templates/585004226f793042a094d3a9/schema"
}
or
{
"error": "invalid_token",
"error_description": "5d7e4ab5-4a88-4571-b4a4-042bce0a076b"
}
So how do I configure the security exception handling for a resource server? All I ever find are examples on how to customize the Auth Server by implementing a custom OAuth2ExceptionRenderer. But I can't find where to wire this to the resource server's security chain.
Our only configuration/setup is this:
#SpringBootApplication
#Configuration
#ComponentScan(basePackages = {"our.packages"})
#EnableAutoConfiguration
#EnableResourceServer
As noted in previous comments the request is rejected by the security framework before it reaches the MVC layer so #ControllerAdvice is not an option here.
There are 3 interfaces in the Spring Security framework that may be of interest here:
org.springframework.security.web.authentication.AuthenticationSuccessHandler
org.springframework.security.web.authentication.AuthenticationFailureHandler
org.springframework.security.web.access.AccessDeniedHandler
You can create implementations of each of these Interfaces in order to customize the response sent for various events: successful login, failed login, attempt to access protected resource with insufficient permissions.
The following would return a JSON response on unsuccessful login attempt:
#Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler
{
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException ex) throws IOException, ServletException
{
response.setStatus(HttpStatus.FORBIDDEN.value());
Map<String, Object> data = new HashMap<>();
data.put("timestamp", new Date());
data.put("status",HttpStatus.FORBIDDEN.value());
data.put("message", "Access Denied");
data.put("path", request.getRequestURL().toString());
OutputStream out = response.getOutputStream();
com.fasterxml.jackson.databind.ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(out, data);
out.flush();
}
}
You also need to register your implementation(s) with the Security framework. In Java config this looks like the below:
#Configuration
#EnableWebSecurity
#ComponentScan("...")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Override
public void configure(HttpSecurity http) throws Exception
{
http
.addFilterBefore(corsFilter(), ChannelProcessingFilter.class)
.logout()
.deleteCookies("JESSIONID")
.logoutUrl("/api/logout")
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/api/login")
.failureHandler(authenticationFailureHandler())
.successHandler(authenticationSuccessHandler())
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
}
/**
* #return Custom {#link AuthenticationFailureHandler} to send suitable response to REST clients in the event of a
* failed authentication attempt.
*/
#Bean
public AuthenticationFailureHandler authenticationFailureHandler()
{
return new RestAuthenticationFailureHandler();
}
/**
* #return Custom {#link AuthenticationSuccessHandler} to send suitable response to REST clients in the event of a
* successful authentication attempt.
*/
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler()
{
return new RestAuthenticationSuccessHandler();
}
/**
* #return Custom {#link AccessDeniedHandler} to send suitable response to REST clients in the event of an attempt to
* access resources to which the user has insufficient privileges.
*/
#Bean
public AccessDeniedHandler accessDeniedHandler()
{
return new RestAccessDeniedHandler();
}
}
In case if you're using #EnableResourceServer, you may also find convenient to extend ResourceServerConfigurerAdapter instead of WebSecurityConfigurerAdapter in your #Configuration class. By doing this, you may simply register a custom AuthenticationEntryPoint by overriding configure(ResourceServerSecurityConfigurer resources) and using resources.authenticationEntryPoint(customAuthEntryPoint()) inside the method.
Something like this:
#Configuration
#EnableResourceServer
public class CommonSecurityConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(customAuthEntryPoint());
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
There's also a nice OAuth2AuthenticationEntryPoint that can be extended (since it's not final) and partially re-used while implementing a custom AuthenticationEntryPoint. In particular, it adds "WWW-Authenticate" headers with error-related details.
You are not able to make use of Spring MVC Exception handler annotations such as #ControllerAdvice because spring security filters kicks in much before Spring MVC.
If you're using token validation URL with config similar to Configuring resource server with RemoteTokenServices in Spring Security Oauth2 which returns HTTP status 401 in case of unauthorized:
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
return tokenService;
}
Implementing custom authenticationEntryPoint as described in other answers (https://stackoverflow.com/a/44372313/5962766) won't work because RemoteTokenService use 400 status and throws unhandled exceptions for other statuses like 401:
public RemoteTokenServices() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
So you need to set custom RestTemplate in RemoteTokenServices config which would handle 401 without throwing exception:
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
RestOperations restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400 and 401
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
}
tokenService.setRestTemplate(restTemplate);
return tokenService;
}
And add dependency for HttpComponentsClientHttpRequestFactory:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
OAuth2ExceptionRenderer is for an Authorization Server. The correct answer is likely to handle it like detailed in this post (that is, ignore that it's oauth and treat it like any other spring security authentication mechanism): https://stackoverflow.com/a/26502321/5639571
Of course, this will catch oauth related exceptions (which are thrown before you reach your resource endpoint), but any exceptions happening within your resource endpoint will still require an #ExceptionHandler method.
We can use this security handler to pass the handler to spring mvc #ControllerAdvice
#Component
public class AuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
private static final Logger LOG = LoggerFactory.getLogger(AuthExceptionHandler.class);
private final HandlerExceptionResolver resolver;
#Autowired
public AuthExceptionHandler(#Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
LOG.error("Responding with unauthorized error. Message - {}", authException.getMessage());
resolver.resolveException(request, response, null, authException);
}
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
LOG.error("Responding with access denied error. Message - {}", accessDeniedException.getMessage());
resolver.resolveException(request, response, null, accessDeniedException);
}
}
Then define the exception by using #ControllerAdvice so that we can manage the global exception handler in one place..
This is possible. Since the original question is for a REST controller that needs to return a custom JSON response, I will write up a complete answer step by step which worked for me. First and foremost, it seems you cannot handle this with a #ControllerAdvice that extends ControllResponseEntityExceptionHandler. You need a separate handler that extends AccessDeniedHandler. Follow the below steps.
Step 1: Create a custom handler class that extends AccessDeniedHandler
#Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private static final String JSON_TYPE = "application/json";
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
MyErrorList errors = new MyErrorList();
errors.addError(new MyError("", "You do not have permission to access this resource."));
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(JSON_TYPE);
OutputStream output = response.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(output, errors);
output.flush();
}
}
'MyError' above is a simple POJO to represent an error json structure and MyErrorList is another POJO that holds a list of 'MyError's.
Step 2: Inject the Handler created above into the Security configuration
#Autowired
private VOMSAccessDeniedHandler accessDeniedHandler;
Step 3: Register the accessDeniedHandler in your configure method
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
With Step 2 and Step 3, Your SecurityConfiguration should look something like this (Note that I am omitting code that is not relevant to this problem to shorten the length of this answer):
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private MyAccessDeniedHandler accessDeniedHandler;
// Other stuff
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/register").permitAll()
.antMatchers("/authenticate").permitAll()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Adapting the accepted answer for use with Spring OAuth2ResourceServer for JWT authentication, because without special configuration, it will register its own BearerTokenAuthenticationEntryPoint, and ignore the one we set in .exceptionHandling().authenticationEntryPoint()
Hence, in our WebSecurityConfigurerAdapter we have:
#Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
protected void configure(HttpSecurity http) throws Exception {
http
// ... all the usual stuff ...
// configure OAuth2 (OIDC) JWT and set a custom authentication failure handler
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt().and()
.authenticationEntryPoint(authenticationFailureHandler));
}
where AuthenticationFailureHandler is coded as suggested in earlier answers:
#Component
public class AuthenticationFailureHandler implements AuthenticationEntryPoint {
public AuthenticationFailureHandler() {
}
// Autowire our own CustomExceptionHandler: must be qualified because Spring Boot has others in the classpath
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws
IOException {
resolver.resolveException(request, response, null, authException);
}
}
In our CustomExceptionHandler (which is autowired above, but not mentioned by class name explicitly) we add a method for AuthenticationException handling:
#ExceptionHandler(value = {AuthenticationException.class})
protected ResponseEntity<?> handleAuthenticationException(RuntimeException ex, WebRequest request) {
return ... something ... // create custom error response here
}
Spring 3.0 Onwards,You can use #ControllerAdvice (At Class Level) and extends org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler class from CustomGlobalExceptionHandler
#ExceptionHandler({com.test.CustomException1.class,com.test.CustomException2.class})
public final ResponseEntity<CustomErrorMessage> customExceptionHandler(RuntimeException ex){
return new ResponseEntity<CustomErrorMessage>(new CustomErrorMessage(false,ex.getMessage(),404),HttpStatus.BAD_REQUEST);
}
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