Why is role based access control not working? - spring-boot

I am trying to implement RBAC on my Spring Boot resource server. Previously I used Keycloak adapters, but now that they are deprecated I am having issues. I have followed the solution proposed here: https://stackoverflow.com/a/74572732/16489856, but it seems like that my configuration is wrong somehow.
Here is my Spring Security configuration:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/workstations").hasRole("USER")
.anyRequest().authenticated();
http
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
}
#SuppressWarnings("unchecked")
#Bean
public Jwt2AuthoritiesConverter authoritiesConverter() {
// Roles are taken from realm_access.roles
return jwt -> {
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
return realmRoles.stream().map(SimpleGrantedAuthority::new).toList();
};
}
public interface Jwt2AuthenticationConverter extends Converter<Jwt, AbstractAuthenticationToken> {
}
#Bean
public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
}
}
Roles are properly set, if I print them like so:
Jwt jwt = (Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Map<String, Object> resourceAccess = jwt.getClaim("realm_access");
Collection<String> resourceRoles = (Collection<String>) resourceAccess.get("roles");
System.out.println((Collection<String>) resourceAccess.get("roles"));
I get:
[default-roles-spring_reservations_realm_0, offline_access, uma_authorization, USER]
which is correct.
However, when I access /workstation endpoint, with a user that has the USER role, it fails with 403 forbidden status, how can I fix this?

Your authentication convert bean is not picked by spring-boot auto-configuration because boot expects a Converter<Jwt, ? extends AbstractAuthenticationToken> when you provide a Converter<Jwt, AbstractAuthenticationToken>.
Two different soultions:
apply the solution exposed in my answer you linked: provide the authentication converter explicitly. For that, replace .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) with .oauth2ResourceServer().jwt().jwtAuthenticationConver(authenticationConverter(authoritiesConverter()) (can be shorter if you use authenticationConverter as filterChain argument: authenticationConverter is bean in your conf):
#Bean
public SecurityFilterChain filterChain(
HttpSecurity http,
/* next is the exact type of your current authentication converter bean */
/* not Converter<Jwt, ? extends AbstractAuthenticationToken> like provided by default (and scanned for overrides) by boot */
Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception {
http.oauth2ResourceServer().jwt().jwtAuthenticationConver(authenticationConverter);
http.cors().disable(); // Are you really sure about this one?
http.authorizeHttpRequests()
// The following requires that either
// - the user is granted with "ROLE_USER" in Keycloak (with that case)
// - the user is granted with "user" in Keycloak and the authorities mapper adds "ROLE_" prefix and forces roles to uppercase
.requestMatchers("/workstations").hasRole("USER")
.anyRequest().authenticated();
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.csrf().disable(); // This is safe because of disabled sessions. Put it together maybe?
return http.build();
}
// Keep the rest of your conf as it is
change the type of your authenticationConverter #Bean to be the one that spring-boot looks for: Converter<Jwt, ? extends AbstractAuthenticationToken> (and not Converter<Jwt, AbstractAuthenticationToken> like in your current conf). This means changing a bit your Jwt2AuthenticationConverter definition so that the configurer destination type is a sub-type of AbstractAuthenticationToken (JwtAuthenticationToken in your case) and not AbstractAuthenticationToken itself. Yes, I know, this Java generics can be tricky sometimes):
public interface Jwt2AuthenticationConverter extends Converter<Jwt, JwtAuthenticationToken> {
}
// keep .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
// or change Converter<Jwt, AbstractAuthenticationToken> to Converter<Jwt, ? extends AbstractAuthenticationToken>
// for the type of bean injected in filterChain if you prefer explicit
// authenticationConverter override in fiterChain config like in the other solution
PS regarding comments
#dur is right regarding CORS: it doesn't look like a great idea to disable it, specially if your API is intended to be consumed by a web app.
For spring, a role is is nothing more than an authority prefixed with ROLE_. As a consequence:
hasRole("USER") is equivalent to hasAuthority("ROLE_USER") (but not hasAuthority("ROLE_user") as case is important)
neither hasAuthority("user") nor hasAuthority("SCOPE_USER") have hasRole equivalent (JWT decoder or not)
hasRole("SCOPE_USER") is equivalent to hasAuthority("ROLE_SCOPE_USER")
etc.
The SCOPE_ prefix is a choice from the default authorities converter because it maps authorities from scope claim (there is no OAuth2 nor OpenID standard claim for roles and it must pick it from somewhere standard). Using this same prefix when you pick authorities from somewhere else than the scope claim (like you do) would be very misleading...
Just use as prefix:
nothing (like in your question) if you are ok with hasAuthority or if roles in the access token are already prefixed with ROLE_
ROLE_ if you prefer reading hasRole over hasAuthority in your code (and roles in access tokens are not prefixed with ROLE_ already)

Here is how to configure Keycloak RBAC for a Spring Boot Resource Server, thanks to #ch4mp and #dur:
Spring Security configuration:
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
#RequiredArgsConstructor
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
private final JwtAuthConverter jwtAuthConverter;
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().disable().csrf().disable();
http.authorizeHttpRequests()
.requestMatchers("/workstations").hasRole("USER")
.anyRequest().authenticated();
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthConverter);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
}
JwtAuthConverter:
import lombok.NonNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
#Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private final JwtAuthConverterProperties properties;
public JwtAuthConverter(JwtAuthConverterProperties properties) {
this.properties = properties;
}
#Override
public AbstractAuthenticationToken convert(#NonNull Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream.concat(
jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
extractResourceRoles(jwt).stream()).collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
}
private String getPrincipalClaimName(Jwt jwt) {
String claimName = JwtClaimNames.SUB;
if (properties.getPrincipalAttribute() != null) {
claimName = properties.getPrincipalAttribute();
}
return jwt.getClaim(claimName);
}
#SuppressWarnings("unchecked")
private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt) {
Map<String, Object> resourceAccess = jwt.getClaim("realm_access");
Collection<String> resourceRoles = (Collection<String>) resourceAccess.get("roles");
return resourceRoles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
}
JwtAuthConverterProperties:
import lombok.Data;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
#Data
#Validated
#Configuration
public class JwtAuthConverterProperties {
private String resourceId;
private String principalAttribute;
}

Related

Spring Boot 3 + Spring Security 6 => 403 Forbidden with "requestMatchers"

For several days now I have been trying to solve a problem with Spring Security 6. I've read almost all the spring documentation for Spring Security 6 and I watched several tutorials and just cannot see where the mistake is. I looked at the code under a magnifying glass:
WebSecurityConfigurer.class:
package com.transfer.market.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
#Configuration
#EnableWebSecurity
public class WebSecurityConfigurer {
private final String ADMIN;
private final String ADMIN_PASS;
private final String SUPER;
private final String SUPER_PASS;
#Autowired
public WebSecurityConfigurer(AppSecurityExternalConfig appSecurityExternalConfig) {
this.ADMIN = appSecurityExternalConfig.getUser().getAdmin();
this.ADMIN_PASS = appSecurityExternalConfig.getPassword().getAdmin();
this.SUPER = appSecurityExternalConfig.getUser().getSup();
this.SUPER_PASS = appSecurityExternalConfig.getPassword().getSup();
}
#Bean
public UserDetailsService users() {
UserDetails admin = User.builder()
.username(ADMIN)
.password(encoder().encode(ADMIN_PASS))
.roles("ADMIN")
.build();
UserDetails sup = User.builder()
.username(SUPER)
.password(encoder().encode(SUPER_PASS))
.roles("ADMIN", "DBA")
.build();
return new InMemoryUserDetailsManager(admin, sup);
}
#Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/resource/**").permitAll()
.requestMatchers("/api/**").hasAnyRole("ADMIN", "DBA")
.requestMatchers("/db/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasRole('DBA')"))
.anyRequest().denyAll()
);
return http.build();
}
#Bean
public static BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
I have printed ADMIN, SUPER and their passwords to the console and they are for sure read correctly from the application.properties. So that is not the problem.
AppSecurityExternalConfig.class:
package com.transfer.market.configuration;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
#Data
#Configuration
#ConfigurationProperties(prefix = "config.security")
public class AppSecurityExternalConfig {
private User user;
private Password password;
#Data
#Component
#ConfigurationProperties(prefix = "user")
public static class User {
private String user;
private String admin;
private String sup;
}
#Data
#Component
#ConfigurationProperties(prefix = "password")
public static class Password {
private String user;
private String admin;
private String sup;
}
}
application.properties:
...
# Security:
config.security.user.admin=admin
config.security.password.admin=pass
config.security.user.sup=super
config.security.password.sup=pass
...
PlayerController.class:
#RestController
#Validated
public class PlayerController {
private final PlayerService playerService;
#Autowired
public PlayerController(PlayerService playerService) {
this.playerService = playerService;
}
#PostMapping("/api/players")
public ResponseEntity<Player> addSingle(#RequestBody #Valid Player player) {
return new ResponseEntity<>(playerService.addSingle(player), HttpStatus.CREATED);
}
...
It just keeps getting "403 Forbidden", but for all end points starting with "/resource" where they are .permitAll() it works and it's 200 OK. Why doesnt the other requestMatchers work? Please help.
#Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth.requestMatchers("/resource/**").permitAll()
.requestMatchers("/api/**")
.hasAnyRole("ADMIN", "DBA")
.requestMatchers("/db/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and
hasRole('DBA')"))
.anyRequest().denyAll());
return http.build();
}
You have to configured the basic authentication. Add the following statement in the SecurityFilterChain bean.
http.httpBasic();
i.e.
#Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth.requestMatchers("/resource/**").permitAll()
.requestMatchers("/api/**")
.hasAnyRole("ADMIN")
.requestMatchers("/db/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasRole('DBA')"))
.anyRequest().denyAll());
http.httpBasic();
return http.build();
}

How to override SecurityFilterChain in Spring Boot context?

I am facing the issue which is not obvious to resolve just by reading the documentation. While migrating to Spring Boot v2.7.4 / Spring Security v5.7.3 I have refactored the configuration not to extend WebSecurityConfigurerAdapter and to look like below:
#Configuration
#EnableWebSecurity
public class CustomSecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.
csrf().disable().
logout().disable().
authorizeRequests().anyRequest().permitAll();
return http.build();
}
}
The above method is called, however has no effect as SecurityFilterChain instance created by OAuth2SecurityFilterChainConfiguration is used instead (I see that from debug by inspecting the list of filter in the stack that has e.g. LogoutFilter which should be disabled by above configuration). Debug log:
2022-10-20 15:49:48.790 [main] o.s.b.a.s.DefaultWebSecurityCondition : Condition DefaultWebSecurityCondition on org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration$OAuth2SecurityFilterChainConfiguration matched due to AllNestedConditions 2 matched 0 did not; NestedCondition on DefaultWebSecurityCondition.Beans #ConditionalOnMissingBean (types: org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter,org.springframework.security.web.SecurityFilterChain; SearchStrategy: all) did not find any beans; NestedCondition on DefaultWebSecurityCondition.Classes #ConditionalOnClass found required classes 'org.springframework.security.web.SecurityFilterChain', 'org.springframework.security.config.annotation.web.builders.HttpSecurity'
2022-10-20 15:49:48.791 [main] a.ConfigurationClassBeanDefinitionReader : Registered bean definition for imported class 'org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration$OAuth2SecurityFilterChainConfiguration'
2022-10-20 15:49:48.792 [main] o.s.b.a.condition.OnBeanCondition : Condition OnBeanCondition on org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration$OAuth2SecurityFilterChainConfiguration#jwtSecurityFilterChain matched due to #ConditionalOnBean (types: org.springframework.security.oauth2.jwt.JwtDecoder; SearchStrategy: all) found bean 'jwtDecoderByJwkKeySetUri'
...
2022-10-20 15:49:49.082 [main] a.ConfigurationClassBeanDefinitionReader : Registering bean definition for #Bean method com.mycompany.CustomSecurityConfig.filterChain()
...
2022-10-20 15:49:52.276 [main] edFilterInvocationSecurityMetadataSource : Adding web access control expression [authenticated] for any request
2022-10-20 15:50:13.348 [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter#33502cfe, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#729d1428, org.springframework.security.web.context.SecurityContextPersistenceFilter#7d0312a, org.springframework.security.web.header.HeaderWriterFilter#6ca97ddf, org.springframework.security.web.csrf.CsrfFilter#38f569d, org.springframework.security.web.authentication.logout.LogoutFilter#1104ad6a, org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter#74ab8610, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#7833407, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#66acaa54, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#115924ba, org.springframework.security.web.session.SessionManagementFilter#6a905513, org.springframework.security.web.access.ExceptionTranslationFilter#5749e633, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#49741e80]
...
2022-10-20 15:50:13.384 [main] edFilterInvocationSecurityMetadataSource : Adding web access control expression [permitAll] for any request
2022-10-20 15:50:17.641 [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter#4a0f4282, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#19d3f4fb, org.springframework.security.web.context.SecurityContextPersistenceFilter#99f75e4, org.springframework.security.web.header.HeaderWriterFilter#118c1faa, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#2b6ff016, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#5aefdb9e, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#43cf97a8, org.springframework.security.web.session.SessionManagementFilter#da5b46f, org.springframework.security.web.access.ExceptionTranslationFilter#11267e87, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#7827cdfc]
Is it expected that the bean CustomSecurityConfig.filterChain participates in DefaultWebSecurityCondition evaluation and OAuth2SecurityFilterChainConfiguration.jwtSecurityFilterChain is not created. Or the issue with DefaultWebSecurityCondition is that the instance of WebSecurityConfigurerAdapter is not in the context anymore (as to issue #10822 it is deprecated)?
The suggestion to add #Order() annotation didn't work:
#Configuration
#EnableWebSecurity
public class CustomSecurityConfig {
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ...
as well as further attempts to exclude the autoconfiguration class like this:
#SpringBootApplication(excludeName = "org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration")
public class Application extends SpringBootServletInitializer { ...
failed probably due to issue #5427 with the following error
java.lang.IllegalStateException: The following classes could not be excluded because they are not auto-configuration classes:
- org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration
at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.handleInvalidExcludes(AutoConfigurationImportSelector.java:222) ~[spring-boot-autoconfigure-2.7.4.jar!/:2.7.4]
This way also does not work:
#ComponentScan(excludeFilters = {#ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*OAuth2ResourceServerJwtConfiguration.*")})
public class Application extends SpringBootServletInitializer { ...
The documentation I read before posting:
Spring Security without the WebSecurityConfigurerAdapter
Spring Security: Upgrading the Deprecated WebSecurityConfigurerAdapter
Spring Security - How to Fix WebSecurityConfigurerAdapter Deprecated
Update
I have created a small Maven project that demonstrates the issue. After project is started, request the controller like this:
$ wget -nv -O - 'http://localhost:8080/spring/test'
Username/Password Authentication Failed.
As one can see, the custom configured SecurityFilterChain is not active because otherwise the access would be granted (as to antMatchers( "/**/test").permitAll()). ContextRefreshedEvent listener dumps two SecurityFilterChain instances (jwtSecurityFilterChain and filterChain), the priority of them is not possible to configure reliably.
As follows from issue #33103, the beans imported from XML via #ImportResource do not participate in Spring Boot autoconfiguration, hence beans scanning should be performed using annotation i.e. #SpringBootApplication(scanBasePackages = "some.package") – this basically solves the issue. –
dma_k
Nov 17, 2022 at 9:21
Thanks to this reply I found the solution.
But since we are using different auth method so it might not work for you. I am using DaoAuthenticationProvider from this tutorial. Then rewrite to a new version without using "WebSecurityConfigurerAdapter".
Hope this solution helps. I got tortured 3-4 hours finding solution.
I created a subpackage called "config", on the same level with "models, controller, service, etc"(or whatever you name them).
Then, manully import this package by "scanBasePackages" along with others.
#SpringBootApplication(scanBasePackages = {"com.example.test.controller", "com.example.test.model", "com.example.test.repo", "com.example.test.service","com.example.test.config"})
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
Here are 3 rewritten files in my "config" folder. All rewrote from the tutorial mentioned above.
a.CustomUserDetails
package com.example.test.config;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.test.model.UserModel;
public class CustomUserDetails implements UserDetails {
//#Autowired no need cuz this is not a bean
private UserModel user;
public CustomUserDetails(UserModel user) {
this.user = user;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
#Override
public String getPassword() {
return user.getPassword();
}
#Override
public String getUsername() {
return user.getEmail();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
public String getFullName() {
return user.getFirstName() + " " + user.getLastName();
}
}
b. CustomUserDetailsService
package com.example.test.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.example.test.repo.UserRepo;
import com.example.test.model.UserModel;
import org.springframework.stereotype.Service;
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private UserRepo userRepo;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserModel user = userRepo.findByEmail(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new CustomUserDetails(user);
}
}
c. SecurityConfiguration
package com.example.test.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
#Configuration
public class SecurityConfiguration{
#Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests ((requests)->requests
.requestMatchers("/", "/register", "/try2Register").permitAll()
.anyRequest().authenticated()
)
.formLogin((form)->form
.usernameParameter("email")
.defaultSuccessUrl("/users")
.permitAll()
)
.logout((logout) -> logout
.logoutSuccessUrl("/").permitAll());
http.authenticationProvider(authenticationProvider());
return http.build();
}
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/images/**", "/js/**", "/webjars/**");
}
}

Java Spring - Active Directory- How can I Get AD User Details (telNumber, full name, mail , address, description)?

In my college project i would like to get user informations from an AD Server such as the telephone number, the mail, the full name after an authentication.
So i use the default spring security login page and after the authentication, i get the dn and the permissions with an Authentication object. I would like to know how can i get the details of an ad user.
I would like to get his phone number to send a message with an API. This part is already working. I just need to extract the Ad user details to do it.
You will find my code below :
SecurityConfiguration.java :
package com.le_chatelet.le_chatelet_back.ldap;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider =
new ActiveDirectoryLdapAuthenticationProvider( "mydomain.com", "ldap://adserverip:389");
activeDirectoryLdapAuthenticationProvider.setConvertSubErrorCodesToExceptions(true);
activeDirectoryLdapAuthenticationProvider.setUseAuthenticationRequestCredentials(true);
return activeDirectoryLdapAuthenticationProvider;
}
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception{
authenticationManagerBuilder
.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity
.authorizeRequests()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin();
}
}
LoginController.java :
package com.le_chatelet.le_chatelet_back.ldap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.stream.Collectors;
#RestController
public class LoginController {
#Autowired
private UserInterface userInterface;
Logger logger = LoggerFactory.getLogger(LoginController.class);
#GetMapping("/hello")
public String sayHello()
{
return "hello world";
}
#GetMapping("/user")
#ResponseBody
public Authentication getLoggedUserDetail(Authentication authentication) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
//get username
String username = authentication.getName();
logger.info("username : "+username);
// concat list of authorities to single string seperated by comma
String authorityString = authentication
.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
String role = "role_A";
boolean isCurrentUserInRole = authentication
.getAuthorities()
.stream()
.anyMatch(role::equals);
return authentication;
}
}
If someone can show me code example it would be appreciated.
You can set the a UserDetailsContextMapper on your Provider which allows custom strategy to be used for creating the UserDetails that will be stored as the principal in the Authentication.
provider.setUserDetailsContextMapper(new PersonContextMapper());
Then you can use the #AuthenticationPrincipal annotation in your Controller to get the Person (or a custom class) instance.
#GetMapping("/phone-number")
public String phoneNumber(#AuthenticationPrincipal Person person) {
return "Phone number: " + person.getTelephoneNumber();
}
You can find a full LDAP sample application provided by the Spring Security team.

Spring Boot Keycloak Multi Tenant Configuration

I have a Keycloak instance and created two realms and one user for each realm.
Realm1 (Tenant1) -> User 1
Realm2 (Tenant2) -> User 2
And i have my spring boot application.yml (resource server - API) for one specific realm and fixed in my code.
keycloak:
realm: Realm1
auth-server-url: https://localhost:8443/auth
ssl-required: external
resource: app
bearer-only: true
use-resource-role-mappings: true
It's working and validate for Realm1.
but now i can receive requests from user2 (tenant2) and the token will not be valid because the public key (realm1) is not valid for the signed request jwt token (realm2).
What is the best way to allow multi tenancy and dynamically configuration for multi realms?
thanks,
There's a whole chapter on it: 2.1.18: Multi-Tenanacy
Instead of defining the keycloak config in spring application.yaml, keep multiple keycloak.json config files, and use a custom KeycloakConfigResolver:
public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
#Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
if (request.getPath().startsWith("alternative")) { // or some other criteria
InputStream is = getClass().getResourceAsStream("/tenant1-keycloak.json");
return KeycloakDeploymentBuilder.build(is); //TODO: cache result
} else {
InputStream is = getClass().getResourceAsStream("/default-keycloak.json");
return KeycloakDeploymentBuilder.build(is); //TODO: cache result
}
}
}
I'm not sure if this works well with the keycloak-spring-boot-starter, but I think it's enough to just wire your custom KeycloakConfigResolver in the KeycloakWebSecurityConfigurerAdapter:
#Configuration
#EnableWebSecurity
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new PathBasedKeycloakConfigResolver();
}
[...]
}
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
#DependsOn("keycloakConfigResolver")
#KeycloakConfiguration
#EnableGlobalMethodSecurity(jsr250Enabled = true)
#ConditionalOnProperty(name = "keycloak.enabled", havingValue = "true", matchIfMissing = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider authenticationProvider = new KeycloakAuthenticationProvider();
authenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(authenticationProvider);
}
/**
* Defines the session authentication strategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.cors()
.and()
.authorizeRequests().antMatchers(HttpMethod.OPTIONS)
.permitAll()
.antMatchers("/api-docs/**", "/configuration/ui",
"/swagger-resources/**", "/configuration/**", "/v2/api-docs",
"/swagger-ui.html/**", "/webjars/**", "/swagger-ui/**")
.permitAll()
.anyRequest().authenticated();
}
}
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.OIDCHttpFacade;
import java.io.InputStream;
import java.util.concurrent.ConcurrentHashMap;
public class PathBasedConfigResolver implements KeycloakConfigResolver {
private final ConcurrentHashMap<String, KeycloakDeployment> cache = new ConcurrentHashMap<>();
#Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
String path = request.getURI();
String realm = "realmName";
if (!cache.containsKey(realm)) {
InputStream is = getClass().getResourceAsStream("/" + realm + "-keycloak.json");
cache.put(realm, KeycloakDeploymentBuilder.build(is));
}
return cache.get(realm);
}
}
import org.keycloak.adapters.KeycloakConfigResolver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.Bean;
#SpringBootApplication()
public class Application {
public static void main(String[] args) {
SpringApplication.run(DIVMasterApplication.class, args);
}
#Bean
#ConditionalOnMissingBean(PathBasedConfigResolver.class)
public KeycloakConfigResolver keycloakConfigResolver() {
return new PathBasedConfigResolver();
}
}

Why is an InMemoryUserDetailsManager injected into this controller even though I've configured a JdbcUserDetailsManager elsewhere?

I'm trying to use Spring Security JDBC Authentication in a Spring Boot web app.
Here's the (much simplified, relevant) configuration:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer;
import javax.sql.DataSource;
#Configuration
public class SecurityConfiguration {
#Autowired
private DataSource dataSource;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema();
}
}
Here's a controller:
import org.adventure.inbound.UserFormData;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/users")
public class UserController {
private final UserDetailsManager userDetailsManager;
public UserController(UserDetailsManager userDetailsManager) {
this.userDetailsManager = userDetailsManager;
}
#PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<Void> registerUser(UserFormData userFormData) {
userDetailsManager.createUser(
User
.withUsername(userFormData.getUsername())
.password(userFormData.getPassword())
.authorities(new SimpleGrantedAuthority("ROLE_USER"))
.build());
return ResponseEntity.created(null).build();
}
}
When I start the app, I can see that:
The AuthenticationManagerBuilder gets a JDBCUDM configured:
but perhaps some step is missing?
The InMemoryUserDetailsManager is instantiated with Spring Security's default user:
The Controller, when instantiated, receives an InMemoryUserDetailsManager:
This means that when I try to create a new user, it uses the InMemoryUDM instead of the JDBCUDM that I'd like to use. Why is that?
Working solution
We already configure the UserDetailsService through JdbcUserDetailsManagerConfigurer, but didn't expose it as a bean.
#Autowired
#Bean
public UserDetailsManager configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema();
return jdbcUserDetailsManagerConfigurer.getUserDetailsService();
}
In your configuration override the userDetailsServiceBean(), and let it call the super method and add the #Bean annotation to make the configured service available. This is also explained here in the Javadocs.
#Configuration
public class SecurityConfiguration extends WebSecutiryConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema();
}
#Bean
public UserDetailsService userDetailsServiceBean()
throws java.lang.Exception {
return super.userDetailsServiceBean();
}
}
Also because you want to use your configuration instead of the Spring Boot one, you might need to add #EnableWebSecurity to disable the Spring Boot defaults.
Make your SecurityConfiguration extend from WebSecurityConfigurerAdapter, add #EnableWebSecurity and expose your JdbcUserDetailsManager as #Bean.
#Configuration
#EnableWebSecurity
public class SecurityConfiguration
extends WebSecurityConfigurerAdapter {
#Bean
public JdbcUserDetailsManager userDetailsManager() {
return yourJdbcUserDetailsManager;
}
/// etc.
}

Resources