on
Implementing OAuth2 in Spring: using scopes (part 2)
We have seen in the previous post basic OAuth2 concepts and how to implement and to perform different grants in Spring. In this post, we are going to go through another important concept of OAuth2: scopes.
OAuth scopes:
Securing access to an application is usually carried out in two steps: authentication and authorization. To understand these two concept, suppose you work in a top secret governement building. Before starting, you were given a card that gives you access to building. The OAuth token can be seen as the card that allows you access. Once you are inside, you decide to go to the third floor to meet one of your colleagues, after trying to use your card to open the third floor’s door, you get a beep telling you that you are not authorized. In OAuth, scopes are a way to define which resources can be accessed by the token and which cannot. Scopes allow access control, and can be seen as an equivalent to user roles in traditional authentication.
Implementation:
To demonstrate scopes, we are going to use the example from part 1.
In the resource server’s controller, we have the following endpoints :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController("/")
public class ResourceController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/foo")
public String foo(){
return "foo";
}
@PostMapping("/bar")
public String bar(){
return "bar";
}
@DeleteMapping("/test")
public String test(){
return "test";
}
}
the first step is to configure the authorization server with the desired scopes:
1
2
3
4
5
6
7
8
9
10
clients.inMemory().withClient("my-trusted-client")
.authorizedGrantTypes("password",
"refresh_token", "implicit", "client_credentials", "authorization_code")
.authorities("CLIENT")
.scopes("read", "write", "trust")
.accessTokenValiditySeconds(60)
.redirectUris("http://localhost:8081/test.html")
.resourceIds("resource")
.secret("mysecret");
To enable scopes checking in the resource server, we have two options: using the security configuration, or using method security.
- using security configuration:
1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.GET,"/hello").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.GET,"/foo").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST,"/bar").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE,"/test").access("#oauth2.hasScope('trust')")
.anyRequest().authenticated().
and().csrf().disable();
}
- using method security:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@PreAuthorize("#oauth2.hasScope('read')")
@GetMapping("/hello")
public String hello(){
return "hello";
}
@PreAuthorize("#oauth2.hasScope('read')")
@GetMapping("/foo")
public String foo(){
return "foo";
}
@PreAuthorize("#oauth2.hasScope('write')")
@PostMapping("/bar")
public String bar(){
return "bar";
}
@PreAuthorize("#oauth2.hasScope('trust')")
@DeleteMapping("/test")
public String test(){
return "test";
}
additionally we need to add @EnableGlobalMethodSecurity(prePostEnabled = true)
to any class that can be picked up by Spring (@Configuration
, @Service
, …etc). In our example, we have added it to the ResourceSecurityConfiguration class. The prePostEnabled = true
tells Spring to enalble pre and post anotations like @PreAuthorize
, @PostFilter
, etc…
For those wondering about expressions like #oauth2.hasScope('trust')
, they are built using the Spring Expression Language(SpEL).
Scopes in action:
By default, if the scopes are not present in the token request, Spring assumes that the token has all the configured scopes. Let’s first request a token with read
scope :
curl -X POST --user my-trusted-client:mysecret localhost:8081/oauth/token -d 'grant_type=client_credentials&client_id=my-trusted-client&scope=read' -H "Accept: application/json"
Response:
1
2
3
4
5
6
7
{
"access_token": "acadbb31-f126-411d-ae5b-6a278cee2ed6",
"token_type": "bearer",
"expires_in": 60,
"scope": "read"
}
Now, we can use the token to access the endpoints with read
scope access:
curl -XGET localhost:8989/hello -H "Authorization: Bearer acadbb31-f126-411d-ae5b-6a278cee2ed6"
hello
curl -XGET localhost:8989/foo -H "Authorization: Bearer acadbb31-f126-411d-ae5b-6a278cee2ed6"
foo
Now, let’s try to use this token on an endpoint that accepts write
scope only:
curl -XPOST localhost:8989/bar -H "Authorization: Bearer acadbb31-f126-411d-ae5b-6a278cee2ed6"
Response:
1
2
3
4
{
"error": "access_denied",
"error_description": "Access is denied"
}
The access is rejected because the token does not have the required scope. Let’s try to obtain a new token with write
scope and try again:
curl -X POST --user my-trusted-client:mysecret localhost:8081/oauth/token -d 'grant_type=client_credentials&client_id=my-trusted-client&scope=write' -H "Accept: application/json"
Response:
1
2
3
4
5
6
{
"access_token": "bf0fa83a-23bd-4633-ac6c-a06f40d53e5f",
"token_type": "bearer",
"expires_in": 3599,
"scope": "write"
}
curl -XPOST localhost:8989/bar -H "Authorization: Bearer bf0fa83a-23bd-4633-ac6c-a06f40d53e5f"
bar
Wrap up :
Scopes are an important aspect of OAuth as the tokens does not carry informations about their user or requester. Scopes allow to limit access to resources for better access control and security. In the next post we will see how to integrate external OAuth providers like Google, and Facebook into the flow.