Spring Cloud Gateway — API Access Control with JWT
API Gateway pattern is one of the commonly used patterns in Microservice Architecture in which API Gateway acts as a reverse proxy routing request from the client to the server.
In this article, we’ll explore one of the most important aspects - API Security & Access Control - at the Gateway layer. The main purpose is to block unauthorized API request at Gateway itself. We are going to use Spring Cloud Gateway and Spring OAuth2 Resource Server along with Okta as Authorization server. Below is high level flow of how it is going to work -
Spring Cloud Gateway is API Gateway implementation by Spring Cloud team on Spring Boot 2.x, Spring WebFlux, and Project Reactor. It requires Netty server to provide non-blocking asynchronous request processing.
What are we going to use
- Java 11
- Spring boot 2.4.3 with Spring Cloud Gateway 3.0.1 and Spring WebFlux 5.3.4
- Spring security OAuth2 resource server 5.4.5
- Okta as Authorization Server. Follow Use the Client Credentials Flow for access token generation flow.
Setting-up Sample Microservice
For routing from Gateway, we are going to create a simple Welcome REST service with two different endpoints (for simplicity, one is considered as unsecured and another one is secured). Following is the sample source code for WelcomeController
-
Setting-up Spring Cloud Gateway
This is where we configure the routing and apply security for our backend services. First, let us create a new spring boot project with spring-cloud-gateway and oauth2-resource-server dependencies. Below is the sample pom.xml
-
Adding Security with OAuth2 Resource Server and JWT
We are now going to add oauth2 resource server configuration to our Gateway. Create a new ResourceServerSecurityConfig.java
to add security for Gateway routes as follows. It will configure the default OAuth2 Resource Server Support with JWT.
To use JWT, we need to provide public key details of Authorization server which can be used to decode incoming access token. This can be set-up in one of the three ways -
- If public key certificate is available, it can be included as part of the project in resources folder and then property
spring.security.oauth2.resourceserver.jwt.public-key-location
can be set to same location. - If Authorization server exposes a JWKS URI, it can be used with property
spring.security.oauth2.resourceserver.jwt.jwk-set-uri
. Since we are using Okta and it exposes the endpoint for the same, we are going to use this in our application property. - Lastly, if Authorization server supports OpenID Connect, we can set OpenID Connect discovery endpoint or an OAuth 2.0 Authorization Server Metadata endpoint with property
spring.security.oauth2.resourceserver.jwt.issuer-uri
.
Once this is set-up, any route accessed through gateway will have to pass an access token with Bearer Authorization header. If Authorization header is not present, Gateway will through 401 without any error response. We’ll see later how to customize it.
Configure Unsecured Routes
At times, we don’t want to secure all the endpoints and leave some of them open and unsecured.
Unsecured (or unprotected) routes are the routes for which no additional security is applicable. They can be accessed by anyone without any Authorization header. Typically, these are routes which doesn’t provide any sensitive information and can be exposed without any security risks.
We can achieve this by two ways -
- Modifying
ResourceServerSecurityConfig.java
with additional filtering of URL patterns and thenpermitAll()
these patterns. - Create a new Configurable
PublicSecurityConfig
which can be used to configure URLs at run time and if required, it can be disabled completely as well -
The unsecured routes configuration can be disabled by setting custom property custom.gateway.expose-unsecured-urls
to false. The URL patterns which need to be unprotected can be configured using custom property custom.gateway.unsecured-urls
.
Putting it together with Application properties
Now let us update the route for Welcome service and make one of the route as unsecured for demo purpose -
Try It Now
GitHub Project Link — https://github.com/akashgupta2703/cloud-gateway-resource-server
- Run Welcome Service at Port 8080 and Run Gateway at Port 9000.
- Access the unsecured endpoint (
http://localhost:9000/api/v1/welcome/unsecured-hello
) without any Authorization header :
3. To access secured endpoint, Generate the access token using Okta client credentials and pass the generated access token as Bearer Token to secured API (http://localhost:9000/api/v1/welcome/secured-hello
). Sample request and response are :
Advanced Customization
- Bearer Token Resolver
By Default, OAuth2 Resource Server resolves the access token from Authorization header. This can be further enhanced to accept access token from ‘access_token’ query parameter as follows -
private ServerAuthenticationConverter serverAuthenticationConverter() {
ServerBearerTokenAuthenticationConverter serverAuthenticationConverter = new ServerBearerTokenAuthenticationConverter();
serverAuthenticationConverter.setAllowUriQueryParameter(true);
return serverAuthenticationConverter;
}
Then refer the same in the configuration -
oauth2ResourceServer().bearerTokenConverter(serverAuthenticationConverter())
This can be again fully customized to accept access token from any place by either implementing ServerAuthenticationConverter
or extending the default authentication converter ServerBearerTokenAuthenticationConverter
class.
- Customize Error Response
By default, OAuth2 Resource server sends 401 along with error details in WWW-Authenticate
response header. This may not be very user friendly. We can customize error response either by implementing ServerAuthenticationEntryPoint
or extending the default authentication entry pointBearerTokenServerAuthenticationEntryPoint
class. This can be attached to security chain similar to Bearer Token Resolver i.e.
oauth2ResourceServer().authenticationEntryPoint(customBearerTokenServerAuthenticationEntryPoint())
- Custom Access Denied Handler
Similar ways, Access Denied can also be customized by either implementing ServerAccessDeniedHandler or extending the default BearerTokenServerAccessDeniedHandler
class.
- Custom Token Validations
By default, OAuth2 resource server only validates the expiry of JWT. We can create our own token validators implementing OAuth2TokenValidator
interface e.g. we can create validator to validate role, authorities, scopes, issuer etc.
We need to then expose NimbusReactiveJwtDecoder
as bean and set the custom validators.
@Bean
public ReactiveJwtDecoder reactiveJwtDecoder() {
NimbusReactiveJwtDecoder reactiveJwtDecoder = new NimbusReactiveJwtDecoder(jwkSetUrl);
reactiveJwtDecoder.setJwtValidator(customOAuth2TokenValidators());
return reactiveJwtDecoder;
}
Check JwtTimestampValidator class for sample implementation.
References
Spring Cloud Gateway Official Documentation — https://cloud.spring.io/spring-cloud-gateway/reference/html/
Okta Authorization Server — https://developer.okta.com/docs/guides/customize-authz-server/create-authz-server/