Lesson 1: Basic Authorization with OAuth2 (text-only)
1. Goal
In this lesson, we'll learn about the basic mechanisms we have to control the access to our Resources when using OAuth2.
2. Lesson Notes
The relevant module you need to import when you're starting with this lesson is: lsso-module3/basic-authorization-start
If you want have a look at the fully implemented lesson, as a reference, feel free to import: lsso-module3/basic-authorization-end
Additionally, before getting started, let’s import the Postman collection from our “master” branch, referring particularly to the “OAuth2 Beyond the Basics - The Resource Server/Basic Authorization with OAuth2” folder.
2.1. Authorization in the Resource Server
Simply put, the Resource Server has to determine if the authorities granted by the Resource Owner to the Client are enough to access a particular resource.
The authorities are usually represented by the scopes granted by the Authorization Server.
This is, naturally, the approach taken by Spring and even though it can be customized, here we’ll stick to this basic behavior.
Let’s explain how this is handled under the hood by the Spring Security framework.
2.2. Authentication Object
Simply put, the security framework bases most of its functionality around the Authentication interface and its implementations.
As you might know, classes implementing this interface have to implement a method that retrieves a collection of authorities. In the case of OAuth then, the framework creates the Authentication object.
The Authentication object is created based on the Access Token, either from the token itself (in the case of JWT), or from the information provided by the introspection endpoint (for Opaque Tokens), creating the collection of authorities from the scopes.
2.3. Authorities
Authorities are usually represented as simple strings.
The framework adds the SCOPE_ prefix to these authorities by default, to differentiate them from other authorities that can be configured in the service. For JWTs, these are resolved from the "scopes" claim value.
Of course, we can easily customize both the claim that is used to obtain the authorities as well as the prefix that is used when mapping these to authorities with application properties:
For simplicity's sake, here we've simply stuck with the default ones.
The bottom line of all this is that we’ll end up with a regular Authentication object, just like the one we obtain when we use other more traditional authentication methods such as username/password authentication.
This means that the mechanisms we can use to control access are the same as the ones used on these other common scenarios, namely:
- Authorizing HTTP Requests (HttpServletRequests) with the AuthorizationFilter; in practice, this usually means setting up a SecurityFilterChain bean to define access rules for the HttpSecurity configuration
- Using Expression-Based Access Control mainly in @Pre and @Post annotations like @PreAuthorize, @PreFilter, @PostAuthorize and @PostFilter
Let’s analyze some simple examples for these two approaches.
2.4. Authorizing HttpServletRequests
Let's look at the Java security configuration for the Resource Server present in the class ResourceSecurityConfig:
We’ve set up a SecurityFilterChain bean by declaring a filterChain() method using the HttpSecurity obtained from the context.
Note: the mechanism we’re using to indicate a MvcRequestMatcher should be used for these requests is not usually necessary. In our case, we need it just because the framework is setting up an H2 server and this generates some issues with the Security setup autoconfiguration.
The authorizeHttpRequests() method allows specifying the access rules for our endpoints.
So here, for example, we’re allowing access to the GET projects endpoint only if the Authentication instance contains the SCOPE_read authority.
This will be true if the Client was granted the read scope when requesting the Access Token.
2.5. Using Expression-Based Access Control
Now let’s have a look at how we can use expression-based access control.
In the same class ResourceSecurityConfig, we can see the service is already configured to process the @Pre and @Post annotations, as it contains the @EnableMethodSecurity annotation:
Next, let's head to the ProjectController.
Here, we'll use a common case example which is simply using the @PreAuthorize annotation to restrict access to a particular method.
For example, if we want to permit access to the findOne method here in the Controller only to Clients that have been granted the email scope, we can add the @PreAuthorize annotation, along with the hasAuthority rule with the expected scope, prefixed correspondingly:
It’s worth mentioning that here we’re using this annotation in the REST controller because, conceptually, it belongs to this layer.
But this can be used in any layer of our service; the request will be effectively executed until it invokes the annotated method and runs the proper validations.
Let's see these configurations in action now; we’ll add a breakpoint in the BearerTokenAccessDeniedHandler#handle method and start the Resource Server in debug mode.
We will also need the Authorization Server up and running, so let's boot it as well.
2.6. Access Control in Action - No Scope
We will run the REST calls present in the Postman collection folder for this lesson.
We’ll first execute the necessary requests consecutively to obtain a new access token:
- Extract Authorization Endpoint - extracts the authorization endpoint with scope equals to none
- Request Authorization Code - gets the Authorization Code from the Authorization Server
- Request Access Token - exchanges the Authorization Code retrieved above to get an Access Token
Here, we've obtained an Access Token, without specifying any scope.
Next, let's execute the call to get the Projects.
Since we've configured this endpoint to require the read scope, the execution will stop at the breakpoint we set.
What happened is that the framework threw an AccessDeniedException because the Authentication naturally didn’t comply with the access rules and therefore, this AccessDeniedHandler is invoked.
As we step further in the same method, we can see that the method adds an informative WWW-Authenticate header as indicated in the Bearer Token Usage specs:
and transforms the exception into the corresponding 403 - Forbidden response:
As we step out from this breakpoint and return to Postman, we can see that we got 403 Forbidden response and a helpful message in the WWW-Authenticate header indicating that this error response is due to insufficient scopes since the request requires higher privileges than the ones provided by the access token.
Next, let's see the same flow in action when requesting an Access Token with read scope.
2.7. Access Control in Action - read Scope
Let's repeat the process of obtaining an Access Token, but this time we'll request one with read scope.
And let's trigger the "Get All Projects" endpoint again.
This time, we're able to successfully retrieve the list of Projects because we have requested the Access Token with read scope.
Next, let's also try to access the "Project by Id" endpoint that is protected by the @PreAuthorize annotation requesting the email scope.
As we send this request, the breakpoint will be triggered again, as the framework throws an AccessDeniedException and the handler proceeds in the exact same way.
We resume back to the Postman and we can see that we have got the same 403 - Forbidden response with the corresponding WWW-Authenticate header.
2.8. Access Control in Action - email and read Scope
Next, let's see the same flow again, but this time with requesting Access Token with both email and read scope.
With this token, we'll execute the endpoint for Request Project by Id, and we can see that the request is invoked successfully now.
2.9. Boot 2 Notes
You’ll notice the core Spring Security configurations were somewhat different for the Spring Boot 2 stack.
Among other things, with the old approach, we would make our ResourceSecurityConfig extend WebSecurityConfigurerAdapter and override its configure(HttpSecurity) method to define the main security configuration.
With older versions, we also had the possibility of using the authorizeRequests method instead of authorizeHttpRequests to rely on the now deprecated FilterSecurityInterceptor authorization framework instead of on the AuthorizationFilter.
Finally, we would have to use @EnableGlobalMethodSecurity instead of @EnableMethodSecurity.