I'm struggling to understand exactly what Spring Security/Spring Boot does under the hood and what is up to me to implement to get form-based authentication up and running (https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html).
For reference, I'm building a web-app and am currently working on the backend, which is developed with Spring Boot. The data is stored in a nonrelational database. I haven't built the frontend yet and I use Postman to test my API's.
I followed this (https://www.youtube.com/watch?v=her_7pa0vrg) and this tutorial (https://www.marcobehler.com/guides/spring-security) to get a sense of how to use Spring Security, given the gargantuan size and dispersive nature of the official docs (https://docs.spring.io/spring-security/reference/features/index.html). Both tutorials use a deprecated class, but I chose to provisionally use it to make it easier to build a functional app - will change it later.
What I managed to understand is that Spring Security filters client requests with a series of methods (contained in a series of Filter classes) and what we do is basically declare how these filters should operate, rather than code them ourselves. This declaration is done through a Java configuration class, which establishes which resources are publically available, which are hidden behind an authentication wall and which need particular permissions, in addition to being authenticated, to be accessed. Furthermore, this configuration file is also where we declare what authentication methods we allow (with form-based authentication falling in this category).
The following is my (edited to ease understanding) configuration file:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final AppUserDetailsService appUserService;
@Autowired
public SecurityConfiguration(PasswordEncoder passwordEncoder, AppUserDetailsService appUserService){
this.passwordEncoder = passwordEncoder;
this.appUserService = appUserService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
// ... other configuration to protect resources
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll()
.logoutSuccessUrl("/login")
.and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider());
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder);
provider.setUserDetailsService(appUserService);
return provider;
}
}
where passwordEncoder and appUserService are two Components, which are declared in their own classes, and should respectively be used to encode user passwords and retrieve user authentication details (which go in a class implementing the interface UserDetails, see https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/userdetails/UserDetails.html and ) from the database.
Now, according to what I understand of the official docs (https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html), the DaoAuthenticationProvider I build in the configuration class should take care of authentication matters. I do not need to define anything else in my code than what I mentioned above. Is that correct? This did not seem to work today, but I might have gotten something wrong in my Postman requests - thank you in advance!
EDIT (refer to my second batch of comments under @Toerktumlare 's answer):
My configuration file now looks like this (omitted UserDetailsService and PasswordEncrypter):
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
.antMatchers("/").permitAll()
.antMatchers("/register/**").permitAll()
.antMatchers("someUrl/{username}").access("@userSecurity.isSameUser(authentication, #username)")
.antMatchers("/someOtherUrl/{username}/**").access("@userSecurity.isSameUser(authentication, #username)")
)
.formLogin((formLogin) ->
formLogin.loginPage("/login")
.permitAll()
)
.logout((logout) ->
logout.deleteCookies("remove")
.invalidateHttpSession(false)
.logoutSuccessUrl("/login")
);
return http.build();
}
}
and I get this compile error: "The method access(AuthorizationManager) in the type AuthorizeHttpRequestsConfigurer.AuthorizedUrl is not applicable for the arguments (String)", which I get. What I don't get is that the official docs do seem to use this .access() method with a String argument (https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html#el-access-web-beans). I guess they're using a different .access() method, but I can't see how.
CodePudding user response:
You are doing the same mistake as most people that start with spring security.
You don't read the architecture chapter and the Authentication Architecture chapter of the spring security documentation, and instead you google and follow an outdated tutorial.
You link to https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html which clearly shows no usage of DaoAuthenticationProvider
but you still have implemented one.
So lets review your code:
WebSecurityConfigurerAdapter
is deprecated, and should not be used so remove it. All you need to do now is to build a security configuration and return it to the context as a bean.
Then you have added both FormLogin
and httpBasic
these are two completely different ways of authenticating. I hope you are aware of it.
Basic is documented here. Form login is documented here.
Lastly, if you provide a UserDetailsService
and a PasswordEncoder
but there is no need to configure your own AuthenticationProvider
. Spring security will understand by itself that you want a DaoAuthenticationProvider
and build it for you as long as you provide them as @Bean
s to the context.
So you can remove most of the rest of your code.
So this is what you final code could look like:
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.formLogin((formLogin) ->
formLogin.loginPage("/login")
.permittAll();
)
.logout((logout) ->
logout.deleteCookies("remove")
.invalidateHttpSession(false)
.logoutSuccessUrl("/login")
);
return http.build();
}
@Bean
public CustomUserDetailsService customUserDetailsService() {
return new AppUserDetailsService();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new SCryptPasswordEncoder();
}
}
you honestly dont need more than that.
CodePudding user response:
I finally managed to get the whole thing to work. Please note I haven't tested this extensively yet, so I'll get back to this once I do and update this answer if necessary.
As to the first part of my question (which originally was the whole question: "What do I need to implement myself?"), the only thing I needed to implement myself was the UserDetailsService interface: this is the class that is responsible for retrieving user details (obviously actual access to the database can be delegated to another class, like Spring Repository's if using Spring Data). Spring Security does provide an implementation in case one has an in-memory database or a relational database, but this wasn't my case. It absolutely makes sense that users would have to write their own implementation, as Spring doesn't force any specific way of storing user credentials on you and therefore has no way of automatically knowing where to retrieve said credentials.
I also implemented a SecurityFilterChain bean (like @Toerktumlare did in his answer). When building your own application, you will most likely need to do this as well, but you might get form-based authentication to work even without declaring a SecurityFilterChain bean*. I haven't tested this, but it is definitely mentioned somewhere in the docs (will link this later if I find it and have the time).
Once your custom UserDetailsService class is written and configured as a bean (which I did with a @Service annotation) and thus made available for Spring to instantiate it on its own, you're all done. You do not need to declare a PasswordEncoder as a bean, because Spring already has its own and will use that one. Once you have your UserDetailsService bean, Spring will automatically add that to its AuthenticationProvider (no need to implement this!) and use it for authentication.
Now, getting to the second part of my question ("How do I get the .access() method to work once I switch to http.authorizeHttpRequests()?"), I eventually had to switch to implementing the functional interface requested by the .access method in this new setup. It now works as expected. Will share the implementation later in case anyone needs a reference, but I'm still baffled by the snippet in the Spring docs where they use that method with a String argument ( N.B.: the link will redirect you to the section, I'm talking about the second snippet in that section (the one titled "Example 1. Refer to method").): either I'm missing something, or it's a mistake on their part. Will also investigate this further if I have the time.
*N.B.: Technically, I think form-based authentication will work even if you do not define your own UserServiceDetails implementation: Spring will use one of its own implementations and generate a user and password for you. It will also print a message saying that for production you'll want to use your own UserDetailsService, so I just mention this for completeness.