Using Spring Cloud Gateway With OAuth 2.0 Patterns - Baeldung
Using Spring Cloud Gateway With OAuth 2.0 Patterns - Baeldung
Spring Cloud
Written by:
Philippe Sevestre Spring Security Security basics for a REST API
OAuth
Spring Cloud Gateway Get access to the video
I just announced the new Learn Spring Security course, including the full
material focused on the new OAuth2 stack in Spring Security 5:
1. Introduction
Spring Cloud Gateway is a library that allows us to quickly create lightweight API gateways based on Spring Boot,
which we’ve already covered in earlier articles.
This time, we’ll show how to quickly implement OAuth 2.0 patterns on top of it.
The OAuth 2.0 standard is a well-established standard used all over the internet as a security mechanism by which
users and applications can securely access resources.
Although it’s beyond the scope of this article to describe this standard in detail, let's start with a quick recap of a few
key terms:
Resource: Any kind of information that can only be retrieved by authorized clients
Client: an application that consumes a resource, usually through a REST API
Resource Server: A service that is responsible for serving a resource to authorized clients
Resource Owner: entity (human or application) that owns a resource and, ultimately, is responsible for granting
access to it to a client
Token: a piece of information got by a client and sent to a resource server as part of the request to authenticate
it
Identity Provider (IdP): Validates user credentials and issues access tokens to clients.
Authentication Flow: Sequence of steps a client must go through to get a valid token.
For a comprehensive description of the standard, a good starting point is Auth0’s documentation on this topic.
In this scenario, any unauthenticated incoming request will initiate an authorization code ow. Once the token is
acquired by the gateway, it is then used when sending requests to a backend service:
A good example of this pattern in action is a social network feed aggregator application: for each supported network,
the gateway would act as an OAuth 2.0 client.
As a result, the frontend – usually a SPA application built with Angular, React, or similar UI frameworks – can
seamlessly access data on those networks on behalf of the end-user. Even more important: it can do so without the
user ever revealing their credentials to the aggregator.
Here, the Gateway acts as a gatekeeper, enforcing that every request has a valid access token before sending it
to a backend service. Moreover, it can also check if the token has the proper permissions to access a given resource
based on the associated scopes:
It is important to notice that this kind of permission check mainly operates at a coarse level. Fine-grained access
control (e.g., object/ eld-level permissions) are usually implemented at the backend using domain logic.
One thing to consider in this pattern is how backend services authenticate and authorize any forwarded request.
There are two main cases:
Token propagation: API Gateway forwards the received token to the backend as-is
Token replacement: API Gateway replaces the incoming token with another one before sending the request.
In this tutorial, we’ll cover just the token propagation case, as it is the most common scenario. The second one is
also possible but requires additional setup and coding that would distract us from the main points we want to show
here.
To show how to use Spring Gateway with the OAuth patterns we’ve described so far, let’s build a sample project that
exposes a single endpoint: /quotes/{symbol}. Access to this endpoint requires a valid access token issued by the
con gured identity provider.
In our case, we’ll use the embedded Keycloak identity provider. The only required changes are the addition of a new
client application and a few users for testing.
To make things a little more interesting, our backend service will return a di erent quote price depending on the user
associated with a request. Users that have the gold role get a lower price, while everybody else gets the regular price
(life is unfair, after all ;^)).
We’ll front this service with Spring Cloud Gateway and, by changing just a few lines of con guration, we’ll be able to
switch its role from an OAuth client to a resource server.
5. Project Setup
The Embedded Keycloak we’ll use for this tutorial is just a regular SpringBoot application that we can clone from
GitHub and build with Maven:
Note: This project currently targets Java 13+ but also builds and runs ne with Java 11. We only have to add -
Djava.version=11 to Maven's command.
Next, we’ll replace the src/main/resources/baeldung-domain.json for this one. The modi ed version has the same
con gurations available in the original one plus an additional client application (quotes-client), two user groups
(golden_ and silver_customers), and two roles (gold and silver).
We can now start the server using the spring-boot:run maven plugin:
$ mvn spring-boot:run
... many, many log messages omitted
2022-01-16 10:23:20.318
INFO 8108 --- [ main] c.baeldung.auth.AuthorizationServerApp : Started AuthorizationServerApp
in 23.815 seconds (JVM running for 24.488)
2022-01-16 10:23:20.334
INFO 8108 --- [ main] c.baeldung.auth.AuthorizationServerApp : Embedded Keycloak started:
http://localhost:8083/auth to use keycloak
To nish the IdP setup, let’s add a couple of users. The rst one will be Maxwell Smart, a member of the
golden_customer group. The second will be John Snow, which we won’t add to any group.
Using the provided con guration, members of the golden_customers group will automatically assume the gold
role.
The quotes backend requires the regular Spring Boot Reactive MVC dependencies, plus the resource server starter
dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.6.2</version>
</dependency>
Notice that we've intentionally omitted the dependency's version. This is the recommended practice when using
SpringBoot's parent POM or the corresponding BOM in the dependency management section.
In the main application class, we must enable web ux security with the @EnableWebFluxSecurity:
@SpringBootApplication
@EnableWebFluxSecurity
public class QuotesApplication {
public static void main(String[] args) {
SpringApplication.run(QuotesApplication.class);
}
}
The endpoint implementation uses the provided BearerAuthenticationToken to check if the current user has or not
the gold role:
@RestController
public class QuoteApi {
private static final GrantedAuthority GOLD_CUSTOMER = new SimpleGrantedAuthority("gold");
@GetMapping("/quotes/{symbol}")
public Mono<Quote> getQuote(@PathVariable("symbol") String symbol,
BearerTokenAuthentication auth ) {
Now, how does Spring get the user roles? After all, this is not a standard claim like scopes or email. Indeed, there’s no
magic here: we must supply a custom ReactiveOpaqueTokenIntrospection that extracts those roles from custom
elds returned by Keycloak. This bean, available online, is basically the same shown in Spring’s documentation on
this topic, with just a few minor changes speci c to our custom elds.
We must also supply the con guration properties needed to access our identity provider:
spring.security.oauth2.resourceserver.opaquetoken.introspection-
uri=http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=quotes-client
spring.security.oauth2.resourceserver.opaquetoken.client-secret=<CLIENT SECRET>
Finally, to run our application, we can either import it in an IDE or run it from Maven. The project's POM contains a
pro le for this purpose:
The application will now be ready to serve requests on http://localhost:8085/quotes. We can check that it is
responding using curl:
$ curl -v http://localhost:8085/quotes/BAEL
As expected, we get a 401 Unauthorized response since no Authorization header was sent.
Securing a Spring Cloud Gateway application acting as a resource server is no di erent from a regular resource
service. As such, it comes with no surprise that we must add the same starter dependency as we did for the backend
service:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.6.2</version>
</dependency>
@SpringBootApplication
@EnableWebFluxSecurity
public class ResourceServerGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceServerGatewayApplication.class,args);
}
}
The security-related con guration properties are the same used in the backend:
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-
connect/token/introspect
client-id: quotes-client
client-secret: <code class="language-css"><CLIENT SECRET>
Next, we just add route declarations the same way we did in our previous article on Spring Cloud Gateway setup:
Notice that, apart from the security dependencies and properties, we didn’t change anything on the gateway
itself. To run the gateway application, we'll use spring-boot:run, using a speci c pro le with the required settings:
Now that we have all pieces of our puzzle, let's put them together. Firstly, we have to make sure we have Keycloak,
the quotes backend, and the gateway all running.
Next, we need to get an access token from Keycloak. In this case, the most straightforward way to get one is to use
a password grant ow (a.k.a, “Resource Owner”). This means doing a POST request to Keycloak passing the
username/password of one of the users, together with the client id and secret for the quotes client application:
$ curl -L -X POST \
'http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=quotes-client' \
--data-urlencode 'client_secret=0e082231-a70d-48e8-b8a5-fbfb743041b6' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'scope=email roles profile' \
--data-urlencode 'username=john.snow' \
--data-urlencode 'password=1234'
The response will be a JSON object containing the access token, along with other values:
{
"access_token": "...omitted",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "...omitted",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "7fd04839-fab1-46a7-a179-a2705dab8c6b",
"scope": "profile email"
}
We can now use the returned access token to access the /quotes API:
{
"symbol":"BAEL",
"price":12.0
}
Let’s repeat this process, this time using an access token for Maxwell Smart:
{
"symbol":"BAEL",
"price":10.0
}
We see that we have a lower price, which means the backend was able to correctly identify the associated user. We
can also check that unauthenticated requests do not get propagated to the backend, using a curl request with
no Authorization header:
$ curl http://localhost:8086/quotes/BAEL
Inspecting the gateway logs, we see that there are no messages related to the request forwarding process. This
shows that the response was generated at the gateway.
For the startup class, we'll use the same one we already have for the resource server version. We'll use this to
emphasize that all security behavior comes from the available libraries and properties.
In fact, the only noticeable di erence when comparing both versions are in the con guration properties. Here, we
need to con gure the provider details using either the issuer-uri property or individual settings for the various
endpoints (authorization, token, and introspection).
We also need to de ne our application client registration details, which include the requested scopes. Those scopes
inform the IdP which set of information items will be available through the introspection mechanism:
Finally, there’s one important change in the route de nitions section. We must add the TokenRelay lter to any route
that requires the access token to be propagated:
spring:
cloud:
gateway:
routes:
- id: quotes
uri: http://localhost:8085
predicates:
- Path=/quotes/**
filters:
- TokenRelay=
Alternatively, if we want all routes to start an authorization ow, we can add the TokenRelay lter to the default-
lters section:
spring:
cloud:
gateway:
default-filters:
- TokenRelay=
routes:
... other routes definition omitted
For the test setup, we also need to make sure we have the three pieces of our project running. This time, however,
we'll run the gateway using a di erent Spring Pro le containing the required properties to make it act as an OAuth 2.0
client. The sample project's POM contains a pro le that allows us to start it with this pro le enabled:
Once the gateway is running, we can test it by pointing our browser to http://localhost:8087/quotes/BAEL. If
everything is working as expected, we’ll be redirected to the IdP’s login page:
Since we've used Maxwell Smart’s credentials, we again get a quote with a lower price:
To conclude our test, we'll use an anonymous/incognito browser window and test this endpoint with John Snow’s
credentials. This time we get the regular quote price:
8. Conclusion
In this article, we’ve explored some of the OAuth 2.0 security patterns and how to implement them using Spring
Cloud Gateway. As usual, all code is available over on GitHub.
I just announced the new Learn Spring Security course, including the full
material focused on the new OAuth2 stack in Spring Security 5:
4 COMMENTS Oldest
View Comments