diff --git a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfig.java b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfig.java index d1b9d4a2..12ae8596 100644 --- a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfig.java +++ b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfig.java @@ -20,11 +20,6 @@ package org.greenbuttonalliance.espi.authserver.config; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.ImmutableJWKSet; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -33,7 +28,6 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -41,7 +35,6 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; -import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; @@ -65,10 +58,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; import java.time.Duration; import java.time.Instant; import java.util.UUID; @@ -132,12 +121,13 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h // /userinfo, etc.). Everything else falls through to // defaultSecurityFilterChain @Order(2). // - // Without this scoping, the resource-server bearer-token filter (added - // by .oauth2ResourceServer().jwt(...)) intercepts POST /oauth2/token - // before the token-endpoint filter can run its basic-auth handler. + // (No resource-server filter is configured on this chain — see below.) OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); - authorizationServerConfigurer.oidc(Customizer.withDefaults()); // OIDC 1.0 + // OIDC intentionally NOT enabled. ESPI uses opaque access tokens only and the + // resource server carries no JWK/JWT (issue #134). OIDC is DEFERRED, not removed + // forever — it returns when multi-utility Third-Party registration is built + // (see #122). Re-add via authorizationServerConfigurer.oidc(...) at that time. RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); http @@ -152,14 +142,11 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) - ) - // Accept access tokens for /userinfo and /connect/register. - // OIDC always issues id_token as JWT, so JWT is the right token - // type here. Outbound tokens to ESPI clients remain opaque via - // accessTokenFormat(REFERENCE) on each RegisteredClient. - .oauth2ResourceServer(resourceServer -> resourceServer - .jwt(Customizer.withDefaults()) ); + // No .oauth2ResourceServer(): the auth-server issues opaque tokens and does not + // validate bearer tokens on its own endpoints. The OAuth2 protocol endpoints + // (token/introspect/revoke) authenticate clients via client_secret_basic; the + // admin/UI endpoints are protected by the @Order(2) session-login chain. return http.build(); } @@ -322,34 +309,6 @@ public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplat return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); } - /** - * JWK Source for JWT Token Signing - * - * Generates RSA key pair for JWT signing and validation. - * - * TODO: Use persistent key store for production - */ - @Bean - public JWKSource jwkSource() { - KeyPair keyPair = generateRsaKey(); - RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); - RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); - RSAKey rsaKey = new RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build(); - JWKSet jwkSet = new JWKSet(rsaKey); - return new ImmutableJWKSet<>(jwkSet); - } - -// /** -// * JWT Decoder for token validation -// */ - @Bean - public JwtDecoder jwtDecoder(JWKSource jwkSource) { - return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); - } - /** * Authorization Server Settings * @@ -357,41 +316,30 @@ public JwtDecoder jwtDecoder(JWKSource jwkSource) { */ @Bean public AuthorizationServerSettings authorizationServerSettings() { + // No .jwkSetEndpoint(): the auth-server has no JWK source (opaque tokens only). return AuthorizationServerSettings.builder() .issuer(issuerUri) .authorizationEndpoint("/oauth2/authorize") .tokenEndpoint("/oauth2/token") - .jwkSetEndpoint("/oauth2/jwks") .tokenRevocationEndpoint("/oauth2/revoke") .tokenIntrospectionEndpoint("/oauth2/introspect") - .oidcClientRegistrationEndpoint("/connect/register") - .oidcUserInfoEndpoint("/userinfo") .build(); } /** - * ESPI Token Customizer - * - * Adds Green Button Alliance specific claims to JWT tokens + * ESPI Token Customizer. + * + * Holds the ESPI logic for adding resource/authorization URIs to the token. + * Currently an OAuth2TokenCustomizer<JwtEncodingContext> that only fires + * when espi.token.format=jwt (experimental); inert for the opaque ESPI flow. + * RETAINED intentionally: it is the sole home of the URI-augmentation logic to + * be migrated to the opaque token-response path (the Energy/Customer/Authorization + * URLs) — see #122 (token-response augmentation). NOT part of the #134 JWK/JWT + * signing strip. */ @Bean public OAuth2TokenCustomizer espiTokenCustomizer() { return new EspiTokenCustomizer(); } - /** - * Generate RSA Key Pair for JWT signing - */ - private static KeyPair generateRsaKey() { - KeyPair keyPair; - try { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(2048); - keyPair = keyPairGenerator.generateKeyPair(); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - return keyPair; - } } \ No newline at end of file diff --git a/openespi-authserver/src/main/resources/application.yml b/openespi-authserver/src/main/resources/application.yml index 011e9780..5828b208 100644 --- a/openespi-authserver/src/main/resources/application.yml +++ b/openespi-authserver/src/main/resources/application.yml @@ -11,10 +11,16 @@ server: spring: application: name: openespi-authorization-server - + + # ESPI is opaque-token only — suppress the Spring Boot auto-config that would + # otherwise generate a JWKSource/JwtDecoder and stand up /oauth2/jwks (#134). + autoconfigure: + exclude: + - org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerJwtAutoConfiguration + profiles: active: dev-mysql - + # Security Configuration security: oauth2: @@ -23,7 +29,6 @@ spring: endpoint: authorization-uri: /oauth2/authorize token-uri: /oauth2/token - jwk-set-uri: /oauth2/jwks token-revocation-uri: /oauth2/revoke token-introspection-uri: /oauth2/introspect diff --git a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfigTest.java b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfigTest.java index 5f103719..32d2db8b 100644 --- a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfigTest.java +++ b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfigTest.java @@ -201,59 +201,8 @@ void shouldConfigureThirdPartyAdminClientCorrectly() { } } - @Nested - @DisplayName("JWK Configuration Tests") - class JwkConfigurationTests { - - @Test - @DisplayName("Should create JWK source with RSA key") - void shouldCreateJwkSourceWithRsaKey() throws Exception { - // When - JWKSource jwkSource = config.jwkSource(); - - // Then - assertThat(jwkSource).isNotNull(); - - // Verify JWK set contains RSA key - List jwkList = jwkSource.get(null, null); - assertThat(jwkList).isNotNull(); - assertThat(jwkList).hasSize(1); - assertThat(jwkList.get(0)).isInstanceOf(RSAKey.class); - - RSAKey rsaKey = (RSAKey) jwkList.get(0); - assertThat(rsaKey.getKeyID()).isNotNull(); - assertThat(rsaKey.toRSAPublicKey()).isNotNull(); - assertThat(rsaKey.toRSAPrivateKey()).isNotNull(); - } - - @Test - @DisplayName("Should create JWT decoder from JWK source") - void shouldCreateJwtDecoderFromJwkSource() { - // Given - JWKSource jwkSource = config.jwkSource(); - - // When - //JwtDecoder jwtDecoder = config.jwtDecoder(jwkSource); - - // Then - // assertThat(jwtDecoder).isNotNull(); - } - - @Test - @DisplayName("Should generate different keys on each call") - void shouldGenerateDifferentKeysOnEachCall() throws Exception { - // When - JWKSource jwkSource1 = config.jwkSource(); - JWKSource jwkSource2 = config.jwkSource(); - - // Then - RSAKey key1 = (RSAKey) jwkSource1.get(null, null).get(0); - RSAKey key2 = (RSAKey) jwkSource2.get(null, null).get(0); - - assertThat(key1.getKeyID()).isNotEqualTo(key2.getKeyID()); - assertThat(key1.toRSAPublicKey()).isNotEqualTo(key2.toRSAPublicKey()); - } - } + // JWK Configuration Tests removed in #134 — the auth-server no longer exposes + // jwkSource()/jwtDecoder() (ESPI is opaque-only; no JWK/JWT). @Nested @DisplayName("Authorization Server Settings Tests") @@ -438,15 +387,11 @@ void shouldCreateAllRequiredBeans() { // When RegisteredClientRepository clientRepository = config.registeredClientRepository(jdbcTemplate); - JWKSource jwkSource = config.jwkSource(); - JwtDecoder jwtDecoder = config.jwtDecoder(jwkSource); AuthorizationServerSettings serverSettings = config.authorizationServerSettings(); OAuth2TokenCustomizer tokenCustomizer = config.espiTokenCustomizer(); // Then assertThat(clientRepository).isNotNull(); - assertThat(jwkSource).isNotNull(); - assertThat(jwtDecoder).isNotNull(); assertThat(serverSettings).isNotNull(); assertThat(tokenCustomizer).isNotNull(); } @@ -460,13 +405,7 @@ void shouldCreateBeansWithCorrectTypes() { // When & Then assertThat(config.registeredClientRepository(jdbcTemplate)) .isInstanceOf(JdbcRegisteredClientRepository.class); - - assertThat(config.jwkSource()) - .isInstanceOf(JWKSource.class); - - assertThat(config.jwtDecoder(config.jwkSource())) - .isInstanceOf(JwtDecoder.class); - + assertThat(config.authorizationServerSettings()) .isInstanceOf(AuthorizationServerSettings.class); @@ -487,21 +426,14 @@ void shouldWorkWithCompleteConfiguration() { // When RegisteredClientRepository clientRepository = config.registeredClientRepository(jdbcTemplate); - JWKSource jwkSource = config.jwkSource(); - JwtDecoder jwtDecoder = config.jwtDecoder(jwkSource); AuthorizationServerSettings serverSettings = config.authorizationServerSettings(); OAuth2TokenCustomizer tokenCustomizer = config.espiTokenCustomizer(); // Then - All components should work together assertThat(clientRepository).isNotNull(); - assertThat(jwkSource).isNotNull(); - assertThat(jwtDecoder).isNotNull(); assertThat(serverSettings).isNotNull(); assertThat(tokenCustomizer).isNotNull(); - - // Test JWT decoder with JWK source - assertThat(jwtDecoder).isNotNull(); - + // Test server settings configuration assertThat(serverSettings.getIssuer()).isNotNull(); assertThat(serverSettings.getTokenEndpoint()).isNotNull(); diff --git a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/integration/ClientRegistrationEndpointIntegrationTest.java b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/integration/ClientRegistrationEndpointIntegrationTest.java index 28fe210b..8ec4c50a 100644 --- a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/integration/ClientRegistrationEndpointIntegrationTest.java +++ b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/integration/ClientRegistrationEndpointIntegrationTest.java @@ -21,6 +21,7 @@ package org.greenbuttonalliance.espi.authserver.integration; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -60,6 +61,10 @@ @ActiveProfiles("test") @DisplayName("Client Registration Endpoint Integration Tests") @Transactional +@Disabled("OIDC Dynamic Client Registration (/connect/register) was removed in #134 — " + + "ESPI is opaque-only and OIDC is deferred until multi-utility Third-Party " + + "registration is built. Restore this suite when OIDC returns (see #122, #134). " + + "Broader auth-server test-suite repair is tracked in #129.") class ClientRegistrationEndpointIntegrationTest { @Autowired diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/config/SecurityConfiguration.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/config/SecurityConfiguration.java index 88eff34f..3d0936c0 100644 --- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/config/SecurityConfiguration.java +++ b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/config/SecurityConfiguration.java @@ -215,8 +215,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti * 1. Uses opaque token introspection (ESPI standard requirement) * 2. Connects to the AuthorizationEntity Server's introspection endpoint * 3. Uses client credentials for introspection authentication - * - * Future enhancement: Add JWT support for dynamic client registration scenarios + * + * NOTE: JWT/JWK support is intentionally absent. ESPI 4.0 mandates opaque + * access tokens; the GBA Resource Server must contain no JWK/JWT functions + * (see issue #134 / #122). */ /**