Skip to content

Phase 2.0 — Stand up auth-server in dev with opaque tokens + subscription-bound introspection (Phase 2 precursor) #122

@dfcoffin

Description

@dfcoffin

Precursor to #119 Phase 2.
Phase 2 (subscription scoping / data-leakage fix) cannot ship until the
authorization server is running, issues opaque tokens per ESPI 4.0, and exposes
a subscription_id-bearing introspection response. Until then every Phase 2a
design choice is unverifiable against a hypothetical token shape.

See the design analysis posted as a comment on #119:
#119 (comment)

Why this is blocking

ESPI 4.0 requires opaque (reference) access tokens — JWTs are not permitted by
the spec. A resource server cannot read claims from an opaque token; it must
call RFC 7662 token introspection to learn what the token grants. Phase 2's
entire OpaqueTokenIntrospector / ApiAccessValidator design is built around
that introspection response. Without a running auth-server returning the
expected response shape (including subscription_id), no Phase 2 code can be
end-to-end tested.

Shipping a security fix that can't be proven to work against a real token is
the worst outcome for an IDOR / data-leakage fix.

Acceptance criteria

Phase 2.0 is complete when one integration test boots both modules, mints a
real token via the authorization code flow, calls a data-custodian endpoint,
and asserts the principal carries subscription_id. Until that test passes,
no Phase 2a code will be merged.

Checklist

Step 1 — Configuration audit (read-only)

  • Find RegisteredClient / OAuth2AuthorizationServerConfiguration; confirm tokenSettings() format (REFERENCE vs SELF_CONTAINED)
  • Confirm /oauth2/introspect is enabled and properly authenticated (client credentials, not bearer)
  • Find any OpaqueTokenIntrospector / OAuth2TokenCustomizer beans
  • Inspect OAuth2Authorization persistence; locate where subscription binding could be stored at grant time
  • Verify oauth2_authorization table exists; check for custom columns or join tables linking authorizations to subscriptions
  • Post audit findings as an issue comment (what is configured vs what Phase 2 needs)

Step 2 — Local dev environment

  • Bring up MySQL (dev-mysql profile)
  • Boot openespi-authserver with dev-mysql; resolve any Flyway migration drift
  • Verify /oauth2/.well-known/oauth-authorization-server returns a valid metadata document
  • Register at least one RegisteredClient (third-party app) and one retail customer via seed data

Step 3 — Verify token format

  • Run authorization code flow end-to-end (manual curl / Postman)
  • Inspect the issued access token: opaque (short, random) vs JWT (eyJ...)
  • If JWT, switch RegisteredClient to OAuth2TokenFormat.REFERENCE and re-test

Step 4 — Add subscription_id to introspection response (the wildcard)

  • Try Approach B first: customize auth-server OAuth2TokenIntrospectionAuthenticationProvider / converter to add subscription_id to the introspection claims
  • If Spring Authorization Server doesn't expose a clean hook, fall back to Approach A: custom resource-server OpaqueTokenIntrospector that enriches principal via DB lookup
  • Document which approach was used and why

Step 5 — Wire data-custodian as resource server

  • Add spring.security.oauth2.resourceserver.opaquetoken.* config pointing at auth-server introspection URL
  • Register resource-server as a RegisteredClient with client_credentials grant + introspection authority
  • Disable any existing JWT resource-server config (mutually exclusive on the same filter chain)

Step 6 — End-to-end smoke test

  • Write one integration test that boots both modules, mints a real token via authorization code flow, calls a data-custodian endpoint, asserts the principal carries subscription_id
  • Test passes → Phase 2.0 complete → Phase 2a unblocked

Non-goals (deferred)

  • Scope-grammar alignment — Phase 7. Phase 2 keeps the existing
    SCOPE_FB_* shorthand and pins parsing behind a single helper for later
    swap.
  • JSON output — Phase 3. Cosmetic produces = JSON declarations stay as-is.
  • Phase 2a/2b/2c code — does not start until step 6 passes.

Estimate

1–2 working days for a senior Spring developer who knows Spring Authorization
Server. Step 4 is the wildcard (Spring Authorization Server's
OAuth2TokenIntrospectionAuthenticationProvider extensibility is the unknown).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    ESPI 4.0Touches the NAESB ESPI 4.0 implementationblockingBlocks other work or CIinfrastructureCI, build, deployment, or developer tooling

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions