Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,15 +28,13 @@
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;
import org.springframework.security.crypto.password.PasswordEncoder;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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();
}
Expand Down Expand Up @@ -322,76 +309,37 @@ 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<SecurityContext> 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<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}

/**
* Authorization Server Settings
*
* Configures OAuth2 endpoint URLs and issuer
*/
@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&lt;JwtEncodingContext&gt; 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<JwtEncodingContext> 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;
}
}
11 changes: 8 additions & 3 deletions openespi-authserver/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SecurityContext> jwkSource = config.jwkSource();

// Then
assertThat(jwkSource).isNotNull();

// Verify JWK set contains RSA key
List<JWK> 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<SecurityContext> 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<SecurityContext> jwkSource1 = config.jwkSource();
JWKSource<SecurityContext> 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")
Expand Down Expand Up @@ -438,15 +387,11 @@ void shouldCreateAllRequiredBeans() {

// When
RegisteredClientRepository clientRepository = config.registeredClientRepository(jdbcTemplate);
JWKSource<SecurityContext> jwkSource = config.jwkSource();
JwtDecoder jwtDecoder = config.jwtDecoder(jwkSource);
AuthorizationServerSettings serverSettings = config.authorizationServerSettings();
OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer = config.espiTokenCustomizer();

// Then
assertThat(clientRepository).isNotNull();
assertThat(jwkSource).isNotNull();
assertThat(jwtDecoder).isNotNull();
assertThat(serverSettings).isNotNull();
assertThat(tokenCustomizer).isNotNull();
}
Expand All @@ -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);

Expand All @@ -487,21 +426,14 @@ void shouldWorkWithCompleteConfiguration() {

// When
RegisteredClientRepository clientRepository = config.registeredClientRepository(jdbcTemplate);
JWKSource<SecurityContext> jwkSource = config.jwkSource();
JwtDecoder jwtDecoder = config.jwtDecoder(jwkSource);
AuthorizationServerSettings serverSettings = config.authorizationServerSettings();
OAuth2TokenCustomizer<JwtEncodingContext> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/

/**
Expand Down
Loading