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 aReactiveOpaqueTokenIntrospector
butSpringOpaqueTokenIntrospector
isn't- your
KeycloakReactiveTokenInstrospector
is (implements) aReactiveOpaqueTokenIntrospector
too but is neither aSpringReactiveOpaqueTokenIntrospector
,SpringOpaqueTokenIntrospector
norOpaqueTokenIntrospector
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>