Skip to content

Latest commit

 

History

History
40 lines (34 loc) · 7.87 KB

File metadata and controls

40 lines (34 loc) · 7.87 KB

AGENTS.md

Project snapshot

  • jwt-demo is a Spring Boot 4.0.3 / Java 25 OAuth2 proxy in front of Keycloak, not an identity store. The backend deliberately does not persist login/refresh/logout client credentials; callers send clientId/clientSecret on each auth request (README.md, src/main/java/lt/satsyuk/service/KeycloakAuthService.java).
  • There are two distinct auth paths: /api/auth/** forwards form-encoded requests to Keycloak via KeycloakAuthService, while protected endpoints validate bearer tokens through opaque introspection using separate resource-server credentials from env; DPoP proofs are forwarded on auth requests and validated for protected endpoints by DpopAuthenticationFilter when tokens are bound via cnf.jkt (src/main/java/lt/satsyuk/config/SecurityConfig.java, src/main/java/lt/satsyuk/security/KeycloakOpaqueTokenIntrospector.java, src/main/java/lt/satsyuk/security/DpopAuthenticationFilter.java, src/main/resources/application.properties).
  • Business data is local PostgreSQL for client, account, and asynchronous request records; auth state stays in Keycloak. POST /api/clients now persists a request, schedules a persistent Quartz job, and the worker eventually creates both client and zero-balance account rows (src/main/resources/db/migration/V2__create_request_table_and_quartz_schema.sql, src/main/resources/db/migration/V3__create_account_table.sql, src/main/java/lt/satsyuk/service/RequestService.java, src/main/java/lt/satsyuk/service/ClientService.java).

Code patterns to follow

  • Controllers expose standardized AppResponse<T> envelopes. Regular CRUD endpoints return AppResponse.ok(...); auth endpoints use ResponseEntity<AppResponse<...>> so they can translate KeycloakAuthException status codes inline (src/main/java/lt/satsyuk/controller/AuthController.java, src/main/java/lt/satsyuk/dto/AppResponse.java).
  • POST /api/clients is intentionally asynchronous: the controller returns 202 Accepted with AppResponse<RequestAcceptedResponse>, and callers must poll GET /api/requests/{id} for a RequestStatusResponse whose response field contains the final nested AppResponse payload. Preserve the PENDING -> PROCESSING -> COMPLETED|FAILED lifecycle and stored JSON shapes when changing this flow (src/main/java/lt/satsyuk/controller/ClientController.java, src/main/java/lt/satsyuk/controller/RequestController.java, src/main/java/lt/satsyuk/service/RequestProcessingService.java).
  • Protected endpoints must declare method security with @PreAuthorize("hasRole('...')"); roles are derived by prefixing Keycloak realm/client roles with ROLE_ in KeycloakOpaqueRoleConverter, so checks use hasRole('CLIENT_CREATE'), not raw claim names.
  • Keep Swagger annotations in sync with security: protected controllers use @SecurityRequirement(name = "bearerAuth"), and OpenAPI bearer scheme is declared centrally in src/main/java/lt/satsyuk/config/OpenApiConfig.java.
  • DTOs are Java records with Bean Validation message keys, e.g. CreateClientRequest. If you add validation or new user-facing messages, update all bundles: messages.properties, messages_en.properties, messages_ru.properties.
  • Domain exceptions carry message codes/parameters and are localized in GlobalExceptionHandler through MessageService; follow ClientNotFoundException / PhoneAlreadyExistsException instead of hardcoding translated text in controllers.
  • Security failures return JSON from JsonAuthEntryPoint and JsonAccessDeniedHandler; do not introduce HTML/error-page defaults.
  • Auth endpoints accept optional DPoP headers, and protected endpoints may use Authorization: DPoP <token> plus DPoP: <proof> when introspection returns cnf.jkt; keep DPoP failures going through the same JSON auth entry point instead of ad hoc controller handling (src/main/java/lt/satsyuk/controller/AuthController.java, src/main/java/lt/satsyuk/security/DpopAuthenticationFilter.java, src/test/java/lt/satsyuk/api/integrationtest/DpopIntegrationIT.java).
  • KeycloakAuthService also emits Micrometer counters auth.login, auth.refresh, auth.logout tagged with result=success|failure; preserve these counters when changing auth flows.
  • Outbound Keycloak calls should reuse the observation-enabled RestTemplate bean from RestTemplateConfig rather than constructing new clients ad hoc.
  • New client creation must also provision a linked Account with zero balance, and /api/accounts/** follows two concurrency patterns: repository-level pessimistic locking for /balance/pessimistic and TransactionTemplate retries for /balance/optimistic (src/main/java/lt/satsyuk/service/ClientService.java, src/main/java/lt/satsyuk/service/AccountService.java, src/main/java/lt/satsyuk/repository/AccountRepository.java).
  • Bucket4j is configured through ordered app.rate-limit.rules[], not hardcoded per-endpoint properties: login is keyed by client IP, while /api/clients/** is keyed by authenticated clientId=spring-app. If you change token claims, client IDs, or add protected endpoints, re-check rule order, key strategy, cache names, and securityService.clientId() behavior (src/main/java/lt/satsyuk/config/RateLimitProperties.java, src/main/java/lt/satsyuk/security/RateLimitingFilter.java, src/main/resources/application.properties).

Testing and developer workflows

  • mvn test runs Surefire unit tests matching *Test*.java and writes coverage to target/site/jacoco/index.html.
  • mvn verify runs Failsafe integration tests matching *IT*.java and writes coverage to target/site/jacoco-it/index.html.
  • Run one integration test with: mvn -DskipTests=false "-Dit.test=ClientIntegrationIT" verify (CONTRIBUTING.md).
  • For local runtime, copy .env.example to .env, fill Keycloak/Postgres/Grafana secrets plus KEYCLOAK_RESOURCE_CLIENT_ID / KEYCLOAK_RESOURCE_CLIENT_SECRET, then start infra with docker compose up -d postgres postgres-app keycloak and run the app with mvn spring-boot:run.
  • On Windows, docs explicitly recommend Docker Desktop 4.28.x; newer versions may break Testcontainers (CONTRIBUTING.md).

Test infrastructure map

  • AbstractIntegrationTest provides shared Postgres Testcontainer, DPoP-aware HTTP helpers, typed AppResponse assertions, and cache cleanup between tests (it preserves filterConfigCache but clears other caches).
  • KeycloakIntegrationTest adds a real Keycloak Testcontainer loaded from keycloak/realm-export.json; use it for happy-path auth/authorization scenarios.
  • WireMockIntegrationTest stubs Keycloak token/introspection/logout endpoints and is the correct base for negative external-failure cases and rate-limit tests.
  • Test rate limits are intentionally shortened to 20 seconds in src/test/resources/application-test.properties; do not “fix” waits in RateLimitingIT without checking that profile.
  • Existing integration tests are the best executable spec for behavior: ClientIntegrationIT covers async client creation + request polling + i18n + authz, AccountIntegrationIT covers account CRUD/locking flows, DpopIntegrationIT covers DPoP forwarding and proof validation, AuthValidationIT covers validation envelopes, KeycloakIntegrationIT covers real Keycloak flows, KeycloakNegativeIT covers malformed/unavailable upstream responses, and RateLimitingIT covers ordered Bucket4j rules.

External systems and seeded data

  • docker-compose.yaml runs Keycloak 26, two Postgres containers (Keycloak DB + app DB), and an observability stack: OTel Collector, Tempo, Loki, Prometheus, Grafana.
  • keycloak/realm-export.json seeds realm my-realm, users user / admin, clients spring-app and resource-server, plus role mappers that populate realm_access.roles and resource_access.spring-app.roles.
  • Localization is request-driven via Accept-Language; Russian messages are asserted in ClientIntegrationIT, so changing error text requires synchronized bundle/test updates.