this is my first project with Spring and I have just started to create the login with Spring Security. I want some pages to be accessible only for the admin and not for the players. I've found some examples on the web and this mechanism works pretty well, I have this secured page that is protected by the login and it's forbidden when the user has no ROLE_ADMIN.
#PreAuthorize("hasAuthority('ROLE_ADMIN')")
#GetMapping("/secured/all")
public String securedHello() {
return "Secured Hello";
}
The problem is that testing my code I found out that Spring authenticates the admin (and the user as well) only checking the username. If I put the wrong password it allows me to enter anyway. I don't understand how this is possible, shouldn't Spring Security do all the authentication work by itself? I've seen somebody suggested to implement an authentication manager or something like that, but I don't understand why and how to insert it in my code. I'm stuck on this since two days, please any advice wuold be really appreciated.
These are my classes:
package model;
import java.io.IOException;
import javax.naming.AuthenticationException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.fasterxml.jackson.databind.ObjectMapper;
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
#EnableJpaRepositories(basePackageClasses = PlayersRepository.class)
#ComponentScan(basePackageClasses= CustomUserDetailsService.class)
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(getPasswordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
//http.csrf().disable();
http.authorizeRequests()
.antMatchers("**/secured/**").access("hasAuthority('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin().permitAll();
}
private PasswordEncoder getPasswordEncoder() {
return new PasswordEncoder() {
#Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
#Override
public boolean matches(CharSequence charSequence, String s) {
return true;
}
};
}
}
package model;
import java.util.ArrayList;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private PlayersRepository usersRepository;
#Autowired
private RoleRepository rolesRepository;
public CustomUserDetailsService(PlayersRepository usersRepository, RoleRepository rolesRepository) {
this.usersRepository=usersRepository;
this.rolesRepository=rolesRepository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Player> optionalUser = usersRepository.findByUsername(username);
optionalUser
.orElseThrow(() -> new UsernameNotFoundException("Username not found"));
Player user= optionalUser.get();
System.out.println(user);
return toUserDetails(new UserObject(user.getUsername(),user.getPassword(),user.getRole()));
}
private UserDetails toUserDetails(UserObject userObject) {
return User.withUsername(userObject.name)
.password(userObject.password)
.roles(userObject.role).build();
}
private static class UserObject {
private String name;
private String password;
private String role;
public UserObject(String name, String password, String role) {
this.name = name;
this.password = password;
this.role = role;
}
}
}
package model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class CustomUserDetails extends Player implements UserDetails {
String role;
public CustomUserDetails(final Player user) {
super(user);
}
public CustomUserDetails(Optional<Player> user, String role) {
super(user);
this.role=role;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
list.add(new SimpleGrantedAuthority("ROLE_"+ role));
System.out.println(list);
return list;
}
#Override
public String getPassword() {
return super.getPassword();
}
#Override
public String getUsername() {
return super.getUsername();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
}
Shouldn't Spring Security do all the authentication work by itself?
Yes, Spring Security does that for you using an AuthenticationManager.
I've seen somebody suggested to implement an authentication manager or something like that, but I don't understand why and how to insert it in my code.
You actually already have an AuthenticationManager, since you built one within the configure() method:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
So, what is exactly the reason this isn't working you may ask. Well, the AuthenticationManager you provided contains two parts:
A part that fetches the user information (CustomUserDetailsService)
Another part that checks the password (the getPasswordEncoder()).
What happens behind the screens is that Spring calls your CustomUserDetailsService to fetch your user information, including your (hashed) password. After fetching that information, it calls your PasswordEncoder.matches() function to verify if the raw entered password matches your hashed password provided by the CustomUserDetailsService.
In your case, your PasswordEncoder.matches() function looks like this:
#Override
public boolean matches(CharSequence charSequence, String s) {
return true;
}
This means that regardless of what password you provide, it will return true. This is exactly what you're experiencing since any password will work.
So, how do you solve this? Well, your PasswordEncoder should actually hash your raw password and compare it to the hashed password that is being passed, for example:
#Override
public boolean matches(CharSequence rawPassword, String hashedPassword) {
String hashedPassword2 = null; // hash your rawPassword here
return hashedPassword2.equals(hashedPassword);
}
The implementation of this method depends on how you store your password in your database. Spring Security already comes with a few implementation including BcryptPasswordEncoder, StandardPasswordEncoder, MessageDigestPasswordEncoder, ... . Some of these implementations are deprecated, mostly to indicate that the hashing mechanisms used by those encoders are considered unsafe. There are no plans at the moment of writing to remove those encoders, as mentioned by the Javadoc:
Digest based password encoding is not considered secure. Instead use an adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or SCryptPasswordEncoder. Even better use DelegatingPasswordEncoder which supports password upgrades. There are no plans to remove this support. It is deprecated to indicate that this is a legacy implementation and using it is considered insecure.
(Emphasis is my own)
If you are free to choose which implementation you pick, then Spring recommends using BCryptPasswordEncoder as mentioned by the Javadoc:
Service interface for encoding passwords. The preferred implementation is BCryptPasswordEncoder.
I just had a Quick scan I found this
private PasswordEncoder getPasswordEncoder() {
return new PasswordEncoder() {
#Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
#Override
public boolean matches(CharSequence charSequence, String s) {
return true;
}
};
}
In your matches you are returning always true.
I guess here you should put logic for checking password for equality something like this
#Override
public boolean matches(CharSequence charSequence, String s) {
return charSequence.toString.equals(s);
}
I would suggest you use something like this
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Related
For several days now I have been trying to solve a problem with Spring Security 6. I've read almost all the spring documentation for Spring Security 6 and I watched several tutorials and just cannot see where the mistake is. I looked at the code under a magnifying glass:
WebSecurityConfigurer.class:
package com.transfer.market.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
#Configuration
#EnableWebSecurity
public class WebSecurityConfigurer {
private final String ADMIN;
private final String ADMIN_PASS;
private final String SUPER;
private final String SUPER_PASS;
#Autowired
public WebSecurityConfigurer(AppSecurityExternalConfig appSecurityExternalConfig) {
this.ADMIN = appSecurityExternalConfig.getUser().getAdmin();
this.ADMIN_PASS = appSecurityExternalConfig.getPassword().getAdmin();
this.SUPER = appSecurityExternalConfig.getUser().getSup();
this.SUPER_PASS = appSecurityExternalConfig.getPassword().getSup();
}
#Bean
public UserDetailsService users() {
UserDetails admin = User.builder()
.username(ADMIN)
.password(encoder().encode(ADMIN_PASS))
.roles("ADMIN")
.build();
UserDetails sup = User.builder()
.username(SUPER)
.password(encoder().encode(SUPER_PASS))
.roles("ADMIN", "DBA")
.build();
return new InMemoryUserDetailsManager(admin, sup);
}
#Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/resource/**").permitAll()
.requestMatchers("/api/**").hasAnyRole("ADMIN", "DBA")
.requestMatchers("/db/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasRole('DBA')"))
.anyRequest().denyAll()
);
return http.build();
}
#Bean
public static BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
I have printed ADMIN, SUPER and their passwords to the console and they are for sure read correctly from the application.properties. So that is not the problem.
AppSecurityExternalConfig.class:
package com.transfer.market.configuration;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
#Data
#Configuration
#ConfigurationProperties(prefix = "config.security")
public class AppSecurityExternalConfig {
private User user;
private Password password;
#Data
#Component
#ConfigurationProperties(prefix = "user")
public static class User {
private String user;
private String admin;
private String sup;
}
#Data
#Component
#ConfigurationProperties(prefix = "password")
public static class Password {
private String user;
private String admin;
private String sup;
}
}
application.properties:
...
# Security:
config.security.user.admin=admin
config.security.password.admin=pass
config.security.user.sup=super
config.security.password.sup=pass
...
PlayerController.class:
#RestController
#Validated
public class PlayerController {
private final PlayerService playerService;
#Autowired
public PlayerController(PlayerService playerService) {
this.playerService = playerService;
}
#PostMapping("/api/players")
public ResponseEntity<Player> addSingle(#RequestBody #Valid Player player) {
return new ResponseEntity<>(playerService.addSingle(player), HttpStatus.CREATED);
}
...
It just keeps getting "403 Forbidden", but for all end points starting with "/resource" where they are .permitAll() it works and it's 200 OK. Why doesnt the other requestMatchers work? Please help.
#Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth.requestMatchers("/resource/**").permitAll()
.requestMatchers("/api/**")
.hasAnyRole("ADMIN", "DBA")
.requestMatchers("/db/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and
hasRole('DBA')"))
.anyRequest().denyAll());
return http.build();
}
You have to configured the basic authentication. Add the following statement in the SecurityFilterChain bean.
http.httpBasic();
i.e.
#Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth.requestMatchers("/resource/**").permitAll()
.requestMatchers("/api/**")
.hasAnyRole("ADMIN")
.requestMatchers("/db/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasRole('DBA')"))
.anyRequest().denyAll());
http.httpBasic();
return http.build();
}
I am new to this technology . I have trying to implement Digest Authentication for my Springboot application . I am getting below error while I am trying to call my application :There is no PasswordEncoder mapped for the id \"null\"","path":"/countryId/"}* Closing connection 0
curl command I am using to invoke : curl -iv --digest -u test:5f4dcc3b5aa765d61d8327deb882cf99 -d {"CountryCode": "INDIA"} http://localhost:9090/countryId/
Classes Details :
package com.sg.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
import org.springframework.stereotype.Component;
#Component
#Configuration
#EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
CustomDigestAuthenticationEntryPoint customDigestAuthenticationEntryPoint;
/*#Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}*/
#Bean
public UserDetailsService userDetailsServiceBean()
{
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("test").password("{noop}password").roles("USER").build());
return manager;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/hello/**").permitAll().anyRequest().authenticated()
.and().exceptionHandling().authenticationEntryPoint(customDigestAuthenticationEntryPoint).and()
.addFilter(digestAuthenticationFilter());
}
//#Bean
DigestAuthenticationFilter digestAuthenticationFilter() throws Exception {
DigestAuthenticationFilter digestAuthenticationFilter = new DigestAuthenticationFilter();
digestAuthenticationFilter.setUserDetailsService(userDetailsServiceBean());
digestAuthenticationFilter.setAuthenticationEntryPoint(customDigestAuthenticationEntryPoint);
return digestAuthenticationFilter;
}
}
package com.sg.config;
import org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
#Component
public class CustomDigestAuthenticationEntryPoint extends DigestAuthenticationEntryPoint {
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("Digest-Realm");
setKey("MySecureKey");
setNonceValiditySeconds(300);
super.afterPropertiesSet();
}
}
I have resolved the issue. Let me explain what went wrong first, in current Spring security, you can not use a plain text password, so have to keep some encrypting logic. But unfortunately Digest doesn't work with a encrypted password.
I have found a work around, instead using a Bean (Bycrypt), I have directly implemented PasswordEncoder interface, in a way, it should able to hold plain text password.
#Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
#Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
#Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
};
}
I'm getting the users of my web-app from the Acive Directory.
So I created a custom UserDetailsContextMapper to save some data of the user to the web-app's MySql Database.
And this is my security configuration about Ldap:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth
.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("myDomain.local", "ldap://LDAP_IP:389/");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setUserDetailsContextMapper(userDetailsContextMapper());
return provider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Bean
public UserDetailsContextMapper userDetailsContextMapper() {
return new LdapUserDetailsContextMapper();
}
I would like to know when and if the data on the AD are changed from last login.
For example if today at 10:00AM I was member of group A inside the AD and now I'm member of group A and B, I would like to update the authorities on MySql.
Is there a field or something inside AD to know that?
EDIT:
I would like to check if something change for a particulare user during the login phase, in this way I can update the information on MySql.
To find when a user was last modified, you can use the "whenchanged" attribute.
if you extend LdapUserDetailsMapper, and override the mapUserFromContext, it might look like this:
package example.active.directory.authentication;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collection;
public class CustomUserMapper extends LdapUserDetailsMapper{
#Override
public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities){
UserDetails details = super.mapUserFromContext(ctx, username, authorities);
String[] changedValues = ctx.getStringAttributes("whenchanged");
if(changedValues != null && changedValues.length > 0){
LocalDateTime lastChangedTime = Arrays.stream(changedValues)
.map(input ->
OffsetDateTime.parse(
input,
DateTimeFormatter.ofPattern("uuuuMMddHHmmss[,S][.S]X")
).toLocalDateTime()
)
.sorted((a, b) -> a.compareTo(b) * -1)
.findFirst()
.orElse(null);
System.out.println(lastChangedTime);
//Do something with value?
}
return details;
}
}
I need some additional data in in the user details of authenticated users. So i wrote a custom details service and as a second approach a custom authentication provider to enrich the data in the user object. But the principal object in the security context stays a string instead of becoming the desired user object and when i'm setting breakpoints im my custom details service and authentication porvider it looks like this code is never used by spring albeit my customized classes are listed in springs authentication manager builder.
This is my custom user details service:
package edu.kit.tm.cm.bamsg.bffweb.iamservice;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.HashSet;
import java.util.Set;
/*** #author schlund*/
public class CustomStudentDetailsService implements UserDetailsService {
private SecurityUserRepository securityUserRepository;
public CustomStudentDetailsService(SecurityUserRepository userSecurityRepository){
this.securityUserRepository=userSecurityRepository;
}
#Override
public SecurityUser loadUserByUsername(String kitID) throws UsernameNotFoundException {
try {
SecurityUser securityPerson = securityUserRepository.findByUsername(kitID);
if (securityPerson == null) {
return null;
}
return securityPerson;
}
catch (Exception e){
throw new UsernameNotFoundException("User not found");
}
}
private Set<GrantedAuthority> getAuthorities(SecurityUser securityPerson){
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(securityPerson.getRole());
authorities.add(grantedAuthority);
return authorities;
}
}
This is my custom authentication provider:
package edu.kit.tm.cm.bamsg.bffweb.iamservice;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication ) throws AuthenticationException {
String password = authentication.getCredentials().toString().trim();
SecurityUser appUser = new SecurityUser();
return new UsernamePasswordAuthenticationToken(appUser, password, null);
}
#Override
public boolean supports(Class<? extends Object> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
This is my web security config:
package edu.kit.tm.cm.bamsg.bffweb;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import edu.kit.tm.cm.bamsg.bffweb.iamservice.*;
#Configuration
#EnableOAuth2Sso
#EnableGlobalMethodSecurity(prePostEnabled = true)
#ComponentScan("edu.kit.tm.cm.bamsg.bffweb.iamservice")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String REALM = "bam";
#Autowired
private CustomAuthenticationProvider authProvider;
#Autowired
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.and()
//endpoints without authentication
.authorizeRequests().antMatchers("/logged", "/userData").permitAll()
.and()
// default with authentication
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
#Bean
public OAuth2FeignRequestInterceptor oAuth2FeignRequestInterceptor(OAuth2ClientContext context, OAuth2ProtectedResourceDetails details) {
return new OAuth2FeignRequestInterceptor(context, details);
}
#Bean
BasicAuthenticationEntryPoint getBasicAuthEntryPoint() {
BasicAuthenticationEntryPoint basicAuth = new BasicAuthenticationEntryPoint();
basicAuth.setRealmName(REALM);
return basicAuth;
}
}
And at least after authentication at the code line with the System.out.println the customized services should have been called, but unfortunatelly they are not. Breakpoints in the customized services have never been reached and the principal is still a string and not my customized user:
#ComponentScan("edu.kit.tm.cm.bamsg.bffweb.iamservice")
#RestController
#RequestMapping("/api/theses")
public class ThesisController {
#Autowired
private ThesisClient thesisClient;
#Autowired
private ThesisPersonLinker linker;
#Autowired
private ThesisPersonFilter filter;
#GetMapping
#PreAuthorize("hasRole('theses')")
public ResponseEntity<Collection<ThesisFrontendDTO>> findAllTheses() {
System.out.println(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
The extended user class looks like that:
package edu.kit.tm.cm.bamsg.bffweb.iamservice;
import org.springframework.security.core.userdetails.User;
public class SecurityUser extends User{
String firstName;
String name;
String password;
private static final long serialVersionUID = 1L;
public SecurityUser() {
super("user", "none", null);
firstName = "Rainer";
name = "Schlund";
password = "meins";
}
public String getRole(){
return "Student";
}
}
The code contains some simplifications for testing like SecurityPerson always returning the same person, but i think that should not be a problem.
To address the problem of "principal object in the security context stays a string instead of becoming the desired user object" if you have gone through the Principal object it has getCreditantial() method returning object only , considering security user is principal object it is not providing enough information to become correct principal object.
Please take a look on UserDetailsPrincipal class for principal implementation :
public class UserDetailsPrincipal extends org.springframework.security.core.userdetails.User implements UserDetails {
/**
*
*/
private static final long serialVersionUID = 1L;
private Member user;
List<GrantedAuthority> authorities;
public UserDetailsPrincipal(Member user, List<GrantedAuthority> authorities ) {
super(user.getLogin(),user.getEncrytedPassword(),authorities);
this.authorities = authorities;
this.user = user;
}
// #Override
// public Collection<? extends GrantedAuthority> getAuthorities() {
// return this.authorities;
// }
#Override
public String getPassword() {
return user.getEncrytedPassword();
}
#Override
public String getUsername() {
return user.getLogin();
}
#Override
public boolean isAccountNonExpired() {
return !user.getIsExpired();
}
#Override
public boolean isAccountNonLocked() {
return !user.getIsLocked() || user.getIsLocked() == null;
}
#Override
public boolean isCredentialsNonExpired() {
return !user.getIsExpired() || user.getIsExpired() == null;
}
#Override
public boolean isEnabled() {
return user.getActive() == 1;
}
}
also used customAuthProvider like this :
#Slf4j
#Component("customAuthProvider")
#Transactional(readOnly = true,propagation = Propagation.REQUIRES_NEW)
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
#Qualifier("userDetailsServiceAdapter")
private UserDetailsServiceAdapter userDetailsService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String login = authentication.getName();
String password = authentication.getCredentials().toString();
/* Member member = userRepository.findUserAccount(login); */
log.info("user for login inside custom auth service service : " + login);
if (!StringUtils.isEmpty(login) && !StringUtils.isEmpty(password)) {
try {
UserDetails userDetail = userDetailsService.loadUserByUsernameAndPassword(login, password);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetail,
userDetail.getPassword(), userDetail.getAuthorities());
token.setDetails(userDetail);
return token;
} catch (UsernameNotFoundException exception) {
return new UsernamePasswordAuthenticationToken(login, password, new ArrayList<>());
}
} else {
return new UsernamePasswordAuthenticationToken(login, password, new ArrayList<>());
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
If you want Spring security to use your Authentication provider you need to provide some entry point for providing auth credentials. Here is example of WebSecuritConfig class:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#ComponentScan
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String REALM = "realm";
#Autowired
private CustomAuthenticationProvider authProvider;
#Autowired
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.and()
// default with authentication
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and().httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthEntryPoint());
}
#Bean
BasicAuthenticationEntryPoint getBasicAuthEntryPoint() {
BasicAuthenticationEntryPoint basicAuth = new BasicAuthenticationEntryPoint();
basicAuth.setRealmName(REALM);
return basicAuth;
}
}
And you need to change SecurityUser constructor, because you cannot pass null authorities to super constructor:
public SecurityUser() {
super("user", "none", new ArrayList<>());
firstName = "Rainer";
name = "Schlund";
password = "meins";
}
When you provide Authentication provider, UserDetailsService is not used. So you need to use it in auth provider.
I seem to be missing something fundamental here:
#SpringBootApplication
public class Application {
User u = new User("USER", "PASSWORD",AuthorityUtils.createAuthorityList(
"ROLE_USER", "ROLE_ADMINISTRATOR"));
#Bean
public UserDetailsService userDetailsService() {
// returning a new User object works fine for every request
return username -> new User("USER", "PASSWORD",
AuthorityUtils.createAuthorityList(
"ROLE_USER", "ROLE_ADMINISTRATOR"));
// returning a previously created User object
// works only for the first request,
// subsequent requests get a 401 error
// return username -> u;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
This Spring Boot (v1.5.1) application using the spring-boot-starter-security dependency knows of only one user as of now. Also, all of its endpoints should only be accessible to this very user. In all of the working examples I have seen, the UserDetailsService always returns a new object of type User, just like in the above example.
But when it returns a previously created object (like the object named u above), only the first request gets authenticated. Why ?
A good complete example, with JPA as well, can be found here
This is just an example. Password still needs to be encrypted/secured.
Application.java
package demo;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.data.repository.CrudRepository;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
#Configuration
#ComponentScan
#EnableAutoConfiguration
#EnableGlobalMethodSecurity(securedEnabled = true)
public class Application extends WebMvcConfigurerAdapter {
#Controller
protected static class HomeController {
#RequestMapping("/")
#Secured("ROLE_ADMIN")
public String home(Map<String, Object> model) {
model.put("message", "Hello World");
model.put("title", "Hello Home");
model.put("date", new Date());
return "home";
}
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/access").setViewName("access");
}
#Bean
public ApplicationSecurity applicationSecurity() {
return new ApplicationSecurity();
}
#Order(Ordered.HIGHEST_PRECEDENCE)
#Configuration
protected static class AuthenticationSecurity extends
GlobalAuthenticationConfigurerAdapter {
#Autowired
private Users users;
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(users);
}
}
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class ApplicationSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.authorizeRequests().antMatchers("/login").permitAll().anyRequest()
.fullyAuthenticated().and().formLogin().loginPage("/login")
.failureUrl("/login?error").and().logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")).and()
.exceptionHandling().accessDeniedPage("/access?error");
// #formatter:on
}
}
}
#Service
class Users implements UserDetailsService {
private UserRepository repo;
#Autowired
public Users(UserRepository repo) {
this.repo = repo;
}
#Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = repo.findByName(username);
if (user == null) {
return null;
}
List<GrantedAuthority> auth = AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_USER");
if (username.equals("admin")) {
auth = AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_ADMIN");
}
String password = user.getPassword();
return new org.springframework.security.core.userdetails.User(username, password,
auth);
}
}
#Repository
interface UserRepository extends CrudRepository<User, Long> {
User findByName(String name);
}
#Entity
class User {
#GeneratedValue
#Id
private Long id;
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
I believe it is due to the Spring User object purging the password after authentication
from -> https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/userdetails/User.html
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.