FC
FACADE
CODE
Lesson 7 of 28
FC
FACADE
CODE

Learn Spring Security

Contents

01. Bootstrap The Application
1. Introduction
2. Install Spring Security
3. Enable Basic Auth
4. Authentication with AppUser
5. Password Encoder
02. Web Layer Security - RBAC
6. Permit Public APIs
7. Role Based Authorization
8. Disable CSRF
9. Current Authenticated User
03. Web Layer Security - PBAC
10. Permission Based Authorization
11. Define Permissions
12. Assign Permissions
13. Remove Role Based Access
04. Service Layer Security
14. PreAuthorize
15. PostAuthorize
16. Authorize Using Spring Beans
05. Domain Object Instance Security
17. Domain Object Instance Security
18. PermissionEvaluator Interface
19. PermissionEvaluator Strategy
20. DB Backed UserDetailsService
06. Token Based Authentication
21. Basic Authentication Revisited
22. Generate Token
23. Persist Token
24. Verify Token
25. Invalidate Token
07. Token Based Authentication and Authorization
26. JSON Web Token
27. Generate JWT
28. Verify JWT

Role Based Authorization

So far we are allowed to access any APIs once authenticated successfully. But there are APIs which are supposed to be accessed only by users having specific roles. In the last two chapters, we accessed one of the ADMIN APIs - ListStudents - using Bob who does not have ADMIN role. In this chapter we will fix it by applying role-based authorization to secure the below APIs.

APIAuthorization
List StudentsAdmin can only view the list of students
List InstructorsAdmin can only view the list of instructors
Create CourseInstructors can only create a new course

Apply role-based restrictions with hasRole()

First let's add the above API urls in the SecurityConstants class like below:


public static final String API_LIST_STUDENTS = "/api/v1/users/students";  
public static final String API_LIST_INSTRUCTORS = "/api/v1/users/instructors";
public static final String API_CREATE_COURSES = "/api/v1/courses";    

We can apply customised restrictions to specific urls using antMatchers() in HttpSecurity configuration. Here we want to apply role-based restrictions which can be achieved using hasRole() with appropriate RoleEnum instance for the above urls as below:


@Configuration
public class ApiSecurityConfig {
    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(auth -> auth
                .antMatchers(GET, PUBLIC_API_LIST).permitAll()
                .antMatchers(API_LIST_STUDENTS, API_LIST_INSTRUCTORS).hasRole(ADMIN.name())
                .antMatchers(POST, API_CREATE_COURSES).hasRole(INSTRUCTOR.name())
                .anyRequest().authenticated()
            )
            .httpBasic();
        return http.build();
    }
}

As ListCourses and CreateCourse endpoints are the same but with different HttpMethod, we have to specify HttpMethod.POST in the antMatchers for CreateCourse API.

Restart the application and send a GET request to the ListStudents API using Admin user. Though we have secured this API for ADMIN role, we will get 403 Forbidden error. This is because Spring Security has no idea who has the ADMIN role, as we have not yet mapped the roles for any users while creating the list of UserDetails object.

403 Forbidden error while accessing ListStudents API as ADMIN user
403 Forbidden error while accessing ListStudents API as ADMIN user

Update UserDetails with Roles

We can use roles() method in UserBuilder class to map the roles from AppUser. It is an overloaded method, here we chose the one which accepts String[]


public List<UserDetails> getAllUserDetails() {
    return appUserRepository.findAll()
        .stream()
        .map(appUser -> User.builder()
            .username(appUser.getUsername())
            .password(appUser.getPassword())
            .authorities(Collections.EMPTY_SET)
            .roles(this.getRoles(appUser.getRoles()))
            .build()
        )
        .collect(Collectors.toList());
}  

private String[] getRoles(Set<AppRole> roles) {  
    return roles.stream()
        .map(role -> role.getName().name())
        .collect(Collectors.toSet())
        .toArray(new String[0]);
}    

If we access the ListStudents API using Admin user after restarting the application, we can see the list of students as response. But if we access the same API using Bob who does not have ADMIN role, we will get 403 Forbidden error now.

List of students for ADMIN user after mapping Roles
List of students for ADMIN user after mapping Roles
403 Forbidden for users other then ADMIN after mapping Roles
403 Forbidden for users other then ADMIN after mapping Roles

Note

Both roles() and authorities() populates the authorities in User class, as it has no specific attribute to hold the roles information. But roles() automatically prefixes each entry with ROLE_. This means builder.roles("ADMIN") is equivalent to builder.authorities("ROLE_ADMIN").

Therefore we should never call them both on UserBuilder because the latter will always override the authorities set by the former. Here we are intentionally calling roles() after authorities() in order to override the Collections.EMPTY_SET.

There will be scenarios where you would want to set both roles and authorities. In such cases it is always preferable to pass the roles as argument to authorities() method, where each role must be explicitly prefixed with ROLE_ by yourself. We will see this in action in the upcoming chapters.