KeyCloak Spring Boot - Add custom code on auth success - spring-boot

I am using KeyCloak integration with Spring Boot as in this guide. I have my security config like below:
class KeycloakSecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests().antMatchers("/**").authenticated()
.anyRequest().permitAll();
}
}
I want to add some custom code for onAuthenticationSuccess before KeyCloak redirects me to the actual resource. I tried implementing a custom class with AuthenticationSuccessHandler and do formLogin().successHandler(...). This didn't work. How can I get this working??

If you still prefer to use Spring Boot KeyCloak, something like this will work.
public class KeyCloakAuthSuccessHandler extends KeycloakAuthenticationSuccessHandler {
public KeyCloakAuthSuccessHandler(AuthenticationSuccessHandler fallback) {
super(fallback);
}
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication.getPrincipal() instanceof KeycloakPrincipal) {
AccessToken token = ((KeycloakPrincipal<?>) authentication.getPrincipal()).getKeycloakSecurityContext().getToken();
}
super.onAuthenticationSuccess(request, response, authentication);
}
}
And in your security config or similar file that extends KeyCloakWebSecurityConfigurerAdapter do the following:
#Bean
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(authenticationManagerBean());
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
filter.setAuthenticationSuccessHandler(successHandler());
return filter;
}
#NotNull
#Bean
public KeyCloakAuthSuccessHandler successHandler() {
return new KeyCloakAuthSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler());
}

Related

How to configure two security configs with two filters in spring boot correctly?

I've implmemented security in my spring boot microservices project, the requirment is to have
two types of configurations, one for user request (from angular) and one from other services.
The design is to use JWT token for user request and API key for system calls.
Here is the config file (one file) but have also try to split it to two files with no impact:
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Configuration
#Order(1)
public static class APISecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${my.api.key.header}")
private String principalRequestHeader;
#Value("${my.api.key.token}")
private String principalRequestValue;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors().disable().csrf().disable();
httpSecurity
.antMatcher("/api/users/**")
.authorizeRequests() //
.anyRequest().authenticated()
.and()
.addFilterBefore(new APIKeyAuthFilter(principalRequestHeader, principalRequestValue), UsernamePasswordAuthenticationFilter.class);
}
}
#Order(2)
#Configuration
public static class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailsService userDetailsService;
#Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/users/**");
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors().disable().csrf().disable();
httpSecurity
.authorizeRequests()
.antMatchers("/users/UserEmailExist", "/users/User/Add", "/users/Authenticate",
"/users/User/ChangePassword")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/users/**").hasAnyRole(ROLE_ADMIN_USER, ROLE_MANAGER_USER)
.anyRequest().authenticated()
.and()
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
}
Each config has a filter attached to it, here the api one:
public class APIKeyAuthFilter extends GenericFilterBean {
private String principalRequestHeader;
private String principalRequestValue;
public APIKeyAuthFilter(String principalRequestHeader, String principalRequestValue) {
super();
this.principalRequestHeader = principalRequestHeader;
this.principalRequestValue = principalRequestValue;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if(request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
String apiKey = getApiKey((HttpServletRequest) request);
if(apiKey != null) {
if(apiKey.equals(principalRequestValue)) {
ApiKeyAuthenticationToken apiToken = new ApiKeyAuthenticationToken(apiKey, AuthorityUtils.NO_AUTHORITIES);
SecurityContextHolder.getContext().setAuthentication(apiToken);
} else {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(401);
httpResponse.getWriter().write("Invalid API Key");
return;
}
}
}
chain.doFilter(request, response);
}
}
Here is the filter for jwt (normal user from angular):
public class AuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtUtils jwtUtils;
#Autowired
private MyUserDetailsService userDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
MSUserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
}
I've created two different controllers, one with prefix /api/users and second /users.
Here is what happen in two different scenarios:
The user login from Angular, get jwt token and process request which end up in the Jwt filter,
this scenarion looking good with no issues as the user is able to process request as long
he is authenticate.
Microservice send a request with api-key to url with /api/users prefix, it ended up on the same
filter the normal user ended which is not correct and without JWT token he is actually
able to proceed to the controller and process the request without going
to the correct filter.
The only solution I have is to have only one filter and process the header
for api-key and jwt but it doesn't seem right.
I've looked online and try to figure out what I'm doing wrong but no clue as of now.
An update on this issue so I hope it will help to the community.
Firstly, I removed the following code and this mainly fix the problem:
// #Override
// public void configure(WebSecurity web) throws Exception {
// web.ignoring().antMatchers("/api/users/**");
// }
The way the solution work as a whole is that the first configuration #Order(1) you
define .antMatcher which means the configuration will work only for urls that match
the prefix.
So now, scenario 1. User from Angular go the the JWT filter only.
scenario 2. API user will lend in the API filter first! But once it's done (After succesfull authentication) it still
continue to the JWT filter but becuase it doesn't have JWT the filter not doing anything.
I would like to avoid to other filter in case of API call but the solution work,
problem solved.
I must say that security in spring boot is the most complex I came across so far from other features.
Because the AuthTokenFilter is instantiated with #Bean, which causes the filter to be added to the ApplicationFilterChain, after the APIKeyAuthFilter is processed, it can also enter the AuthTokenFilter.

Unable to use permitAll() with Spring Boot 2.3.4 to allow access to Swagger UI after integrating with API-Key Authentication

I tried integrating API-Key authentication mechanism to Spring Boot Application in the following way:
Created a CustomAPIKeyAuthFilter that extends AbstractPreAuthenticatedProcessingFilter where it gets the preauthenticated principal from the headers of the request.
public class CustomAPIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private String principalRequestHeader;
private String principalAuthKey;
public CustomAPIKeyAuthFilter(String principalRequestHeader, String principalAuthKey) {
this.principalRequestHeader = principalRequestHeader;
this.principalAuthKey = principalAuthKey;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader(principalRequestHeader);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
// anything to be returned here??
return "TBD";
}
}
Created WebSecurityConfig that extends WebSecurityConfigurerAdapter. In this one, the custom filter is injected inside the overridden method protected void configure(HttpSecurity httpSecurity) {}
#EnableWebSecurity
#Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${superuser}")
private String principalRequestHeader;
#Value("${superuserauthkey}")
private String principalRequestValue;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
CustomAPIKeyAuthFilter filter = new CustomAPIKeyAuthFilter(principalRequestHeader, principalRequestValue);
filter.setAuthenticationManager(new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String principal = (String) authentication.getPrincipal();
if (principalRequestValue.equals(principal)){
authentication.setAuthenticated(true);
} else {
throw new BadCredentialsException("Missing API Key");
}
return authentication;
}
});
httpSecurity.
cors().and().
csrf().disable().authorizeRequests()
.antMatchers("**swagger**").permitAll() // this is the part that is not working for me
.anyRequest().authenticated()
.and()
.addFilter(filter)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
As you can see from the comment above, even though I used permitAll, I get the error 401 No pre-authenticated principal found in request at runtime if I try to access Swagger UI which was working before introducing spring-boot-starter-security related dependencies in my pom.xml. Is there a better way to exclude swagger UI alone from the list of URL end points that need API-key based authentication ?
Note: I am using springfox-swagger2 implementation of Swagger and the version used is 2.8.0.
Swagger have api endpoint which should be allowed in security level, add the below snippet in WebSecurityConfig.class
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**");
}
You could also try permitAll() to the patterns included.This will exclude the swagger from being authenticated.

How does Spring Security Filter Work With Custom Authentication and How To Combine It with Servlet Filter?

So I have a question regarding Spring Security. So I want to check authentication using custom header which then I want to check the token given in the custom header to redis value and set the data object as credentials at custom implementation of abstract authentication token.
I have already followed the tutorial in this web: https://shout.setfive.com/2015/11/02/spring-boot-authentication-with-custom-http-header/, but I can't update the authentication interface in SecurityContextHolder.getContext() (I set the credentials in my implementation of Authentication Interface, but when I get it in the service, the credentials is null).
I also found other problems, I actually want to order the filter like this:
ExceptionHandlerFilter (to catch exception error in the filter) -> Other filter or CustomWebSecurityConfigurerAdapter.
But when the url matches the antMatcher, I found that ExceptionHandlerFilter was skipped by the application.
I was so confused by this and could not find better tutorial in implementing custom authentication using Spring Security. So I want to ask whether you guys can tell me how Spring Security works and how to combine it with Filter?
Here is my first filter to catch exception
#Component
#Order(0)
public class ExceptionHandlerFilter extends OncePerRequestFilter {
private JaminExceptionHandler exceptionHandler;
private ObjectMapper objectMapper = new ObjectMapper();
#Autowired
public ExceptionHandlerFilter(JaminExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Throwable exception) {
ResponseEntity<?> responseEntity = this.exceptionHandler.handleException(exception, request);
response.setStatus(responseEntity.getStatusCode().value());
response.setHeader("Content-Type", "application/json");
response.getWriter().write(this.objectMapper.writeValueAsString(responseEntity.getBody()));
}
}
}
Here is my Auth Filter
#Component
public class AuthFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("J-Auth");
if (token != null) {
Authentication auth = new JaminAuthenticationToken(token);
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
} else {
throw new JaminException("Not authorized", JaminExceptionType.NOT_AUTHORIZED, HttpStatus.UNAUTHORIZED);
}
}
}
Authentication Provider
#Component
public class JaminAuthenticationProvider implements AuthenticationProvider {
private RedisTemplate<String, String> authRedis;
private ObjectMapper objectMapper = new ObjectMapper();
#Autowired
public JaminAuthenticationProvider(#Qualifier("authRedis") RedisTemplate<String, String> authRedis) {
this.authRedis = authRedis;
}
private UserDTO getUserDTO(String token) throws IOException {
String userData = this.authRedis.opsForValue().get(token);
if (userData == null) {
throw new JaminException("Not authorized", JaminExceptionType.NOT_AUTHORIZED, HttpStatus.UNAUTHORIZED);
}
return this.objectMapper.readValue(userData, UserDTO.class);
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JaminAuthenticationToken auth = (JaminAuthenticationToken) authentication;
try {
UserDTO userDTO = this.getUserDTO(auth.getToken());
auth.setCredentials(userDTO);
return auth;
} catch (IOException e) {
e.printStackTrace();
}
throw new JaminException("Not authorized", JaminExceptionType.NOT_AUTHORIZED, HttpStatus.UNAUTHORIZED);
}
#Override
public boolean supports(Class<?> authentication) {
return JaminAuthenticationToken.class.isAssignableFrom(authentication);
}
}
WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
#Order(1)
public class JaminSecurityAdapter extends WebSecurityConfigurerAdapter {
#Autowired
private JaminAuthenticationProvider jaminAuthenticationProvider;
private void disableDefaultSecurity(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();
http.formLogin().disable();
http.logout().disable();
http.httpBasic().disable();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
this.disableDefaultSecurity(http);
http.antMatcher("/auth/check")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(new AuthFilter(), BasicAuthenticationFilter.class);
// http.authorizeRequests().anyRequest().permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(jaminAuthenticationProvider);
}
}
Spring Security has some "before and after" steps. There are a few Handlers that can help. I don't know your code, but if you can get your authentication ok, maybe you just have to extend a SuccessHandler and set the authentication there, like i did in my blog project:
if(checkEmail(authentication)) {
val adminRole = SimpleGrantedAuthority("ROLE_ADMIN")
val oldAuthorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities()
val updateAuthorities = mutableListOf<GrantedAuthority>()
updateAuthorities.add(adminRole)
updateAuthorities.addAll(oldAuthorities)
SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken(authentication.getPrincipal(),
authentication.getCredentials(),
updateAuthorities))
}
And about the filters, maybe you can find your answer here. I don't like using filters and interceptors, but sometimes they are really necessary.

Multiple custom authentication with spring security

I have a spring application which uses a custom Authentication Filter say filter1 to authorize the request, this filter uses an authentication manager for authentication and is applicable for all urls in application.
Now, I want to implement a different Authentication Filter say filter2 which has to authorize special kind of request say with url (/api/). That is the all the request which has the url like (/api/**) has to use filter2.
Below is the code I've tried so far for this purpose.
public class SecurityAppConfig {
#Configuration
#Order(1)
public static class APISecurityConfig extends WebSecurityConfigurerAdapter {
private CustomAuthenticationManager1 manager1 = new CustomAuthenticationManager1();
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.formLogin().disable().csrf().disable().cors().disable().logout().disable();
if (manager1 != null) {
http.addFilterAfter(new Filter1(manager1),
AnonymousAuthenticationFilter.class);
}
}
}
#Configuration
#Order(2)
public static class OtherApiSecurityConfig extends WebSecurityConfigurerAdapter {
private AuthenticationManager2 manager2 = new AuthenticationManager2();
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.formLogin().disable().csrf().disable().cors().disable().logout().disable();
if (manager2 != null) {
http.antMatchers("/api/**").addFilterAfter(new Filter2(manager2),
AnonymousAuthenticationFilter.class);
}
}
}
}
At the time of app start up both the filter are getting registered with their manager but when this ("/api/**") request comes it goes to the first filter for authentication but never goes to the second filter. If I remove the first filter then it works properly but that would override the filters for other api request.
Below is how I've implemented managers and filters
public class Filter1 extends AbstractAuthenticationProcessingFilter {
//implementation omitted for brevity.
}
public class Filter2 extends AbstractAuthenticationProcessingFilter {
//implementation omitted for brevity.
}
public class AuthenticationManager1 implements AuthenticationManager {
//implementation omitted for brevity.
}
public class AuthenticationManager2 implements AuthenticationManager {
//implementation omitted for brevity.
}
Any thoughts on how can I get this working.
I don't think that you need two configs for your case. And I don't see why you need to implement your own authentication manager, even two of them. I guess you should use shared authentication manager instead, implement your own AuthenticationProvider (one for each type of authentication), and implement youe own authentication tokens. Besides that, since you're using AbstractAuthenticationProcessingFilter as a base class for you filters - you can set filterProcessesUrl into it, so your filter knows to which URL's it should be applied. So, in brief:
Authentication Tokens:
public class MyAuth1AuthenticationToken extends AbstractAuthenticationToken {
// Implementation depends on you auth scheme (you can look on
// `UsernamePasswordAuthenticationToken` for example)
}
public class MyAuth2AuthenticationToken extends AbstractAuthenticationToken {
// ...
}
Authentication Providers:
public class MyAuth1AuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Implementation really depends on you auth scheme (you can look on
// `AbstractUserDetailsAuthenticationProvider` for example)
}
#Override
public boolean supports(Class<?> authentication) {
// By this we're saying that this auth provider is responsible for our MyAuth1 auth request
return (MyAuth1AuthenticationToken.class.isAssignableFrom(authentication));
}
}
public class MyAuth2AuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// ...
}
#Override
public boolean supports(Class<?> authentication) {
return (MyAuth2AuthenticationToken.class.isAssignableFrom(authentication));
}
}
Filters:
public class Auth1Filter extends AbstractAuthenticationProcessingFilter {
public Auth1Filter(AuthenticationManager authManager, String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// extract user info here
// ...
// populate auth request with your info
MyAuth1AuthenticationToken authRequest = new MyAuth1AuthenticationToken(...);
// authenticate
return this.getAuthenticationManager().authenticate(authRequest);
}
}
public class Auth2Filter extends AbstractAuthenticationProcessingFilter {
public Auth2Filter(AuthenticationManager authManager, String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// extract user info here
// ...
// populate auth request with your info
MyAuth2AuthenticationToken authRequest = new MyAuth1AuthenticationToken(...);
// authenticate
return this.getAuthenticationManager().authenticate(authRequest);
}
}
Security Config:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
// registering our providers
auth
.authenticationProvider(new MyAuth1AuthenticationProvider())
.authenticationProvider(new MyAuth2AuthenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.csrf().disable()
.cors().disable()
.logout().disable();
AuthenticationManager authManager = http.getSharedObject(AuthenticationManager.class);
http.addFilterAfter(new Auth1Filter(authManager, "/**"), BasicAuthenticationFilter.class);
http.addFilterAfter(new Auth2Filter(authManager, "/api/**"), BasicAuthenticationFilter.class);
}
}
Hope it helps.

Custom Authentication provider with Spring Security and Java Config

How can I define a custom Authentication provider by using Spring Security with Java Configurations?
I would like to perform a login checking credentials on my own database.
The following does what you need (CustomAuthenticationProvider is your implementation which needs to be managed by Spring)
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
/**
* Do your stuff here
*/
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}
}
As shown on baeldung.com, define your authentication provider as follow:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
if (shouldAuthenticateAgainstThirdPartySystem(username, password)) {
// use the credentials
// and authenticate against the third-party system
return new UsernamePasswordAuthenticationToken(
name, password, new ArrayList<>());
} else {
return null;
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
}
and following code is corresponding java config:
#Configuration
#EnableWebSecurity
#ComponentScan("org.project.security")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider authProvider;
#Override
protected void configure(
AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic();
}
}

Resources