Caching UserDetails using Springboot - java

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)

Related

Multiple ways of authentication with spring oauth2 and keycloak

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,...)

Spring Security will not authenticate newly created users and throws NoResultException despite User confirmed to exist in database

The code for the entire project can be found # https://github.com/dcechano/UniversityManagmentSystem
I am using Spring security 5 and Spring MVC project backed by custom dao authentication by a class an
implements UserDetailsService, namely PersonService. The entities are Person, Student and a few others that aren't relevant here. These entities are initialized using sql scripts and logging in with Spring security using these script initialized entities works fine.
However I have added functionality to save a new Student and store it in the H2 database. When I save them in the database and then go back and attempt to log in using this newly created Student (and Person by inheritance) Spring security refuses the authentication. Using the debugger I found that internally its throwing a javax.persistence.NoResultFoundException with message No entity found for query. But I have confirmed at several points throught that the new entity does indeed exist in the database. Below is my Spring Security Config class and some screenshots of me reproducing the problem and some results when running the debugger.
SecurityConfig.java
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private PersonService personService;
private CustomAuthentication customAuthentication;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**",
"/static/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/*").hasAnyRole("STUDENT", "FACULTY_MEMBER", "STAFF_MEMBER")
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/userAuth")
.successHandler(customAuthentication)
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.permitAll();
}
#Bean
public CsrfTokenRepository repo() {
HttpSessionCsrfTokenRepository repo = new HttpSessionCsrfTokenRepository();
repo.setParameterName("_csrf");
repo.setHeaderName("X-CSRF-TOKEN");
return repo;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
auth.setUserDetailsService(personService);
auth.setPasswordEncoder(passwordEncoder());
return auth;
}
#Autowired
public void setPersonService(PersonService personService) {
this.personService = personService;
}
#Autowired
public void setCustomAuthentication(CustomAuthentication customAuthentication) {
this.customAuthentication = customAuthentication;
}
}
My UserDetailsService implementation. (PersonRepo extends UserDetailsService)
#Service
public class PersonServiceImpl implements PersonService {
private PasswordEncoder passwordEncoder;
private PersonRepo personRepo;
#Override
public Person findByUsername(String username) {
return personRepo.findByUsername(username);
}
#Override
public void save(Person person) {
personRepo.save(person);
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Logger logger = Logger.getLogger(getClass().toString());
logger.info("Inside the loadUserByUsername method");
Person person = this.findByUsername(username);
if (person == null) {
logger.info("Person with username: " + username + " was not found!");
throw new UsernameNotFoundException("Could not find Person with username " + username);
}
logger.info("User was successfully retrieved from database");
Collection<GrantedAuthority> grantedAuthorityRoles = person.getRoles().stream().map(
role -> new SimpleGrantedAuthority(role.getRole().name())).collect(Collectors.toList());
return new User(person.getUsername(), person.getPassword(), grantedAuthorityRoles);
}
#Autowired
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
#Autowired
public void setPersonRepo(PersonRepo personRepo) {
this.personRepo = personRepo;
}
}
The MVC Mapping that stores the newly created Student in the database. Note the log statements.
#PostMapping("/save_student")
public String saveStudent(#ModelAttribute("student") Student student) {
student.setUsername(student.getFirstName().charAt(0) + student.getLastName().toLowerCase());
student.setPassword(passwordEncoder.encode(student.getPassword()));
student.setRoles(List.of(roleRepo.getRoleByName(RoleEnum.ROLE_STUDENT.name())));
student.setVersion(1);
studentRepo.save(student);
if (personService.loadUserByUsername(student.getUsername()) == null) {
throw new EntityNotFoundException();
} else {
logger.info("Person with username " + student.getUsername() + " was indeed found.");
}
return "redirect:list_students";
}
#GetMapping("/list_students")
public String listStudents(Model model) {
model.addAttribute("students", studentRepo.findAll());
model.addAttribute("people", personRepo.findAll());
return "all_students";
}
These screenshots attempt to demo the bug. After I hit the register button it triggers the #PostMapping("/save_student") above where the new Student object is saved then redirected to a page that displays all Persons in the database. Not the new Person is listed.
This is the screenshot of the logs that show that the new "Steve Miller" Student can be immediately retrieved after being saved.
The result after being redirected via the #GetMapping(/list_students). Note that "Steve Miller" is listed.
Below is the debugger after attempting to log in under "Steve Miller". The break point is place in the loadUserByUsername() method from the UserDetailsService implimentation.
EDIT: Here are the links to the exception stacktrace and the entire server logs.
https://github.com/dcechano/UniversityManagmentSystem/blob/master/src/main/resources/authentication_failure_log.txt
https://github.com/dcechano/UniversityManagmentSystem/blob/master/src/main/resources/exception_stacktrace.txt

How do you save users who have logged in with OAuth 2 (Spring)?

My main objective is to store the client-id of the each user, once they login with google. This github repo contains most of what I needed till now. The two main files of concern are OAuthSecurityConfig.java and UserRestController.java.
When I navigate to /user, the Principal contains all the details I need on the user. Thus I can use the following snippets to get the data I need:
Authentication a = SecurityContextHolder.getContext().getAuthentication();
String clientId = ((OAuth2Authentication) a).getOAuth2Request().getClientId();
I can then store the clientId in a repo
User user = new User(clientId);
userRepository.save(user);
The problem with this is that users do not have to navigate to /user. Thus, one can navigate to /score/user1 without being registered.
This API is meant to be a backend for an android application in the future, so a jquery redirect to /user would be insecure and would not work.
Things I have tried:
Attempt 1
I created the following class:
#Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
#Autowired
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
}
return new UserRepositoryUserDetails(user);
}
}
and overrode the WebSecurityConfigurerAdapterwith:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
Both overridden methods are not called when a user logs in (I checked with a System.out.println)
Attempt 2
I tried adding .userDetailsService(customUserDetailsService)
to:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// Starts authorizing configurations.
.authorizeRequests()
// Do not require auth for the "/" and "/index.html" URLs
.antMatchers("/", "/**.html", "/**.js").permitAll()
// Authenticate all remaining URLs.
.anyRequest().fullyAuthenticated()
.and()
.userDetailsService(customUserDetailsService)
// Setting the logout URL "/logout" - default logout URL.
.logout()
// After successful logout the application will redirect to "/" path.
.logoutSuccessUrl("/")
.permitAll()
.and()
// Setting the filter for the URL "/google/login".
.addFilterAt(filter(), BasicAuthenticationFilter.class)
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
Both methods were still not called, and I don't feel like I am any closer to the solution. Any help will be greatly appreciated.
The way to go here is to provide a custom OidcUserService and override the loadUser() method because Google login is based on OpenId Connect.
First define a model class to hold the extracted data, something like this:
public class GoogleUserInfo {
private Map<String, Object> attributes;
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public String getId() {
return (String) attributes.get("sub");
}
public String getName() {
return (String) attributes.get("name");
}
public String getEmail() {
return (String) attributes.get("email");
}
}
Then create the custom OidcUserService with the loadUser() method which first calls the provided framework implementiation and then add your own logic for persisting the user data you need, something like this:
#Service
public class CustomOidcUserService extends OidcUserService {
#Autowired
private UserRepository userRepository;
#Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
try {
return processOidcUser(userRequest, oidcUser);
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OidcUser processOidcUser(OidcUserRequest userRequest, OidcUser oidcUser) {
GoogleUserInfo googleUserInfo = new GoogleUserInfo(oidcUser.getAttributes());
// see what other data from userRequest or oidcUser you need
Optional<User> userOptional = userRepository.findByEmail(googleUserInfo.getEmail());
if (!userOptional.isPresent()) {
User user = new User();
user.setEmail(googleUserInfo.getEmail());
user.setName(googleUserInfo.getName());
// set other needed data
userRepository.save(user);
}
return oidcUser;
}
}
And register the custom OidcUserService in the security configuration class:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomOidcUserService customOidcUserService;
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(customOidcUserService);
}
}
Mode detailed explanation can be found in the documentation:
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2login-advanced-oidc-user-service
In case of some one else is stuck with this, my solution was to create a custom class extending from
OAuth2ClientAuthenticationProcessingFilter and then override the successfulAuthentication method to get the user authentication details and save it to my database.
Example (kotlin):
On your ssoFilter method (if you followed this tutorial https://spring.io/guides/tutorials/spring-boot-oauth2) or wharever you used to register your ouath clients, change the use of
val googleFilter = Auth2ClientAuthenticationProcessingFilter("/login/google");
for your custom class
val googleFilter = CustomAuthProcessingFilter("login/google")
and of course declare the CustomAuthProcessingFilter class
class CustomAuthProcessingFilter(defaultFilterProcessesUrl: String?)
: OAuth2ClientAuthenticationProcessingFilter(defaultFilterProcessesUrl) {
override fun successfulAuthentication(request: HttpServletRequest?, response: HttpServletResponse?, chain: FilterChain?, authResult: Authentication?) {
super.successfulAuthentication(request, response, chain, authResult)
// Check if user is authenticated.
if (authResult === null || !authResult.isAuthenticated) {
return
}
// Use userDetails to grab the values you need like socialId, email, userName, etc...
val userDetails: LinkedHashMap<*, *> = userAuthentication.details as LinkedHashMap<*, *>
}
}
You can listen to AuthenticationSuccessEvent. For example:
#Bean
ApplicationListener<AuthenticationSuccessEvent> doSomething() {
return new ApplicationListener<AuthenticationSuccessEvent>() {
#Override
void onApplicationEvent(AuthenticationSuccessEvent event){
OAuth2Authentication authentication = (OAuth2Authentication) event.authentication;
// get required details from OAuth2Authentication instance and proceed further
}
};
}

Spring Security throwing "Reason: Bad credentials" error in UI but correctly finding user during login

I've exhausted every SO post and blog entry I could find trying to figure out what I've done wrong. So, now I'm asking for your help. I'm building a Spring Boot app and leveraging Spring Security for user management/authentication. I think I've set everything up correctly, but during login Spring Security redirects to the login failure URL (/login?error) every time, throwing the error:
Your login attempt was not successful, try again.
Reason: Bad credentials
There is nothing telling or useful in the server logs. As part of debugging, I added a bunch of logging which confirmed that:
The user is being found in the database (via email, with usernameParameter set to email in config
Spring Security's userDetails.User creates the correct user from its new constructor (I've logged the details of the user)
I am new to the framework so it's possible I'm overlooking something, which is where I would really appreciate your help. I've included my security config and user service below (I've cleared out logging to clean it up for your reading)--let me know if any other pieces would be helpful. Thank you in advance!
SecurityConfiguration.java
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Resource(name = "userService")
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/test").hasRole("USER")
.antMatchers("/**").permitAll()
.and().formLogin().usernameParameter("email").defaultSuccessUrl("/register_success");
http.authorizeRequests()
.antMatchers("/resources/**").permitAll();
super.configure(http);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder)
throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
// Define this bean so autowired can find and use it (fixes complaining error)
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
UserService.java
#Service("userService")
public class UserService implements UserDetailsService {
private static final String LOG_TAG = UserService.class.getSimpleName();
private Logger logger = LoggerFactory.getLogger(LOG_TAG);
private UserRepository userRepository;
#Autowired
SessionFactory sessionFactory;
#Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Must provide email address as username argument
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username);
if (user == null) {
throw new UsernameNotFoundException("There is no user with this email address.");
}
org.springframework.security.core.userdetails.User springUser = new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPassword(), getAuthority());
return springUser;
}
public User findByEmail(String email) {
return userRepository.findByEmail(email);
}
public User findByConfirmationToken(String confirmationToken) {
return userRepository.findByConfirmationToken(confirmationToken);
}
#Transactional
public List<User> findAll() {
Criteria criteria = sessionFactory.openSession().createCriteria(User.class);
return (List<User>) criteria.list();
}
public void saveUser(User user) {
userRepository.save(user);
}
//TODO: Figure out how to use this properly
public List getAuthority() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
}
You need to use PasswordEncoder to encrypt passwords when adding new users.
like this:
#Autowired
private BCryptPasswordEncoder passwordEncoder;
public void saveUser(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
userRepository.save(user);
}

How to configure multiple HttpSecurity with UserDetailsService using spring boot security?

I'm working on with spring boot security layer to authenticate and authorize the user.Now, i would like to do some sample app using multi http security configuration.I have the scenario like there will be two login pages with different URL mappings("/managementLogin","/othersLogin").
I can understood how to configure multi httpsecurity configs but i need to validate the users from two tables.If the management users loggedIn i need to validate the user from management table through DAO layer using UserDetailsService else if any other users loggedIn i need to validate from other_users table.
Could anybody help me to know how to configure the multi http config and dao layer using UserDetailsService with spring boot security ?
Here is my basic code snippet,
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
#Autowired
#Qualifier("userDetailsService")
UserDetailsService userDetailsService;
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
#Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// for testing authentication purpose using inMemory db
/*
* auth.inMemoryAuthentication().withUser("user").password("user").roles
* ("USER").and().withUser("admin") .password("admin").roles("ADMIN");
*/
// Dao based authentication
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll();
http.authorizeRequests().antMatchers("/rest/**").authenticated();
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.formLogin().successHandler(authenticationSuccessHandler);
http.formLogin().failureHandler(authenticationFailureHandler);
http.logout().logoutSuccessUrl("/");
// CSRF tokens handling
http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
}
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/registerUser","/register.html");
}
#Bean
public PasswordEncoder passwordEncoder() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
}
TIA..,
Implement a custom UserDetailsService like this:
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private UserDaoTableOne userDaoTableOne;
#Autowired
private UserDaoTableTwo userDaoTableTwo;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = userDaoTableOne.find(username);
if(user == null){
user = userDaoTableTwo.find(username);
}
if (user == null) {
throw new UsernameNotFoundException(String.format("Username '%s' not found", username));
}
return user;
}
}
Implement two DaoAuthenticationProvider with his own UserDetailsService and inject both providers to the authenticationManager.
I don't know what is the requisite for two distinct login endpoints but at first I think is a bad idea.
You can create different Authentication objects an let the AuthenticationManager choose the correct AuthenticationProvider based in the supports method.
Indeed you will need to use, two user detail services. But, that wont be enough. I suggest you to create another ApplicationSecurity2 class with different order.
Spring security is built on an ordered list of filter chains.
see the answer given here by Dave Sayer. Then you can handle different urls, as you want.
in my case I checked into two repositories, Below an exemple that I use:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AbstractUser user;
try {
user = clientRepository.findByUsername(username);
}
catch (Exception userException) {
try {
user = adminRepository.findByUsername(username);
}
catch (Exception adminException) {
throw new UsernameNotFoundException("No user present with username : " + username);
}
}
return user;
}
I have to handle around same issue , i have autowired httprequest class in userdetail service and get request params type and drive my logic based on that.
you can directly solve the issue as the recommended solutions, but you can create a simple trick to define two different UserDetailsService as here I have two user one as a normal user and another as an editor :
editor
#Log4j2
#RequiredArgsConstructor
#Service
public class EditorService implements UserDetailsService {
private final EditorRepository editorRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(username == null || "".equals(username)){
throw new UsernameNotFoundException("null value");
}
Optional<Editor> editor = editorRepository.findByUsername(username);
if(editor.isPresent()){
log.info("created under editor service: " + editor.get());
return editor.get();
}
throw new UsernameNotFoundException("does not exists");
}
}
user
#Log4j2
#RequiredArgsConstructor
#Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(username == null || "".equals(username)){
throw new UsernameNotFoundException("null");
}
Optional<User> user = userRepository.findByUsername(username);
if(user.isPresent()){
log.info("cretaed under User service : " + user.get());
return user.get();
}
throw new UsernameNotFoundException("does not exists");
}
}
then on the configurations side, we can use of spring order mechanism :
user config :
#EnableWebSecurity
#Configuration
#RequiredArgsConstructor
#Order(1)
public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserService userService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/user/**")
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}
#Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(10);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userService).passwordEncoder(passwordEncoder());
}
}
Editor config :
#EnableWebSecurity
#Configuration
#RequiredArgsConstructor
public class EditorWebSecurityConfig extends WebSecurityConfigurerAdapter {
private final EditorService editorService;
#Lazy
private final PasswordEncoder passwordEncoder;
#Override
protected void configure(HttpSecurity http) throws Exception {
http // all other requests handled here
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.editorService).passwordEncoder(passwordEncoder);
}
}

Categories

Resources