Reputation: 2818
In a Spring Boot application, I have an in-memory Spring Security setup. And it works as desired.
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("kevin").password("password1").roles("USER").and()
.withUser("diana").password("password2").roles("USER", "ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/foos").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/foos/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PATCH, "/foos/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/foos/**").hasRole("ADMIN")
.and()
.csrf().disable();
}
}
Now, I convert it to a database based approach with the following code.
@Entity
class Account {
enum Role {ROLE_USER, ROLE_ADMIN}
@Id
@GeneratedValue
private Long id;
private String userName;
// @JsonIgnore
private String password;
@ElementCollection(fetch = FetchType.EAGER)
Set<Role> roles = new HashSet<>();
...
}
The repository:
@RepositoryRestResource
interface AccountRepository extends CrudRepository<Account, Long>{
@PreAuthorize("hasRole('USER')")
Optional<Account> findByUserName(@Param("userName") String userName);
}
The UserDetailsService:
@Component
class MyUserDetailsService implements UserDetailsService {
private AccountRepository accountRepository;
MyUserDetailsService(AccountRepository accountRepository){
this.accountRepository = accountRepository;
}
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
Optional<Account> accountOptional = this.accountRepository.findByUserName(name);
if(!accountOptional.isPresent())
throw new UsernameNotFoundException(name);
Account account = accountOptional.get();
return new User(account.getUserName(), account.getPassword(),
AuthorityUtils.createAuthorityList(account.getRoles().stream().map(Account.Role::name).toArray(String[]::new)));
}
}
And the modification of the WebSecurityConfigurerAdapter configuration:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private MyUserDetailsService userDetailsService;
SecurityConfiguration(MyUserDetailsService userDetailsService){
this.userDetailsService = userDetailsService;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService); // <-- replacing the in-memory anthentication setup
}
...
}
When I send the same request with a pair of username and password as the basic authentication as for the in-memory version, I, however, get the 401 error:
{
"timestamp": 1489430818803,
"status": 401,
"error": "Unauthorized",
"message": "An Authentication object was not found in the SecurityContext",
"path": "/foos"
}
After reading through some related document and sample code, I can't see the cause of the error. What the error message says is that the user isn't in the Spring Security context. The line of AuthenticationManagerBuilder usage in the userDetailsService(userDetailsService) shall take care of setting up these users in the SecurityContext, isn't it?
The Spring Boot version is 1.4.3.RELEASE.
Upvotes: 6
Views: 21881
Reputation: 199
Your real problem lies in the UserDetailService.loadUseByUsername
- there you call:
Optional<Account> accountOptional = this.accountRepository.findByUserName(name);
and it is being done during te authorization process. But the accountRepository
method is annotated with PreAuthorize
- and can be called only from context
containing Authentication Object
.
Do a simple test to understand it:
@RepositoryRestResource
interface AccountRepository extends CrudRepository<Account, Long>{
@PreAuthorize("hasRole('USER')")
Page<Account> findAll(Pageable pageable);
Optional<Account> findByUserName(@Param("userName") String userName);
}
Then check, that you can query for all users correctly. The only problem is in the recursive nature of findByUserName
method.
Cheers!
Upvotes: 0
Reputation: 3444
One problem is, that your repository finder is annotated with @PreAuthorize("hasRole('USER')")
. This method is invoked before a request can be (fully) authenticated, so it can never pass the check and the exception is raised even if you pass the proper credentials.
Another point is, do you really want to expose your user account data as ReST resources? At the moment a request like curl http://localhost:8080/accounts
will return all your user data as JSON+HAL response, even to anonymous users.
It would return something like:
{
"_embedded" : {
"accounts" : [ {
"userName" : "admin",
"password" : "admin",
"roles" : [ "ROLE_USER", "ROLE_ADMIN" ],
"_links" : {
"self" : {
"href" : "http://localhost:8080/accounts/1"
},
"account" : {
"href" : "http://localhost:8080/accounts/1"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/accounts"
},
"profile" : {
"href" : "http://localhost:8080/profile/accounts"
},
"search" : {
"href" : "http://localhost:8080/accounts/search"
}
}
}
So @RepositoryRestResource
is dangerous here, using @Repository
is the way to go if you just want to use the database table to auth your requests.
For details see the documentation.
Even if you removed the passwords from the responses as your code suggests (with the commented @JsonIgnore
annotation in your Account
class) you would expose security related information like usernames and roles. Beside that it's a bad practice to "abuse" your Entity to shape your response object. Better use Projections instead.
If you want to keep your /accounts
endpoint you should also secure it by maybe at least adding .antMatchers("/accounts/**").hasRole("ADMIN")
to your config.
Upvotes: 3
Reputation: 2969
remove this preAuthorize
annotation from the crud repository
. this method is used when ever you are trying to login but with the preauthorize
annotation it expects a user to be logged in.
@PreAuthorize("hasRole('USER')")
Optional<Account> findByUserName(@Param("userName") String userName);
and i made some changes to the configure
method in websecurityconfigureadapter
you might need to allow access to the sign in and login urls in the configure method without login permission.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeUrls()
.antMatchers("/signup","/about").permitAll() // #4
.antMatchers("/admin/**").hasRole("ADMIN") // #6
.anyRequest().authenticated() // 7
.and()
.formLogin() // #8
.loginUrl("/login") // #9
.permitAll(); // #5
}
be careful when requiring admin permission to /foo/**
these kind of urls this tells that all urls begin with foo
are only allowed for admins.
Upvotes: 5