FC
FACADE
CODE
Lesson 18 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

PermissionEvaluator Interface

We said PermissionEvaluator interface is intended to be the bridge between SpEL and Spring Security's ACL system, but it has no hard dependencies to use only ACL module. So we can swap the Spring Security's ACL implementation with our own implementation to define ABAC rules.

Let's implement the PermissionEvaluator interface for Course entity and override the two hasPermission() methods. We will find the targetDomainObject inside the method which receives targetId and targetType, and then call the other hasPermission() method which receives the targetDomainObject.


@Component
public class CoursePermissionEvaluator implements PermissionEvaluator {
    // Autowired repositories omitted for brevity

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        if (targetDomainObject != null) {
            Course course = (Course) targetDomainObject;
            PermissionEnum permissionEnum = PermissionEnum.valueOf((String) permission);

            switch(permissionEnum) {
                case UPDATE_COURSE:
                    return this.isCreatedBy(authentication, course);
                case PLAY_COURSE:
                    return this.isEnrolledStudent(authentication, course.getId()) || 
                            this.isCreatedBy(authentication, course);
            }
        }
        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        if (targetId != null) {
            Long courseId = (Long) targetId;
            Optional<Course> course = courseRepository.findById(courseId);
            if (course.isPresent()) {
                return this.hasPermission(authentication, course.get(), permission);
            }
        }
        return false;
    }
}

By this way we can have the authorization check in only one method, but still we can use any of the two hasPermission() expressions based on the availability of either courseId or Course object defined in the service method signature.

Remember we defined the authorization logic for PLAY_COURSE and UPDATE_COURSE permissions in the previous chapter. We can use them inside CoursePermissionEvaluator like below:


// PLAY_COURSE permission - Check if the course is enrolled by the authenticated user
private boolean isEnrolledStudent(Authentication authentication, Long courseId) {
    Optional<AppUser> student = appUserRepository.findByUsername(authentication.getName());
    if (student.isPresent()) {
        return student.get()
                .getEnrolledCourses()
                .stream()
                .anyMatch(course -> course.getId().equals(courseId));
    }
    return false;
}

// UPDATE_COURSE permission - Check if the course is created by the authenticated user
private boolean isCreatedBy(Authentication authentication, Course course) {
    return course.getCreatedBy()
            .getUsername()
            .equalsIgnoreCase(authentication.getName());
}

Register CoursePermissionEvaluator

Spring Security will not be aware of our PermissionEvaluator implementation unless we register it with DefaultMethodSecurityExpressionHandler. Let's do this in a Config class extending GlobalMethodSecurityConfiguration and overriding it's createExpressionHandler() method. Also it will be more appropriate to move @EnableGobalMethodSecurity to this new SecurityConfig.


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
    @Autowired
    private CoursePermissionEvaluator coursePermissionEvaluator;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler
                = new DefaultMethodSecurityExpressionHandler();
        defaultMethodSecurityExpressionHandler.setPermissionEvaluator(coursePermissionEvaluator);
        return defaultMethodSecurityExpressionHandler;
    }
}

Sending a PlayCourse or an UpdateCourse API request will now go through the CoursePermissionEvaluator to perform authorization check before reaching it's respective Service method. Let's send a PlayCourse API request as Bob who is a Student for one of his enrolled courses, and we will get the course details like below:

Authorized request to PlayCourse API as Bob for one of his enrolled courses
Authorized request to PlayCourse API as Bob for one of his enrolled courses

Similarly if we send the same request for any other courses not enrolled by him we can expect to get 403 Forbidden error like below:

Unauthorized access request as Bob to play one of the coursed not enrolled by him
Unauthorized access request as Bob to play one of the coursed not enrolled by him

Exercise

Remember we have an authorization check isInstructor() implemented in ServiceSecurity for ViewProfile service. In order to be consistent I encourage you to create another PermissionEvaluator implementation for AppUser entity and move the authorization check to here. We need to change the SpEL expression from hasAuthority() to hasPermission() in Authority constants class for VIEW_PROFILE permission. And we can remove ServiceSecurity class as we no longer need to dump all sorts of authorization checks there.

We know that Spring Security will not be aware of the new AppUserPermissionEvaluator unless we register it. But we can register only one implementation with DefaultMethodSecurityExpressionHandler. Let's see how we can use multiple PermissionEvaluator implementations in the next chapter.