loading...

Getting started with Spring Security - Authentication and Authorization

jhonifaber profile image Jonathan Faber Updated on ・4 min read

I've been reading a bit about spring security in the last days so I'm going to share some of the things I have learned.

Authentication: confirm users identity.
Authorization: what kind of permissions users have.

Begin by adding Spring Boot Starter Security dependency to pom.xml to enable basic authentication.
Create an endpoint in your controller and try to consume it, a login form provided by spring security will be shown.

login form

Have a look at the console when you run the application, you should see a an auto-generated password, username is by default 'user'.

console message

We can even configure our own user credentials in application.properties

spring.security.user.name=jhoni
spring.security.user.password=1234

To create a customised security class, we need to use @EnableWebSecurity and extends the class with @WebSecurityConfigurerAdapter so that we can redefine some of the methods provided by this class. Spring security also enforces you tu hash your passwords so that they are not saved in plain text. For the next example we can use PasswordEncoder, of course this should not be used in production, but for this example is fine. An alternative could be BCryptPasswordEncoder.

Authentication

In intelliJ cmd+N and have a look at the methods you can override. We are going to use configure method with AuthenticationManagerBuilder param. auth has different methods like like jdbcAuthentication, ldapAuthentication, userDetailsService... but we will use inMemoryAuthentication for this basic example. As its name says, user credentials are stored in memory.

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user1")
                .password("123")
                .roles("APPRENTICE")
                .and()
                .withUser("user2")
                .password("123")
                .roles("SENSEI");
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}


Authorization

Here we define which URL paths should be secured and which should not. We now use the configure method with HttpSecurity param. I'm giving access to any user who has role SENSEI.

@Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**").hasRole("SENSEI")
                .and().formLogin();

    }

If we try to log in with user2, access will be granted. The /** means allow access to current level and any inner levels. We can create different endpoints and give different restrictions as the following example. It's important to know that the most restrictive rules should be at the top.

@Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/sensei").hasRole("SENSEI")
                .antMatchers("/apprentice").hasRole("APPRENTICE")
                .antMatchers("/").permitAll()
                .and().formLogin();
    }

If you would like to have different roles try hasAnyRole(). We can also add filters, their goal is to intercept requests and each of them have their own responsibility (we will add one later)

.hasAnyRole("ROLE1", "ROLE2", "ROLE3")
.addFilterBefore()
.addFilterAfter()


UserDetailsService

We are going to configure Spring Security to depend on a UserDetailsService. It basically loads user-specific data. auth has a method called userDetailsService() that allows a customised authentication based on the UserDetailsService interface

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailService myUserDetailService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService);
    }

Let's create our own UserDetailService and UserDetail class. In MyUserDetailService we override loadUserByUsername method that will receive the username as a parameter and pass it to our MyUserDetails.

@Service
public class MyUserDetailService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new MyUserDetails(username);
    }
}

When we implement UserDetails interface, we can override several methods. I'm going to create a field username to get the username passed when user logs in. The rest of the methods will have harcoded values. getAuthorities returns the permissions granted to the user, in this case I will just add the SENSEI role.

@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyUserDetails implements UserDetails {

    private String username;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { 
        return List.of(new SimpleGrantedAuthority("ROLE_SENSEI"));
    }

    @Override
    public String getPassword() {
        return "pass";
    }

    @Override
    public String getUsername() {
        return username;
    }

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

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

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

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

The flow is as follows:

  1. WebSecurity class is loaded
  2. When user types username + password, the authentication filter intercepts the request and a UsernamePasswordAuthenticationToken object is created with the request credentials.
  3. loadUserByUsername receives username.
  4. A MyUserDetails object is created with whatever username is sent and everything else hardcoded(pretending to be a user that we have in our database) and it is compared with the UsernamePasswordAuthenticationToken object.
  5. If everything is okay 😃, otherwise ⛔️

Instead of hardcoding the creation of MyUserDetails in the service, you can inject the repository and retrieve the user details from database. Of course, the MyUserDetails class would have to be changed so that instead of receiving a string as we did before, it would receive a User.

@Service
@RequiredArgsConstructor
public class MyUserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
              .orElseThrow(() -> new UsernameNotFoundException("Username does not exist"));
        return new MyUserDetails(user);
    }
}

I'm going to continue by adding JWT in another post so this one doesn't get too long.

Posted on by:

Discussion

markdown guide
 

Hello! Thanks for the interesting reading. I have just stumbled upon your second part and ended up reading the first part as well. Good stuff!

If you ever decide to update this post and maybe run out of topics to continue with, I'd have two suggestions:

  1. The first code snippet showing that we can set up our own credentials would surely benefit from using actually a strong password, in place of "1234" - considering this being a security related post.

  2. In the future you could also write up some theory on why is hashing considered necessary as opposed to plaintext/encoding/encrypting when it comes to storing passwords. Such post could be then interlinked from the section where you mention encoding is not a good choice for production.

Thanks for the time you have put into writing this down, I will be looking forward to your next posts.

 

Thank you for your feedback Daniel, I'll keep it in mind for possible future posts :)