diff --git a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/backchannel/BackchannelRequest.java b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/backchannel/BackchannelRequest.java index c65921d9..8031e983 100644 --- a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/backchannel/BackchannelRequest.java +++ b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/backchannel/BackchannelRequest.java @@ -36,7 +36,6 @@ public record BackchannelRequest( @JsonProperty("client_id") String clientId, @JsonProperty("granted_scope") String grantedScope, @JsonProperty("retail_customer_id") Long retailCustomerId, - @JsonProperty("selected_usage_point_ids") List selectedUsagePointIds, - @JsonProperty("customer_resource_uri") String customerResourceUri + @JsonProperty("selected_usage_point_ids") List selectedUsagePointIds ) { } diff --git a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/grant/GrantContext.java b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/grant/GrantContext.java index f57b4465..4a56b846 100644 --- a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/grant/GrantContext.java +++ b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/grant/GrantContext.java @@ -42,14 +42,11 @@ * narrowing); {@code ;}-delimited ESPI tokens * @param selectedUsagePointIds usage points the customer chose to share with the TP; may be empty * for grants that include only Customer/PII scope - * @param customerResourceUri absolute URI for the customer/PII resource; non-null iff the scope - * includes a Customer/PII Function Block (54–62) */ public record GrantContext( String correlationId, Long retailCustomerId, String approvedScope, - List selectedUsagePointIds, - String customerResourceUri + List selectedUsagePointIds ) implements Serializable { } diff --git a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextEnrichingAuthorizationService.java b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextEnrichingAuthorizationService.java index 43e85eaf..4be03e92 100644 --- a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextEnrichingAuthorizationService.java +++ b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextEnrichingAuthorizationService.java @@ -71,7 +71,6 @@ public class GrantContextEnrichingAuthorizationService implements OAuth2Authoriz public static final String ATTR_RETAIL_CUSTOMER_ID = "espi.retail_customer_id"; public static final String ATTR_GRANTED_SCOPE = "espi.granted_scope"; public static final String ATTR_SELECTED_USAGE_POINT_IDS = "espi.selected_usage_point_ids"; - public static final String ATTR_CUSTOMER_RESOURCE_URI = "espi.customer_resource_uri"; public static final String ATTR_CORRELATION_ID = "espi.correlation_id"; private final OAuth2AuthorizationService delegate; @@ -131,9 +130,6 @@ private OAuth2Authorization maybeEnrich(OAuth2Authorization authorization) { .attribute(ATTR_GRANTED_SCOPE, ctx.approvedScope()) .attribute(ATTR_SELECTED_USAGE_POINT_IDS, serializeUuids(ctx.selectedUsagePointIds())) .attribute(ATTR_CORRELATION_ID, ctx.correlationId()); - if (ctx.customerResourceUri() != null && !ctx.customerResourceUri().isBlank()) { - b.attribute(ATTR_CUSTOMER_RESOURCE_URI, ctx.customerResourceUri()); - } return b.build(); } diff --git a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/service/GrantBackchannelTokenCustomizer.java b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/service/GrantBackchannelTokenCustomizer.java index c7b22336..aaeb1c32 100644 --- a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/service/GrantBackchannelTokenCustomizer.java +++ b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/service/GrantBackchannelTokenCustomizer.java @@ -106,8 +106,6 @@ public void customize(OAuth2TokenClaimsContext context) { GrantContextEnrichingAuthorizationService.ATTR_CORRELATION_ID); String selectedUpsSerialized = authorization.getAttribute( GrantContextEnrichingAuthorizationService.ATTR_SELECTED_USAGE_POINT_IDS); - String customerResourceUri = authorization.getAttribute( - GrantContextEnrichingAuthorizationService.ATTR_CUSTOMER_RESOURCE_URI); String clientId = context.getRegisteredClient().getClientId(); List selectedUps = GrantContextEnrichingAuthorizationService @@ -118,8 +116,7 @@ public void customize(OAuth2TokenClaimsContext context) { clientId, grantedScope, Long.valueOf(retailCustomerIdStr), - selectedUps, - customerResourceUri); + selectedUps); BackchannelResponse response; try { diff --git a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/web/delegate/AuthorizeContinueController.java b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/web/delegate/AuthorizeContinueController.java index 85a07dec..4b14f67d 100644 --- a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/web/delegate/AuthorizeContinueController.java +++ b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/web/delegate/AuthorizeContinueController.java @@ -151,8 +151,7 @@ private String handleAllow(SignedHandoff.Return payload, payload.correlationId(), retailCustomerId, payload.approvedScope(), - payload.selectedUsagePointIds(), - payload.customerResourceUri())); + payload.selectedUsagePointIds())); // Redirect back to /oauth2/authorize with the original TP query params. Spring AS finds // the saved authorization request (still in session), sees authenticated user + matching diff --git a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/backchannel/DataCustodianBackchannelClientTest.java b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/backchannel/DataCustodianBackchannelClientTest.java index 25507236..5c58c23c 100644 --- a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/backchannel/DataCustodianBackchannelClientTest.java +++ b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/backchannel/DataCustodianBackchannelClientTest.java @@ -82,7 +82,7 @@ void happyPath() { """.formatted(AUTH_ID, RES_SUB_ID, CUST_SUB_ID, RES_SUB_ID, AUTH_ID, CUST_SUB_ID))); BackchannelResponse response = client.provision(new BackchannelRequest( - "corr-1", "tp-1", "FB_1;FB_4_5", 42L, List.of(UP_1), null)); + "corr-1", "tp-1", "FB_1;FB_4_5", 42L, List.of(UP_1))); assertThat(response) .extracting(BackchannelResponse::authorizationId, @@ -110,7 +110,7 @@ void clientError() { """)); assertThatThrownBy(() -> client.provision( - new BackchannelRequest("c", "tp", "", 1L, List.of(), null))) + new BackchannelRequest("c", "tp", "", 1L, List.of()))) .isInstanceOf(DataCustodianBackchannelException.class) .hasMessageContaining("400") .hasMessageContaining("granted_scope is missing"); @@ -125,7 +125,7 @@ void serverError() { .body("{\"error\":\"internal\"}")); assertThatThrownBy(() -> client.provision( - new BackchannelRequest("c", "tp", "FB_1", 1L, List.of(), null))) + new BackchannelRequest("c", "tp", "FB_1", 1L, List.of()))) .isInstanceOf(DataCustodianBackchannelException.class) .hasMessageContaining("500"); } @@ -137,7 +137,7 @@ void unexpectedStatus() { .andRespond(withRawStatus(418).body("no coffee")); assertThatThrownBy(() -> client.provision( - new BackchannelRequest("c", "tp", "FB_1", 1L, List.of(), null))) + new BackchannelRequest("c", "tp", "FB_1", 1L, List.of()))) .isInstanceOf(DataCustodianBackchannelException.class) .hasMessageContaining("418"); } diff --git a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextEnrichingAuthorizationServiceTest.java b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextEnrichingAuthorizationServiceTest.java index 1428207c..be19f034 100644 --- a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextEnrichingAuthorizationServiceTest.java +++ b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextEnrichingAuthorizationServiceTest.java @@ -75,8 +75,7 @@ void enrichesFromSessionOnFirstSave() { MockHttpSession session = new MockHttpSession(); request.setSession(session); GrantContext ctx = new GrantContext( - "corr-1", 42L, "FB_1;FB_4_5", List.of(UP_1, UP_2), - "https://dc.example/RetailCustomer/42"); + "corr-1", 42L, "FB_1;FB_4_5", List.of(UP_1, UP_2)); sessionStore.put(session, ctx); service.save(blankAuthorization("auth-1")); @@ -93,9 +92,6 @@ void enrichesFromSessionOnFirstSave() { assertThat(saved.getAttribute( GrantContextEnrichingAuthorizationService.ATTR_CORRELATION_ID)) .isEqualTo("corr-1"); - assertThat(saved.getAttribute( - GrantContextEnrichingAuthorizationService.ATTR_CUSTOMER_RESOURCE_URI)) - .isEqualTo("https://dc.example/RetailCustomer/42"); // Selected UPs round-trip via parseUuids String serialized = saved.getAttribute( @@ -109,7 +105,7 @@ void enrichesFromSessionOnFirstSave() { void sessionConsumedSingleUse() { MockHttpSession session = new MockHttpSession(); request.setSession(session); - GrantContext ctx = new GrantContext("corr-2", 1L, "FB_1", List.of(), null); + GrantContext ctx = new GrantContext("corr-2", 1L, "FB_1", List.of()); sessionStore.put(session, ctx); service.save(blankAuthorization("auth-2a")); @@ -132,7 +128,7 @@ void alreadyEnrichedNotDoubled() { MockHttpSession session = new MockHttpSession(); request.setSession(session); // Session has fresh context... - sessionStore.put(session, new GrantContext("corr-3", 1L, "FB_1", List.of(), null)); + sessionStore.put(session, new GrantContext("corr-3", 1L, "FB_1", List.of())); // ...but the incoming authorization already has the marker attribute, so the wrapper // must leave it alone. diff --git a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextSessionStoreTest.java b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextSessionStoreTest.java index a099ce21..370248c4 100644 --- a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextSessionStoreTest.java +++ b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/grant/GrantContextSessionStoreTest.java @@ -37,8 +37,7 @@ void setUp() { void putThenConsumeReturnsContextOnce() { GrantContext ctx = new GrantContext( "corr-1", 42L, "FB_1;FB_4_5", - List.of(UUID.fromString("00000000-0000-5000-8000-000000000001")), - "https://dc.example/cust/42"); + List.of(UUID.fromString("00000000-0000-5000-8000-000000000001"))); store.put(session, ctx); assertThat(store.consume(session)).isEqualTo(ctx); @@ -55,7 +54,7 @@ void consumeFreshSessionReturnsNull() { @Test @DisplayName("peek does not remove the entry") void peekDoesNotRemove() { - GrantContext ctx = new GrantContext("corr-2", 99L, "FB_1", List.of(), null); + GrantContext ctx = new GrantContext("corr-2", 99L, "FB_1", List.of()); store.put(session, ctx); assertThat(store.peek(session)).isEqualTo(ctx); diff --git a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/integration/AuthCodeFlowOrchestrationIntegrationTest.java b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/integration/AuthCodeFlowOrchestrationIntegrationTest.java index ff26e136..55efb4e0 100644 --- a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/integration/AuthCodeFlowOrchestrationIntegrationTest.java +++ b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/integration/AuthCodeFlowOrchestrationIntegrationTest.java @@ -79,7 +79,6 @@ class AuthCodeFlowOrchestrationIntegrationTest extends AbstractAuthserverIntegra private static final String REDIRECT_URI = "https://tp.example/cb"; private static final String SCOPE = "FB_1"; private static final String CUSTOMER_ID = "42"; - private static final String CUSTOMER_RESOURCE_URI = "https://dc.example/RetailCustomer/42"; @Autowired private MockMvc mockMvc; @@ -148,7 +147,6 @@ void fullAuthCodeFlowAugmentsTokenResponse() throws Exception { UUID.randomUUID().toString().replace("-", ""), // single-use nonce CUSTOMER_ID, List.of(selectedUsagePoint), - CUSTOMER_RESOURCE_URI, SignedHandoff.Return.CONSENT_ALLOW, SCOPE)); @@ -175,7 +173,7 @@ void fullAuthCodeFlowAugmentsTokenResponse() throws Exception { assertThat(code).isNotBlank(); // 3) Exchange the code at /oauth2/token -> back-channel fires, response augmented with URIs. - mockMvc.perform(post("/oauth2/token") + MvcResult tokenResult = mockMvc.perform(post("/oauth2/token") .with(httpBasic(clientId, clientSecret)) .param("grant_type", "authorization_code") .param("code", code) @@ -190,7 +188,10 @@ void fullAuthCodeFlowAugmentsTokenResponse() throws Exception { // non-standard and removed in #160. .andExpect(jsonPath("$.authorization_id").doesNotExist()) .andExpect(jsonPath("$.resource_subscription_id").doesNotExist()) - .andExpect(jsonPath("$.customer_subscription_id").doesNotExist()); + .andExpect(jsonPath("$.customer_subscription_id").doesNotExist()) + .andReturn(); + String accessToken = new com.fasterxml.jackson.databind.ObjectMapper() + .readTree(tokenResult.getResponse().getContentAsString()).get("access_token").asText(); // The back-channel was called with the customer-selection context carried through the flow. ArgumentCaptor captor = ArgumentCaptor.forClass(BackchannelRequest.class); @@ -201,7 +202,22 @@ void fullAuthCodeFlowAugmentsTokenResponse() throws Exception { assertThat(sent.grantedScope()).isEqualTo(SCOPE); assertThat(sent.retailCustomerId()).isEqualTo(42L); assertThat(sent.selectedUsagePointIds()).containsExactly(selectedUsagePoint); - assertThat(sent.customerResourceUri()).isEqualTo(CUSTOMER_RESOURCE_URI); + + // 4) Introspection (#160 follow-up): /oauth2/introspect must surface the SAME ESPI URI claims + // + active + FB-grammar scope, and must NOT carry the removed *_id fields. (Verifies the C4 + // claims actually reach introspection, not just the /oauth2/token response.) + mockMvc.perform(post("/oauth2/introspect") + .with(httpBasic(clientId, clientSecret)) + .param("token", accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.active").value(true)) + .andExpect(jsonPath("$.scope").value(SCOPE)) + .andExpect(jsonPath("$.resourceURI").value(resourceUri)) + .andExpect(jsonPath("$.authorizationURI").value(authorizationUri)) + .andExpect(jsonPath("$.customerResourceURI").value(customerResourceUri)) + .andExpect(jsonPath("$.authorization_id").doesNotExist()) + .andExpect(jsonPath("$.resource_subscription_id").doesNotExist()) + .andExpect(jsonPath("$.customer_subscription_id").doesNotExist()); } @Test diff --git a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/service/GrantBackchannelTokenCustomizerTest.java b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/service/GrantBackchannelTokenCustomizerTest.java index caadf84a..63c67297 100644 --- a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/service/GrantBackchannelTokenCustomizerTest.java +++ b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/service/GrantBackchannelTokenCustomizerTest.java @@ -74,8 +74,7 @@ void setUp() { void withGrantContextWritesClaims() { OAuth2Authorization auth = authorizationWithGrantContext( "corr-9", 42L, "FB_1;FB_4_5", - UP_1 + ";" + UP_2, - "https://dc.example/RetailCustomer/42"); + UP_1 + ";" + UP_2); when(client.provision(any())).thenReturn(new BackchannelResponse( AUTH_ID, RES_SUB_ID, CUST_SUB_ID, @@ -93,10 +92,8 @@ void withGrantContextWritesClaims() { .extracting(BackchannelRequest::correlationId, BackchannelRequest::clientId, BackchannelRequest::grantedScope, - BackchannelRequest::retailCustomerId, - BackchannelRequest::customerResourceUri) - .containsExactly("corr-9", "tp-1", "FB_1;FB_4_5", 42L, - "https://dc.example/RetailCustomer/42"); + BackchannelRequest::retailCustomerId) + .containsExactly("corr-9", "tp-1", "FB_1;FB_4_5", 42L); assertThat(sent.selectedUsagePointIds()).containsExactly(UP_1, UP_2); Map claims = ctx.getClaims().build().getClaims(); @@ -127,7 +124,7 @@ void noGrantContextSkipsBackchannel() { @DisplayName("back-channel failure surfaces as OAuth2AuthenticationException(server_error)") void backchannelFailureSurfacesAsOAuth2Error() { OAuth2Authorization auth = authorizationWithGrantContext( - "corr-fail", 7L, "FB_1", "", null); + "corr-fail", 7L, "FB_1", ""); when(client.provision(any())).thenThrow(new DataCustodianBackchannelException("DC 503")); OAuth2TokenClaimsContext ctx = buildContext(auth); @@ -144,7 +141,7 @@ void backchannelFailureSurfacesAsOAuth2Error() { @DisplayName("response with only resource URI (PII-less grant): customer claim omitted") void piiLessGrantOmitsCustomerClaim() { OAuth2Authorization auth = authorizationWithGrantContext( - "corr-pii-less", 8L, "FB_1", UP_1.toString(), null); + "corr-pii-less", 8L, "FB_1", UP_1.toString()); when(client.provision(any())).thenReturn(new BackchannelResponse( AUTH_ID, RES_SUB_ID, null, "https://dc.example/Subscription/" + RES_SUB_ID, @@ -165,7 +162,7 @@ void piiLessGrantOmitsCustomerClaim() { @DisplayName("refresh-token customization: skipped") void refreshTokenSkipped() { OAuth2Authorization auth = authorizationWithGrantContext( - "corr-ref", 1L, "FB_1", "", null); + "corr-ref", 1L, "FB_1", ""); OAuth2TokenClaimsContext ctx = OAuth2TokenClaimsContext .with(OAuth2TokenClaimsSet.builder()) @@ -199,8 +196,7 @@ private static OAuth2Authorization blankAuthorization() { } private static OAuth2Authorization authorizationWithGrantContext(String cid, long retailCustomerId, - String scope, String selectedUps, - String customerResourceUri) { + String scope, String selectedUps) { OAuth2Authorization.Builder b = OAuth2Authorization.withRegisteredClient(registeredClient()) .id("auth-" + cid) .principalName(String.valueOf(retailCustomerId)) @@ -211,10 +207,6 @@ private static OAuth2Authorization authorizationWithGrantContext(String cid, lon .attribute(GrantContextEnrichingAuthorizationService.ATTR_GRANTED_SCOPE, scope) .attribute(GrantContextEnrichingAuthorizationService.ATTR_SELECTED_USAGE_POINT_IDS, selectedUps == null ? "" : selectedUps); - if (customerResourceUri != null) { - b.attribute(GrantContextEnrichingAuthorizationService.ATTR_CUSTOMER_RESOURCE_URI, - customerResourceUri); - } return b.build(); } diff --git a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/web/delegate/AuthorizeContinueControllerTest.java b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/web/delegate/AuthorizeContinueControllerTest.java index 148232a1..ad9d6acd 100644 --- a/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/web/delegate/AuthorizeContinueControllerTest.java +++ b/openespi-authserver/src/test/java/org/greenbuttonalliance/espi/authserver/web/delegate/AuthorizeContinueControllerTest.java @@ -237,7 +237,7 @@ void expiredTokenIs400() throws Exception { Instant past = Instant.now().minusSeconds(3600); SignedHandoff.Return expired = SignedHandoff.Return.of( "corr-expired", past.minusSeconds(600), past, "nonce-x", - "42", List.of(), "https://dc.example/cust/1", + "42", List.of(), SignedHandoff.Return.CONSENT_ALLOW, "FB_1"); String token = codec.encode(expired); @@ -293,7 +293,6 @@ private static SignedHandoff.Return allowReturn(String cid, String approvedScope UUID.randomUUID().toString().replace("-", ""), "42", List.of(), - "https://dc.example/RetailCustomer/1", SignedHandoff.Return.CONSENT_ALLOW, approvedScope); } @@ -305,7 +304,6 @@ private static SignedHandoff.Return denyReturn(String cid) { UUID.randomUUID().toString().replace("-", ""), "42", List.of(), - "https://dc.example/RetailCustomer/1", SignedHandoff.Return.CONSENT_DENY, null); } diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/SubscriptionProvisioningService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/SubscriptionProvisioningService.java index 0de71b5b..429ae9e5 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/SubscriptionProvisioningService.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/SubscriptionProvisioningService.java @@ -33,8 +33,8 @@ *
    *
  • Always creates exactly one energy Subscription (the {@code resource_uri} target).
  • *
  • Creates a customer/PII Subscription only when the granted scope includes a - * Customer/PII Function Block (FB 54–62). The {@code customer_resource_uri} from the - * command is stored verbatim on the Authorization aggregate.
  • + * Customer/PII Function Block (FB 54–62). DC builds the canonical + * {@code customerResourceURI} itself and stores it on the Authorization aggregate. *
  • Persistence happens through the Authorization aggregate; Subscriptions are never saved * independently.
  • *
@@ -70,16 +70,13 @@ public interface SubscriptionProvisioningService { * ids; tracked separately from the UUID5 migration) * @param selectedUsagePointIds usage points the customer chose to share with this TP; may be * empty for grants that include only customer/PII scope - * @param customerResourceUri absolute URI for the customer/PII resource the TP may GET; - * must be present iff the scope includes a Customer/PII FB */ record SubscriptionProvisionCommand( String correlationId, String clientId, String grantedScope, Long retailCustomerId, - List selectedUsagePointIds, - String customerResourceUri + List selectedUsagePointIds ) {} /** diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/SubscriptionProvisioningServiceImpl.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/SubscriptionProvisioningServiceImpl.java index 1062a1e1..a4d9e745 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/SubscriptionProvisioningServiceImpl.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/SubscriptionProvisioningServiceImpl.java @@ -92,14 +92,6 @@ public SubscriptionProvisionResult provisionFromGrant(SubscriptionProvisionComma throw new IllegalArgumentException( "Grant must include at least one selected usage point OR a Customer/PII FB scope"); } - if (includesPii && (command.customerResourceUri() == null || command.customerResourceUri().isBlank())) { - throw new IllegalArgumentException( - "customer_resource_uri is required when scope includes a Customer/PII FB (54-62)"); - } - if (!includesPii && command.customerResourceUri() != null && !command.customerResourceUri().isBlank()) { - throw new IllegalArgumentException( - "customer_resource_uri must be absent when scope does not include a Customer/PII FB"); - } AuthorizationEntity authorization = newAuthorization(command, application, customer, includesPii); @@ -173,9 +165,7 @@ private AuthorizationEntity newAuthorization(SubscriptionProvisionCommand comman authorization.setStatus(AuthorizationEntity.STATUS_ACTIVE); if (includesPii) { // Build the canonical ESPI Batch/RetailCustomer URI from the retail-customer id DC already - // holds, rather than trusting the round-tripped handoff value (#160). The handoff's - // customer_resource_uri is now vestigial — removing it from the back-channel request/handoff - // is a follow-up structural cleanup. + // holds (#160). DC owns this value end-to-end; it is never round-tripped through the AS. authorization.setCustomerResourceURI( EspiBatchUri.batchRetailCustomer(resourceBaseUri, command.retailCustomerId())); } diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/SubscriptionProvisioningServiceImplTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/SubscriptionProvisioningServiceImplTest.java index a5730721..0a97b2b6 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/SubscriptionProvisioningServiceImplTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/SubscriptionProvisioningServiceImplTest.java @@ -60,7 +60,6 @@ class SubscriptionProvisioningServiceImplTest { private static final String CLIENT_ID = "test-tp"; private static final String CORRELATION_ID = "corr-123"; private static final Long CUSTOMER_ID = 42L; - private static final String PII_CUSTOMER_URI = BASE_URI + "/RetailCustomer/42/Customer/xyz"; @Mock private AuthorizationRepository authorizationRepository; @Mock private ApplicationInformationRepository applicationInformationRepository; @@ -98,7 +97,7 @@ void energyOnlyGrant_createsResourceSubscriptionAndNoPiiSubscription() { when(applicationInformationRepository.findByClientId(CLIENT_ID)).thenReturn(Optional.of(application)); SubscriptionProvisionResult result = service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4_5_15", CUSTOMER_ID, List.of(upId), null)); + CORRELATION_ID, CLIENT_ID, "FB=4_5_15", CUSTOMER_ID, List.of(upId))); assertThat(result.resourceSubscriptionId()).isNotNull(); assertThat(result.customerSubscriptionId()).isNull(); @@ -130,7 +129,7 @@ void grantWithPiiScope_createsBothSubscriptions() { when(applicationInformationRepository.findByClientId(CLIENT_ID)).thenReturn(Optional.of(application)); SubscriptionProvisionResult result = service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4_5_15_54", CUSTOMER_ID, List.of(upId), PII_CUSTOMER_URI)); + CORRELATION_ID, CLIENT_ID, "FB=4_5_15_54", CUSTOMER_ID, List.of(upId))); assertThat(result.resourceSubscriptionId()).isNotNull(); assertThat(result.customerSubscriptionId()).isNotNull(); @@ -147,7 +146,7 @@ void piiOnlyGrant_createsOnlyCustomerSubscription_noResourceUri() { when(applicationInformationRepository.findByClientId(CLIENT_ID)).thenReturn(Optional.of(application)); SubscriptionProvisionResult result = service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4_54", CUSTOMER_ID, List.of(), PII_CUSTOMER_URI)); + CORRELATION_ID, CLIENT_ID, "FB=4_54", CUSTOMER_ID, List.of())); assertThat(result.resourceSubscriptionId()).isNull(); assertThat(result.resourceUri()).isNull(); @@ -165,41 +164,17 @@ void emptyGrant_isRejected() { when(applicationInformationRepository.findByClientId(CLIENT_ID)).thenReturn(Optional.of(application)); assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4", CUSTOMER_ID, List.of(), null))) + CORRELATION_ID, CLIENT_ID, "FB=4", CUSTOMER_ID, List.of()))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("at least one selected usage point OR a Customer/PII"); } - @Test - void piiScopeWithoutCustomerUri_isRejected() { - UUID upId = stubUsagePoint(customer); - when(retailCustomerRepository.findById(CUSTOMER_ID)).thenReturn(Optional.of(customer)); - when(applicationInformationRepository.findByClientId(CLIENT_ID)).thenReturn(Optional.of(application)); - - assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4_54", CUSTOMER_ID, List.of(upId), null))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("customer_resource_uri is required"); - } - - @Test - void customerUriWithoutPiiScope_isRejected() { - UUID upId = stubUsagePoint(customer); - when(retailCustomerRepository.findById(CUSTOMER_ID)).thenReturn(Optional.of(customer)); - when(applicationInformationRepository.findByClientId(CLIENT_ID)).thenReturn(Optional.of(application)); - - assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4_5_15", CUSTOMER_ID, List.of(upId), PII_CUSTOMER_URI))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("customer_resource_uri must be absent"); - } - @Test void unknownClientId_isRejected() { when(applicationInformationRepository.findByClientId("ghost")).thenReturn(Optional.empty()); assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, "ghost", "FB=4_5_15", CUSTOMER_ID, List.of(UUID.randomUUID()), null))) + CORRELATION_ID, "ghost", "FB=4_5_15", CUSTOMER_ID, List.of(UUID.randomUUID())))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Unknown client_id"); } @@ -210,7 +185,7 @@ void unknownRetailCustomer_isRejected() { when(retailCustomerRepository.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4_5_15", 999L, List.of(UUID.randomUUID()), null))) + CORRELATION_ID, CLIENT_ID, "FB=4_5_15", 999L, List.of(UUID.randomUUID())))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Unknown retail_customer_id"); } @@ -229,7 +204,7 @@ void usagePointBelongingToDifferentCustomer_isRejected() { when(usagePointRepository.findById(upId)).thenReturn(Optional.of(foreign)); assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4_5_15", CUSTOMER_ID, List.of(upId), null))) + CORRELATION_ID, CLIENT_ID, "FB=4_5_15", CUSTOMER_ID, List.of(upId)))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("does not belong to retail_customer_id"); } @@ -242,7 +217,7 @@ void unknownUsagePoint_isRejected() { when(usagePointRepository.findById(upId)).thenReturn(Optional.empty()); assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4_5_15", CUSTOMER_ID, List.of(upId), null))) + CORRELATION_ID, CLIENT_ID, "FB=4_5_15", CUSTOMER_ID, List.of(upId)))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Unknown usage_point_id"); } @@ -253,22 +228,22 @@ void unparseableScope_isRejected() { when(retailCustomerRepository.findById(CUSTOMER_ID)).thenReturn(Optional.of(customer)); assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=not_a_number", CUSTOMER_ID, List.of(), null))) + CORRELATION_ID, CLIENT_ID, "FB=not_a_number", CUSTOMER_ID, List.of()))) .isInstanceOf(IllegalArgumentException.class); } @Test void blankRequiredFields_areRejected() { assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, "", "FB=4_5_15", CUSTOMER_ID, List.of(), null))) + CORRELATION_ID, "", "FB=4_5_15", CUSTOMER_ID, List.of()))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("client_id"); assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, " ", CUSTOMER_ID, List.of(), null))) + CORRELATION_ID, CLIENT_ID, " ", CUSTOMER_ID, List.of()))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("granted_scope"); assertThatThrownBy(() -> service.provisionFromGrant(new SubscriptionProvisionCommand( - CORRELATION_ID, CLIENT_ID, "FB=4_5_15", null, List.of(), null))) + CORRELATION_ID, CLIENT_ID, "FB=4_5_15", null, List.of()))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("retail_customer_id"); } diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/authorize/AuthorizeScreenController.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/authorize/AuthorizeScreenController.java index b1f948c9..5c5f6537 100644 --- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/authorize/AuthorizeScreenController.java +++ b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/authorize/AuthorizeScreenController.java @@ -144,8 +144,6 @@ public String submit(@RequestParam("handoff") String handoffToken, nonceService.generate(), String.valueOf(customer.getId()), List.copyOf(selectedUps), - /* customerResourceUri */ null, // DC produces the actual URI in PR B2's flow; the - // Return handoff just signals approvalshapes consent, approvedScope); diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/SubscriptionProvisioningController.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/SubscriptionProvisioningController.java index afcf8578..694c7f9c 100644 --- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/SubscriptionProvisioningController.java +++ b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/SubscriptionProvisioningController.java @@ -61,8 +61,7 @@ public ResponseEntity provision( request.clientId(), request.grantedScope(), request.retailCustomerId(), - request.selectedUsagePointIds(), - request.customerResourceUri())); + request.selectedUsagePointIds())); SubscriptionProvisionResponse body = new SubscriptionProvisionResponse( result.authorizationId(), diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/dto/SubscriptionProvisionRequest.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/dto/SubscriptionProvisionRequest.java index 6b7afd77..6a6220b0 100644 --- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/dto/SubscriptionProvisionRequest.java +++ b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/dto/SubscriptionProvisionRequest.java @@ -38,8 +38,6 @@ * @param retailCustomerId DC primary key of the authenticated customer * @param selectedUsagePointIds UUIDs of the usage points the customer chose to share with this TP; * may be empty for grants that include only customer/PII scope - * @param customerResourceUri absolute URI for the customer/PII resource; must be present iff the - * scope includes a Customer/PII FB (54–62) */ public record SubscriptionProvisionRequest( @JsonProperty("correlation_id") @@ -55,8 +53,5 @@ public record SubscriptionProvisionRequest( @NotNull Long retailCustomerId, @JsonProperty("selected_usage_point_ids") - List selectedUsagePointIds, - - @JsonProperty("customer_resource_uri") - String customerResourceUri + List selectedUsagePointIds ) {} diff --git a/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/SubscriptionProvisioningControllerTest.java b/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/SubscriptionProvisioningControllerTest.java index 02e6fb27..f17abbc4 100644 --- a/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/SubscriptionProvisioningControllerTest.java +++ b/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/internal/backchannel/SubscriptionProvisioningControllerTest.java @@ -189,8 +189,7 @@ void requestDto_serializesUnderscoreCaseRoundTrip() throws Exception { "client_id": "test-tp", "granted_scope": "FB=4", "retail_customer_id": 42, - "selected_usage_point_ids": [], - "customer_resource_uri": "https://x/Customer/1" + "selected_usage_point_ids": [] } """; var parsed = objectMapper.readValue(json, @@ -200,6 +199,5 @@ void requestDto_serializesUnderscoreCaseRoundTrip() throws Exception { assertThat(parsed.grantedScope()).isEqualTo("FB=4"); assertThat(parsed.retailCustomerId()).isEqualTo(42L); assertThat(parsed.selectedUsagePointIds()).isEqualTo(List.of()); - assertThat(parsed.customerResourceUri()).isEqualTo("https://x/Customer/1"); } } diff --git a/openespi-handoff/src/main/java/org/greenbuttonalliance/espi/handoff/SignedHandoff.java b/openespi-handoff/src/main/java/org/greenbuttonalliance/espi/handoff/SignedHandoff.java index a5223558..83b9c3bf 100644 --- a/openespi-handoff/src/main/java/org/greenbuttonalliance/espi/handoff/SignedHandoff.java +++ b/openespi-handoff/src/main/java/org/greenbuttonalliance/espi/handoff/SignedHandoff.java @@ -121,8 +121,6 @@ public static Outbound of(String correlationId, Instant issuedAt, Instant expire * @param principal DC's authenticated retail-customer identifier * @param selectedUsagePointIds usage points the customer chose to share; empty list for a * PII-only or denied grant - * @param customerResourceUri absolute URI of the customer/PII resource the customer - * approved sharing, or {@code null} if not granted * @param consent {@code "allow"} if the customer granted; {@code "deny"} if not * @param approvedScope the effective scope after the customer's checkbox decisions * — subset of the originally-requested scope. The AS @@ -144,7 +142,6 @@ record Return( @JsonProperty("nonce") String nonce, @JsonProperty("sub") String principal, @JsonProperty("up") List selectedUsagePointIds, - @JsonProperty("cust_uri") String customerResourceUri, @JsonProperty("consent") String consent, @JsonProperty("approved_scope") String approvedScope ) implements SignedHandoff { @@ -155,9 +152,9 @@ record Return( /** Convenience factory that fills in {@code version} and {@code direction}. */ public static Return of(String correlationId, Instant issuedAt, Instant expiresAt, String nonce, String principal, List selectedUsagePointIds, - String customerResourceUri, String consent, String approvedScope) { + String consent, String approvedScope) { return new Return(CURRENT_VERSION, DIRECTION_RETURN, correlationId, issuedAt, expiresAt, - nonce, principal, selectedUsagePointIds, customerResourceUri, consent, approvedScope); + nonce, principal, selectedUsagePointIds, consent, approvedScope); } } } diff --git a/openespi-handoff/src/test/java/org/greenbuttonalliance/espi/handoff/SignedHandoffCodecTest.java b/openespi-handoff/src/test/java/org/greenbuttonalliance/espi/handoff/SignedHandoffCodecTest.java index 8fe8e334..bbb6852c 100644 --- a/openespi-handoff/src/test/java/org/greenbuttonalliance/espi/handoff/SignedHandoffCodecTest.java +++ b/openespi-handoff/src/test/java/org/greenbuttonalliance/espi/handoff/SignedHandoffCodecTest.java @@ -67,7 +67,6 @@ void return_roundTripsThroughEncodeAndDecode() { "nonce-2", "customer-42", List.of(UUID.randomUUID(), UUID.randomUUID()), - "https://dc.example.com/.../Customer/abc", SignedHandoff.Return.CONSENT_ALLOW, "FB=4_5_15;IntervalDuration=3600"); @@ -78,7 +77,7 @@ void return_roundTripsThroughEncodeAndDecode() { } @Test - void return_withDenyAndNoCustomerUri_roundTrips() { + void return_withDeny_roundTrips() { SignedHandoff.Return original = SignedHandoff.Return.of( "corr-2", NOW, @@ -86,7 +85,6 @@ void return_withDenyAndNoCustomerUri_roundTrips() { "nonce-3", "customer-42", List.of(), - null, SignedHandoff.Return.CONSENT_DENY, null);