When I try to use password encoder to check. It hasn't worked, I see "Bad password".
How should I receive authentication.getCredentials() to check passwords?
If I save user without encoding and try to log in, it works.
#Component
public class AuthClient implements AuthenticationProvider {
#Autowired
private ClientRepository clientRepository;
#Autowired
PasswordEncoder passwordEncoder;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String checkPassword = passwordEncoder.encode(authentication.getCredentials().toString());
Client client = this.clientRepository.findByUsername(username);
if (client == null) {
throw new UsernameNotFoundException("Invalid username/password");
}
String password = client.getPassword();
if (!password.equals(checkPassword)) {
throw new BadCredentialsException("Bad password");
}
Collection<? extends GrantedAuthority> authorities = translate();
return new UsernamePasswordAuthenticationToken(username,password,authorities);
}
private Collection<? extends GrantedAuthority> translate() {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_CLIENT"));
return authorities;
}
#Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
you are doing it wrong if you want to test two passwords you need to use passwordEncoder like this:
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, client.getPassword())) {
throw new BadCredentialsException("Bad password");
}
Related
Using the new spring-authorization-server 0.2.3 and following https://github.com/spring-projects/spring-authorization-server/tree/main/samples as reference I was able to setup an authorization server, resource server and a client successfully when using an InMemoryUserDetailsManager as follows
#EnableWebSecurity
public class DefaultSecurityConfig {
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.build();
}
#Bean
UserDetailsService users() {
User.UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user1").password("password").roles("USER").build());
manager.createUser(users.username("admin").password("password").roles("USER", "ADMIN").authorities("r1","r2","r3").build());
return manager;
}
}
This works well, In the client, I can see the authorities Granted Authorities=["r1","r2","r3"] present.
Now when I attempt to implement my own UserDetailsService which retrieves users from a Mongo Database, I stop seeing the GrantedAuthorities being passed to the client and only see Granted Authorities=[ROLE_USER, SCOPE_openid]
This is what I now have in the DefaultSecurityConfig
#EnableWebSecurity
public class DefaultSecurityConfig {
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.build();
}
#Autowired
private MongoTemplate mongoTemplate;
#Bean
UserDetailsService users() {
return new CustomUserDetailsService(mongoTemplate);
}
}
And my CustomUserDetailsService looks like the following:
public class CustomUserDetailsService implements UserDetailsService {
private final MongoTemplate mongoTemplate;
public CustomUserDetailsService(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Criteria criteria = Criteria.where("email").is(username);
CustomUser user = mongoTemplate.findOne(new Query(criteria), CustomUser.class, "vOAuthUser");
if (user != null) {
log.info("Found user {}", user.email());
List<GrantedAuthority> authorities = getUserAuthority(user.groups());
return buildUserForAuthentication(user, authorities);
} else {
throw new UsernameNotFoundException("username not found");
}
}
private UserDetails buildUserForAuthentication(CustomUser user, List<GrantedAuthority> authorities) {
return new org.springframework.security.core.userdetails.User(user.email(), user.password(), authorities);
}
private List<GrantedAuthority> getUserAuthority(Set<String> groups) {
List<GrantedAuthority> authorities = new ArrayList<>();
groups.forEach(s -> {
Criteria criteria = Criteria.where("name").is(s);
CustomRole role = mongoTemplate.findOne(new Query(criteria), CustomRole.class, "vRole");
if (role != null) {
authorities.addAll(role.grantedAuthorities());
}
});
return authorities;
}
}
Any help is greatly appreciated.
Have you defined a OAuth2TokenCustomizer bean in your security configuration? You can add Granted Authorities there if you need, like in the following code:
#Bean
#SuppressWarnings("unused")
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
JoseHeader.Builder headers = context.getHeaders();
JwtClaimsSet.Builder claims = context.getClaims();
Authentication principal = context.getPrincipal();
Set<String> authorities = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
claims.claim("authorities", authorities);
};
}
I was working on Spring Boot Security project and i did the authentication on some endpoints to prevent from access from USERS and granted the authority to ADMIN when ever i hit this URL : admin/users i got the login page i put the credentials and instead of returning the actual content its return type=Forbidden, status=403
here is my SecurityConfig
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// inject spring user details interface
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(encoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/api/**" , "/").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and().formLogin();
}
#Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
}
admin users controller
#RestController
#RequestMapping("admin/users")
public class UserController {
//
#Autowired
private UserRepository userRepository;
#Autowired
private BCryptPasswordEncoder passwordEncoder;
// get all users
#PreAuthorize("hasAnyRole('ADMIN')")
#GetMapping
public List<User> getAllUsers(){
return userRepository.findAll();
}
// test
#PreAuthorize("hasAnyRole('ADMIN')")
#GetMapping("/test")
public String message() {
return "secured end point";
}
//create new users
#PreAuthorize("hasAnyRole('ADMIN')")
#PostMapping
public User registerUser(#RequestBody User user){
String password = user.getPassword();
String encodedPassword = passwordEncoder.encode(password);
user.setPassword(encodedPassword);
return userRepository.save(user);
}
}
userDetail
public class CustomUserDetails implements UserDetails {
/**
*
*/
private static final long serialVersionUID = 1256711395932122675L;
private User user;
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
}
#Override
public String getPassword() {
return user.getPassword();
}
#Override
public String getUsername() {
return user.getUsername();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return true;
}
#Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
}
#Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return true;
}
}
userDetailsService
[![#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
CustomUserDetail userDetail = null;
if(user != null) {
userDetail = new CustomUserDetail();
userDetail.setUser(user);
}
else {
throw new UsernameNotFoundException("user not exist with username " + username);
}
System.out.println("user details" + userDetail);
return userDetail;
}
}][2]][2]
I fixed the issue by simply correcting the getAuthorities() I removed "ROLE_" in simpeGrantedAuthority() , extracted the return into a variable in userDetail.java and used hasAuthority() instead of hasRole()
the code which was getting 403 - forbidden
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
}
**the solution **
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = user.getRoles().stream().map(role ->
new SimpleGrantedAuthority(role.getRole()))
.collect(Collectors.toList());
return authorities;
}
I wanna authenticate users via Keycloak, but I need to add additional roles to Authentication object, that is using by Spring Security. Adding roles are saved in Postgres database.
I tried to override configureGlobal with custom AuthenticationProvider, but it didn't work.
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
ApplicationAuthenticationProvider provider = new ApplicationAuthenticationProvider();
provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(provider);
}
#Component
public class ApplicationAuthenticationProvider extends KeycloakAuthenticationProvider {
#Autowired
private UserService userService;
private GrantedAuthoritiesMapper grantedAuthoritiesMapper;
public void setGrantedAuthoritiesMapper(GrantedAuthoritiesMapper grantedAuthoritiesMapper) {
this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
String username = ((KeycloakAuthenticationToken) authentication)
.getAccount().getKeycloakSecurityContext().getToken().getPreferredUsername();
List<Role> roles = userService.findRoles(username);
for (Role role : roles) {
grantedAuthorities.add(new KeycloakRole(role.toString()));
}
return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), mapAuthorities(grantedAuthorities));
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private Collection<? extends GrantedAuthority> mapAuthorities(
Collection<? extends GrantedAuthority> authorities) {
return grantedAuthoritiesMapper != null
? grantedAuthoritiesMapper.mapAuthorities(authorities)
: authorities;
}
}
Tried to add additional filter, but i'm not sure in correct configuration.
#Bean
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
RequestMatcher requestMatcher =
new OrRequestMatcher(
new AntPathRequestMatcher("/api/login"),
new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN),
// We're providing our own authorization header matcher
new IgnoreKeycloakProcessingFilterRequestMatcher()
);
return new KeycloakAuthenticationProcessingFilter(authenticationManagerBean(), requestMatcher);
}
// Matches request with Authorization header which value doesn't start with "Basic " prefix
private class IgnoreKeycloakProcessingFilterRequestMatcher implements RequestMatcher {
IgnoreKeycloakProcessingFilterRequestMatcher() {
}
public boolean matches(HttpServletRequest request) {
String authorizationHeaderValue = request.getHeader("Authorization");
return authorizationHeaderValue != null && !authorizationHeaderValue.startsWith("Basic ");
}
}
Now I use Keycloak only for login/password. Roles and permissions now saved in local DB.
I implemented the login of my Spring Boot web app using OAuth2 and everything works fine.
The only problem is that the logged in user does not has the authorities information saved inside the session so each time I request a url and the controller has the annotation #PreAuthorize("hasRole('USER')") I get rejected.
SecurityConfiguration class:
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
#EnableJpaRepositories(basePackageClasses = UserRepository.class)
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private CustomOAuth2UserService customOAuth2UserService;
#Autowired
private CustomUserDetailsService userDetailsService;
#Autowired
private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error=true")
.and()
.logout()
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.and()
.oauth2Login()
.loginPage("/login")
.failureUrl("/login?error=true")
.userInfoEndpoint()
.userService(customOAuth2UserService)
.and()
.failureHandler(oAuth2AuthenticationFailureHandler);
}
#Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
This is the CustomOAuth2UserService class:
#Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
#Autowired
private UserService userService;
#Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
try {
return processOAuth2User(oAuth2UserRequest, oAuth2User);
}catch (Exception ex) {
// Throwing an instance of AuthenticationException will trigger the OAuth2AuthenticationFailureHandler
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes());
if(StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
throw new RuntimeException("Id not found from OAuth2 provider");
}
User user;
try {
user = userService.getByEmail(oAuth2UserInfo.getEmail());
if(!user.getProvider().toString().equalsIgnoreCase(oAuth2UserRequest.getClientRegistration().getRegistrationId())) throw new EmailAlreadyTakenException("email-already-taken");
} catch (UserNotFoundException e) {
user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo);
}
return new CustomUserDetails(user);
}
private User registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) {
User user = new User();
user.setProvider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()));
Identity identity = new Identity(user);
if(oAuth2UserInfo.getFirstName() != null && !oAuth2UserInfo.getFirstName().equalsIgnoreCase(""))
identity.setFirstName(oAuth2UserInfo.getFirstName());
if(oAuth2UserInfo.getLastName() != null && !oAuth2UserInfo.getLastName().equalsIgnoreCase(""))
identity.setSecondName(oAuth2UserInfo.getLastName());
user.setIdentity(identity);
user.setEmail(oAuth2UserInfo.getEmail());
user.setConfirmedRegistration(true);
boolean flag = false;
String username = oAuth2UserInfo.getName().toLowerCase().replaceAll("\\s+", "");
user.setUsername(username);
return userService.addFacebookUser(user);
}
}
This a part of the application.properties file:
spring.security.oauth2.client.registration.facebook.client-id=***
spring.security.oauth2.client.registration.facebook.client-secret=***
spring.security.oauth2.client.registration.facebook.scope=email,public_profile
spring.security.oauth2.client.registration.google.client-id=***
spring.security.oauth2.client.registration.google.client-secret=***
spring.security.oauth2.client.registration.google.scope=email,profile
spring.security.oauth2.client.provider.facebook.authorizationUri = https://www.facebook.com/v3.0/dialog/oauth
spring.security.oauth2.client.provider.facebook.tokenUri = https://graph.facebook.com/v3.0/oauth/access_token
spring.security.oauth2.client.provider.facebook.userInfoUri = https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture
Once logged in the user can call this url /users/{username} but when he login with facebook or google through OAuth2, he gets rejected because the authorities list is empty. When he login with his webapp credential, the authorities list contains USER_ROLE and he is allowed to procede.
#PreAuthorize("hasRole('USER')")
#GetRequest("users/{username}")
public String getUser(#PathVariable String username, #PathVariable String subsection, Model model, Principal principal) throws IllegalAccessException, UserNotFoundException {
User user = userService.getByUsername(principal.getName());
model.addAttribute("user", user);
return "user";
}
Inside principal object there are:
When logged in with OAuth2:
principal: type CustomUserDetails (user information)
authorizedClientRegistrationId: type String ("google", "facebook")
authorities: type Collections$UnmodifiableRandomAccessList (empty)
details: null
authenticated: type boolean (true)
When logged in with local credentials:
principal: type CustomUserDetails (user information)
credentials: null
authorities: type Collections$UnmodifiableRandomAccessList
index:0 type SimpleGrantedAuthority ("USER_ROLE")
details: type WebAuthenticationDetails (remote address, sessionId)
authenticated: type boolean (true)
After some time of debugging I found the solution! I was not configuring correctly the roles of my user.
Inside the registerNewUser method of my custom OAuth2UserService I wasn't setting the Role of the User. I just added the line:
user.setRoles(new HashSet<>(Collections.singletonList(new Role("ROLE_USER"))));
and everything started to work! So now when the OAuth2User's authorities get asked, it just calls the getAuthorities of CustomUserDetails (my implementation of OAuth2User) and it calls the getRoles method of the User.
CustomUserDetails class:
public class CustomUserDetails extends User implements UserDetails, OAuth2User {
public CustomUserDetails() {
}
public CustomUserDetails(String username, String email, String password, Set<Role> roles) {
super(username, email, password, roles);
}
public CustomUserDetails(User user) {
super(user.getUsername(), user.getEmail(), user.getPassword(), user.getRoles());
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return getRoles()
.stream()
.map(role -> new SimpleGrantedAuthority(role.getRole()))
.collect(Collectors.toList());
}
#Override
public Map<String, Object> getAttributes() {
return null;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
#Override
public String getName() {
return null;
}
}
I'm trying to secure a REST API with JWT and I have a problem during the sign in.
UserService.java
private final UserRepository repository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final AuthenticationManager authenticationManager;
#Autowired
public UserService(UserRepository repository, PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider, AuthenticationManager authenticationManager) {
this.repository = repository;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
this.authenticationManager = authenticationManager;
}
public String signin(String email, String password) {
try {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(email, password);
authenticationManager.authenticate(token);
return jwtTokenProvider.createToken(email, Collections.singletonList(ROLE_CLIENT));
} catch (AuthenticationException e) {
throw new CustomException("Invalid email/password supplied", HttpStatus.UNPROCESSABLE_ENTITY);
}
}
public String signup(UsersEntity user) {
if (!repository.existsByEmail(user.getEmail())) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
repository.save(user);
return jwtTokenProvider.createToken(user.getEmail(), Collections.singletonList(ROLE_CLIENT));
} else {
throw new CustomException("Email is already in use", HttpStatus.UNPROCESSABLE_ENTITY);
}
}
It successfully signs up user, but when I'm trying to login it always throws an AuthenticationException on authenticationManager.authenticate(token);.
What m I doing wrong?
Full stacktrace
According to the stack traces you provided, the BadCredentialsException is thrown by the method additionalAuthenticationChecks from the DaoAuthenticationProvider class. Either the authentication token you provide is null or either the presented password extractred from this authentication token doesn't match with the password returned by the getPassword method of the userDetails object.
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}