All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- CI — Dependabot para el control de composición de software (SCA)
- Configuración de actualizaciones de seguridad para
gradle,github-actionsy las imágenes base de Docker, alineada coneudistack-core-issuer. Complementa el escaneo Trivy que ya corre enpr.yml: Trivy detecta, Dependabot propone el arreglo.
- Configuración de actualizaciones de seguridad para
-
EUD-219 — Removal of GPL-3.0 dependency: the
io.github.novacrypto:Base58dependency (GPL-3.0 license) has been removed and replaced with a customBase58Codecimplementation. -
Removal of JitPack: the JitPack repository has been removed from the build configuration, as it is not required for any remaining dependencies.
-
OTLP log export to OpenObserve: logs now flow through the same OTel Collector pipeline already used for traces/metrics, so the
business.credential.verifiedevents added below (and every other INFO+ log line) become queryable by SQL in OpenObserve instead of living only as plain text in CloudWatch. NewMaskingOpenTelemetryAppender(shared/config, extendsio.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender) wraps everyILoggingEvent(DelegatingLoggingEvent, new) to mask the rendered message and every MDC value throughMaskingPatternLayout.applyMaskingbefore export — the OTel appender reads the raw event, bypassing theMaskingPatternLayoutthe console appender uses, so redaction has to happen here or JWTs/emails/tokens would reach OpenObserve unmasked. MDC values are re-assembled as"key=value"before masking, becauseMaskingPatternLayout's sensitive-key patterns (tx_code,access_token,password,secret, …) only fire when key and value appear together in one string — an MDC map hands them over as separate entries.applyMaskingwidened fromprivateto package-private to be reusable rather than duplicated (this is PII/secret redaction in an eIDAS product; two copies would drift). NewOpenTelemetryAppenderInitializer(@Component implements InitializingBean) callsOpenTelemetryAppender.install(openTelemetry)on startup — Logback initializes before the Spring context, so the appender has no SDK to export to until this runs. Wired viamanagement.otlp.logging.{endpoint,transport,export.enabled}(newMANAGEMENT_OTLP_LOGGING_EXPORT_ENABLED/MANAGEMENT_OTLP_LOGGING_TRANSPORTenv vars, defaulting to off so local runs/tests never attempt export), reusing the existingOTEL_COLLECTOR_GRPC_URL— no new SSM parameter, IAM change, or security group rule needed. NewOTLPappender inlogback-spring.xml, referenced from<root>and from bothadditivity="false"loggers in the dormantprod,stgspringProfileblock (so it doesn't silently stop receiving Spring Security/Nimbus logs if that profile is ever activated), gated at INFO via aThresholdFilter— STG runses.in2.vcverifierand Spring Security at DEBUG, and shipping that volume to OpenObserve is pure S3 cost with no signal. Not affected by the existingSuppressEndpointLogFilterturboFilter: that filter only denies DEBUG-level/health//.well-knownnoise, already excluded by theThresholdFilter. Tests:MaskingOpenTelemetryAppenderTest(new), covering JWT/email masking in the message, key-name-only secrets in MDC (validates the key=value re-assembly), non-sensitive MDC passthrough, andgetArgumentArray()nulled out so no downstream consumer re-renders the unmasked original. -
Business event log for verified credentials: new
CredentialVerificationLogger(domain portCredentialVerificationLoggerPortinverifier/domain/port+ implCredentialVerificationLoggerinverifier/infrastructure/logging, following the same port/adapter split asSsoMetricsPort) logs one line per verification outcome —event=business.credential.verified tenant=<tenant> configurationId=<id> outcome=ok|error(errorType=<SimpleName>added on failure). Tenant is resolved from the servlet request context (TenantDomainFilter.getCurrentTenant), withunknownas fallback for tenant/configurationId, matching the previous counter's semantics.- Logged at the two flows that actually verify a credential:
AuthorizationResponseProcessorServiceImpl.handleAuthResponse(OID4VP/oid4vp/auth-response, the wallet path) andClientCredentialsValidationWorkflow.validateClientCredentialsGrant(M2Mclient_credentialsgrant with a VP inside theclient_assertion). - In the OID4VP flow the
configuration_idis only known once the credential schema dispatcher's decision is resolved; averificationLoggedflag +finallyguarantees exactly one log line per call, and failures that happen after a successful verification (unregistered client, persistence, SSE) are correctly not double-logged as verification errors — pre-dispatch failures fall back toconfigurationId=unknownsince the wallet-asserted type cannot be trusted before signature verification passes. - Replaces the previous Micrometer counter
business.credential.verified(finding H-06 of the verifier observability review) — the portCredentialVerificationMetricsPortand adapterCredentialVerificationMetricsRecorder(verifier/infrastructure/metrics), along with itsAtomicBooleanone-shot failure-log guard, are removed; a plainlog.info/log.warncall cannot throw the wayMeterRegistry.counter(...)could on a meter-type collision.dome_verifier_dispatcher_total,verifier_sso_established_total,dome_verifier_dispatcher_duration_msandhttp.server.requestsare unaffected. - Tests:
CredentialVerificationLoggerTest(new, LogbackListAppender: event/tenant/outcome/errorType content,unknownfallbacks),AuthorizationResponseProcessorServiceImplTestandClientCredentialsValidationWorkflowTest(verify()on the mockedCredentialVerificationLoggerPortinstead of counter assertions).
- Logged at the two flows that actually verify a credential:
SsoSessionJdbcRepositorysilently broke schema-per-tenant isolation (tech debt): every method opened its JDBC connection withautoCommit=true, soSET LOCAL search_path/statement_timeoutreverted before the business statement ran — reads/writes could silently fall back to the connection's default schema instead of the tenant's. Extracted a sharedinTransaction(tenant, op)helper (setAutoCommit(false)+ explicitcommit()/rollback()) and applied it to all 9 affected methods (save,findActiveByTenantAndHolder,findActiveById,findById,updateLastUsedAt,supersedeActive,terminateActive,findClientsBySession,recordClientActivity);revokeAllByTenant(already fixed inline in a prior PR) was migrated onto the same shared helper to remove duplication.save()'s supersede-and-retry-on-unique-violation path now runs inside the same transaction/connection instead of opening a second one, closing an atomicity gap between the two statements. Existing per-method fail-open/fail-closed/best-effort exception semantics preserved. Tests:SsoSessionJdbcRepositoryTest(new, unit, Mockito — commit/rollback ordering and the single-connection supersede+retry path).
- US-06 — Single Logout intra-tenant iniciado desde un aplicativo (EUDISTACK-551):
GET/POST /oidc/logoutinvalida la sesión SSO del Holder (ACTIVE → TERMINATED, transición idempotente por rows-affected) y notifica el cierre al resto de aplicativos vivos del tenant vía OIDC Back-Channel Logout 1.0 (FR-11, FR-12, FR-13, FR-14). Nuevos componentes:TerminateSsoSessionWorkflow(enganchado tras la validación estándar del RP-Initiated Logout de Spring AS mediante unLogoutHandler/LogoutSuccessHandlercompuesto, AD-1 — nunca sustituye ni reimplementa esa validación anti open-redirect),LogoutTokenFactory(construye ellogout_tokenJWT:iss,aud=client_id,sid,iat,exp,jti,events, sinnonce, firmado ES256 contyp=logout+jwt),BackChannelLogoutDispatcher(dispatch asíncrono en pool acotado dedicado, AD-2 — la redirección de post-logout al iniciador no depende del número ni latencia de los callees, NFR-P-551-01; retry con backoff exponencial 3× + jitter, circuit breaker perclient_ida los 5 fallos consecutivos/60s, deduplicación por(sid, client_id)). Invalidación local transaccional confirmada antes de planificar el dispatch (AD-3, ventana 0, NFR-S-03) — un fallo de persistencia es fail-closed (ES-04: no se despacha nada, la sesión permaneceACTIVE). Origen delbackchannel_logout_uripor callee:RegisteredClient/ClientSettings(fuente primaria) con fallback aTenantSsoConfig.eligibleClients[].backchannelLogoutUri(AD-4) — ambas fuentes validadas conSafeUrlValidator(SEC-14: rechaza esquemas no-HTTPS y rangos privados/loopback/link-local/metadata de nube; una URI rechazada se trata como ausente,backchannel_skipped), y con enforcement HTTPS adicional en el registro estático (ClientLoaderConfig). Cálculo de callees vía la nueva tablasso_session_client(rastreo de actividad por aplicativo, ADR-108/AD-5, migraciónV6) poblada aditivamente porEstablishSsoSessionWorkflow(US-02) yReuseSsoSessionWorkflowImpl(US-03) — retrofit aditivo, ninguna StoryDonese reabre. Elid_tokenemitido en establecimiento/reutilización de sesión ahora estampa el claimsid = sso_session.idcuando el tenant tiene SSO habilitado y existe sesiónACTIVE(TokenGenerationWorkflow.buildIdToken, AD-6/ADR-109) — condición necesaria para que el RP pueda correlacionar ellogout_tokenrecibido con su propia sesión, verificada end-to-end contra una fila real desso_session(Testcontainers Postgres). Convivencia legacy garantizada (AC-05, NFR-M-01): sin sesión SSO o converifier.sso.enabled=false, el logout se comporta exactamente igual que el RP-Initiated Logout estándar preexistente. Auditoría (AC-06, NFR-O-01):sso_logout_initiated(conholder_hashde la sesión terminada),backchannel_delivered,backchannel_failed,backchannel_skipped,sso_logout_rejected,sso_logout_store_error— cada evento lleva unoutcomede un conjunto cerrado de literales (success/error/skipped/noop/rejected) y, cuando aplica, unreasondescriptivo separado (p. ej.no_backchannel_uri,timeout,circuit_open,invalid_id_token_hint,unregistered_redirect_uri); ninguno incluye elsuben claro (NFR-S-551-01) ni elsession_id/holder_hashcompletos (NFR-S-551-02, solo prefijo de 8 caracteres). MigraciónV7añadeterminated_atasso_session. ACs cubiertos: AC-01..AC-06, EC-01..EC-03, ES-01..ES-04, NFR-S-551-01, NFR-S-551-02, NFR-P-551-01.
- SSO reuse (US-03, EUDISTACK-548) never completed the authorization flow:
ReuseSsoSessionWorkflowImpl.reuse()correctly computedALLOWEDand publishedSSO_SESSION_REUSED, butCustomAuthorizationRequestConverternever acted on it, always falling through to a fresh OID4VP/QR challenge.ALLOWEDnow issues a real authorization code directly from a credential-claims snapshot captured at session establishment (newcacheStoreForSsoSessionCredential, fails closed toLOGIN_REQUIREDon cache miss) — no VP is re-presented on reuse. Validatesredirect_uriagainst the requesting client's registered redirect URIs before issuing (security fix from review; fails closed on mismatch). Restores anOAuth2AuthorizationRequestattribute dropped by the same refactor that Spring Authorization Server's own stockOAuth2AuthorizationCodeAuthenticationProviderreads internally for PKCE validation — its absence broke every login (not just reuse) with a raw NPE /403onPOST /oidc/token. Also hardens ID token handling in the SSO flow (TokenGenerationWorkflow,CustomAuthenticationProvider). - Concurrent logins/SSO-reuse of the same client silently overwrote each other's authorization:
OAuth2Authorization.id()was set toregisteredClient.getId()— a value fixed per client, identical across every login of that client.InMemoryOAuth2AuthorizationServiceindexes authorizations byid(), so a second tab/session's login (or SSO reuse) clobbered the first's authorization (and itsid_token), silently breaking that earlier tab's later token exchange or logout. Fixed with a randomUUIDper authorization. - RP-Initiated Logout intermittently threw an NPE ("random" failure):
ClientLoaderConfig.refreshClients()(scheduled every 5 min) assigned a fresh random UUID to everyRegisteredClient.id()on each reload, whileOAuth2Authorizationrecords (in-memory, no expiry) captured that id asregisteredClientIdat token-issuance time — orphaning it as soon as one refresh happened after login. Spring's RP-Initiated Logout provider then received anullRegisteredClientand NPE'd ongetClientId(). Fixed by using the already-unique, stableclientIdas the internal id instead of a random UUID per reload.
EUD-156 — US-03: Verifier rejects non-machine or untrusted credentials in a traceable manner:
- New domain exception InvalidProofOfPossessionException (oauth2/domain/exception): clearly flags that the client_assertion (private_key_jwt) of an M2M client fails to prove key possession (invalid signature, incorrect iss/sub/aud, expired exp, or already consumed/replay jti).
- ClientCredentialsValidationWorkflow now throws InvalidProofOfPossessionException instead of a generic IllegalArgumentException when verifyClientAssertionJWTClaims returns false, preserving the existing check order (type eligibility → proof of possession → presentation validation).
- CustomTokenRequestConverter explicitly catches InvalidProofOfPossessionException and IssuerNotAuthorizedException (the latter already existed, now audited) prior to the generic catch, publishing two new audit reason values: invalid_proof_of_possession and issuer_not_trusted. Both cases continue to return only invalid_client to the Relying Party (without error_description/error_uri).
- With this, the 3 M2M failure types explicitly specified in the SRS (invalid proof of possession, non-machine credential, untrusted issuer) are fully auditable with mutually distinguishable reasons, without leaking details to the Relying Party.
- Test coverage: unit (ClientCredentialsValidationWorkflowTest, CustomTokenRequestConverterTest, OAuth2ErrorTranslatorTest) and end-to-end integration against /oidc/token (M2MRejectionIT, new) covering all 3 rejection types, deterministic precedence, binary verdict, 100% audit coverage, and no regression for pre-registered clients.
- Token tag in metrics.
EUD-155 — Two critical M2M gaps found during real end-to-end stg validation, not caught by 3.2.1:
- Security bypass via Spring's built-in provider: the unregistered-client placeholder (
UnregisteredM2MClientAuthenticationProvider) declaredAuthorizationGrantType.CLIENT_CREDENTIALS. Spring'sOAuth2ClientAuthenticationConfigurer/OAuth2TokenEndpointConfigurerappend custom providers to Spring's own internal list rather than replacing it, so the built-inOAuth2ClientCredentialsAuthenticationProviderremained in the chain and could independently mint a fully valid access token for the placeholder — bypassing every credential/tenant checkCustomAuthenticationProviderperforms. Fixed by giving the placeholder a bogus, non-standard grant type (urn:eudistack:oauth:grant-type:unregistered-vc-placeholder) that no built-in provider recognizes, so only our own validation can ever produce a token. - Wrong field used for tenant derivation:
CustomAuthenticationProviderderived the authorized tenant fromcredentialSubject.mandate.mandator.organizationIdentifier— the mandator's fiscal/VAT identifier, never a tenant slug. This both rejected legitimate credentials (any realorganizationIdentifierfails to match a tenant slug likesandbox/dome) and, in principle, could accept ones with a coincidentally matching value. Fixed to readcredentialSubject.mandate.power[].domaininstead — the field already used for tenant-scoped authorization elsewhere in the platform (PolicyContextFactory.resolveTenantAdminin the Issuer), matching any power whose domain equals the request tenant.
EUD-155 — Hardened M2M without pre-registration: The client_credentials fallback for non-pre-registered machines (CustomAuthenticationProvider) is now reachable end-to-end, with the remaining gaps closed in 3.2.2 above.
- Fail-closed tenant isolation in both directions (credential and request) — the credential-side field used was corrected in 3.2.2.
- Structured auditing for every attempt, whether accepted or rejected.
- Standard OAuth2 errors, avoiding internal information leakage.
- New client authentication step in Spring Security: requests without pre-registration are now successfully validated (previously, they were rejected with an empty 401) — the placeholder client's grant type was hardened against a bypass in 3.2.2.
- Zero impact on already pre-registered clients.
- SSO audit & observability per tenant (US-07): completes the FR-13 audit event catalog with the lifecycle events
SSO_SESSION_EXPIRED,SSO_LOGOUT_INITIATEDandSSO_BACKCHANNEL_DELIVEREDplus an optionalreasonfield (backward-compatible, nullable). Adds FR-16 functional metrics via a domainSsoMetricsPort+SsoMetricsRecorder(Micrometer):verifier_sso_reuse_total{tenant,client_id},verifier_sso_oid4vp_avoided_total{tenant}andverifier_sso_established_total{tenant}, exposed per tenant through the new admin endpointGET /tenant/sso/metrics(scoped to the authenticated tenant, fail-closed cross-tenant) with the reuse ratio (division-by-zero guarded). The establish/reuse workflows are instrumented andSsoAuditAdapteris hardened to best-effort: emission never propagates a failure to the business flow (ES-01), applies explicit defaults + an anomaly marker on missing mandatory fields (ES-04), and keeps thesub/session-id PII redacted for the whole catalog (AC-06). Audit and metric emission are non-blocking (AD-1).
- CGCOM — VCT rename
doctorid.sd.1→urn:es.cgcom:doctorid:1: updatedcredential-configuration-idinapplication.yamlto the canonical URN-based VCT, aligning with the DoctorID issuer and the CGCOM verifier DCQL profiles / trusted-issuers configuration.
- SSO emergency cut per tenant (US-09): new admin endpoint +
RevokeTenantSessionsWorkflowthat revokes every active SSO session of a tenant in a single call, driven bySsoSessionRepositoryPort.revokeAllByTenant(tenantId). Emits anEMERGENCY_REVOKEaudit event (event=sso_emergency_revoke) withcount_revoked,correlation_idand outcome (success/failure); on repository error the transaction is rolled back, afailureaudit is still emitted, and aTenantRevocationExceptionis raised. - Observability configuration with OTLP exporter and Micrometer metrics
vcclaim in the access token is now always a nested JSON object: previouslyJwsAccessTokenBuilderonly emittedvcas a JSON object when the credential's schema profile hadwrap_vc_in_access_token: true; otherwise it emitted a stringified (escaped) JSON. Since no schema profile in the codebase set that flag (it defaulted tofalse), both legacy (LEGACY_V1_1) and bumped (BUMPED_V2_0) credentials were emitted as a string in practice — the object form only ever appeared in tests that set the flag manually. The builder now serializesvcas an object unconditionally, so consumers never need a secondJSON.parse. Breaking change for relying parties that parsed thevcclaim as a string.
wrap_vc_in_access_tokenschema-profile flag removed: it was a second, redundant legacy/bumped classifier (the authoritative source isverifier.dispatch.rules[].formatinapplication.yaml) whose only effect — thevcstring-vs-object shape — no longer exists now thatvcis always an object. Removed fromSchemaProfile,ReaderResult,LocalSchemaProfileRegistry, and the profile JSON schema. It never influenced thedome.legacy-read-enabled/bumped-read-enabledsunset gating, which is driven exclusively by the dispatch-rules catalog — gating behaviour is unchanged.DispatchDecisionremoved fromBuildContext: the token-build path only ever readcredentialConfigurationIdfrom it, soBuildContextnow carries thatStringdirectly.TokenGenerationWorkflowno longer fabricates a syntheticDispatchDecision(its twoissueAccessTokenoverloads are collapsed into one) and no longer depends onSchemaProfileRegistry.DispatchDecisionremains the return type of the dispatcher (CredentialSchemaDispatcher) and its OID4VP / M2M consumers.- Dead
CredentialReaderlayer removed:CredentialReader(SPI),LegacyCredentialReader,BumpedCredentialReaderandReaderResultwere@Componentbeans that were never injected or invoked anywhere in production — the token-build path goes straight from the dispatched credential toJwsAccessTokenBuilder. Removed together with their unit tests; thedualformatflow tests were reworked to exercise the real dispatcher + token builder without the reader indirection.
EUD-155 — Hardened M2M without pre-registration: The client_credentials fallback for non-pre-registered machines (CustomAuthenticationProvider) is now secure and reachable end-to-end.
- Fail-closed tenant isolation in both directions (credential and request).
- Structured auditing for every attempt, whether accepted or rejected.
- Standard OAuth2 errors, avoiding internal information leakage.
- New client authentication step in Spring Security: requests without pre-registration are now successfully validated (previously, they were rejected with an empty 401).
- Zero impact on already pre-registered clients.
client_assertionaudrejected duringprivate_key_jwtclient authentication (authorization_code / refresh_token flows): afterserver.servlet.context-path=/verifierwas introduced (3.1.0), the issuer the Authorization Server derives dynamically from the request (issuer
not pinned, to support multi-tenant subdomains) started including the/verifierprefix. Spring's defaultJwtClientAssertionDecoderFactoryonly acceptsaudvalues derived from that issuer (https://host/verifier[/oidc/token]), but legacy clients still point at the clean public URL
(without/verifier) and sign theclient_assertionwith that audience — causinginvalid_client: The aud claim is not validwhen exchanging
the code atPOST /oidc/token. A customJwtClientAssertionDecoderFactoryis now registered viaOAuth2AuthorizationServerConfigurer.clientAuthentication(...)on theJwtClientAssertionAuthenticationProvider, with aClientAssertionJwtValidatorFactorythat mirrors Spring's default validation (iss/sub/exp/signature unchanged) and replaces only theaudcheck withClientAssertionAudienceValidator: it accepts the audience with and without the servlet context-path (derived dynamically from the request,/verifiernot hardcoded), preserving per-host isolation (multi-tenant) and still rejecting foreign hosts. Adds areceivedvsexpectedlog (DEBUG on success, WARN on failure) that did not exist before. Legacy clients no longer need to change their URL.- Same
audtolerance extended to the M2M (client_credentials) flow:ClientAssertionValidationServiceImpl.validateAudiencevalidated
audwith an exactequalsagainstbackendConfig.getUrl()(which now includes/verifier), so a legacy M2M client sending the clean URL would have failed too. It is replaced by a set of accepted audiences (canonical URL with and without the context-path, derived from the request),
consistent with the authorization_code validator. Foreign-host rejection and the strictiss/sub/jti/expvalidation are preserved.
- US-08 — Legacy application coexistence on the Verifier IdP (EUDISTACK-553): 0 regressions for non-migrated tenants/applications during the transition to SSO (FR-14, NFR-M-01). Legacy tenant (
sso.enabled=falseor notenant_ssoentry) → fail-closed without creating a session or cookie;prompt=noneon a legacy tenant →error=login_requiredto theredirect_uri(no QR render, residual SSO cookie ignored, nocode/id_token). Coexistence verification suite:EstablishSsoSessionWorkflow_SsoDisabledTest/_ConfigAbsentTest(guard unit tests),LegacyConvivenciaIT(SSO+legacy coexistence and flag flip) andPromptNoneLegacyIT(AC-03/ES-02). ACs covered: AC-01..AC-03, EC-01..EC-02, ES-01..ES-02, NFR-S-553-01, NFR-S-553-02.
sso-config.yaml: examplerootDomainvalues replaced with real domains: the placeholder values (*.example.com) for tenantssandbox,cgcom,kpmg,domeandplatformare replaced with the real root domains (*.stg.eudistack.netand, fordome,dome-marketplace-lcl.org) used by the per-tenant SSO catalog (EUDISTACK-550 US-05).
- EUDISTACK-546:
- US-01: Custom domain operativo per tenant en el Verifier IdP
- US-02: Sesión SSO establecida tras presentación OID4VP exitosa
- US-03: Reutilización silenciosa de la sesión SSO en aplicativos adicionales
- US-05 — Catálogo per tenant de aplicativos elegibles para SSO (EUDISTACK-550): Catálogo de clientes elegibles para reutilización silenciosa de sesión SSO por tenant (FR-09, FR-10, FR-14). Nuevos componentes de dominio:
SsoEligibleClient(value object con normalización trim canónica, EC-04),TenantSsoCatalog(value object agrupador con fail-closed AC-03:contains()devuelve false si el catálogo está vacío o el clientId no figura). Política AD-2:TenantSsoPolicy.evaluate(6 params)evalúa tres condiciones AND en orden estricto — (1) cliente registrado en el servidor OAuth (REJECT_SESSION si falla), (2) sesión vigente según TTL absoluto (REJECT_SESSION si falla), (3) cliente en el catálogo SSO del tenant (REJECT_CATALOG → interaction_required si falla). API de administración:TenantSsoCatalogAdminControllercon endpointsGET/POST/DELETE /tenant/sso/eligible-clientsprotegidos por sesión autenticada (AD-3); validación cross-tenant ES-03 (401 si no hay contexto de tenant, 403 si el tenant de la sesión autenticada difiere del tenant de la solicitud). Persistencia AD-1: alta/baja escriben sobre el fichero YAML/EFS (TenantSsoConfigYamlAdapterimplementaSsoCatalogRepositoryPort) y refrescan el caché en memoria; la decisión de reuse lee del mismo caché. Auditoría NFR-O-01:SSO_CATALOG_CLIENT_ADDED/SSO_CATALOG_CLIENT_REMOVEDemitidos en cada operación. ACs cubiertos: AC-01..AC-05, EC-01..EC-04, ES-01..ES-03, NFR-S-550-01, NFR-P-550-01. - US-04 — TTL de sesión SSO configurable per tenant (EUDISTACK-549): TTL de sesión SSO configurable por dimensión per tenant (FR-07) con valor por defecto de sistema (FR-08, default ADR-106: 8h absoluto / 30min idle). Nuevos componentes de dominio:
SsoTtlRange(constantes canónicas ADR-106 — rango absoluto [1h, 24h], idle [5min, 60min], defaults 8h/30min),SsoSessionTtl(value object inmutable que encapsula el par absolute+idle validado en construcción),TenantSsoTtlPolicy(servicio de dominio convalidate(absolute, idle)— rangos cerrados inclusivos, log estructurado de rechazo por dimensión — yresolve(overrideAbsolute, overrideIdle)— override por dimensión independiente con cap idle≤absolute). Extensiones:SsoSession.isValid(now, idleTtl)(criterio combinadonow < expiresAt AND now − lastUsedAt ≤ idleTtl),TenantSsoConfigPort.resolveTtl(tenant),TenantSsoConfigYamlAdapter(parseo de camposttlAbsolute/ttlIdleen YAML per tenant, fail-safe a última config válida ante fallo de lectura ES-02, aislamiento per-tenant de valores mal formados ES-01),EstablishSsoSessionWorkflow(fijaexpiresAtdesde TTL resuelto) yReuseSsoSessionWorkflowImpl(evalúaisValidcon TTL idle vigente en config). ACs cubiertos: AC-01..AC-05, EC-01..EC-03, ES-01..ES-02, NFR-S-549-01, NFR-P-549-01.
- Redirect/CORS origin allowlisting hardened (SEC-S7 follow-up):
CustomErrorResponseHandler.isAllowedRedirectUriya no confía en el origen del propio verifier derivado dinámicamente deBackendConfig.getUrl()(que a su vez resolvía el Host de la request víaForwardedHeaderFilter). Nuevo campoverifier.backend.additional-urls(BackendProperties.additionalUrls, opcional,List<String>) modela explícitamente los dominios alias del verifier (SSO multi-tenant/multi-app), separado deverifier.backend.url(que sigue siendo elStringcanónico usado comoiss/audiencia/response_uri— sin ambigüedad de "cuál es el principal").BackendConfig.getAllUrls()devuelveurlseguido deadditionalUrls; el nuevoBackendConfig.getTrustedVerifierOrigins()normaliza ese conjunto completo como orígenes confiables para los redirects propios del verifier — estático y configurable, sin depender de qué Host reenvíe el proxy. Nueva utilidadOriginNormalizernormaliza scheme/host (minúsculas, puerto por defecto omitido, comparación de scheme case-insensitive) en las tres comparaciones de origen existentes (ClientLoaderConfig,CustomErrorResponseHandler, y el matching deredirect_urienCustomAuthorizationRequestConverter.validateRedirectUri), evitando rechazos por diferencias puramente sintácticas (RFC 3986 §3.1/§3.2.2) sin relajar la comparación exacta de path/query enredirect_uri.
CustomErrorResponseHandlerTestdesalineado tras merge demain: el stubbackendConfig.getStaticUrl()no se correspondía con el método realmente invocado porCustomErrorResponseHandler.isAllowedRedirectUri()(backendConfig.getUrl()), causandoWantedButNotInvokedentestOnAuthenticationFailure_WithVerifierOwnOrigin_ShouldRedirectytestOnAuthenticationFailure_WithVerifierOwnOriginErrorPage_ShouldRedirect. Corregido el stub para apuntar agetUrl().CryptographicBindingValidatorTestcon stub incompleto:validateCryptographicBinding_allStrategiesMiss_throwsInvalidScopeExceptionno stubabagetClaim("vc"), dependiendo de que elcatchinterno deextractMandateeIdFromVcabsorbiera elPotentialStubbingProblemde Mockito. Añadido el stub explícito para eliminar la fragilidad del test.
- DOME legacy
PlainListEntityrevocation skip:VpServiceImpl.validateCredentialNotRevokedshort-circuits tonot revoked(WARN log) whencredentialStatus.type == "PlainListEntity". Resolves the previousUnsupported credentialStatus.typeexception that broke OID4VP login for DOME legacy credentials, whose revocation lists are plain JSON arrays of{ "nonce": "<id>" }(no JWT, no signature) and not exposed by any existingCredentialStatusVerifierstrategy. Intentional during the DOME legacy sunset window; inlineTODOcaptures the open decision (migrate legacy toBitstringStatusListEntryvs implement a realPlainListEntityVerifieradapter).
- Upgraded
org.bouncycastle:bcprov-jdk18onfrom1.80to1.84. - Upgraded
org.bouncycastle:bcpkix-jdk18onfrom1.80to1.84. - Removed explicit version pin from
jackson-dataformat-yamlto use the Spring Boot managed BOM version.
- SD-JWT credential types missing from dispatch catalogue:
learcredential.employee.sd.1,learcredential.machine.sd.1anddoctorid.sd.1were absent from theverifier.dispatch.rulesintroduced in eba0126 (PR #31). Any wallet presenting an SD-JWT VC receivedUnknownCredentialFormatException → HTTP 400after full JWT + KB-JWT + status-list verification had already passed. Added the three SD-JWT config IDs to thebumpedrule set.FlagDefaultsTestupdated to assert the new catalogue sizes (6 legacy / 6 bumped / 12 total) and verify the SD-JWT entries explicitly.
- Dual-format dispatcher (US-08 / EUDISTACK-145): new
CredentialSchemaDispatcherport +ContextAndTypeCredentialSchemaDispatcheradapter that classifies every incoming credential asLEGACY_V1_1orBUMPED_V2_0fromtype[]+@context, deterministically and without try/catch fallback (AD-3). The decision drives aLegacyCredentialReader/BumpedCredentialReaderSPI and anAccessTokenBuilder(JwsAccessTokenBuilder) that wraps the credential undervconly for VCDM v2.0 — preserving the legacy wrap for v1.1 (FR-06a). Domain ports:CredentialReader,AccessTokenBuilder,CredentialSchemaDispatcher,TenantConfigPort. Records:DispatchDecision,DispatchRule,DispatchReason,CredentialFormat,BuildContext,ReaderResult,TenantDomeConfig. - Independent feature flags
verifier.dome.legacy-read-enabledandverifier.dome.bumped-read-enabled(TenantDomeConfigProperties+PropertiesTenantConfigAdapter): boolean toggles (defaulttrue) that gate legacy and bumped credential acceptance per tenant. Closing the legacy flag triggersLegacyFormatSunsetClosedException → 410 Gone; disabling the bumped flag triggersBumpedFormatTemporarilyDisabledException → 503 Service Unavailable. Source is currently@ConfigurationProperties; the spec target (DB-backedtenant_dome_configwith TTL ≤ 60 s hot-reload, AC-07 / NFR-S-145-03) is documented as follow-up. - Dispatcher catalogue in
application.yaml(verifier.dispatch.rules.legacy/bumped):DispatchProperties+DispatchConfigurationregister aList<DispatchRule>ofcredential-configuration-id → CredentialFormatfrom configuration so adding new DOME or EUDIStack types is config-only, no recompile. Default catalogue coverslearcredential.{employee,machine}.w3c.{2,3,4}andgx.labelcredential.w3c.{1,2}plus the raw DOME type aliasesLEARCredentialEmployeeandLEARCredentialMachine. - RFC 9457 Problem+JSON error mapping (
DomeDispatchExceptionHandler): three new exceptions (LegacyFormatSunsetClosedException,BumpedFormatTemporarilyDisabledException,UnknownCredentialFormatException) mapped to410/503/400with a stableproperties.errorcode (legacy_format_sunset_closed,bumped_format_temporarily_disabled,unknown_credential_format) so callers can branch on the machine-readable identifier instead of the human-readabledetail. - Micrometer instrumentation: counter
dome_verifier_dispatcher_total{tenant, format, decision, reason}and timerdome_verifier_dispatcher_duration_ms{tenant}in the dispatcher; counterdome_verifier_legacy_replay_after_sunset_total{tenant}in the exception handler. Drives the cutover dashboard / sunset-closure alerting (architecture.md§9.3).
AuthorizationResponseProcessorServiceImpl.handleAuthResponse: after JWT VP validation, the workflow now invokesCredentialSchemaDispatcher.dispatch(credential)so the OID4VP user-driven login path is subject to the same format gating as the M2Mclient_credentialsgrant (US-08 AC-07 / AC-10). The three dispatcher exceptions are added to the outer catch tree and trigger an SSEFORMAT_GATEDevent so the wallet can surface a specific message.VerifyPresentationWorkflow: returns a(credential, dispatchDecision)record after dispatch so downstream workflows can read the resolved format and config-id without re-classifying.TokenGenerationWorkflow: access-token construction delegated to the newAccessTokenBuilderport. The legacy/bumped distinction is honoured by the builder viaSchemaProfile.wrapVcInAccessToken: VCDM v1.1 credentials pass through unmodified (their JWT already carries thevcwrap); VCDM v2.0 credentials are wrapped undervcexclusively at the verifier (FR-06a, FR-06b: issuers must not pre-wrap).id_tokenconstruction stays inside the workflow.ClientCredentialsValidationWorkflow: invokes the dispatcher to enrich logs/metrics on M2M flows and gate access at the same point as OID4VP.grantEligibilitylookup againstSchemaProfileRegistryunchanged.SchemaProfile: new boolean fieldwrapVcInAccessToken(defaultfalse, expectedtruefor bumped profiles loaded fromeudistack-platform-assets). Decouples format detection from access-token construction.LocalSchemaProfileRegistry: now scans thelegacy/subdirectory under the external schemas path, registerstype → profilealiases viaregisterCredentialTypeAliasesso credentials carrying bare semantic types intype[](e.g.LEARCredentialEmployee) resolve to their versioned profile, and applies the canonical W3C VCDM defaultissuer.idtoissuerIdPathwhen the schema does not declarevalidation.issuer_id_path. Sample profiles matching*.sample*.jsonare skipped.LocalTrustedIssuersProvider: when a lookup by the credential'sissuer.idmisses (e.g. DOME credentials in full DID formdid:elsi:VATES-...), the provider strips thedid:elsi:prefix and retries once. A singletrusted-issuers.yamlentry per organisation (plain identifier) now covers both EUDIStack-issued and DOME-issued credentials without duplication.CertificateValidationServiceImpl.processCertificate: normalises the expected issuer id by stripping thedid:elsi:prefix before matching the certificate'sorganizationIdentifier(OID2.5.4.97). ResolvesMismatchOrganizationIdentifierExceptionon DOME credentials whose JWT signs with a QTSP certificate whose DN carries only the bare VATES code.
- OID4VP login bypassed the sunset flag (US-08 AC-07 / AC-10): prior to this branch the legacy/bumped feature flags only affected the M2M
client_credentialsgrant; OID4VP user-driven logins through/oid4vp/auth-responseskipped the dispatcher entirely and accepted any well-formed VP, so closing the legacy flag would still mint authorization codes for legacy credentials presented from a wallet. Both code paths now share the same gating point — closingverifier.dome.legacy-read-enabledreturns410 Goneconsistently across M2M and user-driven flows.
- Unified URL generation — canonical/non-canonical distinction removed: all requests now arrive with the
/verifierservlet context path, soBackendConfig.getUrl()always appendsrequest.getContextPath()unconditionally. TheX-Tenant-based branch that stripped the context path for non-canonical routes has been deleted, along withIssuerOverrideFilterand its test.AuthorizationServerSettingsno longer needs a custom filter to override the issuer; Spring AS derives it correctly from the request URL. Stale testgetUrl_nonCanonical_returnsBaseWithoutContextPathupdated to reflect the new behavior.
- Discovery document URLs include
/verifierfor non-prefixed access: Spring Authorization Server derives the issuer fromrequest.getRequestURI(), which always includes the servlet context path (/verifier). For non-canonical deployments (where the external URL has no/verifierprefix), the discovery document URLs were incorrect. AddedIssuerOverrideFilter, which runs after Spring AS'sAuthorizationServerContextFilterand replaces the issuer inAuthorizationServerContextHolderwith the value fromBackendConfig.getUrl()— which already strips the context path when theX-Tenantheader is present. Proxy must setX-Tenantfor non-prefixed routes.
- CORS on public discovery endpoints:
/.well-known/**and/oidc/jwkswere served by the Authorization Server filter chain (highest precedence), which applied the registered-clients CORS policy and blocked cross-origin requests from unregistered origins. These endpoints are public by spec (OpenID Connect Discovery 1.0, RFC 8414, RFC 7517) and now return a wildcard CORS configuration regardless of the requesting origin. - Error/login redirect blocked by SSRF check:
CustomErrorResponseHandlerwas rejecting redirects to the verifier's own/loginand/errorpages because the verifier's origin was not inallowedClientsOrigins. The handler now also allows the verifier's own origin, derived dynamically fromBackendConfig.getUrl().
- Enhance app URL generation to handle canonical and non-canonical requests based on X-Tenant header.
- Tenant Resolution Header Support:
TenantDomainFilternow resolves the tenant from theX-Tenantrequest header first, validating and normalizing the value to lowercase before storing it as a request attribute and in the MDC. If the header is missing, blank, or invalid, tenant resolution falls back to the first valid hostname segment obtained fromrequest.getServerName(). Added theX_TENANT_HEADERconstant toConstants. - Build
allowedClientsOriginsfrom registered redirect URIs to support multi-domain clients like DOME. - Validate certificate chain
- Improved GDPR compliance by reducing PII logging.
- Cryptographic Binding:
validateCryptographicBinding()now follows a priority fallback chain instead of failing immediately whencnf.jwkis absent. Chain: (1)cnf.jwk— direct JWK Thumbprint comparison (RFC 7638), (2)cnf.kid— DID resolution viaDIDService+ thumbprint, (3)credentialSubject.mandate.mandatee.id— DID resolution viaDIDService+ thumbprint. Supports both W3C (credentialSubject.mandate.mandatee.id) and SD-JWT flat (mandate.mandatee.id) credential formats.
- Add support deferred critical extensions in JWS verification
- Extract mandator organization identifier without trust framework call
- Authorization Flow: Updated the
authorization-requestlogic to support metadata transmission. - ClientMetadata: Updated the
clientMetadatastructure to align with the latest metadata specifications and requirements. - Testing: Updated existing tests to validate the integrity of the updated
clientMetadataand authorization workflows.
- Implemented a custom Logback
PatternLayout(MaskingPatternLayout) for theCONSOLEappender to redact PII and secrets in application logs (emails, JWTs, Bearer tokens,tx_code,access_token,refresh_token, passwords andsecret). - Avoid cryptographic binding validation for client credentials presentation.
- CI deploy health check is now warning-only:
deploy.ymlwas failing the deploy on non-200 responses, but the configured host (verifier-stg.api.altia.eudistack.net) does not resolve from the GitHub runner —altiais not published in Route53, only tenant subdomains (<tenant>-stg.eudistack.net) go through CloudFront. The issuer workflow has always treated the same condition as a warning, which is why its deploys kept "passing". Aligned the verifier step to emit::warning::instead of::error::+exit 1, so a broken post-deploy probe no longer blocks rollouts while the real target-group health check (managed byaws ecs wait services-stable) keeps validating task health. True end-to-end validation should be performed manually againsthttps://<tenant>-stg.eudistack.net/verifier/health.
- CI deploy health check:
deploy.ymlprobedhttps://verifier-<env>.api.altia.eudistack.net/health, but the ALB only routes/verifier/*to the verifier target group and Spring exposes the endpoint at/verifier/health(context-path introduced in 3.1.0). The health step returned HTTP 000 for five attempts and failed the deploy. UpdatedHEALTH_URLto/verifier/health.
application.yaml:server.forward-headers-strategy: frameworkremains hardcoded, but the matchingSERVER_FORWARD_HEADERS_STRATEGYenv var has been removed from the ECS task definition ineudistack-platform-iacto eliminate redundancy. Behaviour unchanged.
PublicCorsConfigTest: aligned assertion with production config that includesCache-Controlin allowed headers (added in 3.0.3). CI:testtask was failing withexpected: <[Content-Type, Authorization]> but was: <[Content-Type, Authorization, Cache-Control]>.
- AWS deployment readiness (CloudFront + ALB, no nginx): previously the verifier relied on nginx to strip the
/verifier/prefix before forwarding requests; Spring controllers were mapped without the prefix (e.g.@RequestMapping("/api/login")). On AWS, requests arrive at the pod with/verifier/...intact and Spring did not match them.application.yaml: addedserver.servlet.context-path: ${APP_CONTEXT_PATH:/verifier}so Spring itself handles the prefix. The default keeps local dev via nginx working (nginx still forwards with the prefix) and AWS direct routing works without extra infrastructure.CustomAuthorizationRequestConverter: replaced hardcoded"/verifier/login"and"/verifier/error"strings with dynamic construction fromHttpServletRequest.getContextPath(). The value is captured inconvert()and propagated throughAuthorizationContextso login and error redirect URLs honour whatever context-path is active.SecurityHeadersFilterandRateLimitFilternow strip the context-path before matching request URIs, so security headers and rate limiting work consistently regardless of the active context-path.- OID4VP / OIDC endpoints moved under
/verifier/(Spring Authorization Server auto-prepends the context-path). - Added two unit tests verifying that the login and error redirect URLs are built from the request's context-path and contain no hardcoded
/verifiersegment. Full suite: 497 tests pass.
- Added
Cache-Controlto allowed headers inPublicCorsConfigto support caching directives from wallets and prevent CORS errors on certain requests (e.g. VP submission with cache hints).
- EUDI-033: Optional
loginPageUrifield in OIDC client registry for custom login page redirects - SSE event notifications for VP validation failures (error feedback to frontend)
- EUDI-013: Dual VCDM v1.1/v2.0 credential extraction in
extractVCFromPayload()— detects format by presence ofvcclaim - EUDI-013: Legacy schema profiles for
LEARCredentialEmployeeandLEARCredentialMachine(DOME v2-v3 backward compatibility) - EUDI-013: DCQL queries for legacy credential types alongside new
.w3c.4/.w3c.3types - RSA key support in
CertificateValidationServiceImplfor W3C VP path (QTSP compatibility) - RSA minimum key size enforcement (reject < 2048 bits, warn 2048-3072)
- Nested SD-JWT verification (RFC 9901) —
SdJwtVerificationServiceImplrecursively resolves_sdarrays at any nesting depth. Supports mandate wrapper structure. (EUDI-012) - Empty path embed resolution —
SchemaProfileClaimsExtractorsupports empty path to embed the full credential asvcclaim in access tokens for DOME compatibility. (EUDI-033)
- SD-JWT issuer signature: x5c takes priority over DID resolution (EUDISTACK-154) — When the SD-JWT
issclaim started withdid:(e.g.did:elsi:VATES-...) but the JWT was signed via QTSP with anx5ccertificate chain in the header, the verifier attempted DID resolution first and failed because onlydid:keyis supported. Nowx5ctakes priority when present, falling back to DID only when no certificate chain exists. - Error logging includes exception message —
ErrorResponseFactorynow logsex.getMessage()alongside the error type, making 401 failures diagnosable from logs without reproducing. - Issuer identification uses profile
issuer_id_path—VpServiceImplnow resolves the issuer ID from the schema profile path (e.g.issuer.organizationIdentifierfor W3C,issfor SD-JWT) instead of the JWTissclaim. PreviouslyextractIssFromJwt()returneddid:elsi:VATES-...which didn't match the trusted issuers list. RemovedextractIssFromJwt(). - RSA key rejection in BitstringStatusListVerifier —
CertificateValidationServiceImpl.verifyJWTSignaturenow accepts both EC and RSA public keys. Previously only EC was accepted, causing Status List Credential validation to fail when the issuer signs with an RSA certificate.
- Dead embedded schemas — Removed
src/main/resources/schemas/LEARCredential*.jwt_vc_json.v*.jsonfiles (old naming convention, never resolved byLocalSchemaResolver). - Embedded local/ fallback — Removed
src/main/resources/local/directory (clients.yaml,trusted-issuers.yaml). All configuration is mounted externally via Docker volumes.
- DCQL profiles simplified — Reduced to two scopes (
learcredential,doctorid) instead of redundantlearcredential.employee/learcredential.machinesub-profiles. - EUDI-013: Rename credential type IDs:
learcredential.employee.w3c.1→.w3c.4,learcredential.machine.w3c.1→.w3c.3 - EUDI-013:
extractIssFromJwt()now supports v2.0 issuer property (string or object) in addition to JWTissclaim - Actuator config migrated to Spring Boot 3.5
accessAPI — Replace deprecatedenabled-by-default: false/enabled: truewithaccess: none/access: unrestricted. - Health probes enabled — Added
livenessandreadinessstate indicators. Parameterizedshow-detailsviaMANAGEMENT_HEALTH_SHOW_DETAILSenv var (default:when-authorized).
- PKCE S256 enforced — PLAIN method rejected per HAIP / RFC 7636 §4.2 (S1).
- Revocation fail-closed — Credential rejected if revocation status cannot be determined, both JWT VP and SD-JWT paths (S2).
- Cache DoS protection — All
CacheStoreinstances bounded withmaximumSize(10000)(S3/F5). - Per-IP rate limiting —
RateLimitFilterwith 120 req/min general, 30 req/min on auth endpoints, atomic counters (S4). - Token Status List signature verification — JWT signature verified via x5c or DID before trusting status data (S5).
- Health endpoint hardened —
show-details: when-authorized(S6). - Open redirect prevention —
CustomErrorResponseHandlervalidates redirect URI against portal domain (S7). - ES256 preferred, RSA accepted —
SdJwtVerificationServiceImplandTokenStatusListVerifieraccept both EC (ES256) and RSA (RS256/PS256) signatures for QTSP compatibility (S8). See RSA deprecation plan below. - SSRF bypass for local dev —
verifier.ssrf.allow-privateproperty (defaultfalse) disables private/loopback IP checks inSafeUrlValidator. Required for local dev because*.127.0.0.1.nip.ioresolves to loopback. Must remainfalsein production (S14). - Log sanitization — Authorization codes truncated, DN/keys/JWKS not logged at INFO, state truncated in SSE logs (S9/F4).
- Input validation —
@Validated+@NotBlank/@SizeonOid4vpControllerparameters (F1). - Security headers —
SecurityHeadersFilteradds HSTS, X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy, Permissions-Policy, conditional Cache-Control (F2). - Error message leak prevention — All
GlobalExceptionHandlermethods usehandleSafe;handleWithremoved fromErrorResponseFactory(F3/O1). - SSE connection limit —
SseEmitterStorebounded at 5000 concurrent emitters (F6). - Refresh token rotation — Token invalidated on use in
CustomTokenRequestConverter(F10). - Swagger UI disabled by default — Controlled via
SPRINGDOC_ENABLEDenv var (F8). - Validation exception handlers —
ConstraintViolationExceptionandHandlerMethodValidationExceptionreturn 400 (W6). - Dependency updates —
org.json20230227→20240303,jackson-dataformat-yaml2.17.2→2.18.2 (F7).
- Schema-agnostic credential pipeline —
GenericCredential(JsonNode, SchemaProfile)replaces all typed LEARCredential POJOs. Validation, claims extraction, revocation, and M2M eligibility are driven by.profile.jsonfiles. Adding a new credential type requires zero Java code (EUDI-020 FR-10). - Profile-driven validation metadata —
SchemaProfileextended withValidationPaths,RevocationPaths,grantEligibility,schemaRequired,issuerIdPath,mandatorOrgIdPath. All.profile.jsonfiles updated. - Classpath schema auto-discovery —
LocalSchemaProfileRegistryscansclasspath:schemas/*.jsonautomatically viaResourcePatternResolver. No hardcoded filename arrays. - JSON Schema validation in VP pipeline —
CredentialValidatorwired intoVpServiceImplas Step 2b. Schema failures throwCredentialSchemaValidationException. - OID4VP
client_metadata— Authorization Request JWT includesclient_metadatawithvp_formats_supported(ES256 fordc+sd-jwtandjwt_vc_json) when client_id usesx509_hash:ordid:prefix (OID4VP §5.1) (EUDI-020 FR-05). - OpenAPI annotations —
@Tag,@Operation,@ApiResponse,@Parameteron all 4 custom endpoints.@Schemaon response models. Swagger UI at/swagger-ui.html(EUDI-020 FR-07). - CORS policy tests — 28 tests: integration tests for public endpoint wildcard CORS, unit tests for
PublicCorsConfigandRegisteredClientsCorsConfig(EUDI-020 FR-08). - Tenant claim in access token — Signed
tenantclaim in JWT access token from OIDC client registration (EUDI-017 Phase A). - DCQL query support — SD-JWT VC credential queries using DCQL for OID4VP 1.0 compliance.
- SD-JWT VC verification — Full SD-JWT VC (RFC 9901) verification pipeline with selective disclosure validation.
- LEARCredential typed models — Deleted entire
lear/model hierarchy (39 Java files),LEARCredentialTypeenum,CredentialMapperService,IssuerDeserializer,Issuer/SimpleIssuer/DetailedIssuer— replaced byGenericCredential(EUDI-020 FR-10). - Hardcoded M2M type checks —
MACHINE_CONFIG_IDSset andstartsWith("learcredential.machine.")replaced by profile-drivengrant_eligibility(EUDI-020 FR-10). - Hardcoded constants —
LOGIN_TIMEOUT,LOGIN_TIMEOUT_CHRONO_UNIT,IS_NONCE_REQUIRED_ON_FAPI_PROFILEremoved fromConstants.java(EUDI-020 FR-06).
- VP validation pipeline —
VpServiceImplusesGenericCredentialFactory+ profile-driven paths for time window, revocation, issuer org ID, and mandator validation. Mandator check is conditional on profile configuration (EUDI-020 FR-10). - Configurable login timeout and FAPI nonce —
verifier.backend.login-timeout-secondsandfapi-nonce-requiredinapplication.yamlwith env var overrides (EUDI-020 FR-06). - Credential type detection — Switched from hardcoded type strings to
credential_configuration_idpattern. - Token claim extraction — Refactored
CredentialClaimsExtractorto support both W3C and SD-JWT VC formats. - JTI replay cache —
JtiTokenCachenow usesCacheStore<String>with TTL-based expiry (1800s) instead of unboundedHashSet. - OID4VP authorization request —
audset tohttps://self-issued.me/v2per OID4VP §5.8;client_id_schemeremoved per §5.9. - Virtual threads — Enabled Spring virtual threads for I/O-bound operations.
- Hexagonal architecture: Reorganized entire codebase into 2 bounded contexts (
verifier/,oauth2/) +shared/module with ports & adapters pattern. - Application workflows: Extracted business logic from OAuth2 filters into testable workflow classes (AuthorizationRequestBuildWorkflow, TokenGenerationWorkflow, ClientCredentialsValidationWorkflow, VerifyPresentationWorkflow).
- External file injection: Clients YAML, trusted issuers YAML, and JSON Schemas can now be injected via Docker volumes or Kubernetes ConfigMaps without rebuilding the image (
VERIFIER_BACKEND_LOCALFILES_CLIENTSPATH,VERIFIER_BACKEND_SSO_CONFIG_PATH,VERIFIER_BACKEND_LOCALFILES_TRUSTEDISSUERSPATH,VERIFIER_BACKEND_LOCALFILES_SCHEMASDIR). - ArchUnit enforcement: 17 architecture rules validating hexagonal layers, bounded context isolation, naming conventions, and dependency constraints.
- Deployment guide: Comprehensive deployment documentation at
.claude/docs/deployment.md. - SSE login notification: New
SseEmitterStore+LoginSseController(/api/login/events?state=...) replaces WebSocket for cross-device QR login flow. - External frontend support: New
VERIFIER_FRONTEND_PORTALURLconfig property.CustomAuthorizationRequestConverterredirects to external Angular SPA instead of embedded Thymeleaf pages. - Portal CORS: New
PortalCorsConfigallows the external SPA (portalUrl) to access/api/login/**endpoints.
- Java 17 -> 25: Updated to Java 25 with Eclipse Temurin runtime.
- Gradle 8.8 -> 9.1.0: Updated build tool and wrapper.
- Spring Boot 3.3.2 -> 3.5.11: Major framework upgrade.
- Dockerfile:
gradle:9.1.0-jdk25build stage +eclipse-temurin:25-jre-alpineruntime. - OAuth2 filters slimmed down: CustomAuthorizationRequestConverter (524->250 lines), CustomAuthenticationProvider (392->200 lines), CustomTokenRequestConverter (229->150 lines) — all delegate to application workflows.
- ArchUnit 1.3.0 -> 1.4.1: Java 25 bytecode support.
- OWASP dependency-check 9.1.0 -> 12.2.0, SonarQube plugin 5.1.0 -> 6.0.1, Swagger 2.2.22 -> 2.2.28.
- AuthorizationResponseProcessorServiceImpl:
SimpMessagingTemplatereplaced bySseEmitterStore.send(state, redirectUrl). - FrontendProperties: Simplified to a single
portalUrlfield. Colors, assets, URLs, and defaultLang moved to Angular SPAtheme.json.
- Thymeleaf: Removed
spring-boot-starter-thymeleaf, 6 HTML templates (login-en/es/ca, client-authentication-error-en/es/ca), all static CSS/JS/images. - WebSocket: Removed
spring-boot-starter-websocket,WebSocketConfig, SockJS/STOMP infrastructure. - QR server-side: Removed
com.github.kenglxn.QRGen,LoginQrController,QRCodeGenerationException. QR is now generated client-side by the Angular SPA. - ClientErrorController: Error page now served by Angular SPA at
{portalUrl}/error.
- Read bitstring-encoded lists using MSB-first ordering.
- Add support for BitstringStatusListEntry credential status type.
- Added support for cryptographic binding
- In login template, enhance logo responsiveness.
- In login template, change 'dark-primary' variable name to 'secondary', and remove QR padding.
-
- Resolve logo and favicon URLs dynamically using a configurable images base URL and paths.
- Altia and ISBE favicons.
- Rename DOME favicon.
- Small text fixes in login template.
- Remove hardcoded visible "DOME" references in UI.
- For frontend pages, set language from Accept-Language header before using default language.
- Get default language from configuration, use it to translate HTML templates.
- Implement Authorization Code Flow with PKCE
- New major version to align with the new major version of EUDIStack project.
- Added revocation function for new credentials with credentialStatus.
- Test for verify that is working the revocation
- Added access for prometheus at spring security at matcher.
- Added access for prometheus at spring security.
- Validated audience and nonce for OpenID4VP.
- Added specific OpenID4VP exceptions.
- Handled type claim in Authorization Request.
- Modify the response token according to the grant type (client_credentials should not include id_token or refresh_token).
- Set the scopes profile and email in the response id_token, regardless of whether they are sent in the request.
- Change the client_id_schema to did:key in the authorization request.
- Modify the client_id in the response access_token so that it returns the URL.
- Add LEARCredentialMachine.
- Extract DID Key as environment variable.
- Add compatibility on LEARCredentialEmployee v2.0 for LEARCredential v1.0 claims
- Problem related to the M2M vp_token validation
- Problem logging in with token when the login time has run out.
- Problem with issuer serialization
- Access token timeout
- Error on JsonProperty annotation in the LEARCredential
- Compatibility for LEARCredentialEmployee v2.0
- Updated DOME Logo
- Updated Login page UI
- Refactor configuration parameters: removed unnecessary ones and grouped internal ones into frontend/backend categories.
- Add refresh token support for the OpenID Connect flow
- Add nonce support for the OpenID Connect authorization code flow
- Add documentation for OIDC client registration and interaction with the verifier.
- Add time window validation for the credential in the Verifiable Presentation
- Fix token serialization issue
- Add cors config for registered clients
- Rename the verifiableCredential claim of the access token to vc
- Fix contact us link not working
- Unauthorized Http response code for failed validation of VP token
- Add cors configuration to allow requests from external wallets, on the endpoints the wallet use.
- Add an error page for errors during the client authentication request.
- Fix images url
- Fix spacing between navbar and content for tablets width range
- Fix color contrast
- Use brand colors, font and favicon
- Fix layout responsiveness
- Fix the JWKS endpoint response to use the claim
usewithsigvalue.
- Authentication request fix to comply with the OpenID Connect Core standard.
- Token response fix to comply with the OpenID Connect Core standard.
- Fix security issue with the signature verification.
- Support for OpenID Connect.
- Only uses Authentication using the Authorization Code Flow (without PKCE).
- Only uses Claims with Requesting Claims using Scope Values (openid learcredential)
- Only uses Passing Request Parameters as JWTs (Passing a Request Object by Reference).
- Only use Client Authentication method with Private Key JWT.
- Only uses for P-256 ECDSA keys for Signing Access Token.
- Support for OpenID for Verifiable Presentations (OID4VP).
- Implement VP Proof of Possession verification.
- Implement Issuers, Participants and Services verification against the DOME Trust Framework.
- Implement VC verification against the DOME Revoked Credentials List.
- Support FAPI
- Only use request_uri as a REQUIRED claim in the Authentication Request Object.
- Implement DOME Human-To-Machine (H2M) authentication.
- Implement Login page with QR code.
- Implement DOME Machine-To-Machine (M2M) authentication.
- Integrate with the DOME Trust Framework.
- Fix the issue with Login page not showing Wallet URL.
- Fix the issue with Login page not valid Registration URL.
- Fix the issue with Login page not redirecting to the Relying Party after expiration of the QR code.