3

I want to secure a Spring Boot REST API which is consumed by an Angular app. No user and no other app should have access to this API.

I have no control over this angular app, but I have been told that the actual authentication of the users is being done on the Angular app, which sets and sends a JWT after authenticating it's users. Since the angular app and my API are deployed on the same domain, I am indeed able to access the JWT which is sent as a cookie with the request. I am also able to successfully extract the username from it.

I must now authenticate and authorize the request using the JWT username and the roles associated with it to restrict access to certain endpoints. In order to do this, I have been given access to several database views which contain the required information, however I don't get any sort of password information. I have no control over the views or anything else in that database. Obviously, the user is unauthorized if the username from the token is not found in these views. Same goes for the roles.

My question is how do I configure WebSecurityConfigurerAdapter and UserDetails?

Whatever I try, I either instantly get 401 unauthorized or SecurityContextHolder.getContext().getAuthentication().getPrincipal() returns "anonymousUser" instead of the UserDetails object when called from the controller code, even though it is being set correctly.

This is just a guess, but maybe it returns 401 because the overriden getPassword() method in my UserDetails class returns an empty string (return "";), but what should it return since I don't, at any point, have access to anything resembling a user's password?

WebSecurityConfigurerAdapter:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity.cors().and().csrf().disable()
                     // I will eventually filter multiple endpoints by roles
                     // but at this point I just want it to work 
                    .authorizeRequests().anyRequest().authenticated().and() // I am guessing it fails here because of the password issue?
                    .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

UserDetails:

public class MyUserDetails implements UserDetails
{
    private static final long serialVersionUID = 7371936004280236913L;

    private final MyUserDto MyUser;

    public MyUserDetails(MyUserDto MyUser)
    {
        this.MyUser = MyUser;
    }

    public MyUserDto getMyUser()
    { return MyUser; }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        List<MyUserAuthority> authorities = new ArrayList<MyUserAuthority>();
        for(MyRoleDto userRole : MyUser.getRoles())
            authorities.add(new MyUserAuthority(userRole));
        return authorities;
    }

    @Override
    public String getPassword()
    { return ""; } // What should this return?

    @Override
    public String getUsername()
    { return this.MyUser.getUserId(); }

    @Override
    public boolean isAccountNonExpired()
    { return true; }

    @Override
    public boolean isAccountNonLocked()
    { return true; }

    @Override
    public boolean isCredentialsNonExpired()
    { return true; }

    @Override
    public boolean isEnabled()
    { return true; }
}
user1969903
  • 810
  • 13
  • 26
  • So much answers. So many help. – user1969903 Sep 26 '20 at 06:47
  • Please, where do your ```Jwt*``` classes come from? You do not need a password here. – jccampanero Oct 02 '20 at 11:13
  • Thanks for your time. I created them. The JwtRequestFilter extends OncePerRequestFilter, and the other two implement the classes with the same name but without the "Jwt": AuthenticationEntryPoint and UserDetailsService respectively. If you think they are useful, I can add them to the question. – user1969903 Oct 02 '20 at 11:21
  • Yes please, I think it would be helpful. – jccampanero Oct 02 '20 at 11:24

3 Answers3

2

Well, since there are so many answers, as befits a programming language and framework as popular and as well documented as java/spring boot, I've decided to just use the username as the password.

@Override
public String getPassword()
{
    // I am using BCryptPasswordEncoder but you may 
    // have to change to whatever you're using.
    return new BCryptPasswordEncoder().encode(this.getUsername()); 
}

Keep in mind that you have to encode it, as the password provider expects it to be encoded. Otherwise, the provider will say that the password does not match.

I have no idea if this is "best practice" but it works.

E_net4
  • 27,810
  • 13
  • 101
  • 139
user1969903
  • 810
  • 13
  • 26
2

Your problem have nothing to do with setting or not the user password.

As you indicated, the user has been authenticated in the frontend, possibly by interacting with an OAuth2 client. As a result, the OAuth2 client will provide your frontend a JWT token.

In every HTTP interaction, the frontend will send this JWT token to your backend.

The backend have two responsibilities:

  • On one hand, verify that the received token is valid (expiration, with a valid signature if it is digitally signed, from a trusted origin, with the right audience, etcetera).
  • On the other hand, you need to identify the user which corresponds to the received JWT token.

For this second responsibility you need some attribute that uniquely identifies at the user among all those defined in the JWT token.

This attribute must be the one used to fetch the user information from your database.

In the case of Spring Security, you need to return some implementation of the UserDetails interface from the loadByUserName method in your UserDetailsService implementation.

If, as you indicated, you are able to obtain an username, just create an UserDetailsService implementation that use that information to query your views in the database to fetch the user information, and define some class that implements UserDetails, and do not pay attention on the password, you do not need one, you do not need to authenticate the user again.

This UserDetailsService implementation will be of relevance in your Spring Security configuration.

Your user is preauthenticated and you need preauthentication stuff. Include the following in your WebSecurityConfig class:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> wrapper = new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(userDetailsService);
  PreAuthenticatedAuthenticationProvider preAuthProvider = new PreAuthenticatedAuthenticationProvider();
  preAuthProvider.setPreAuthenticatedUserDetailsService(wrapper);
  auth.authenticationProvider(preAuthProvider);
}

If your JwtRequestFilter is a OncePerRequestFilter as you indicated, once you validate the JWT token and you obtain the unique user identifier, you need to provide this information to the Spring Security AuthenticationManager in order to obtain a reference to the appropriate UserDetails implementation, and store this reference in the SecurityContext, something like:

PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(username, jwtToken);
Authentication authResult = authenticationManager.authenticate(authRequest); // Will call your UserDetailsService
SecurityContextHolder.getContext().setAuthentication(authResult);

You can modify this filter and make it extend AbstractPreAuthenticatedProcessingFilter. This will simplify the authentication workflow. You only need to return the username derived from the JWT token and Spring Security will do the rest:

@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
  String jwtToken = resolveToken(httpServletRequest);
  String username = getUsernameFromToken(token); // Take the idea
  return username;
}

@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
  return "-";
}

You do not need to configure any kind of PasswordEncoder. The rest of your configuration looks fine.

jccampanero
  • 50,989
  • 3
  • 20
  • 49
1

I think user1969903 has answered the solution to the problem. However, I would recommend you to add a pinch of salt to the solution.

@Override
public String getPassword()
{
    // I am using BCryptPasswordEncoder but you may 
    // have to change to whatever you're using.
    return new BCryptPasswordEncoder().encode(this.getUsername() + someString); 
}

someString can be something you can have it configured on your environment ot may be something part of user profile data, like last name, email, zipcode, etc. This way, if someone happens to get the token, it is not easy for them to decode it.

reflexdemon
  • 836
  • 8
  • 21
  • You don't need to explicitly add salt, the `BCryptPasswordEncoder` will add salt for you., see https://stackoverflow.com/questions/25844419/why-bcryptpasswordencoder-from-spring-generate-different-outputs-for-same-input – Qwerky Oct 06 '20 at 11:43
  • Gotcha. I almost forgot that – reflexdemon Oct 06 '20 at 11:46