Skip to content

Registration access token: issue it on RFC 7591 registration, renew via FB=36_40, and require it for ApplicationInformation #251

Open

Description

@dfcoffin

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, with registration_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)

Proposed scope

  1. AS: issue the initial registration access token on RFC 7591 registration. Return registration_access_token and registration_client_uri in the registration response, as an opaque token bound to the new client. Options:

    Decide where registration_client_uri points: 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).

  2. AS: renewal with FB=36_40. Allow the client_credentials grant with scope FB=36_40 for 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.

  3. 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 whose clientId matches the token's client_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_Access from the ApplicationInformation endpoints. It remains valid for Authorization, Batch/Bulk and the other Third Party admin resources.
  4. Common: add FB_36 to FunctionBlock with its REQ.21 name and category, so the scope is recognised rather than UNKNOWN.

  5. 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_40 client_credentials token is only issued to registered clients, and only renews registration access;
    • ThirdParty_Admin_Access gets 403 on ApplicationInformation;
    • an expired registration access token gets 401.

References

🤖 Generated with Claude Code

Activity

  1. dfcoffin commented on Nov 14, 2026

    @dfcoffin
    ContributorAuthor

    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

    1. OIDC was removed on purpose (Strip unused OIDC/JWK/JWT from auth-server (ESPI is opaque-only) #134). ESPI uses no ID tokens and no UserInfo, and issues opaque tokens only. Spring's OIDC registration endpoint only works with the OIDC configurer enabled, which brings back the OpenID discovery and OIDC metadata surface that we would then have to restrict and explain to CMD certification.
    2. The OIDC endpoint doesn't match ESPI. Its registration_client_uri points 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.
    3. Standards fit. RFC 7591 §3.2.1 allows extra response members, and registration_access_token / registration_client_uri are exactly the members RFC 7592 defines. This is the OAuth 2.1 route.
    4. The codebase is built for it. The AS already wires OAuth2ClientRegistrationAuthenticationProvider by hand on clientRegistrationSecurityFilterChain (feat(#196): RFC 7591 client registration with opaque tokens only; replace hand-written /connect/register #207). Spring Security 7.1.2 exposes setClientRegistrationConverter(...) and setAuthenticationValidator(...) on that provider, and tokens are already opaque references in OAuth2AuthorizationService, which the DC validates by introspection. No new token format and no new DC validation path.
    5. One mechanism for issuing and renewing. The same component mints the initial token at registration and the FB=36_40 renewal token, so lifetime, client binding and revocation of the previous token are implemented once.
    6. Easy to replace later. If Spring adds RFC 7592 support for plain OAuth registration, the decorator can be swapped for the built-in version.

    Implementation outline

    • A decorator around 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 with registration_access_token and registration_client_uri added. Minting happens in this processing step rather than in the response converter, because it has a side effect.
    • Renewal: client_credentials with scope FB=36_40, issued only to registered Third Party clients and only for renewing the registration access token.
    • DC: ApplicationInformation accepts DataCustodian_Admin_Access (all records) or the registration access token (own record only); ThirdParty_Admin_Access is removed from this endpoint.

    Spike: questions to answer first

    1. Does Spring's OAuth2ClientRegistrationHttpMessageConverter write the added members in the registration response?
    2. Can the decorator replace the registration result without Spring re-validating or dropping the added members?
    3. Where does the ApplicationInformation id (for registration_client_uri) come from at registration time: the AS→DC back-channel, or an id agreed up front?

    🤖 Generated with Claude Code

  2. dfcoffin commented on Nov 14, 2026

    @dfcoffin
    ContributorAuthor

    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 OAuth2ClientRegistrationEndpointFilter and its OAuth2ClientRegistrationHttpMessageConverter, 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 with OAuth2ClientRegistration.withClaims(original).claim(...) and returns a new token with the same principal; every member Spring produced is preserved, client_secret included.

    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:

    • the AS has no ApplicationInformation code (only a stale reference in EspiTokenCustomizer);
    • the DC back-channel only provisions Subscriptions (POST /internal/backchannel/v1/subscriptions);
    • nothing in the DC calls 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

    1. Production decorator: mint an opaque registration access token bound to the new client (token generator + OAuth2AuthorizationService), and wire it into clientRegistrationSecurityFilterChain.
    2. Back-channel POST /internal/backchannel/v1/application-information, called during registration, returning the ApplicationInformation id.
    3. FB=36_40 client_credentials renewal, restricted to registered Third Party clients.
    4. DC ApplicationInformation: accept DataCustodian_Admin_Access or the registration access token (own record only); remove ThirdParty_Admin_Access.
    5. FB_36 in FunctionBlock.

    🤖 Generated with Claude Code

  3. dfcoffin commented on Nov 14, 2026

    @dfcoffin
    ContributorAuthor

    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:

    Token Obtained by Accepted only by
    Client access token client_credentials, scope ThirdParty_Admin_Access ESPI Authorization APIs and Bulk (Batch/Bulk) APIs, for that Third Party's own records
    Registration access token the RFC 7591 registration response; after expiry, client_credentials with scope FB=36_40 its own ApplicationInformation (registration_client_uri)
    Customer access token authorization_code with the customer's consent, ESPI function-block scope that customer's ESPI resources

    Added to this issue's scope

    1. Registration grants client_credentials. Today's ESPI registration validator allows only authorization_code (+ refresh_token). A registered Third Party must also receive client_credentials, with its admin scopes (ThirdParty_Admin_Access, FB=36_40) assigned by the server, not requested freely.
    2. Per-grant scope enforcement (security). Spring checks a requested scope against the client's whole registered scope set, regardless of grant type. Without an extra check, a Third Party registered for FB=4_5_15 and client_credentials could obtain a customer-data token without any customer's consent. Add a client_credentials authentication validator that allows only ThirdParty_Admin_Access and FB=36_40, and make sure authorization_code never yields those admin scopes.
    3. DC ApplicationInformation accepts only the registration access token (own record) and 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.
    4. Tests:
      • client_credentials requesting an FB scope → invalid_scope;
      • client_credentials requesting ThirdParty_Admin_Access → OK;
      • authorization_code requesting ThirdParty_Admin_Access → invalid_scope;
      • each token type is rejected by the other two token types' APIs.

    The Bulk API itself is tracked in a separate issue (filed alongside this comment); the client access token's resourceURI / authorizationURI are in #202.

    🤖 Generated with Claude Code

  4. added 2 commits that reference this issue on Nov 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions