vic
vic

Reputation: 2818

"An Authentication object was not found in the SecurityContext" - UserDetailsService Setup in Spring Boot

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

Answers (3)

supertramp
supertramp

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

Kevin Peters
Kevin Peters

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

Priyamal
Priyamal

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

Related Questions