Spring Keycloak: Get User ID - java

I have developed a Spring Boot Webservice and use Keycloak for Access Management.
The website stores some userdata in a database. I try to connect these data with the user logged in.
At the moment I store the username with the data. But I like to store the user id instead the username. How can I do that?
I try to get SecurityContext by this:
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public KeycloakSecurityContext getKeycloakSecurityContext() {
return ((KeycloakPrincipal<KeycloakSecurityContext>) getRequest().getUserPrincipal()).getKeycloakSecurityContext();
}
But I get an error:
There was an unexpected error (type=Internal Server Error, status=500).
Error creating bean with name 'scopedTarget.getKeycloakSecurityContext'
defined in com.SiteApplication: Bean instantiation via factory method
failed; nested exception is
org.springframework.beans.BeanInstantiationException: Failed to
instantiate [org.keycloak.KeycloakSecurityContext]: Factory method
'getKeycloakSecurityContext' threw exception; nested exception is
java.lang.ClassCastException:
org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken
cannot be cast to org.keycloak.KeycloakPrincipal
Is this the right way? What is missing?
Thank you!

I found a much simpler solution than the above:
#GetMapping
public ResponseEntity getEndpoint(String someParam, HttpServletRequest request) {
KeycloakAuthenticationToken principal = (KeycloakAuthenticationToken) request.getUserPrincipal();
String userId = principal.getAccount().getKeycloakSecurityContext().getIdToken().getSubject();
//....do something
return new ResponseEntity(HttpStatus.OK);
}
I think the exception you are getting above is because you are trying to cast getRequest().getUserPrincipal() to KeycloakPrincipal<KeycloakSecurityContext> while it is of type KeycloakAuthenticationToken, so ((KeycloakAuthenticationToken) getRequest().getUserPrincipal()) would work.

I've done something similar in our code.
public class AuthenticationTokenProcessingFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!(request instanceof HttpServletRequest)) {
throw new RuntimeException("Expecting a HTTP request");
}
RefreshableKeycloakSecurityContext context = (RefreshableKeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
if (context == null) {
handleNoSecurityContext(request, response, chain);
return;
}
AccessToken accessToken = context.getToken();
Integer userId = Integer.parseInt(accessToken.getOtherClaims().get("user_id").toString());
chain.doFilter(request, response);
}
}
Before you can do this you must add the user_id to the access tokens being issued by keycloak. You can do this through a mapper as shown in the screenshot below.
Also, don't forgot to add the processing filter from above to your application lifecycle by adding a #Bean method to your application class.
#Configuration
#EnableAutoConfiguration(exclude = {FallbackWebSecurityAutoConfiguration.class, SpringBootWebSecurityConfiguration.class, DataSourceAutoConfiguration.class})
#ComponentScan
#EnableAsync
public class MyServiceClass {
public static void main(String[] args) {
Properties properties = new Properties();
new SpringApplicationBuilder(MyServiceClass.class)
.properties(properties)
.bannerMode(Banner.Mode.OFF)
.run(args);
}
#Bean
public AuthenticationTokenProcessingFilter authFilter() {
return new AuthenticationTokenProcessingFilter();
}
}

All solutions above are using the (very) deprecated Keycloak Spring adapters, which was a solution 2 years ago, but isn't anymore.
2 Alternatives to Keycloak Spring adapters, both having very easy way to access-token claims from Authentication "auto-magically" injected by Spring as #Controller method parameter.
spring-addons-webmvc-jwt-resource-server
Sample here: almost everything configurable from properties
#GetMapping
#PreAuthorize("isAuthenticated()")
public ResponseEntity getEndpoint(OAuthentication<OpenidClaimSet> auth) {
final var preferredUsername = auth.getClaims().getPreferredUsername();
final var subject = auth.getClaims().getSubject();
//....do something
return new ResponseEntity(HttpStatus.OK);
}
spring-boot-starter-oauth2-resource-server
Sample there: requires much more Java config
#GetMapping
#PreAuthorize("isAuthenticated()")
public ResponseEntity getEndpoint(JwtAuthenticationToken auth) {
final var preferredUsername = auth.getToken().getClaimAsString(StandardClaimNames.SUB);
final var subject = auth.getToken().getClaimAsString(StandardClaimNames.PREFERRED_USERNAME);
//....do something
return new ResponseEntity(HttpStatus.OK);
}

Related

How to validate a custom session in spring

I'm new to spring, so just looking for a general direction / advice.
I'm building a small microservice. The service auth is already setup to validate a JWT that is passed to it (issued by a different microservice).
The JWT contains a sessionid claim. I want to be able to get that claim, and validate that the session is still active.
For now that will be by querying the database directly, although in future when we have a dedicated session-service, I will change it to call that service instead.
I can obviously get the claim in each controller that should validate the session (basically all of them), but I am looking for a more global option, like a request interceptor (that only triggers AFTER the JWT is validated, and has access to the validated JWT).
Is there a recommended way to do this kind of thing in spring?
You have several options to do that: using a ControllerAdvice, perhaps AOP, but probably the way to go will be using a custom security filter.
The idea is exemplified in this article and this related SO question.
First, create a filter for processing the appropriate claim. For example:
public class SessionValidationFilter extends GenericFilterBean {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String AUTHORIZATION_BEARER = "Bearer";
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
// We will provide our own validation logic from scratch
// If you are using Spring OAuth or something similar
// you can instead use the already authenticated token, something like:
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// if (authentication != null && authentication.getPrincipal() instanceof Jwt) {
// Jwt jwt = (Jwt) authentication.getPrincipal();
// String sessionId = jwt.getClaimAsString("sessionid");
// ...
// Resolve token from request
String jwt = getTokenFromRequest(httpServletRequest);
if (jwt == null) {
// your choice... mine
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// If the token is not valid, raise error
if (!this.validateToken(jwt)) {
throw new BadCredentialsException("Session expired");
}
// Continue filter chain
filterChain.doFilter(servletRequest, servletResponse);
}
// Resolve token from Authorization header
private String getTokenFromRequest(HttpServletRequest request){
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.isNotEmpty(bearerToken) && bearerToken.startsWith(AUTHORIZATION_BEARER)) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
// Validate the JWT token
// We can use the jjwt library, for instance, to process the JWT token claims
private boolean validateToken(String token) {
try {
Claims claims = Jwts.parser()
// .setSigningKey(...)
.parseClaimsJws(token)
.getBody()
;
String sessionId = (String)claims.get("sessionid");
// Process as appropriate to determine whether the session is valid or not
//...
return true;
} catch (Exception e) {
// consider logging the error. Handle as appropriate
}
return false;
}
}
Now, assuming for instance that you are using Java configuration, add the filter to the Spring Security filter chain after the one that actually is performing the authentication:
#Configuration
public class SecurityConfiguration
extends WebSecurityConfigurerAdapter {
// the rest of your code
#Override
protected void configure(HttpSecurity http) throws Exception {
// the rest of your configuration
// Add the custom filter
// see https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/reference/htmlsingle/#filter-stack
// you can name every provided filter or any specified that is included in the filter chain
http.addFilterAfter(new SessionValidationFilter(), BasicAuthenticationFilter.class);
}
}

Spring with websocket and session interceptor + Tomcat = problem

I am facing weird problem. I am developing web application based on Spring Boot with WebSockets. I need to track anonymous web socket connections, therefore I am using custom Principal implementation which I am creating in custom DefaultHandshakeHandler -> determineUser method. To create such Principal object, I need some data from httpsession, therefore I am using HttpSessionHandshakeInterceptor to fill Map<String, Object> attributes attribute in mentioned method.
Everything works like a charm, until I switch application packaging from JAR (using embeded Tomcat) to WAR and deploy it on standalone Tomcat. Suddenly, attribute attributes is empty as if WS connection is using different session than HTTP connection and therefore HttpSessionHandshakeInterceptor will not pass required attributes. Any ideas, why it is behaving differently?
A few parts of the code:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/ws");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("ws-endpoint")
.setHandshakeHandler(new AnonymousHandshakeHandler())
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS();
}
}
Custom handshake handler:
public class AnonymousHandshakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
Principal principal = request.getPrincipal();
if (principal == null) {
SessionData sd = (SessionData) attributes.get(AppVariables.MODEL_PARAM_SESSION);
if (sd != null){
principal = new AnonymousPrincipal();
((AnonymousPrincipal) principal).setName(sd...);
}
}
return principal;
}
}
UPDATE:
Using custom handshake interceptor I can see that WS connections have different session IDs. That explains empty attributes, but why? Why requests on embeded Tomcat are using the same session but on standalone Tomcat they are creating new session on every request?
Handshake interceptor:
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession();
System.out.println(session.getId());
attributes.put(AppVariables.MODEL_PARAM_SESSION, session.getAttribute(AppVariables.MODEL_PARAM_SESSION));
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
}
}
I resolved my problem. It consisted of two problems + my stupidity:
I am using <CookieProcessor sameSiteCookies="none" /> and I forgot about it. That is why every request was generating new session (sameSite without https was not creating JSESSIONID cookie).
My proxy was not set up well. That is why my AnonymousHandshakeHandler was not used. See: link

Spring Security: how can i update/reload usernames and passwords used by inMemoryAuthentication

I'm new to Spring and have problems with the inMemoryAuthentication. I set up a very basic Spring MVC app whichs uses Spring Security to authenticate the users. Spring security gets the Username & Password from a HashMap.
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
Map<String, String> userMap = passwordService.getPasswordMap();
for (Map.Entry<String, String> entry : userMap.entrySet()) {
auth.inMemoryAuthentication().withUser(entry.getKey()).password(entry.getValue()).roles("USER");
}
}
The Problem is, the new Password is only accepted if i restart the app. A simple logout and login doenst work. Here is my logout code:
#RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout(ModelMap model, HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (null != auth) {
new SecurityContextLogoutHandler().logout(request, response, auth);
request.getSession().invalidate();
}
How can i force the app to reload the information from the HashMap on runtime?
You can create a bean InMemoryUserDetailsManager, and inject it to AuthenticationManagerBuilder
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(inMemoryUserDetailsManager());
}
#Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
// load you user here
final Properties users = new Properties();
return new InMemoryUserDetailsManager(users);
}
Then you can inject your bean inMemoryUserDetailsManager in your service and update the user in inMemoryUserDetailsManager.

Spring boot HTTP Basic pass through to JDBC

I have configured a JDBC data source and autowired the JDBCTemplate to execute custom SQL queries. I also have a simple HTTP Basic authentication:
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
However, I would like to use the user and password used for HTTP Basic to authenticate the user to the data base itself, i.e pass through the credentials of HTTP Basic to the data source and execute queries as the user who logged in with HTTP Basic authentication. I'm facing two issues here, one is that the username and password are in the application.properties file that I want to override every time a user authenticates and also (reload?) execute queries as that user instead of the ones specified in the properties file.
Update 1:
I could programmatically use username and password like below:
#Bean
#Primary
public DataSource dataSource() {
return DataSourceBuilder
.create()
.username("")
.password("")
.url("")
.driverClassName("")
.build();
}
But how to call this every time a user logs with the HTTP Basic auth with those credentials?
Use UserCredentialsDataSourceAdapter as #"M. Deinum" have suggested with some kind of filter or handling AuthenticationSuccessEvent.
Basically you should just call setCredentialsForCurrentThread method with current principal username and password.
You'll have to disable credential erasure for authentication manager in order to be able to retrieve user password after authentication.
#EnableWebSecurity
public static class Security extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.eraseCredentials(false) // for password retrieving
.inMemoryAuthentication()
.withUser("postgres").password("postgres1").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().mvcMatchers("/").fullyAuthenticated();
}
}
Datasource adapter:
#Bean
public UserCredentialsDataSourceAdapter dataSource(DataSourceProperties properties) {
final UserCredentialsDataSourceAdapter dataSourceAdapter = new UserCredentialsDataSourceAdapter();
dataSourceAdapter.setTargetDataSource(DataSourceBuilder.create()
.driverClassName(properties.getDriverClassName())
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.type(SimpleDriverDataSource.class) // disable pooling
.build());
((SimpleDriverDataSource) dataSourceAdapter.getTargetDataSource()).setDriverClass(org.postgresql.Driver.class); //binder won't set it automatically
return dataSourceAdapter;
}
AuthenticationSuccessHandler:
#Component
public static class AuthenticationHandler /*implements ApplicationListener<AuthenticationSuccessEvent> use that if your spring version is less than 4.2*/ {
private final UserCredentialsDataSourceAdapter dataSourceAdapter;
#Autowired
public AuthenticationHandler(UserCredentialsDataSourceAdapter dataSourceAdapter) {
this.dataSourceAdapter = dataSourceAdapter;
}
#EventListener(classes = AuthenticationSuccessEvent.class)
public void authenticationSuccess(AuthenticationSuccessEvent event) {
final Authentication authentication = event.getAuthentication();
final User user = (User) authentication.getPrincipal();
dataSourceAdapter.setCredentialsForCurrentThread(user.getUsername(), user.getPassword()); // <- the most important part
}
}
Or you can use Filter instead of event listener:
#Component
public static class DataSourceCredentialsFilter extends GenericFilterBean {
private final UserCredentialsDataSourceAdapter dataSourceAdapter;
#Autowired
public DataSourceCredentialsFilter(UserCredentialsDataSourceAdapter dataSourceAdapter) {
this.dataSourceAdapter = dataSourceAdapter;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
final User user = (User) authentication.getPrincipal();
dataSourceAdapter.setCredentialsForCurrentThread(user.getUsername(), user.getPassword());
chain.doFilter(request, response);
dataSourceAdapter.removeCredentialsFromCurrentThread();
}
}
See full example here.

Handle Security exceptions in Spring Boot Resource Server

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);
}

Categories