Home > Net >  I can't build a Spring Cloud Gateway project with OAuth 2.0
I can't build a Spring Cloud Gateway project with OAuth 2.0

Time:10-28

I'm trying to build a Backend Service project using the example from the site

Using Spring Cloud Gateway with OAuth 2.0 Patterns

Here is the repository itself backend

Added dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>ru.test.gw.oauth.resource</groupId>
    <artifactId>backresource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>backresource</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>14</java.version>     
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

I moved the properties from the quotes-application.properties file to this project

server.port=11002
# Resource server settings
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:8484/auth/realms/demo/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=gateway
spring.security.oauth2.resourceserver.opaquetoken.client-secret=dfdslksfkljweewrfsd

Added a class

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import reactor.core.publisher.Mono;

// Custom ReactiveTokenIntrospector to map realm roles into Spring GrantedAuthorities
public class KeycloakReactiveTokenInstrospector implements ReactiveOpaqueTokenIntrospector {
    
    private final ReactiveOpaqueTokenIntrospector delegate;
   
    public KeycloakReactiveTokenInstrospector(ReactiveOpaqueTokenIntrospector delegate) {
        this.delegate = delegate;
    }

    @Override
    public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
        
        return delegate.introspect(token)
         .map( this::mapPrincipal);
    }
    
    protected OAuth2AuthenticatedPrincipal mapPrincipal(OAuth2AuthenticatedPrincipal principal) {
        
        return new DefaultOAuth2AuthenticatedPrincipal(
            principal.getName(),
            principal.getAttributes(),
            extractAuthorities(principal));
    }
    
    protected Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
        
        //
        Map<String,List<String>> realm_access = principal.getAttribute("realm_access");
        List<String> roles = realm_access.getOrDefault("roles", Collections.emptyList());
        List<GrantedAuthority> rolesAuthorities = roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        
        Set<GrantedAuthority> allAuthorities = new HashSet<>();
        allAuthorities.addAll(principal.getAuthorities());
        allAuthorities.addAll(rolesAuthorities);
        
        return allAuthorities;
    }
}

And the main class of the project

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import ru.test.gw.oauth.resource.backresource.security.KeycloakReactiveTokenInstrospector;

@SpringBootApplication
//@PropertySource("classpath:quotes-application.properties")
@EnableWebFluxSecurity
public class BackresourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(BackresourceApplication.class, args);
    }

    @Bean
    public SpringOpaqueTokenIntrospector keycloakIntrospector(OAuth2ResourceServerProperties props) {
        
        NimbusReactiveOpaqueTokenIntrospector delegate = new NimbusReactiveOpaqueTokenIntrospector(
           props.getOpaquetoken().getIntrospectionUri(),
           props.getOpaquetoken().getClientId(),
           props.getOpaquetoken().getClientSecret());
        
        return new KeycloakReactiveTokenInstrospector(delegate);
    }
    
}

And in this class I get an error on SpringOpaqueTokenIntrospector, writes that it is not defined. Although all the imports completely coincide with the training example. If I add a dependency that the IDE tells me to

import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;

, then I get an error

Type mismatch: cannot convert from KeycloakReactiveTokenInstrospector to SpringOpaqueTokenIntrospector 

What's the problem here? Is there some kind of dependency missing?

I completely repeated the structure of the project from the training material. So far, I would like to build a project without errors.

CodePudding user response:

Marcus is right in his comment, your keycloakIntrospector @Bean type should be ReactiveOpaqueTokenIntrospector (and not SpringOpaqueTokenIntrospector as declared in your conf)

Few facts:

  • SpringReactiveOpaqueTokenIntrospector is a ReactiveOpaqueTokenIntrospector but SpringOpaqueTokenIntrospector isn't
  • your KeycloakReactiveTokenInstrospector is (implements) a ReactiveOpaqueTokenIntrospector too but is neither a SpringReactiveOpaqueTokenIntrospector, SpringOpaqueTokenIntrospector nor OpaqueTokenIntrospector

Side notes

Introspection VS JWT decoding

Keycloak issues JWTs. JWT decoding is far more efficient than introspection: resource-server needs to fetch public-key only once from authorization-server to validate all incoming JWTs when introspection requires to submit access-token to authorization-server for each and every incoming request.

Also, you might not be able to implement multi-tenant scenarios with introspection: how to figure out by which issuer (Keycloak instance or realm) an opaque token was emitted? => you would have to "try" introspection on each issuer until one responds positively :/

Overriding introspector VS providing an authentication converter

If you switch to spring-security 5.8 or higher, customizing introspection is easier: you don't have to override the all introspector but can just provide a ReactiveOpaqueTokenAuthenticationConverter bean instead:

http.oauth2ResourceServer().opaqueToken().authenticationConverter(
    (String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> 
        new BearerTokenAuthentication(...));

This bean is called after introspection was successfuly completed (and token attributes retrieved) but before Authentication is instanciated and put in security-context which allows you to just map authorities from any attribute you like or completely switch the authentication implementation.

Simplifying your resource-server configuration

I host a set of libs to ease OAuth2 resource-server testing and configuration. There are various spring-boot starters depending on introspection or JWT decoding is used into servlet or reactive apps.

According to your case (reactive app with introspection), you should have a look at this sample with BearerTokenAuthentication and this other one with a custom authentication.

Configuration can be as simple as:

@EnableMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {
}
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://localhost:8443/realms/master/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=spring-addons-confidential
spring.security.oauth2.resourceserver.opaquetoken.client-secret=change-me

com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-webflux-introspecting-resource-server</artifactId>
    <version>6.0.3</version><!-- warning, this version goes with spring-boot 3.0.0-RC1 -->
</dependency>
  • Related