Repository navigation
Description
Summary
Per NAESB REQ.21, a Third Party reads, updates or deletes its own ApplicationInformation at the Data Custodian only with its registration access token:
- the initial registration access token is returned when the Third Party registers, following RFC 7591 (
registration_access_token, withregistration_client_uri), in line with OAuth 2.1; - an expired registration access token is replaced by a new one that the Third Party obtains through the client_credentials grant with the pre-defined scope
FB=36_40. That scope is used only for this renewal.
Today neither half works, and the DC accepts the wrong token for ApplicationInformation.
Current state (main, 4339f18)
- AS, initial token: dynamic registration (
POST /oauth2/register, Spike: restore dynamic client registration (RFC 7591) with opaque tokens only — no OIDC/JWK #196/feat(#196): RFC 7591 client registration with opaque tokens only; replace hand-written /connect/register #207) uses Spring Authorization Server's plain RFC 7591 support. It issues noregistration_access_tokenand has no client configuration endpoint (see the note inAuthorizationServerConfig). Only Spring's OIDC client registration issues one, together with a client read endpoint, and OIDC was removed in Strip unused OIDC/JWK/JWT from auth-server (ESPI is opaque-only) #134. - AS, renewal: nothing in the AS defines or validates
FB=36_40, and no seeded client offers client_credentials with it.EspiTokenCustomizerstill refers to aROLE_TP_REGISTRATION→/ApplicationInformation/**mapping from the JWT era. - DC:
ApplicationInformationRESTControlleracceptsDataCustodian_Admin_AccessorThirdParty_Admin_Access. fix(#247): export DC collection endpoints as Atom feeds instead of a bare List #250 scopedThirdParty_Admin_Accessto the caller's own record (AdminCaller,client_idmatch; private advisory filed), but under REQ.21ThirdParty_Admin_Accessshould not reach ApplicationInformation at all. The original ownership check lived in the legacyResourceValidationFilter(ROLE_TP_REGISTRATION: GET/PUT/DELETE its own ApplicationInformation only), which is no longer registered. - DC token parsing:
EspiScopeOpaqueTokenIntrospectorturnsFB=36_40into authoritiesFB_36andFB_40.FunctionBlockdefinesFB_40("Offline Authorization (Energy Usage)") but notFB_36(categoryOf(36)is UNKNOWN).
Proposed scope
-
AS: issue the initial registration access token on RFC 7591 registration. Return
registration_access_tokenandregistration_client_uriin the registration response, as an opaque token bound to the new client. Options:- wrap or replace the registration
AuthenticationProviderto mint the token and add both parameters to the response; - selectively bring back the OIDC registration support, which provides the token and the read endpoint, accepting the OIDC surface removed in Strip unused OIDC/JWK/JWT from auth-server (ESPI is opaque-only) #134.
Decide where
registration_client_uripoints: in ESPI it is the DC's.../espi/1_1/resource/ApplicationInformation/{id}, which ties into how registration creates the DC's ApplicationInformation (back-channel). - wrap or replace the registration
-
AS: renewal with
FB=36_40. Allow the client_credentials grant with scopeFB=36_40for registered Third Party clients, and only for that scope, to obtain a new registration access token when the previous one has expired. Define that token's lifetime and whether issuing a new one revokes the old one. -
DC: accept only the right tokens on ApplicationInformation.
DataCustodian_Admin_Access: all records.- A registration access token (identified by its scope or authority, e.g.
FB_36, as the design settles): only the record whoseclientIdmatches the token'sclient_id, for GET, and for PUT/DELETE once write endpoints return (chore(#119): defer ESPI resource CRUD writes — delete write/admin controllers #166). - Remove
ThirdParty_Admin_Accessfrom the ApplicationInformation endpoints. It remains valid for Authorization, Batch/Bulk and the other Third Party admin resources.
-
Common: add
FB_36toFunctionBlockwith its REQ.21 name and category, so the scope is recognised rather than UNKNOWN. -
Tests:
- registration returns
registration_access_token, and it works against the Third Party's own ApplicationInformation; - it is rejected for another Third Party's record (404) and for other resources;
- a
FB=36_40client_credentials token is only issued to registered clients, and only renews registration access; ThirdParty_Admin_Accessgets 403 on ApplicationInformation;- an expired registration access token gets 401.
- registration returns
References
- RFC 7591 (Dynamic Client Registration), RFC 7592 (client configuration management), OAuth 2.1
- Spike: restore dynamic client registration (RFC 7591) with opaque tokens only — no OIDC/JWK #196 / feat(#196): RFC 7591 client registration with opaque tokens only; replace hand-written /connect/register #207 (RFC 7591 registration with opaque tokens), Strip unused OIDC/JWK/JWT from auth-server (ESPI is opaque-only) #134 (OIDC removed), DC feed (collection) endpoints return 500: a bare List is passed to JAXB instead of an Atom feed #247 / fix(#247): export DC collection endpoints as Atom feeds instead of a bare List #250 (feeds, row-level scope), chore(#119): defer ESPI resource CRUD writes — delete write/admin controllers #166 (write endpoints deferred)
🤖 Generated with Claude Code
Activity
dfcoffin commentedon Nov 14, 2026
Decision: extend the RFC 7591 flow (do not bring back OIDC registration)
Chosen approach: issue the registration access token by extending our existing RFC 7591 registration. Do not re-enable Spring's OIDC client registration.
Why
registration_client_uripoints at the AS's own client-read endpoint, whereas in ESPI the registration access token is used at the Data Custodian's.../espi/1_1/resource/ApplicationInformation/{id}. The URI, scope and ApplicationInformation binding would need custom work either way.registration_access_token/registration_client_uriare exactly the members RFC 7592 defines. This is the OAuth 2.1 route.OAuth2ClientRegistrationAuthenticationProviderby hand onclientRegistrationSecurityFilterChain(feat(#196): RFC 7591 client registration with opaque tokens only; replace hand-written /connect/register #207). Spring Security 7.1.2 exposessetClientRegistrationConverter(...)andsetAuthenticationValidator(...)on that provider, and tokens are already opaque references inOAuth2AuthorizationService, which the DC validates by introspection. No new token format and no new DC validation path.FB=36_40renewal token, so lifetime, client binding and revocation of the previous token are implemented once.Implementation outline
OAuth2ClientRegistrationAuthenticationProvider. It calls Spring's provider to register the client, then mints an opaque registration access token bound to the new client (token generator +OAuth2AuthorizationService), and returns the registration response withregistration_access_tokenandregistration_client_uriadded. Minting happens in this processing step rather than in the response converter, because it has a side effect.FB=36_40, issued only to registered Third Party clients and only for renewing the registration access token.DataCustodian_Admin_Access(all records) or the registration access token (own record only);ThirdParty_Admin_Accessis removed from this endpoint.Spike: questions to answer first
OAuth2ClientRegistrationHttpMessageConverterwrite the added members in the registration response?registration_client_uri) come from at registration time: the AS→DC back-channel, or an id agreed up front?🤖 Generated with Claude Code
dfcoffin commentedon Nov 14, 2026
Spike results (branch
spike/251-registration-access-token)Test:
openespi-authserver/src/test/java/.../config/RegistrationAccessTokenSpikeTest.java. A unit test with no Spring context and no Docker.Q1. Does Spring write the added members? Yes. Through Spring Security 7.1.2's real
OAuth2ClientRegistrationEndpointFilterand itsOAuth2ClientRegistrationHttpMessageConverter, the response is HTTP 201 with:{ "client_id" : "tp-registered-client", "client_secret" : "generated-client-secret", "client_name" : "Spike TP", "redirect_uris" : [ "https://tp.example/cb" ], "grant_types" : [ "authorization_code" ], "response_types" : [ "code" ], "token_endpoint_auth_method" : "client_secret_basic", "scope" : "FB=4_5_15;IntervalDuration=3600;BlockDuration=monthly;HistoryLength=13", "registration_access_token" : "rat-opaque-tp-registered-client", "registration_client_uri" : "https://dc.example/DataCustodian/espi/1_1/resource/ApplicationInformation/2b2c3f8e-…", "client_secret_expires_at" : 0 }Q2. Can a decorator replace the result safely? Yes. The filter writes the response from
OAuth2ClientRegistrationAuthenticationToken.getClientRegistration()with no re-validation. The decorator rebuilds it withOAuth2ClientRegistration.withClaims(original).claim(...)and returns a new token with the same principal; every member Spring produced is preserved,client_secretincluded.Q3. Where does the ApplicationInformation id come from? Nowhere yet. This is a gap to close in #251. Registering a Third Party at the AS does not create an ApplicationInformation record at the Data Custodian:
EspiTokenCustomizer);POST /internal/backchannel/v1/subscriptions);ApplicationInformationService.save.So #251 also needs an AS→DC back-channel step that creates the ApplicationInformation at registration time and returns its id, which then becomes
registration_client_uri. It should follow the existing Subscription back-channel pattern: HTTP Basic,/internal/**chain.Next steps
OAuth2AuthorizationService), and wire it intoclientRegistrationSecurityFilterChain.POST /internal/backchannel/v1/application-information, called during registration, returning the ApplicationInformation id.FB=36_40client_credentials renewal, restricted to registered Third Party clients.DataCustodian_Admin_Accessor the registration access token (own record only); removeThirdParty_Admin_Access.FB_36inFunctionBlock.🤖 Generated with Claude Code
dfcoffin commentedon Nov 14, 2026
Scope update: a registered Third Party's three tokens, and a per-grant scope rule
Agreed with the project lead: a production AS starts with no Third Party clients, and each Third Party onboards through RFC 7591. A registered Third Party then has exactly three kinds of token, and each one is accepted only by its own APIs:
client_credentials, scopeThirdParty_Admin_AccessBatch/Bulk) APIs, for that Third Party's own recordsclient_credentialswith scopeFB=36_40registration_client_uri)authorization_codewith the customer's consent, ESPI function-block scopeAdded to this issue's scope
client_credentials. Today's ESPI registration validator allows onlyauthorization_code(+refresh_token). A registered Third Party must also receiveclient_credentials, with its admin scopes (ThirdParty_Admin_Access,FB=36_40) assigned by the server, not requested freely.FB=4_5_15andclient_credentialscould obtain a customer-data token without any customer's consent. Add a client_credentials authentication validator that allows onlyThirdParty_Admin_AccessandFB=36_40, and make sureauthorization_codenever yields those admin scopes.DataCustodian_Admin_Access. The client access token is removed there; since fix(#247): export DC collection endpoints as Atom feeds instead of a bare List #250 it is limited to the Third Party's own record.invalid_scope;ThirdParty_Admin_Access→ OK;ThirdParty_Admin_Access→invalid_scope;The Bulk API itself is tracked in a separate issue (filed alongside this comment); the client access token's
resourceURI/authorizationURIare in #202.🤖 Generated with Claude Code
fix(authserver): make the per-grant scope rules allowlists (#251)
test(#251): request an administrative scope in the client_credentials…