I have a Spring boot project which runs authentication with spring oauth2 token provider.
Now there is a idea to support a autentication with Keycloak, so that username and password will be stored in Keycloak and it will provide the access token.
Idea is to keep the oauth token store and provider, and as well to have a keycloak one, but to keep the roles and acces right part in spring. Keycloak will only be used for some users as a token provider instead of Spring one and to have a refresh token. So all the user data and access rights and roles are still be done by spring, from database, only the part where it authenticates username and password will be in Keycloak which provides a token.
#EnableWebSecurity
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ClientDetailsService clientDetailsService;
private AccessDecisionManager accessDecisionManager;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().accessDecisionManager(accessDecisionManager)
.antMatchers("/service/*").fullyAuthenticated()
.anyRequest().permitAll().and().httpBasic().and().csrf().disable();
}
#Override
#Bean(name = "authenticationManagerBean")
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
#Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore) {
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
#Bean
public AffirmativeBased accessDecisionManager() {
List<AccessDecisionVoter<?>> accessDecisionVoters = new ArrayList<>();
accessDecisionVoters.add(new ScopeVoter());
accessDecisionVoters.add(new RoleVoter());
accessDecisionVoters.add(new AuthenticatedVoter());
AffirmativeBased accessDecisionManager = new AffirmativeBased(accessDecisionVoters);
return accessDecisionManager;
}
And there is a Custom client service which grants access rights:
#Component
public class CustomClientService implements ClientDetailsService {
private static Map<String, BaseClientDetails> cache = new ConcurrentHashMap<>();
#Autowired
UserService userService;
#Autowired
AccessRightsService accessRightsService;
#Override
public ClientDetails loadClientByClientId(String paramString) throws ClientRegistrationException {
...
Also there is a custom TokenStore class:
public class MyTokenServices extends DefaultTokenServices {
private static Logger log = LoggerFactory.getLogger(MyTokenServices.class);
public UserService userService;
public AccessRightsService accessRightService;
private TokenStore my_tokenStore;
#Override
public void setTokenStore(TokenStore tokenStore) {
// TODO Auto-generated method stub
super.setTokenStore(tokenStore);
my_tokenStore = tokenStore;
}
#Override
#Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken retVal= super.createAccessToken(authentication);
if(retVal instanceof DefaultOAuth2AccessToken) {
DefaultOAuth2AccessToken defRetVal = (DefaultOAuth2AccessToken)retVal;
log.info("New loging request"+ defRetVal.toString());
// defRetVal.setExpiration( Date.from(LocalDateTime.now().plus(8,ChronoUnit.HOURS).atZone(ZoneId.systemDefault()).toInstant()));
my_tokenStore.storeAccessToken(defRetVal, authentication);
}
return retVal;
}
#Override
public OAuth2Authentication loadAuthentication(String accessTokenValue)
throws AuthenticationException, InvalidTokenException {
OAuth2Authentication retVal = super.loadAuthentication(accessTokenValue);
OAuth2Request oldRequest = retVal.getOAuth2Request();
User user = userService.getUserByUsername(oldRequest.getClientId());
if(changeAutheticator(retVal, user)) {
HashSet<GrantedAuthority> authorities = new HashSet<>();
user.getRoles().forEach(a->authorities.add(new SimpleGrantedAuthority(a.getRoleName())));
Set<String> accessRights = accessRightService.getUserAccessRights(user);
if(accessRights != null) {
accessRights.forEach(rihgt->{
authorities.add(new SimpleGrantedAuthority(rihgt));
});
}
OAuth2Request newRequest = new OAuth2Request(retVal.getOAuth2Request().getRequestParameters(),
oldRequest.getClientId(), authorities, oldRequest.isApproved(), oldRequest.getScope(),
oldRequest.getResourceIds(), oldRequest.getRedirectUri(), oldRequest.getResponseTypes(), oldRequest.getExtensions());
retVal = new OAuth2Authentication(newRequest, retVal.getUserAuthentication());
}
return retVal;
}
/**
* Method that check do we need to change authenticator
* #param retVal
* #param user
* #return
*/
private boolean changeAutheticator(OAuth2Authentication auth, User user) {
if(user == null) return false;
if(user != null ) {
if(user.getRoles() != null) {
if(auth.getOAuth2Request()!=null && auth.getOAuth2Request().getAuthorities() != null){
for(Role role:user.getRoles()){
if(!auth.getOAuth2Request().getAuthorities().stream().anyMatch(a->a.getAuthority().equals(role.getRoleName()))){
return true;
}
}
for(GrantedAuthority ga : auth.getOAuth2Request().getAuthorities()) {
if(!user.getRoles().stream().anyMatch(a->a.getRoleName().equals(ga.getAuthority()))){
return true;
}
}
}
}
}
return false;
}
I was trying to implement a multiple authentiaction like the one from other stackoverflow but that was not a solution. Thinking that I should provide a custom authentication provider with Keycloak or still like not having a solution in head.
I use Keycloak to federate other identities: the only issuers that clients and resource-servers trust are Keycloak instances or realms. Other identity sources are hidden behind it.
With that config, roles are put into tokens by Keycloak, just as any other claims (for the last project I worked on, roles referential is LDAP but it could be a custom database table or Keycloak default one).
It's pretty easy to connect Keycloak to your user database and it comes with many features that I don't want to code and maintain (multi-factor authentication, social login, users, clients and resource-servers management screens,...)
Related
I have this vanilla spring boot/azure/starter app, connecting to our internal azure service.
https://learn.microsoft.com/de-de/azure/developer/java/spring-framework/configure-spring-boot-starter-java-app-with-azure-active-directory
Generally it works as designed.
What options do i have if i want to add custom roles for authorization?
I want that flow:
Login to azure with user/pw (works as expected)
Load user´s roles from a local database (postgres)
Inject/Add this roles into the list of spring's GrantedAuthority
With spring security we generally use a custom AuthenticationProvider
Currently i have this working code:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends AadWebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeHttpRequests()
.anyRequest()
.authenticated()
.and()
.oauth2Login();
}
}
I want something like this:
#Component
#RequiredArgsConstructor(onConstructor = #__(#Autowired))
#Slf4j
public class ThdAuthenticationProvider implements AuthenticationProvider {
private final
#NonNull
IApplicationUserService userService;
/**
* Performs authentication with the same contract as .
*
* #param authentication the authentication request object.
* #return a fully authenticated object including credentials. May return <code>null</code> if the
* <code>AuthenticationProvider</code> is unable to support authentication of the passed
* <code>Authentication</code> object. In such a case, the next <code>AuthenticationProvider</code> that
* supports the presented <code>Authentication</code> class will be tried.
* #throws AuthenticationException if authentication fails.
*/
#Override
public org.springframework.security.core.Authentication authenticate(org.springframework.security.core.Authentication
authentication)
throws AuthenticationException {
final String name = authentication.getName().toLowerCase();
final String password = authentication.getCredentials().toString();
// go to azure, login with name/password
// come back if sucessfull
List<String> roles = userService.fetchRoles(name);
List<GrantedAuthority> grantedAuth = new ArrayList<>();
grantedAuth.addAll(roles);
return new UsernamePasswordAuthenticationToken(name, password, grantedAuth);
}
EDIT
I ended up this way:
Based on this documentation: https://docs.spring.io/spring-security/site/docs/5.2.12.RELEASE/reference/html/oauth2.html#oauth2login-advanced-map-authorities-oauth2userservice
My custom user service - where the roles will be fetched from database or elsewhere:
#Service
public class UserService {
List<String> fetchUserRoles(String user){
return List.of("Administrator", "Product Owner", "Developer");
}
}
My custom security chain applying these roles:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends AadWebSecurityConfigurerAdapter {
private final UserService userService;
#Autowired
public SecurityConfiguration(UserService userService) {
this.userService = userService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeHttpRequests()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.userInfoEndpoint(userInfoEndpointConfig -> {
userInfoEndpointConfig.oidcUserService(this.oidcUserService());
});
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
// TODO
// 1) Fetch the authority information from the protected resource using accessToken
// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
// 3) Create a copy of oidcUser but use the mappedAuthorities instead
List<String> dummy = userService.fetchUserRoles("dummy");
dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> user));
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
}
Spring Boot Azure AD custom roles
Please follow below link it has detail explanation about:
Register web API application and configure API scope
Assign these roles for the user
Register client application in Azure AD and configure API permissions
Reference:
Using Azure AD premium custom roles with spring security for role based access
#thomas-lang Thanks a lot Thomas!!! Your post helped me a lot!
Attaching my variation of the code
User Service
#Service
public class UserService {
private final PeopleService peopleService;
public UserService(PeopleService peopleService) {
this.peopleService = peopleService;
}
public Set<Role> fetchUserRoles(String user, String email){
Person loggedPerson = peopleService.findPersonByEmail(email);
return loggedPerson.getRoles();
}
}
Security Configuration
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfigurationAad extends AadWebSecurityConfigurerAdapter {
private final UserService userService;
#Autowired
public SecurityConfigurationAad(UserService userService) {
this.userService = userService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeHttpRequests()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.userInfoEndpoint(userInfoEndpointConfig -> {
userInfoEndpointConfig.oidcUserService(this.oidcUserService());
});
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
DecodedToken token = DecodedToken.getDecoded(userRequest.getAccessToken().getTokenValue());
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
// TODO
// 1) Fetch the authority information from the protected resource using accessToken
// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
// 3) Create a copy of oidcUser but use the mappedAuthorities instead
Set<Role> dummy = userService.fetchUserRoles("dummy", token.unique_name);
dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> String.valueOf(user)));
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
}
DecodedToken implementation I got from here
https://www.lenar.io/how-to-decode-jwt-authentication-token/
I am trying to implement a UserCache in my application to avoid to make multiple calls to the User table in the case I am using the basic authentication. I created my CacheConfig following the accepted answer of this topic, in which the CachingUserDetailsService is used to manage the user cache. Bellow is the code of the UserService, CacheConfig and SecurityConfig:
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
public UserService(UserRepository repository) {
this.userRepository = repository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AddInUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("O usuário " + username + " não pode ser encontrado!"));
UserDetails userDetails = User
.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles("USER")
.build();
return userDetails;
}
#Transactional
public AddInUser save(AddInUser user) {
return userRepository.save(user);
}
}
#EnableCaching
#Configuration
public class CacheConfig {
public static final String USER_CACHE = "userCache";
/**
* Define cache strategy
*
* #return CacheManager
*/
#Bean
public CacheManager cacheManager() {
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
List<Cache> caches = new ArrayList<>();
//Failure after 5 minutes of caching
caches.add(new GuavaCache(CacheConfig.USER_CACHE,
CacheBuilder.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build()));
simpleCacheManager.setCaches(caches);
return simpleCacheManager;
}
#Bean
public UserCache userCache() throws Exception {
Cache cache = cacheManager().getCache("userCache");
return new SpringCacheBasedUserCache(cache);
}
}
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
};
#Autowired
private UserRepository userRepository;
#Autowired
private UserCache userCache;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())
.and()
.authorizeRequests()
.antMatchers("/api/**")
.authenticated()
.and()
.httpBasic();
}
#Override
#Bean
public UserDetailsService userDetailsService() {
UserService userService = new UserService(userRepository);
CachingUserDetailsService cachingUserService = new CachingUserDetailsService(userService);
cachingUserService.setUserCache(this.userCache);
return cachingUserService;
}
}
The first call works well because it makes the call to the UserRepository. But on the second, it does not make the call to the repository (as expected) but I am getting the following WARN from BCryptPasswordEncoder:
2020-09-24 08:43:51.327 WARN 24624 --- [nio-8081-exec-4] o.s.s.c.bcrypt.BCryptPasswordEncoder : Empty encoded password
The warning is clear in its meaning and it fails to authenticate de user because of the null password. But I cannot understand why the user retried from cache has a null password if it was correctly stored. I am not sure how to solve it using the cache. Any thoughts?
Thank you very much for your help!
#M.Deinum comment is absolutely correct. You can refer to the doc here.
Note that this implementation is not immutable. It implements the
CredentialsContainer interface, in order to allow the password to be
erased after authentication. This may cause side-effects if you are
storing instances in-memory and reusing them. If so, make sure you
return a copy from your UserDetailsService each time it is invoked.
You can check the Spring-security source code if you are curious more:
private boolean eraseCredentialsAfterAuthentication = true;
...
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
And User.java source code:
#Override
public void eraseCredentials() {
this.password = null;
}
By the way, it looks weird to cache login user that way. During login, it is better to get fresh record from DB instead of from cache. You can use cached user at other place but seldom see it is used during login.
If you really need to do that, you can change the default flag to false as mentioned in doc, just inject AuthenticationManager and call:
setEraseCredentialsAfterAuthentication(false)
I have created an OAuth Authorization Server using default spring boot configurations, where the client is redirected to the auto-generated login page, userDetailsService looks up the User table and authenticates, and after successful authentication the server returns a jwt token. Now I want customize this and change two things but I am having difficulty in doing it.
1) Use my own login.jsp page instead of the auto-generated login page so I can have an extra field(eg. dropdownlist) and use this along with the username and password for authentication since I have different user tables
2) Instead of using the default UserDetailsService I am trying to implement my own AuthenticationProvider, this is because I have multiple users table and want to search for user in the correct table based on the value from the extra field (dropdown list mentioned in 1). Also how to get the dropdownlist value in the AuthenticationProvider?
In my properties file I have set:
spring.mvc.view.prefix: /WEB-INF/jsp/ and
spring.mvc.view.suffix: .jsp
#Configuration
#EnableWebSecurity
public class ServerWebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new DefaultAuthenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize")
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
#Configuration
#EnableAuthorizationServer
#Import(ServerWebSecurityConfig.class)
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
#Qualifier("dataSource")
private DataSource dataSource;
#Autowired
private UserDetailsService userDetailsService;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).accessTokenConverter(accessTokenConverter()).authenticationManager(authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("permitAll()");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
clients.withClientDetails(jdbcClientDetailsService);
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("secret");
return converter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
#Bean
public PasswordEncoder userPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
public class DefaultAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication.getName() == null || authentication.getCredentials() == null
|| authentication.getName().isEmpty() || authentication.getCredentials().toString().isEmpty()) {
return null;
}
final String userName = authentication.getName();
final String password = (String) authentication.getCredentials();
// final String userTable = how to get this?
// make db query in correct table based on value of userTable
User user = null;
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for (UserAuthority authority : user.getUserAuthorities()) {
authorities.add(new CustomGrantedAuthority(authority.getAuthority().getName()));
}
Map<String, String> userDetails = new HashMap<>();
userDetails.put("username", userName);
return new UsernamePasswordAuthenticationToken(userDetails, password, authorities);
}
#Override
public boolean supports(Class<?> authentication) {
return false;
}
}
#Controller
public class OAuthController {
#RequestMapping("/login")
public String login() {
return "login";
}
}
I am expecting that my client app is redirected to the custom login page, once login button is pressed my custom AuthenticationProvider will lookup for user in the correct table based on the extra field in the custom login page.
For the first point you just have to configure your custom login-path for the login in your WebSecurityConfigurerAdapter.
Add .formLogin().loginPage("/login").permitAll()
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize")
.and()
.formLogin().loginPage("/login").permitAll()
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
I store information about users in seperated tables owners,employees,users i am trying to use java configuration in spring security.
I have created three diferent authentication providers for each user type, but only the Users Provider is being triggered. I have read the spring security docs and the only way to do this seems to be is create class with multiple embedded classes extended from WebSecurityConfigurerAdapter but i don't want to do it this way because it requires a lot of duplicating code, is there any other way
I tryed to use the simple userDetailService inside which i send request to all tables in databese but still there is not results, only one query is buing executed and nothing, the only responce i get is:
2016-02-09 23:06:25.976 DEBUG 8780 --- [nio-8080-exec-1]
.s.a.DefaultAuthenticationEventPublisher : No event was found for the
exception
org.springframework.security.authentication.InternalAuthenticationServiceException
2016-02-09 23:06:25.976 DEBUG 8780 --- [nio-8080-exec-1]
o.s.s.w.a.www.BasicAuthenticationFilter : Authentication request for
failed:
org.springframework.security.authentication.InternalAuthenticationServiceException:
No entity found for query; nested exception is
javax.persistence.NoResultException: No entity found for query
But i never throw any exception!! And the most strange is that i can see in the debugger how the execution rapidly stops right after em.createQuery(..).getSingleResult().. and that's it, nothing more! There is no return statement no exception nothing, wtf!!
This is part of my current configuration:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(createAuthenticationProvider(employeeDetailService()))
.authenticationProvider(createAuthenticationProvider(ownerDetailsService()))
.authenticationProvider(createAuthenticationProvider(userDetailsService()));
}
#Bean
public OwnerDetailsService ownerDetailsService() {
return new OwnerDetailsService();
}
#Bean
public EmployeeDetailServiceImpl employeeDetailService() {
return new EmployeeDetailServiceImpl();
}
#Bean
public UserDetailsServiceImpl userDetailsService() {
return new UserDetailsServiceImpl();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(6);
}
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new MySimpleUrlAuthenticationSuccessHendler();
}
private AuthenticationProvider createAuthenticationProvider(UserDetailsService service) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(service);
provider.setPasswordEncoder(passwordEncoder());
provider.setHideUserNotFoundExceptions(true);
return provider;
}
User detail services:
#Service
public abstract class CustomUserDetailService implements UserDetailsService{
#Autowired
IDBBean dao;
protected CustomUserDetails getUser(GetUserByNameFunction function, String name) {
return createUser(function.get(name));
}
protected CustomUserDetails createUser(Authenticational user) {
return new CustomUserDetails(user, getAuthorities(user.getAuthority()));
}
protected List<GrantedAuthority> getAuthorities(String authority) {
return Collections.singletonList(new SimpleGrantedAuthority(authority));
}
}
Implementations
public class EmployeeDetailServiceImpl extends CustomUserDetailService {
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return super.getUser(dao::getEmployeeByEmail, email);
}
}
public class OwnerDetailsService extends CustomUserDetailService {
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return super.getUser(dao::getOwnerByEmail, email);
}
}
public class UserDetailsServiceImpl extends CustomUserDetailService {
#Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
return super.getUser(dao::getUserByEmail, userName);
}
}
Custom user details:
private Long id;
private String userEmail;
public CustomUserDetails(Authenticational user,
Collection<? extends GrantedAuthority> authorities) {
super(
user.getName(),
user.getPassword().toLowerCase(),
user.isEnabled(),
true,
true,
true,
authorities);
upadateValues(user);
}
private void upadateValues(Authenticational user) {
this.id = user.getId();
this.userEmail = user.getEmail();
}
Just to clarify something from the other answer:
Your authentication providers are stored in a list inside ProviderManager that iterates your authentication request through them. If your authentication provider throws AuthenticationException (BadCredentialsException extends AuthenticationException), then the ProviderManager will try another provider. If you set the hideUserNotFoundExceptions property, then it will also wrap and ignore UsernameNotFoundException and try another provider in this case too.
If I were you I would start by placing a debugging point inside ProviderManager's authenticate method. From there you can find out why the other authentication providers are not being called for their authenticate method.
Also I would think about having only one authentication provider with one UserDetailsService. It seems to me that you are doing a lot of complex not really needed operations like passing function to your abstract implementation when all you could do would be to have one UserDetailsService that would ask all your DAOs for a user. Which is basically what you're trying to accomplish but minus 2 authentication providers, minus 1 abstract class and minus 2 UserDetailsService implementations.
Spring Security will not try other authentication providers if a provider throws an AccountStatusException or if a UserDetailsService throws a UserNameNotFoundException or any other AuthenticationException
If you want other providers to be tried, then the loadUserByUserName methods of your UserDetailsServiceImpl and OwnerDetailsService should not throw the UserNameNotFound exception.
You should decide if you either want to return a dummy anonymous UserDetails object that will be used exclusively for fallback or some other mechanism to not throw the exception when a user is not available in your UserDetailsService implementation
this is an example I'v done to provide multi-auth for my application which have two diffirent user : admin and client .
ps: the admin and the client are two diffirent model.
public class CustomUserDetails implements UserDetails{
private Admin admin;
private Client client;
public CustomUserDetails(Admin admin) {
this.admin = admin;
}
public CustomUserDetails(Client client) {
this.client = client;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
#Override
public String getPassword() {
if((admin != null)&&(client==null)) {
return admin.getPassword();
}
else {
return client.getPassword();
}
}
#Override
public String getUsername() {
if((admin != null)&&(client==null)) {
return admin.getEmail();
}
else {
return client.getMail();
}
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
}
the CustomUserDetailsService class :
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
AdminRepository adminRepo;
#Autowired
ClientRepository clientRepo;
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Admin admin = adminRepo.findByEmail(email);
Client client = clientRepo.findByEmail(email);
if((admin == null)&&(client==null)) {
throw new UsernameNotFoundException("User not found");
}
else if((admin != null)&&(client==null)) {
return new CustomUserDetails(admin);
}
else {
return new CustomUserDetails(client);
}
}
}
I have Spring Security working within my application to authenticate a user with one password. I'm trying to meet a requirement that an override password will also authenticate that same user.
How can I do this with Spring Security?
It is possible, you will have to implement your own AuthenticationProvider possibly by extending the existing DaoAuthenticationProvider (see additionalAuthenticationChecks() in there).
Also the user is only associated with a single password by default (UserDetails.getPassword()), so you will need to have an extension of that class holding multiple passwords, and a corresponding implementation of UserDetailsService that knows how to load the user along with its passwords.
It is easy to do by providing multiple 'AuthenticationProvider' with 'UserDetailsService'.
private DaoAuthenticationProvider userAuthProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(passwordEncoder);
provider.setUserDetailsService(userDetailsService);
return provider;
}
private DaoAuthenticationProvider superVisorAuthProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setUserDetailsService(supervisorDetailService);
return provider;
}
then
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(userAuthProvider());
auth.authenticationProvider(superVisorAuthProvider());
}
As has been mentioned - you can overwrite 'additionalAuthenticationChecks'
Hope this helps somebody.
#Slf4j
#Service
class FlexibleAuthenticationProvider extends DaoAuthenticationProvider implements AuthenticationProvider {
#Autowired
UserDetailsService userDetailsService
#Autowired
PasswordEncoder passwordEncoder
#PostConstruct
def init() {
super.setPasswordEncoder(passwordEncoder)
super.setUserDetailsService(userDetailsService)
}
#Override
protected void additionalAuthenticationChecks(
UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
try {
super.additionalAuthenticationChecks(userDetails, authentication)
} catch (AuthenticationException e) {
log.error('Unable to authenticate with regular credentials')
try {
def mutableUserDetails = new MutableUser(userDetails)
mutableUserDetails.password = 'alternatepassword'
return super.additionalAuthenticationChecks(mutableUserDetails, authentication)
} catch (AuthenticationException err) {
log.error('Token based authentication failed')
}
throw e
}
}
static class MutableUser implements UserDetails {
private String password
private final UserDetails delegate
MutableUser(UserDetails user) {
this.delegate = user
this.password = user.password
}
String getPassword() {
return password
}
void setPassword(String password) {
this.password = password
}
....
}
}
#Configuration
class AuthWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
FlexibleAuthenticationProvider flexibleAuthenticationProvider
....
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(flexibleAuthenticationProvider)
}
....
}