Spring Cloud Gateway and Apereo CAS Integration

Deniz G
5 min readDec 18, 2020

--

There are 2 components in CAS Terminology. The first one is CAS server whose main responsibility is to provide single sign-on service. The other is CAS client which are applications that delegate authentication to CAS server.

The cas-client library must exist in the programming language used in order to integrate CAS clients with the CAS server. Today, you can connect your Spring application to CAS because the cas client library made for Spring
available. However, you cannot integrate Spring Cloud Gateway with Cas server using this cas client library. Why?

Problem

Traditional Spring applications are based on a Servlet/MVC stack and provide blocking operation. However, applications developed with the reactive approach are based on WebFlux, which in contrast provides non-blocking operation. So, libraries that depend on Servlet/MVC cannot run on reactive libraries.

The cas client library developed for Spring is based on Servlet/MVC. Spring Cloud Gateway is a reactive library built on WebFlux. So if the cas client is added to the Spring Cloud Gateway and the Spring Cloud Gateway is requested to be the cas client, this will not work.

So is there a reactive cas client for spring right now? No. Then, we will either write our own reactive cas client library or wait for someone to write it.

https://github.com/spring-cloud/spring-cloud-gateway/issues/987

https://github.com/spring-projects/spring-security/issues/5887

Solution

Use OpenID Connect (OIDC). This is simply authentication using the OAuth2 protocol. Spring Cloud Gateway supports OAuth2 client, and Apereo CAS supports to be used as Authorization Server. Bingo!! Then we can integrate each other with OpenID connect.

Let’s start from Apereo CAS. (https://apereo.github.io/2019/02/19/cas61-as-oauth-authz-server/)

Firstly, add OAuth dependency to build.gradle

compile "org.apereo.cas:cas-server-support-oauth-webflow:${casServerVersion}"

Then, add OAuth credentials to your service registry.

{
"@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService",
...
"bypassApprovalPrompt": true,
"clientId": "abc",
"clientSecret": "123",
"supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ],
"supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ]
...
}

After these settings, Apereo CAS will provide endpoints for use in the OpenID Connect process. (https://apereo.github.io/cas/6.2.x/installation/OAuth-OpenId-Authentication.html#endpoints)

  • /oauth2.0/accessToken
  • /oauth2.0/authorize
  • /oauth2.0/profile
  • /oauth2.0/instrospect

Apereo CAS is now complete. Turn at Spring Cloud Gateway.

Add OAuth2 client dependency.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

Then, configure security.

@EnableWebFluxSecurity
public class WebSecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { http.authorizeExchange().anyExchange().authenticated().and().oauth2Login();
return http.build();
}
}

Then, provide Authorization Server endpoints and credentials.

spring:
security:
oauth2:
client:
registration:
login-client:
provider: uaa
client-id: abc
client-secret: 123
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: custom_mod
provider:
uaa:
authorization-uri: ${cas-address}/oauth2.0/authorize
token-uri: ${cas-address}/oauth2.0/accessToken
user-info-uri: ${cas-address}/oauth2.0/profile
user-name-attribute: id
user-info-authentication-method: form

Last but not least. Add TokenRelay filter to your route to pass access token during downstream. In OAuth2 Terminology, your microservices behind gateway are resource server. So, Spring Cloud Gateway sends access tokens to resource servers, and they are required to validate this tokens.

spring:
cloud:
gateway:
routes:
- id: test_route
predicates:
- Path=/test/**
filters:
- TokenRelay=
uri: http://test

So how will the flow work with these settings?

  • When you request to Spring Cloud Gateway, it will redirects to Apereo CAS to get authorization code (or access code). (/oauth2.0/authorize)
  • When you are redirect to Apereo CAS to authorization code, you will login if you are not yet logged into CAS.
  • After sucessful login, Apereo CAS will redirect back to Spring Cloud Gateway with autohorization code.
  • When the request with authorization code comes back Spring Cloud Gateway, it will use this code and sends access token request to Apereo CAS. (/oauth2.0/accessToken)
  • Apereo CAS will validate authorization code and provide access token to Spring Cloud Gateway.
  • After get access token, Spring Cloud Gateway will send profile request to Apereo CAS. Because, it needs to create principal/authentication object within itself. (/oauth2.0/profile)

Authentication is completed now.

Let’s authorize access token passed to resource server.

First, add security and resource server dependencies to your microservices.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>8.22</version>
<scope>runtime</scope>
</dependency>

Then, configure security.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2ResourceServer().opaqueToken();
}
}

Then, provide Authorization server endpoint and credentials.

spring.security.oauth2.resourceserver.opaquetoken.client-id=abc
spring.security.oauth2.resourceserver.opaquetoken.client-secret=123
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=${cas-address}/oauth2.0/introspect

Done! If you open debug log level for RestTemplate object, you will see validation request to Apereo CAS.

...
logging.level.org.springframework.web.client.RestTemplate=debug

Access Token Types

There are 2 types of access token. First one is called opaque token (or reference token). It is non-sense and random string value (like AT-5-l5-uexxsnSAs5uFSWwIF91XW8a9jOrE1). This token is only known by the authorization server that assigned it.

So, when the resource server receives this token, it should send a request to the authorization server to make sense and verify access token.

Apereo CAS provides opaque token as access token by default in the examples I have given above. So, for every request coming to the resource server, I have to request the authorization server again to verify this. It is network round-trip cost.

The other access token type is called self-contained token (or JWT). This token contains all the necessary information and we don’t need anything else to understand what is it. So, for every request coming to the resource server, there is no need to request the authorization server verify this.

Let’s change opaque token to JWT.

Add this to your service registry at Apereo CAS.

“jwtAccessToken”: true

JWT is also divided into 3.

  • JWT: Json Web Token (unsigned JWT. signature part is empty.)
  • JWS: Json Web Signature (signed JWT. there is a signature part.)
  • JWE: Json Web Encrpytion (signed and encrypted. there is a signature part, and also payload part is encrpyted.)

When Apereo CAS starts, you will see encrpytion and signing keys at logs (it means that it creates JWE) if you don’t configure these keys. Set it up to avoid creating new keys over and over at every startup. I prefer to JWS to prevent overwork on encrpytion. So, I disable encrpytion and want only signing.

cas.authn.oauth.access-token.crypto.encryption-enabled=false
cas.authn.oauth.access-token.crypto.signing-enabled=true
cas.authn.oauth.access-token.crypto.signing.key=abBkYP2TGd1qobBQnW0mraR1jJ5_uBT65LlnpP8xe_sy3IiNQ_6SnNUxagwcPxHUudOdsf_hEPRRUHxaAsTzcd

There is no need to change in Spring Cloud Gateway. Let’s continue from resource server.

Change token type from opaque to jwt.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt().decoder(jwtDecoder());
}
@Bean
public JwtDecoder jwtDecoder() {
String signKey = "abBkYP2TGd1qobBQnW0mraR1jJ5_uBT65LlnpP8xe_sy3IiNQ_6SnNUxagwcPxHUudOdsf_hEPRRUHxaAsTzcd";
SecretKey secretKey = new SecretKeySpec(signKey.getBytes(), "HMACSHA512");
return NimbusJwtDecoder
.withSecretKey(secretKey)
.macAlgorithm(MacAlgorithm.HS512)
.build();
}
}

That is it. No longer need to go to the authorization server to verify the access token every time a request comes up. If you break/disrupt your key at resource server, it will respond HTTP 401 Unauthorized.

--

--