From 29930614e3df956f96adcb302a4251a030658123 Mon Sep 17 00:00:00 2001 From: "Donald F. Coffin" Date: Sun, 24 May 2026 23:56:30 -0400 Subject: [PATCH] fix(authserver): six pre-existing defects blocking dev-mysql boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovered during Phase 2.0 auth-server bring-up (#122). The auth-server had not been booted clean against a fresh MySQL in living memory; static audit only surfaced these defects under actual runtime. Six independent blockers patched here; one more (filter chain canonical pattern) tracked as #124, and ApplicationInformation schema repair as #123. Patch 1 — MySQL V1 FK uniqueness V1_0_0__create_oauth2_schema.sql declared an FK from espi_application_info.client_id to oauth2_registered_client.client_id, but the referenced column had no unique constraint (only PRIMARY KEY on id). MySQL rejected the CREATE TABLE. Added UNIQUE KEY on oauth2_registered_client.client_id and removed the now-redundant non-unique CREATE INDEX. PostgreSQL V1 was already correct. Patch 2 — Flyway location config / source layout mismatch Source files live at db/vendor/{h2,mysql,postgresql}/ but 4 profile YAMLs plus base application.yml plus docker-compose env vars pointed at classpath:db/migration/{mysql,postgresql,h2}/. Boot only "worked" historically because of stale target/classes/db/migration/ artifacts from a prior layout; mvn clean exposed the gap. Updated all 6 config references to classpath:db/vendor/... per the user's preference to keep the current vendor-organized source layout. Patch 3 — Flyway target=2.0.0 (workaround for #123) V3+ migrations INSERT into espi_application_info columns that don't exist in MySQL V1 (client_description, contact_name, contact_email, scope, grant_types, response_types). Root cause is schema drift from the ESPI 4.0 XSD; full repair tracked as #123. Set spring.flyway.target: "2.0.0" in application-dev-mysql.yml so boot succeeds with V1+V2 schema (sufficient for OAuth2 grant + introspect; V3+ is seed/demo data). Patch 4 — JWT-only resource server filter chain AuthorizationServerConfig.authorizationServerSecurityFilterChain declared both .opaqueToken(Customizer.withDefaults()) and OIDC (which auto-wires JWT validation) on the same chain. Spring Security 7.x refuses by design: "Spring Security only supports JWTs or Opaque Tokens, not both at the same time." Removed .opaqueToken; the chain now uses .jwt(Customizer.withDefaults()). Outbound tokens to ESPI clients remain opaque — controlled per-RegisteredClient via accessTokenFormat(OAuth2TokenFormat.REFERENCE), unaffected by this filter-chain change. Patch 5 — Disabled @Order(0) header-injection chains HttpsEnforcementConfig declared two SecurityFilterChain @Order(0) beans (httpsEnforcementFilterChain for prod, developmentSecurityFilterChain for dev profiles) with securityMatcher("/**") that monopolized every request, preempting the auth-server's @Order(1) chain. Result: every OAuth2 endpoint returned 404. Header injection should be a HeaderWriter or Filter, not a SecurityFilterChain claiming /**. Disabled both @Bean annotations; AuthorizationServerConfig's own chain already configures equivalent security headers. Re-introducing the dev-friendly cache-control headers via a proper mechanism is a follow-up cleanup item. Patch 6 — HikariCP auto-commit application-dev-mysql.yml had auto-commit: false but JdbcRegisteredClientRepository.save() (and likely other repository methods) declares no @Transactional. With auto-commit off and no declared transaction, INSERTs execute against an uncommitted connection and silently roll back when the connection returns to the pool. Boot logged "Inserted registered client: 1" three times while the DB had 0 rows. Set auto-commit: true to make the seed flow work. Architectural fix (proper @Transactional throughout the repository) deferred — tracked as part of #122 / Phase 2.0 cleanup. What works after these six patches - mvn clean install of openespi-authserver succeeds - mvn -pl openespi-authserver spring-boot:run reaches Started state in ~35-42s - Flyway V1+V2 apply cleanly to a fresh MySQL 8.4 - Tomcat listens on port 9999 - Three default ESPI clients persist correctly: data_custodian_admin, third_party, third_party_admin - Discovery endpoints return 200: /.well-known/oauth-authorization-server /.well-known/openid-configuration /oauth2/jwks /login What does NOT work yet (tracked separately) - POST /oauth2/token returns 401 — resource-server bearer filter preempts the token endpoint. Fix tracked as #124 (filter chain needs the canonical OAuth2AuthorizationServerConfiguration .applyDefaultSecurity pattern with a second @Order(2) chain). - JdbcRegisteredClientRepository.findAll() returns empty list even when rows exist. Boot logs "Default ESPI Clients: 0" while DB has 3 rows. Defect #8 from the #122 audit; not yet diagnosed. - V3-V6 Flyway migrations skipped via target=2.0.0 pending #123. Refs: #122 #123 #124 Co-Authored-By: Claude Opus 4.7 --- openespi-authserver/docker/docker-compose.yml | 2 +- .../authserver/config/AuthorizationServerConfig.java | 11 +++++++---- .../authserver/config/HttpsEnforcementConfig.java | 11 +++++++++-- .../src/main/resources/application-dev-mysql.yml | 11 +++++++++-- .../src/main/resources/application-dev-postgresql.yml | 2 +- .../src/main/resources/application-local.yml | 2 +- .../src/main/resources/application-prod.yml | 2 +- .../src/main/resources/application.yml | 2 +- .../db/vendor/mysql/V1_0_0__create_oauth2_schema.sql | 4 ++-- 9 files changed, 32 insertions(+), 15 deletions(-) diff --git a/openespi-authserver/docker/docker-compose.yml b/openespi-authserver/docker/docker-compose.yml index 57063029..241a1122 100644 --- a/openespi-authserver/docker/docker-compose.yml +++ b/openespi-authserver/docker/docker-compose.yml @@ -27,7 +27,7 @@ services: # Flyway Configuration SPRING_FLYWAY_ENABLED: true - SPRING_FLYWAY_LOCATIONS: classpath:db/migration/mysql + SPRING_FLYWAY_LOCATIONS: classpath:db/vendor/mysql SPRING_FLYWAY_BASELINE_ON_MIGRATE: true # Security Configuration diff --git a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfig.java b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfig.java index 3f1bd2b2..96d4b171 100644 --- a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfig.java +++ b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/AuthorizationServerConfig.java @@ -136,11 +136,14 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) ) - // Accept access tokens for User Info and/or Client Registration + // Accept access tokens for User Info and/or Client Registration. + // OIDC auto-configures JWT validation for self-protected endpoints + // (id_token signing requires JWT). Outbound tokens to ESPI clients + // remain opaque via accessTokenFormat(REFERENCE) on each RegisteredClient. + // Cannot configure both .jwt() and .opaqueToken() on the same chain + // in Spring Security 7.x. .oauth2ResourceServer(resourceServer -> resourceServer - .opaqueToken(Customizer.withDefaults()) - - //.jwt(Customizer.withDefaults()) + .jwt(Customizer.withDefaults()) ) // HTTPS Channel Security for Production //should be able to use property server.ssl.enabled=true diff --git a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/HttpsEnforcementConfig.java b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/HttpsEnforcementConfig.java index 5af81942..a7cbebd7 100644 --- a/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/HttpsEnforcementConfig.java +++ b/openespi-authserver/src/main/java/org/greenbuttonalliance/espi/authserver/config/HttpsEnforcementConfig.java @@ -98,7 +98,12 @@ public void logSecurityConfiguration() { * * Enforces TLS 1.3 for all requests in production environment (NAESB ESPI 4.0) */ - @Bean + // DISABLED: this chain's securityMatcher("/**") at @Order(0) preempts the + // authorization-server filter chain at @Order(1), causing every OAuth2 endpoint + // to 404. Headers should be injected via a HeaderWriter or Filter, not via a + // SecurityFilterChain that monopolizes /**. AuthorizationServerConfig's own + // chain (@Order(1)) already configures equivalent security headers. + // @Bean @Profile("prod") @Order(0) public SecurityFilterChain httpsEnforcementFilterChain(HttpSecurity http) throws Exception { @@ -151,7 +156,9 @@ public SecurityFilterChain httpsEnforcementFilterChain(HttpSecurity http) throws * * Allows HTTP for development while still providing security headers */ - @Bean + // DISABLED: same reason as httpsEnforcementFilterChain above — securityMatcher("/**") + // at @Order(0) blocks the auth-server endpoints. + // @Bean @Profile({"dev", "dev-mysql", "dev-postgresql", "local"}) @Order(0) public SecurityFilterChain developmentSecurityFilterChain(HttpSecurity http) throws Exception { diff --git a/openespi-authserver/src/main/resources/application-dev-mysql.yml b/openespi-authserver/src/main/resources/application-dev-mysql.yml index ebb94d4a..358ed991 100644 --- a/openespi-authserver/src/main/resources/application-dev-mysql.yml +++ b/openespi-authserver/src/main/resources/application-dev-mysql.yml @@ -14,7 +14,11 @@ spring: minimum-idle: 5 idle-timeout: 300000 max-lifetime: 1200000 - auto-commit: false + # auto-commit must be true: JdbcRegisteredClientRepository.save() and other + # repository methods do not declare @Transactional, so without auto-commit + # their INSERTs are never committed and silently roll back when the + # connection returns to the pool. Architectural cleanup tracked separately. + auto-commit: true # JPA/Hibernate Configuration for MySQL jpa: @@ -36,11 +40,14 @@ spring: flyway: enabled: true baseline-on-migrate: true - locations: classpath:db/migration/mysql + locations: classpath:db/vendor/mysql schemas: oauth2_authserver user: ${spring.datasource.username} password: ${spring.datasource.password} url: ${spring.datasource.url} + # Skip V3+ pending ESPI 4.0 XSD-aligned schema repair (see issue #123). + # V1+V2 provide enough for OAuth2 grant + introspection; V3 onwards is seed/demo data. + target: "2.0.0" # Development Logging logging: diff --git a/openespi-authserver/src/main/resources/application-dev-postgresql.yml b/openespi-authserver/src/main/resources/application-dev-postgresql.yml index 3d9643e7..2f7c59d2 100644 --- a/openespi-authserver/src/main/resources/application-dev-postgresql.yml +++ b/openespi-authserver/src/main/resources/application-dev-postgresql.yml @@ -38,7 +38,7 @@ spring: flyway: enabled: true baseline-on-migrate: true - locations: classpath:db/migration/postgresql + locations: classpath:db/vendor/postgresql schemas: public # Security Configuration diff --git a/openespi-authserver/src/main/resources/application-local.yml b/openespi-authserver/src/main/resources/application-local.yml index 27a2a7ef..d3ee3dfa 100644 --- a/openespi-authserver/src/main/resources/application-local.yml +++ b/openespi-authserver/src/main/resources/application-local.yml @@ -37,7 +37,7 @@ spring: flyway: enabled: true baseline-on-migrate: true - locations: classpath:db/migration/h2 + locations: classpath:db/vendor/h2 schemas: oauth2_authserver # Local Development Logging diff --git a/openespi-authserver/src/main/resources/application-prod.yml b/openespi-authserver/src/main/resources/application-prod.yml index 72c382ba..e54e3af7 100644 --- a/openespi-authserver/src/main/resources/application-prod.yml +++ b/openespi-authserver/src/main/resources/application-prod.yml @@ -89,7 +89,7 @@ spring: flyway: enabled: true baseline-on-migrate: true - locations: classpath:db/migration/mysql + locations: classpath:db/vendor/mysql schemas: oauth2_authserver validate-on-migrate: true clean-disabled: true diff --git a/openespi-authserver/src/main/resources/application.yml b/openespi-authserver/src/main/resources/application.yml index 7dc7b511..011e9780 100644 --- a/openespi-authserver/src/main/resources/application.yml +++ b/openespi-authserver/src/main/resources/application.yml @@ -50,7 +50,7 @@ spring: flyway: enabled: true baseline-on-migrate: true - locations: classpath:db/migration,classpath:db/vendor/h2 + locations: classpath:db/vendor/h2 #schemas: oauth2_authserver jackson: default-property-inclusion: non_null diff --git a/openespi-authserver/src/main/resources/db/vendor/mysql/V1_0_0__create_oauth2_schema.sql b/openespi-authserver/src/main/resources/db/vendor/mysql/V1_0_0__create_oauth2_schema.sql index ec1cd2ad..5f4eff21 100644 --- a/openespi-authserver/src/main/resources/db/vendor/mysql/V1_0_0__create_oauth2_schema.sql +++ b/openespi-authserver/src/main/resources/db/vendor/mysql/V1_0_0__create_oauth2_schema.sql @@ -64,7 +64,8 @@ CREATE TABLE oauth2_registered_client ( scopes varchar(1000) NOT NULL, client_settings varchar(2000) NOT NULL, token_settings varchar(2000) NOT NULL, - PRIMARY KEY (id) + PRIMARY KEY (id), + UNIQUE KEY uk_oauth2_registered_client_client_id (client_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ESPI Application Information mapping @@ -109,5 +110,4 @@ CREATE INDEX idx_oauth2_authorization_client_principal ON oauth2_authorization ( CREATE INDEX idx_oauth2_authorization_code ON oauth2_authorization (authorization_code_value(255)); CREATE INDEX idx_oauth2_authorization_access_token ON oauth2_authorization (access_token_value(255)); CREATE INDEX idx_oauth2_authorization_refresh_token ON oauth2_authorization (refresh_token_value(255)); -CREATE INDEX idx_oauth2_registered_client_id ON oauth2_registered_client (client_id); CREATE INDEX idx_espi_application_client_id ON espi_application_info (client_id); \ No newline at end of file