From a06f00cf509e331dbd8c63638ef5471dd3ad2b68 Mon Sep 17 00:00:00 2001 From: Boubaker Khanfir Date: Wed, 6 May 2026 13:39:37 +0100 Subject: [PATCH] feat: Centralize and Enrich OAuth Token Introspection and Validation This change will provide additional validation over 'SpringOpaqueTokenIntrospector' Token validation in order to prepare to multi-resource-servers startegy (not just mcp-server). --- .../configuration/OAuthJwtConfiguration.java | 47 ----------- .../OAuthSecurityConfiguration.java | 26 +++++- ... => OAuthAccessTokenAudienceProvider.java} | 2 +- ...=> OAuthAccessTokenAuthorityProvider.java} | 2 +- ...essTokenAudienceTokenRequestProvider.java} | 4 +- ...ccessTokenAuthorityPrincipalProvider.java} | 4 +- .../plugin/OAuthRefreshTokenGenerator.java | 83 +++++++++++++++++++ ...=> OAuthAccessTokenCustomizerService.java} | 78 ++++++++++------- ...DummyOAuthAccessTokenAudienceProvider.java | 36 ++++++++ 9 files changed, 197 insertions(+), 85 deletions(-) rename auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/{OAuthJwtAudienceProvider.java => OAuthAccessTokenAudienceProvider.java} (95%) rename auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/{OAuthJwtAuthorityProvider.java => OAuthAccessTokenAuthorityProvider.java} (95%) rename auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/{OAuthJwtAudienceTokenRequestProvider.java => OAuthAccessTokenAudienceTokenRequestProvider.java} (93%) rename auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/{OAuthJwtAuthorityPrincipalProvider.java => OAuthAccessTokenAuthorityPrincipalProvider.java} (90%) create mode 100644 auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthRefreshTokenGenerator.java rename auth-server-service/src/main/java/io/meeds/oauth2/server/service/{OAuthJwtCustomizerService.java => OAuthAccessTokenCustomizerService.java} (54%) create mode 100644 auth-server-service/src/test/java/io/meeds/oauth2/server/test/DummyOAuthAccessTokenAudienceProvider.java diff --git a/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/OAuthJwtConfiguration.java b/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/OAuthJwtConfiguration.java index 154c3fe..7027325 100755 --- a/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/OAuthJwtConfiguration.java +++ b/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/OAuthJwtConfiguration.java @@ -18,33 +18,17 @@ */ package io.meeds.oauth2.server.configuration; -import java.time.Instant; -import java.util.UUID; - import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.OAuth2RefreshToken; -import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; -import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; -import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; -import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; -import org.springframework.security.oauth2.server.authorization.token.JwtGenerator; -import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; import io.meeds.oauth2.server.service.OAuthJwkService; -import io.meeds.oauth2.server.service.OAuthJwtCustomizerService; -import io.meeds.oauth2.server.util.Utils; @Configuration public class OAuthJwtConfiguration { @@ -68,35 +52,4 @@ JWKSource jwkSource(OAuthJwkService oAuthJwkService) { return (jwkSelector, securityContext) -> jwkSelector.select(oAuthJwkService.getJwkSet()); } - @Bean - OAuth2TokenCustomizer jwtAccessTokenCustomizer(OAuthJwtCustomizerService oAuthJwtCustomizerService) { - return oAuthJwtCustomizerService::customizeAccessTokenClaims; - } - - @Bean - OAuth2TokenGenerator tokenGenerator(JwtEncoder jwtEncoder) { // NOSONAR - JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder); - return new DelegatingOAuth2TokenGenerator(jwtGenerator, - new OAuth2AccessTokenGenerator(), - this::generateRefreshToken); - } - - private OAuth2RefreshToken generateRefreshToken(OAuth2TokenContext context) { - if (OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType()) - && context.getAuthorizedScopes().contains(Utils.OFFLINE_ACCESS_SCOPE) - && context.getRegisteredClient() - .getAuthorizationGrantTypes() - .contains(AuthorizationGrantType.REFRESH_TOKEN)) { - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(context.getRegisteredClient() - .getTokenSettings() - .getRefreshTokenTimeToLive()); - - String value = UUID.randomUUID() + "-" + UUID.randomUUID(); - return new OAuth2RefreshToken(value, issuedAt, expiresAt); - } else { - return null; - } - } - } diff --git a/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/OAuthSecurityConfiguration.java b/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/OAuthSecurityConfiguration.java index 4a33bf0..5cd4d2e 100755 --- a/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/OAuthSecurityConfiguration.java +++ b/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/OAuthSecurityConfiguration.java @@ -46,6 +46,8 @@ import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationEndpointConfigurer; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; @@ -53,6 +55,10 @@ import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration; import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.WebAttributes; @@ -74,11 +80,13 @@ import io.meeds.oauth2.server.configuration.model.OAuthDefaultSettings; import io.meeds.oauth2.server.plugin.OAuthAuthorizationRequestConverter; import io.meeds.oauth2.server.plugin.OAuthDcrHttpAuthenticationConverter; +import io.meeds.oauth2.server.plugin.OAuthRefreshTokenGenerator; import io.meeds.oauth2.server.plugin.OAuthRefreshTokenPublicAuthenticationProvider; import io.meeds.oauth2.server.plugin.OAuthRefreshTokenPublicClientAuthenticationConverter; import io.meeds.oauth2.server.security.OAuthCimdAuthenticationProvider; import io.meeds.oauth2.server.security.OAuthDcrAuthenticationProvider; import io.meeds.oauth2.server.security.OAuthPortalAuthenticationProvider; +import io.meeds.oauth2.server.service.OAuthAccessTokenCustomizerService; import io.meeds.oauth2.server.service.OAuthClientService; import io.meeds.oauth2.server.service.OAuthSettingService; import io.meeds.oauth2.server.web.OAuthCorsConfigurationSource; @@ -114,6 +122,8 @@ SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, OAuthRefreshTokenPublicAuthenticationProvider oAuthRefreshTokenPublicAuthenticationProvider, OAuthRefreshTokenPublicClientAuthenticationConverter oAuthRefreshTokenPublicClientAuthenticationConverter, SecurityContextRepository securityContextRepository, + @Qualifier("oauthTokenGenerator") + OAuth2TokenGenerator tokenGenerator, @Qualifier("oauthAuthenticationProvider") OAuthDcrAuthenticationProvider oauthAuthenticationProvider, @Qualifier("oauthAuthenticationEntryPoint") @@ -128,7 +138,8 @@ SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, .addFilterBefore(portalPreAuthenticatedFilter, AbstractPreAuthenticatedProcessingFilter.class) .authenticationProvider(portalAuthenticationProvider) .with(authorizationServer, - a -> a.authorizationEndpoint(e -> customizeAuthorizationEndpoint(e, + a -> a.tokenGenerator(tokenGenerator) + .authorizationEndpoint(e -> customizeAuthorizationEndpoint(e, cimdAuthenticationProvider, oAuthAuthorizationRequestConverter)) .authorizationServerMetadataEndpoint(oauth -> oauth.authorizationServerMetadataCustomizer(c -> customizeMetadata(c, @@ -247,6 +258,19 @@ OAuthDcrAuthenticationProvider oauthAuthenticationProvider(OAuthClientService oA } // @formatter:on + @Bean("oauthTokenGenerator") + OAuth2TokenGenerator tokenGenerator(JwtEncoder jwtEncoder, // NOSONAR + OAuthAccessTokenCustomizerService oAuthAccessTokenCustomizerService, + OAuthRefreshTokenGenerator oAuthRefreshTokenGenerator) { + JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder); + jwtGenerator.setJwtCustomizer(oAuthAccessTokenCustomizerService::customize); + OAuth2AccessTokenGenerator oAuth2AccessTokenGenerator = new OAuth2AccessTokenGenerator(); + oAuth2AccessTokenGenerator.setAccessTokenCustomizer(oAuthAccessTokenCustomizerService); + return new DelegatingOAuth2TokenGenerator(jwtGenerator, + oAuth2AccessTokenGenerator, + oAuthRefreshTokenGenerator); + } + @Bean RestClient restClient() { HttpClient httpClient = HttpClient.newBuilder() diff --git a/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthJwtAudienceProvider.java b/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthAccessTokenAudienceProvider.java similarity index 95% rename from auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthJwtAudienceProvider.java rename to auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthAccessTokenAudienceProvider.java index a14f713..263ddf8 100755 --- a/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthJwtAudienceProvider.java +++ b/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthAccessTokenAudienceProvider.java @@ -24,7 +24,7 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; @FunctionalInterface -public interface OAuthJwtAudienceProvider { +public interface OAuthAccessTokenAudienceProvider { List provideAudiences(OAuth2TokenContext context); diff --git a/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthJwtAuthorityProvider.java b/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthAccessTokenAuthorityProvider.java similarity index 95% rename from auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthJwtAuthorityProvider.java rename to auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthAccessTokenAuthorityProvider.java index 37a9cb7..9d174d3 100755 --- a/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthJwtAuthorityProvider.java +++ b/auth-server-service/src/main/java/io/meeds/oauth2/server/configuration/plugin/OAuthAccessTokenAuthorityProvider.java @@ -24,7 +24,7 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; @FunctionalInterface -public interface OAuthJwtAuthorityProvider { +public interface OAuthAccessTokenAuthorityProvider { Set provideAuthorities(OAuth2TokenContext context); diff --git a/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthJwtAudienceTokenRequestProvider.java b/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthAccessTokenAudienceTokenRequestProvider.java similarity index 93% rename from auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthJwtAudienceTokenRequestProvider.java rename to auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthAccessTokenAudienceTokenRequestProvider.java index 06145c5..8267515 100755 --- a/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthJwtAudienceTokenRequestProvider.java +++ b/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthAccessTokenAudienceTokenRequestProvider.java @@ -29,14 +29,14 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; import org.springframework.stereotype.Service; -import io.meeds.oauth2.server.configuration.plugin.OAuthJwtAudienceProvider; +import io.meeds.oauth2.server.configuration.plugin.OAuthAccessTokenAudienceProvider; import io.meeds.oauth2.server.service.OAuthSettingService; import lombok.extern.slf4j.Slf4j; @Service @Slf4j -public class OAuthJwtAudienceTokenRequestProvider implements OAuthJwtAudienceProvider { +public class OAuthAccessTokenAudienceTokenRequestProvider implements OAuthAccessTokenAudienceProvider { @Autowired private OAuthSettingService oAuthSettingService; diff --git a/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthJwtAuthorityPrincipalProvider.java b/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthAccessTokenAuthorityPrincipalProvider.java similarity index 90% rename from auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthJwtAuthorityPrincipalProvider.java rename to auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthAccessTokenAuthorityPrincipalProvider.java index c61d64c..2dc5762 100755 --- a/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthJwtAuthorityPrincipalProvider.java +++ b/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthAccessTokenAuthorityPrincipalProvider.java @@ -27,11 +27,11 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; import org.springframework.stereotype.Service; -import io.meeds.oauth2.server.configuration.plugin.OAuthJwtAuthorityProvider; +import io.meeds.oauth2.server.configuration.plugin.OAuthAccessTokenAuthorityProvider; @Service @Order(Ordered.LOWEST_PRECEDENCE) -public class OAuthJwtAuthorityPrincipalProvider implements OAuthJwtAuthorityProvider { +public class OAuthAccessTokenAuthorityPrincipalProvider implements OAuthAccessTokenAuthorityProvider { @Override public Set provideAuthorities(OAuth2TokenContext context) { diff --git a/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthRefreshTokenGenerator.java b/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthRefreshTokenGenerator.java new file mode 100644 index 0000000..b9c6fe9 --- /dev/null +++ b/auth-server-service/src/main/java/io/meeds/oauth2/server/plugin/OAuthRefreshTokenGenerator.java @@ -0,0 +1,83 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.oauth2.server.plugin; + +import static io.meeds.oauth2.server.util.EntityMapper.CLIENT_IS_CIMD_SETTING; +import static io.meeds.oauth2.server.util.EntityMapper.CLIENT_IS_DCR_SETTING; + +import java.time.Instant; +import java.util.Base64; +import java.util.Objects; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.stereotype.Component; + +import io.meeds.oauth2.server.util.Utils; + +@Component +public final class OAuthRefreshTokenGenerator implements OAuth2TokenGenerator { + + private final StringKeyGenerator refreshTokenGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), + 96); + + @Value("${meeds.oauth.allow_public_refresh_tokens:true}") + private boolean allowPublicRefreshTokens; + + @Override + public OAuth2RefreshToken generate(OAuth2TokenContext context) { + if (!OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType())) { + return null; + } + RegisteredClient client = context.getRegisteredClient(); + boolean isPublicClient = Objects.equals(client.getClientSettings().getSetting(CLIENT_IS_CIMD_SETTING), true) + || Objects.equals(client.getClientSettings().getSetting(CLIENT_IS_DCR_SETTING), true) + || client.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.NONE); + if (isPublicClient && !allowPublicRefreshTokens) { + return null; + } + + if (OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType()) + && context.getAuthorizedScopes().contains(Utils.OFFLINE_ACCESS_SCOPE) + && client.getScopes() + .contains(Utils.OFFLINE_ACCESS_SCOPE) + && client.getAuthorizationGrantTypes() + .contains(AuthorizationGrantType.REFRESH_TOKEN)) { + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(client + .getTokenSettings() + .getRefreshTokenTimeToLive()); + + return new OAuth2RefreshToken(this.refreshTokenGenerator.generateKey(), + issuedAt, + expiresAt); + } else { + return null; + } + } + +} diff --git a/auth-server-service/src/main/java/io/meeds/oauth2/server/service/OAuthJwtCustomizerService.java b/auth-server-service/src/main/java/io/meeds/oauth2/server/service/OAuthAccessTokenCustomizerService.java similarity index 54% rename from auth-server-service/src/main/java/io/meeds/oauth2/server/service/OAuthJwtCustomizerService.java rename to auth-server-service/src/main/java/io/meeds/oauth2/server/service/OAuthAccessTokenCustomizerService.java index 8e9a83b..b51b2df 100755 --- a/auth-server-service/src/main/java/io/meeds/oauth2/server/service/OAuthJwtCustomizerService.java +++ b/auth-server-service/src/main/java/io/meeds/oauth2/server/service/OAuthAccessTokenCustomizerService.java @@ -24,83 +24,99 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.BiFunction; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; import org.springframework.stereotype.Service; import org.exoplatform.container.PortalContainer; -import io.meeds.oauth2.server.configuration.plugin.OAuthJwtAudienceProvider; -import io.meeds.oauth2.server.configuration.plugin.OAuthJwtAuthorityProvider; +import io.meeds.oauth2.server.configuration.plugin.OAuthAccessTokenAudienceProvider; +import io.meeds.oauth2.server.configuration.plugin.OAuthAccessTokenAuthorityProvider; import jakarta.annotation.PostConstruct; @Service -public class OAuthJwtCustomizerService { +public class OAuthAccessTokenCustomizerService implements OAuth2TokenCustomizer { @Autowired - private PortalContainer portalContainer; + private PortalContainer portalContainer; - private List audienceProviders; + private List audienceProviders; - private List authorityProviders; + private List authorityProviders; @PostConstruct public void init() { - this.audienceProviders = new ArrayList<>(portalContainer.getComponentInstancesOfType(OAuthJwtAudienceProvider.class)); - this.audienceProviders.sort((p1, p2) -> p1.getOrder() - p2.getOrder()); - this.authorityProviders = new ArrayList<>(portalContainer.getComponentInstancesOfType(OAuthJwtAuthorityProvider.class)); - this.authorityProviders.sort((p1, p2) -> p1.getOrder() - p2.getOrder()); + this.audienceProviders = new ArrayList<>(portalContainer.getComponentInstancesOfType(OAuthAccessTokenAudienceProvider.class)); + this.audienceProviders.sort((p1, p2) -> p2.getOrder() - p1.getOrder()); + this.authorityProviders = + new ArrayList<>(portalContainer.getComponentInstancesOfType(OAuthAccessTokenAuthorityProvider.class)); + this.authorityProviders.sort((p1, p2) -> p2.getOrder() - p1.getOrder()); } - public void addProvider(OAuthJwtAudienceProvider audienceProvider) { + public void addProvider(OAuthAccessTokenAudienceProvider audienceProvider) { this.audienceProviders.add(audienceProvider); - this.audienceProviders.sort((p1, p2) -> p1.getOrder() - p2.getOrder()); + this.audienceProviders.sort((p1, p2) -> p2.getOrder() - p1.getOrder()); } - public void addProvider(OAuthJwtAuthorityProvider authorityProvider) { + public void addProvider(OAuthAccessTokenAuthorityProvider authorityProvider) { this.authorityProviders.add(authorityProvider); - this.authorityProviders.sort((p1, p2) -> p1.getOrder() - p2.getOrder()); + this.authorityProviders.sort((p1, p2) -> p2.getOrder() - p1.getOrder()); } - public void customizeAccessTokenClaims(JwtEncodingContext context) { + @Override + public void customize(OAuth2TokenClaimsContext context) { if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { return; } - RegisteredClient client = context.getRegisteredClient(); + customize(context, context.getClaims()::claim); + } + + public void customize(JwtEncodingContext context) { + if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { + return; + } + customize(context, context.getClaims()::claim); + } + + private void customize(OAuth2TokenContext tokenContext, BiFunction claimFn) { + RegisteredClient client = tokenContext.getRegisteredClient(); boolean serviceToken = client.getClientSettings().getSetting(CLIENT_SERVICE_SETTING) != null && Boolean.parseBoolean(client.getClientSettings() .getSetting(CLIENT_SERVICE_SETTING) .toString()); - JwtClaimsSet.Builder claims = context.getClaims(); - claims.claim("client_id", client.getClientId()); - claims.claim("azp", client.getClientId()); - claims.claim("grant_type", context.getAuthorizationGrantType().getValue()); - claims.claim("scope", context.getAuthorizedScopes()); - claims.claim("token_kind", serviceToken ? "service" : "user"); - if (context.getPrincipal() != null) { - claims.subject(context.getPrincipal().getName()); + claimFn.apply("client_id", client.getClientId()); + claimFn.apply("azp", client.getClientId()); + claimFn.apply("grant_type", tokenContext.getAuthorizationGrantType().getValue()); + claimFn.apply("scope", tokenContext.getAuthorizedScopes()); + claimFn.apply("token_kind", serviceToken ? "service" : "user"); + if (tokenContext.getPrincipal() != null) { + claimFn.apply(OAuth2TokenClaimNames.SUB, tokenContext.getPrincipal().getName()); } - Set roles = computeJwtAuthorities(context); + Set roles = computeJwtAuthorities(tokenContext); if (roles != null) { - claims.claim("authorities", new HashSet<>(roles)); + claimFn.apply("authorities", new HashSet<>(roles)); } - List audiences = computeJwtAudiences(context); + List audiences = computeJwtAudiences(tokenContext); if (audiences != null) { - claims.audience(new ArrayList<>(audiences)); + claimFn.apply(OAuth2TokenClaimNames.AUD, new ArrayList<>(audiences)); } } - private List computeJwtAudiences(JwtEncodingContext context) { + private List computeJwtAudiences(OAuth2TokenContext context) { return audienceProviders.stream() .map(p -> p.provideAudiences(context)) .filter(CollectionUtils::isNotEmpty) @@ -110,7 +126,7 @@ private List computeJwtAudiences(JwtEncodingContext context) { null))); } - private Set computeJwtAuthorities(JwtEncodingContext context) { + private Set computeJwtAuthorities(OAuth2TokenContext context) { return authorityProviders.stream() .map(p -> p.provideAuthorities(context)) .filter(CollectionUtils::isNotEmpty) diff --git a/auth-server-service/src/test/java/io/meeds/oauth2/server/test/DummyOAuthAccessTokenAudienceProvider.java b/auth-server-service/src/test/java/io/meeds/oauth2/server/test/DummyOAuthAccessTokenAudienceProvider.java new file mode 100644 index 0000000..4facca9 --- /dev/null +++ b/auth-server-service/src/test/java/io/meeds/oauth2/server/test/DummyOAuthAccessTokenAudienceProvider.java @@ -0,0 +1,36 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.oauth2.server.test; + +import java.util.List; + +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.stereotype.Service; + +import io.meeds.oauth2.server.configuration.plugin.OAuthAccessTokenAudienceProvider; + +@Service +public class DummyOAuthAccessTokenAudienceProvider implements OAuthAccessTokenAudienceProvider { + + @Override + public List provideAudiences(OAuth2TokenContext context) { + return List.of(context.getRegisteredClient().getClientId()); + } + +}