From 74a5b2c9e377cd2afe7f42fede2dea293fa5b203 Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:58:39 -0500 Subject: [PATCH 01/13] update gitignore --- .gitignore | 4 +++- src/main/java/com/wallet/secure/user/dto/RegisterRequest.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d82e68e..c4a67e2 100644 --- a/.gitignore +++ b/.gitignore @@ -112,4 +112,6 @@ yarn-error.log* # Virtual environments (if using Python for scripts) venv/ -.venv/ \ No newline at end of file +.venv/ + +QA.md diff --git a/src/main/java/com/wallet/secure/user/dto/RegisterRequest.java b/src/main/java/com/wallet/secure/user/dto/RegisterRequest.java index 37a15dc..9f7962b 100644 --- a/src/main/java/com/wallet/secure/user/dto/RegisterRequest.java +++ b/src/main/java/com/wallet/secure/user/dto/RegisterRequest.java @@ -31,7 +31,7 @@ public class RegisterRequest { * OWASP A07: enforce strong password policy at API entry point. */ @NotBlank(message = "Password is required") - @Size(min = 12, max = 72, message = "Password must be between 8 and 72 characters") + @Size(min = 12, max = 72, message = "Password must be between 12 and 72 characters") @Pattern( regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&_.#^])[A-Za-z\\d@$!%*?&_.#^]{12,}$", message = "Password must contain uppercase, lowercase, number and special character" From 7ac4af01fc903d0f4b6fb03bb288f8025b21b045 Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:51:38 -0500 Subject: [PATCH 02/13] docs: align README with implementation project --- README.md | 287 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 178 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 9a87d9b..82e1db5 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,104 @@ # Secure Wallet API -Multi-currency digital savings wallet — REST API with authentication, transactions, and audit trail. +Educational-professional REST API for a simulated multi-currency savings wallet, with an emphasis on applied security, traceability, and DevSecOps practices. Built with **Java 17**, **Spring Boot 3.4.2**, and **PostgreSQL 16**. -> This is a learning project built with AI assistance to explore how a savings wallet API works under the hood — authentication flows, ACID financial transactions, audit logging, and security practices. Some sections still need review and polish. +> This project is designed for local learning and portfolio demonstration. All balances and operations are simulated: it does not process real money and is not intended for public production deployment. +> +> The project is under active development. Confirmed requirements, detected defects, pending decisions, and verification work are tracked in [`docs/IMPLEMENTATION-PLAN.md`](docs/IMPLEMENTATION-PLAN.md). --- ## What is this -A REST API for a digital savings wallet where users can: +Secure Wallet API explores how the main components of a savings-wallet backend work together: -- Register and manage their profile -- Create savings wallets in multiple currencies (USD, EUR, COP, MXN, ARS) -- Deposit, withdraw, and transfer funds between wallets -- Track authentication, wallet, and transaction events through an audit trail -- Authenticate with access and refresh JWT tokens +- User registration and profile management +- Savings wallets in USD, EUR, COP, MXN, and ARS +- Simulated deposits, withdrawals, and transfers +- JWT authentication with access and refresh tokens +- Role- and ownership-based authorization +- Security and transaction audit trails +- PostgreSQL constraints and ACID transaction boundaries +- Automated build, test, dependency scanning, and static analysis workflows -The idea came from wanting to understand the moving parts of a real savings wallet — not just the happy path, but edge cases, concurrency, token rotation, and what happens when things go wrong. +The goal is to study both successful flows and failure scenarios: unauthorized access, invalid tokens, insufficient balances, concurrent updates, session revocation, and auditability. --- -## Features +## Feature status + +Status labels used below: + +- **Implemented** — code for the capability exists. +- **Verified** — automated or documented functional evidence exists. +- **Partial** — some parts exist, but the complete requirement is not yet satisfied or verified. +- **Planned** — confirmed requirement or candidate decision that has not been fully implemented. ### User Management -- Registration, profile updates, and soft-delete accounts -- Role-based access: `USER`, `ADMIN`, `MANAGER` -- Account lockout after repeated failed login attempts + +- **Implemented:** public registration creates `USER` accounts; profile read/update and soft deactivation are available. +- **Implemented:** the target business roles are `USER` and `ADMIN`; privileged access uses Spring Security method authorization. +- **Planned:** the first `ADMIN` will be created through a controlled one-time bootstrap without public default credentials. +- **Planned:** administrators will manage regular users but will not create other administrators through the public API. +- **Partial:** account-state and failed-login controls exist in the model, but the complete lockout flow still requires correction and functional verification. ### Multi-Currency Savings Wallets -- One wallet per user per currency -- Supported currencies: USD, EUR, COP, MXN, ARS -- Wallet states: `ACTIVE`, `SUSPENDED`, `CLOSED` -- Balance updates with pessimistic locking + +- **Implemented:** one wallet per user and currency. +- **Implemented:** USD, EUR, COP, MXN, and ARS currencies. +- **Implemented:** `ACTIVE`, `SUSPENDED`, and `CLOSED` wallet states. +- **Partial:** ownership and administrative wallet operations are implemented but still require complete authorization tests. ### Financial Transactions -| Operation | Description | -|-----------|-------------| -| Deposit | Add funds from an external source | -| Withdrawal | Move funds out to an external destination | -| Transfer | Move funds between two wallets in the system | -| History | State-change log per transaction | -- Pessimistic locking on balance reads to prevent race conditions -- Unique reference code per transaction +| Operation | Description | Status | +|-----------|-------------|--------| +| Deposit | Add simulated external funds to a wallet | Implemented | +| Withdrawal | Remove simulated funds from a wallet | Implemented | +| Transfer | Move simulated funds between wallets | Implemented | +| History | Read transaction state changes | Implemented | + +- **Implemented:** `BigDecimal`/`DECIMAL(19,4)` amounts, transactional service boundaries, reference codes, and pessimistic balance locking. +- **Partial:** concurrency, rollback, ownership, and complete audit behavior require PostgreSQL-backed and Postman verification. +- **Pending decision:** configurable transaction limits and thresholds must be reviewed before full implementation. ### Authentication & Security -- Dual-token JWT: Access Token (short-lived) + Refresh Token (longer-lived with rotation) -- Refresh token stored as SHA-256 hash — real revocation on logout -- UUID primary keys on all entities (no sequential IDs) -- The default configuration prevents stack traces and internal exception details from reaching clients -- Input validation on every endpoint + +- **Implemented:** short-lived access JWTs and longer-lived refresh JWTs. +- **Implemented:** JWT signature, expiration, issuer, and token-type validation. +- **Implemented:** BCrypt password hashing and Spring Security authentication. +- **Implemented:** refresh-session tracking and revocation support. +- **Partial:** refresh-token rotation and removal of legacy raw-token storage remain required. +- **Partial:** disabled or locked accounts must be rejected consistently during both login and JWT-authenticated requests. +- **Pending decision:** TOTP-based 2FA requires a separate design review before full implementation. ### Audit Trail -- Every sensitive operation generates a log entry -- IP address, User-Agent, timestamp, and contextual details per event -- Database triggers for automatic state-change tracking + +- **Implemented:** authentication, wallet, transaction, and account events have audit-writing support. +- **Implemented:** audit entries can include user, IP address, User-Agent, timestamp, severity, and contextual JSON details. +- **Partial:** complete event coverage and failure resilience still require end-to-end verification. --- ## Architecture -**Pattern:** Domain-Driven Design — code organized by business domain, not by layer type. +**Pattern:** domain-oriented layered architecture inspired by Domain-Driven Design. Code is grouped by business domain rather than by global technical layer. -``` +```text com.wallet.secure/ -├── config/ # SecurityConfig, OpenApiConfig, AuditConfig, JwtConfig -├── auth/ # Authentication: JWT, 2FA, sessions +├── config/ # Security, JWT, OpenAPI, and audit configuration +├── auth/ # Authentication, JWT validation, and sessions │ ├── controller/, dto/, entity/, repository/, security/, service/ ├── user/ # Users and profiles -├── wallet/ # Wallets and balances -├── transaction/ # Transactions and history (business core) -├── audit/ # Security audit log -└── common/ # ApiResponse, exceptions, enums, validators +├── wallet/ # Wallet ownership, balances, and lifecycle +├── transaction/ # Financial operations and state history +├── audit/ # Security and business audit trail +└── common/ # Shared responses, exceptions, enums, and utilities ``` -Each domain owns its controllers, DTOs, entities, repositories, and services. Nothing lives at the root level grouped by type. +Each domain owns its controllers, DTOs, entities, repositories, and services. Controllers handle HTTP concerns, services enforce business rules, repositories provide persistence access, and DTOs limit the data accepted from or returned to clients. --- @@ -87,22 +109,29 @@ Each domain owns its controllers, DTOs, entities, repositories, and services. No | Language | Java 17 | | Framework | Spring Boot 3.4.2 | | Security | Spring Security + jjwt 0.12.6 | -| Database | PostgreSQL 16 (Docker) | +| Database | PostgreSQL 16 | | ORM | Spring Data JPA / Hibernate | -| Logging | Log4j2 | +| Logging | Log4j2 2.25.3 | | Validation | Spring Bean Validation | -| API Docs | SpringDoc OpenAPI 2.8.8 (Swagger) | -| Boilerplate | Lombok | -| Tests | JUnit 5 + Mockito + AssertJ | -| CI/CD | GitHub Actions + CodeQL + OWASP Dependency Check | +| API Docs | SpringDoc OpenAPI 2.8.8 | +| Boilerplate | Lombok 1.18.36 | +| Tests | JUnit 5 + Mockito + AssertJ + H2 security integration test | +| DevSecOps | GitHub Actions + CodeQL + OWASP Dependency Check | +| Local runtime | Docker Compose | --- -## Getting Started +## Getting Started with Docker Compose + +Docker Compose is the primary way to run the complete local environment: PostgreSQL and the Spring Boot API. ### Prerequisites -- Java 17+ -- Docker and Docker Compose + +- Docker Engine or Docker Desktop +- Docker Compose v2 +- Git + +Java and Maven are not required to run the packaged application through Docker Compose. Java 17 is required for local development and for running Maven tests outside the containers. ### Setup @@ -110,99 +139,138 @@ Each domain owns its controllers, DTOs, entities, repositories, and services. No git clone https://github.com/DJAngel973/Secure-Wallet-API.git cd Secure-Wallet-API -# Copy and fill in the environment variables +# Create the local, untracked environment file cp .env.example .env -# Start PostgreSQL -docker compose up -d +# Replace every CHANGE_ME value and configure FILE_DATA_POSTGRES + +# Build and start PostgreSQL and the API +docker compose up -d --build + +# Confirm container health and startup status +docker compose ps + +# Follow API logs when troubleshooting startup +docker compose logs -f wallet-api +``` + +The API is available at `http://localhost:8080` unless `SERVER_PORT` is changed in `.env`. + +> PostgreSQL executes files mounted under `docker-entrypoint-initdb.d` only when initializing an empty data directory. The current development seed is tracked as a remediation item because public default administrative credentials must not be part of the standard initialization path. -# Run the application (dev profile) -./mvnw spring-boot:run -Dspring-boot.run.profiles=dev +### Stop the environment + +```bash +docker compose down ``` -### Run tests +This does not delete the configured PostgreSQL data directory. + +### Run automated tests ```bash -./mvnw test # Complete automated test suite -./mvnw test -Dtest=TransactionServiceTest # Single test class +./mvnw test +./mvnw test -Dtest=TransactionServiceTest ``` -The suite contains Mockito unit tests for the business services and a small Spring Security integration test using H2. PostgreSQL is not required to run the test suite. +The current suite primarily contains Mockito unit tests plus a small Spring Security integration test using H2. It does not yet prove complete PostgreSQL or HTTP end-to-end behavior. + +### Explore with Postman + +Postman is the primary tool for demonstrating complete local API flows. The planned functional verification covers: -### Interactive API docs +1. Register and login as a regular user. +2. Store the access and refresh tokens as environment variables. +3. Send `Authorization: Bearer ` to protected endpoints. +4. Create wallets and perform simulated deposits, withdrawals, and transfers. +5. Verify ownership failures, insufficient-balance failures, refresh, logout, and session revocation. +6. Use a controlled bootstrap administrator to verify `USER` versus `ADMIN` authorization. -Once running: [http://localhost:8080/swagger-ui/index.html](http://localhost:8080/swagger-ui/index.html) +A versioned Postman collection is planned and tracked in the implementation plan. -All responses follow a standard wrapper: +### Interactive API documentation + +Swagger UI is available locally at: + +[http://localhost:8080/swagger-ui/index.html](http://localhost:8080/swagger-ui/index.html) + +All API responses use a standard wrapper: ```json { "success": true, "message": "Operation completed", - "data": { } + "data": {} } ``` --- -**Note:** endpoints listed in Features above have not been tested end-to-end yet. The Swagger UI at `http://localhost:8080/swagger-ui/index.html` lists all available endpoints for exploration when the app is running. - ---- - ## Database -PostgreSQL 16 with schema managed through numbered SQL scripts. The default published configuration uses `ddl-auto: validate`, so Hibernate validates the schema without creating or modifying it. +PostgreSQL 16 is managed through numbered SQL scripts. The published application configuration uses `ddl-auto: validate`, so Hibernate validates the schema instead of creating or updating it. -| # | Script | Purpose | -|---|--------|---------| -| 01 | `extensions.sql` | PostgreSQL extensions (pgcrypto, uuid-ossp) | -| 02 | `types.sql` | Custom ENUMs (roles, currencies, statuses) | -| 03 | `tables.sql` | Core tables with constraints and foreign keys | -| 04 | `index.sql` | Partial, composite, and GIN indexes | -| 05 | `triggers.sql` | Auto-timestamps, balance validation, state-change audit | -| 06 | `functions.sql` | `process_transaction()` with ACID guarantees | -| 07 | `seed.sql` | Development test data | -| 08 | `migrations.sql` | Incremental schema changes | +| # | Script | Purpose | Current status | +|---|--------|---------|----------------| +| 01 | `extensions.sql` | PostgreSQL extensions | Implemented | +| 02 | `types.sql` | Roles, currencies, and status enums | Implemented | +| 03 | `tables.sql` | Tables, constraints, and foreign keys | Implemented | +| 04 | `index.sql` | Partial, composite, and GIN indexes | Implemented | +| 05 | `triggers.sql` | Timestamp, balance, and history triggers | Implemented | +| 06 | `functions.sql` | Database transaction-processing functions | Implemented | +| 07 | `seed.sql` | Current development-only test data | Must be separated from automatic initialization | +| 08 | `migrations.sql` | Incremental schema changes | Implemented | + +The database schema is part of the security boundary: unique constraints, ownership relationships, non-negative balances, transaction consistency, and append-oriented audit/history records complement application-layer validation. --- -## OWASP Top 10 (2025) coverage +## OWASP Top 10:2025 alignment + +OWASP Top 10 is used as an awareness and review framework, not as a certification that every risk has been eliminated. -This project uses the OWASP Top 10 as a guide for security decisions. Here is what is currently addressed: +| Category | Current project alignment | Status | +|----------|---------------------------|--------| +| A01 — Broken Access Control | JWT-derived identity, role checks, ownership-aware repository queries, and non-enumerating `404` patterns | Partial — broader authorization tests required | +| A02 — Security Misconfiguration | Environment-based secrets, controlled error responses, container hardening, and restricted published Actuator exposure | Partial — development settings and seed initialization require review | +| A03 — Software Supply Chain Failures | Pinned key dependency versions, OWASP Dependency Check, and CodeQL workflows | Implemented; scan results remain environment-dependent | +| A04 — Cryptographic Failures | BCrypt passwords, signed JWTs, short access-token lifetime, and hashed session tokens | Partial — legacy raw refresh-token storage must be removed | +| A05 — Injection | DTO validation plus parameterized persistence through Spring Data JPA/Hibernate | Implemented; complete endpoint verification pending | +| A06 — Insecure Design | Transaction boundaries, pessimistic locking, database constraints, audit design, and documented threat decisions | Partial — configurable limits and TOTP remain pending decisions | +| A07 — Authentication Failures | Dual-token authentication, account-state modeling, session revocation, and authentication auditing | Partial — rotation, lockout, and account-state enforcement require correction and tests | +| A08 — Software or Data Integrity Failures | Versioned SQL, CI analysis, controlled schema validation, and transaction-history records | Partial — release and integration verification can be expanded | +| A09 — Security Logging & Alerting Failures | Database audit events, severity levels, IP/User-Agent context, and fallback logging design | Partial — coverage and alert behavior require functional verification | +| A10 — Mishandling of Exceptional Conditions | Centralized exception handling and transactional rollback boundaries | Partial — HTTP failure paths require end-to-end verification | -| # | Risk | How it is handled | -|---|------|-------------------| -| A01 | Broken Access Control | `userId` always from JWT — never from request body. `404` on ownership mismatch. RBAC via Spring Security. | -| A02 | Cryptographic Failures | BCrypt strength 12 for passwords. SHA-256 for token hashing. `DECIMAL(19,4)` for amounts. | -| A03 | Software Supply Chain Failures | OWASP Dependency Check in CI. Pinned dependency versions. | -| A04 | Injection | Hibernate prepared statements. Bean Validation on all inputs. | -| A05 | Security Misconfiguration | No internals exposed in errors. No default credentials. Secure headers. | -| A06 | Insecure Design | Transaction and daily limits. 2FA threshold. Pessimistic locking on balance reads. | -| A07 | Auth Failures | Dual-token JWT with rotation. Account lockout. TOTP 2FA. | -| A08 | Data Integrity Failures | CodeQL in CI. SQL scripts versioned in Git. `ddl-auto: validate`. | -| A09 | Logging & Monitoring | All security events written to `audit_logs`. No passwords or tokens in logs. | -| A10 | Mishandled Exceptions | Centralized exception handler. Never "fail open". | +See [`SECURITY.md`](SECURITY.md) for the threat model. Where that document describes a target control not yet verified, the implementation plan is the authoritative status tracker. --- ## CI/CD -| Job | When | What | -|-----|------|------| -| Build & Test | Push to `main` / PR targeting `main` | Compile and run the automated test suite | -| OWASP Dependency Check | Push to `main` / PR targeting `main` | Scan dependencies for known vulnerabilities | -| CodeQL Analysis | Push to `main`, PR targeting `main`, and weekly | Static security analysis | -| Package | Push to `main` | Generate and upload the application JAR | +| Job | Trigger | Purpose | Status | +|-----|---------|---------|--------| +| Build & Test | Push to `main` / PR targeting `main` | Compile and run automated tests | Implemented | +| OWASP Dependency Check | Push to `main` / PR targeting `main` | Scan dependencies for known vulnerabilities | Implemented | +| CodeQL Analysis | Push/PR to `main` and weekly schedule | Static security analysis | Implemented | +| Package | Push to `main` after prior jobs | Generate and upload the application JAR | Implemented | + +These workflows provide automated evidence, but passing CI does not replace functional security testing or a production-readiness assessment. --- -## Future plans +## Roadmap and implementation status + +The prioritized remediation and verification plan is maintained in: + +[`docs/IMPLEMENTATION-PLAN.md`](docs/IMPLEMENTATION-PLAN.md) + +The plan distinguishes: -- Review and test all endpoints end-to-end -- Polish existing code sections marked for improvement -- Postman collection for easier API exploration -- TOTP-based two-factor authentication for high-value transactions -- Enforcement of configurable per-transaction and daily limits +- Confirmed architectural requirements +- Detected implementation defects +- Decisions that require further review +- Automated and Postman acceptance evidence --- @@ -210,10 +278,11 @@ This project uses the OWASP Top 10 as a guide for security decisions. Here is wh | Document | What it covers | |----------|---------------| -| [`CONTEXT.md`](CONTEXT.md) | Architecture, conventions, full stack details | -| [`DECISIONS.md`](DECISIONS.md) | 8 Architecture Decision Records with reasoning | -| [`SECURITY.md`](SECURITY.md) | Threat model and security decisions in depth | -| [`database/README.md`](database/README.md) | ER diagram, foreign key rules, DB design | +| [`docs/IMPLEMENTATION-PLAN.md`](docs/IMPLEMENTATION-PLAN.md) | Prioritized remediation, implementation, and verification work | +| [`CONTEXT.md`](CONTEXT.md) | Architecture, conventions, and stack context | +| [`DECISIONS.md`](DECISIONS.md) | Architecture Decision Records and rationale | +| [`SECURITY.md`](SECURITY.md) | Threat model and target security controls | +| [`database/README.md`](database/README.md) | Database model, constraints, and local setup | --- From 8e7b85de071f78b422d17b50a56e9b4de10dad6e Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:14:50 -0500 Subject: [PATCH 03/13] docs: add the implementation plan file to refine and finalize the project's safety steps --- docs/IMPLEMENTATION-PLAN.md | 552 ++++++++++++++++++++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 docs/IMPLEMENTATION-PLAN.md diff --git a/docs/IMPLEMENTATION-PLAN.md b/docs/IMPLEMENTATION-PLAN.md new file mode 100644 index 0000000..7854d2a --- /dev/null +++ b/docs/IMPLEMENTATION-PLAN.md @@ -0,0 +1,552 @@ +# Secure Wallet API — Implementation Plan + +This plan records the work required to align the implementation, tests, and documentation with the confirmed architecture of Secure Wallet API. An item listed here is not evidence that the behavior is already implemented. + +## Scope and status model + +Secure Wallet API is an educational-professional backend for a simulated multi-currency savings wallet. It runs locally through Docker Compose and demonstrates its HTTP flows primarily with Postman. It does not process real money and is not intended for public production deployment. + +| Label | Meaning | +|-------|---------| +| **Confirmed requirement** | Approved target behavior that implementation must satisfy | +| **Detected defect** | Current behavior contradicts an approved requirement or security expectation | +| **Pending decision** | Design must be confirmed before implementation | + +Priorities: + +- **P0 — Security blocker:** resolve before treating the related control as functional. +- **P1 — Core correctness:** required for reliable authorization, transactions, or evidence. +- **P2 — Verification/documentation:** required for repeatable demonstration and accurate claims. +- **P3 — Future design:** intentionally deferred until its requirements are approved. + +## Dependency order + +```text +Development seed separation + ↓ +First-admin bootstrap + ↓ +Administrative user management + +Session storage cleanup + ↓ +Refresh-token rotation + ↓ +Session and JWT functional tests + +Account lockout correction + ↓ +JWT account-state enforcement + ↓ +Authentication security tests + +Wallet ownership review + ↓ +Transaction/concurrency/audit verification + ↓ +Postman critical-flow collection +``` + +--- + +## P0 — Security blockers + +### SEC-001 — Separate development seed data from automatic initialization + +**Classification:** Detected defect +**Priority:** P0 +**Depends on:** None + +**Current problem** + +`database/07-seed.sql` creates an administrator with publicly know credentials during PostgreSQL initialization. + +**Expected behavior** + +- Remove 07-seed.sql if no application or test dependency requirest it. +- Configure Docker Compose to initialize only the required database schema. +- Do not create default users or administrators during SQL initialization. +- Keep Docker Compose as the primary way to start PostgreSQL and Spring Boot. + +**Acceptance criteria** + +- [ ] No default administrator is created by SQL initialization. +- [ ] No public administrator password remains in the repository's current tracked files. +- [ ] PostgreSQL initializes correctly using Docker Compose. +- [ ] Existing tests continue to pass. + +### SEC-002 — Add controlled one-time bootstrap for the first administrator + +**Classification:** Confirmed requirement +**Priority:** P0 +**Depends on:** SEC-001 + +**Current problem** + +The first administrator is currently supplied only by the public development seed. No controlled bootstrap exists. + +**Expected behavior** + +- The first `ADMIN` is created exactly once through a controlled bootstrap. +- Bootstrap credentials come from an untracked environment or secret input and are never logged. +- Public registration always creates `USER`. +- The bootstrap refuses to create additional administrators after the first valid administrator exists. + +**Possibly affected files** + +- New bootstrap component under the `auth` or `user` domain +- `UserRepository.java` +- Security configuration properties +- `.env.example` +- Bootstrap unit/integration tests +- Docker environment wiring + +**Acceptance criteria** + +- [ ] First startup with valid bootstrap input creates one BCrypt-hashed `ADMIN`. +- [ ] A second startup creates no additional administrator and changes no existing credentials. +- [ ] Missing or invalid bootstrap input fails safely without exposing secrets. +- [ ] Bootstrap secrets are absent from logs, API responses, Git history, and persisted configuration. +- [ ] `POST /auth/register` cannot create an `ADMIN`, including when the JSON includes a `role` field. + +### SEC-003 — Remove original refresh-token storage from the user record + +**Classification:** Detected defect +**Priority:** P0 +**Depends on:** None + +**Current problem** + +`SessionService` stores a SHA-256 token hash in `user_sessions`, but `AuthService` also assigns the original refresh JWT to `User.refreshToken`. This contradicts the requirement that reusable tokens are not stored directly. + +**Expected behavior** + +- Only a non-reusable token identifier or approved one-way representation is persisted. +- Refresh validation and revocation use the session model as the single source of truth. +- Database disclosure does not expose a directly reusable refresh token. + +**Possibly affected files** + +- `AuthService.java` +- `User.java` +- `SessionService.java` +- `SessionRepository.java` +- `database/03-tables.sql` +- `database/08-migrations.sql` +- Authentication/session tests + +**Acceptance criteria** + +- [ ] No database column stores the original refresh JWT. +- [ ] Login, refresh, logout, single-session revocation, and all-session revocation still work. +- [ ] Tests prove that a stored session value cannot be sent as a refresh token. +- [ ] Schema migration preserves or explicitly invalidates existing sessions safely. + +### SEC-004 — Implement and verify refresh-token rotation + +**Classification:** Confirmed requirement +**Priority:** P0 +**Depends on:** SEC-003 + +**Current problem** + +The refresh endpoint issues only a new access token and keeps the same refresh token. Reuse detection and atomic rotation are not implemented. + +**Expected behavior** + +- Every successful refresh returns a new access token and a new refresh token. +- The previous refresh token is invalidated atomically. +- Reuse of an already-rotated token is rejected and produces an auditable security event. +- Concurrent refresh attempts cannot both succeed with the same token. + +**Possibly affected files** + +- `AuthService.java` +- `SessionService.java` +- `Session.java` +- `SessionRepository.java` +- `AuthResponse.java` +- Authentication/session tests +- Database indexes or migration if token-family tracking is selected + +**Acceptance criteria** + +- [ ] A successful refresh returns two new tokens. +- [ ] The previous refresh token fails immediately afterward. +- [ ] Exactly one of two concurrent refresh requests with the same token succeeds. +- [ ] Rotation and reuse failures create audit evidence without logging token values. +- [ ] Logout revokes the current rotated session. + +### SEC-005 — Correct failed-login lockout + +**Classification:** Detected defect +**Priority:** P0 +**Depends on:** None + +**Current problem** + +Failed login events are audited, but the authentication flow does not demonstrably call repository operations that increment attempts and set `lockedUntil`. Values also conflict: application configuration states five attempts while the database constrains the counter to three. + +**Expected behavior** + +- One authoritative, configurable threshold and lock duration are used. +- Failed attempts are incremented atomically. +- Reaching the threshold locks the account for the configured duration. +- A successful login resets the counter. +- Unknown-email and wrong-password responses remain non-enumerating. + +**Possibly affected files** + +- `AuthService.java` +- `UserRepository.java` +- `User.java` +- Security configuration properties +- `application.yml` +- `database/03-tables.sql` +- `database/08-migrations.sql` +- `GlobalExceptionHandler.java` +- Authentication tests + +**Acceptance criteria** + +- [ ] The configured number of consecutive failures locks the account. +- [ ] PostgreSQL accepts every valid counter state under the selected policy. +- [ ] A correct password during the lock period cannot authenticate. +- [ ] Authentication succeeds after expiry according to the chosen unlock policy. +- [ ] Success resets failed attempts to zero. +- [ ] Unit and PostgreSQL-backed tests cover boundary and concurrency cases. + +### SEC-006 — Reject JWT authentication for disabled or locked accounts + +**Classification:** Detected defect +**Priority:** P0 +**Depends on:** SEC-005 + +**Current problem** + +Password authentication maps active/locked state into Spring Security. During access-token authentication, however, `JwtAuthFilter` constructs an authenticated token after loading `UserDetails` without explicitly rejecting disabled or locked state. + +**Expected behavior** + +- Disabled or locked users cannot establish a `SecurityContext`, even with an unexpired access token. +- Account deactivation and administrative suspension revoke relevant refresh sessions. +- Response behavior is consistent and does not expose sensitive account details. + +**Possibly affected files** + +- `JwtAuthFilter.java` +- `UserDetailsServiceImpl.java` +- `UserService.java` +- `SessionService.java` +- Security integration tests + +**Acceptance criteria** + +- [ ] An existing access token fails after its user is disabled. +- [ ] An existing access token fails while its user is locked. +- [ ] Disabling an account revokes all refresh sessions. +- [ ] Active, unlocked users continue to authenticate normally. + +--- + +## P1 — Authorization and financial correctness + +### AUTHZ-001 — Implement controlled management of regular users + +**Classification:** Confirmed requirement +**Priority:** P1 +**Depends on:** SEC-002, SEC-006 + +**Current problem** + +Administrative read/deactivation operations exist, but there is no consolidated and verified administrative user-management contract. The confirmed model allows an administrator to manage regular users but not create or promote other administrators through the API. + +**Expected behavior** + +- `ADMIN` can perform explicitly approved management operations on `USER` accounts. +- `USER` cannot invoke administrative operations. +- No public or administrative API operation creates, promotes, or modifies another `ADMIN`. +- Every administrative action is audited with actor and target identifiers. + +**Possibly affected files** + +- `UserController.java` +- `UserService.java` +- User administration DTOs if approved +- `SecurityConfig.java` +- `AuditService.java` +- Authorization tests + +**Acceptance criteria** + +- [ ] `USER` receives `403` for every administrative endpoint. +- [ ] `ADMIN` can manage only operations explicitly allowed for regular users. +- [ ] API requests containing privileged role changes are rejected or ignored safely and never persisted. +- [ ] No API route can create or promote another administrator. +- [ ] Administrative actions produce immutable audit records. + +### AUTHZ-002 — Review wallet permissions and ownership + +**Classification:** Confirmed requirement +**Priority:** P1 +**Depends on:** AUTHZ-001 + +**Current problem** + +Wallet controllers derive identity from the JWT and services include ownership patterns, but complete endpoint-by-endpoint authorization evidence is missing. + +**Expected behavior** + +- Users can read and operate only their own wallets. +- Ownership mismatches use non-enumerating responses where appropriate. +- Administrative wallet lifecycle operations require `ADMIN` and are audited. +- Request bodies cannot override the authenticated owner. + +**Possibly affected files** + +- `WalletController.java` +- `WalletService.java` +- `WalletRepository.java` +- Wallet DTOs +- `SecurityConfig.java` +- Wallet authorization tests + +**Acceptance criteria** + +- [ ] User A cannot read, debit, close, suspend, or restore User B's wallet. +- [ ] User-controlled input cannot assign a wallet to a different owner. +- [ ] Administrative actions require `ROLE_ADMIN` and generate audit records. +- [ ] Automated tests cover every wallet endpoint with owner, non-owner, anonymous, and admin actors. + +### TX-001 — Verify transactions, concurrency, rollback, and audit behavior + +**Classification:** Confirmed requirement +**Priority:** P1 +**Depends on:** AUTHZ-002 + +**Current problem** + +Transaction services use transactional boundaries and pessimistic locking, but unit tests do not prove real PostgreSQL locking, concurrent updates, trigger behavior, or complete rollback/audit semantics. + +**Expected behavior** + +- Deposits, withdrawals, and transfers preserve financial invariants. +- Concurrent requests cannot lose updates, create negative balances, or double-spend. +- Failed operations leave balances unchanged. +- Ownership, currency, wallet state, and reference-code rules are enforced. +- Success and failure paths create the required audit/history evidence. + +**Possibly affected files** + +- `TransactionService.java` +- `TransactionRepository.java` +- `WalletRepository.java` +- `TransactionHistoryService.java` +- `AuditService.java` +- Transaction and wallet entities +- Database functions, triggers, and migrations if defects are confirmed +- PostgreSQL integration tests + +**Acceptance criteria** + +- [ ] Failed withdrawals and transfers leave all balances unchanged. +- [ ] Concurrent withdrawal/transfer tests preserve balance invariants. +- [ ] Duplicate reference codes are rejected deterministically. +- [ ] Users cannot transact from wallets they do not own. +- [ ] Transaction status history and security audit records match the final outcome. +- [ ] PostgreSQL-backed tests demonstrate locking and rollback behavior. + +--- + +## P2 — Verification and delivery evidence + +### TEST-001 — Expand automated security and integration tests + +**Classification:** Confirmed requirement +**Priority:** P2 +**Depends on:** SEC-004, SEC-005, SEC-006, AUTHZ-001, AUTHZ-002, TX-001 + +**Current problem** + +The suite primarily proves service logic through Mockito and contains limited Spring Security integration coverage. It does not provide complete evidence for PostgreSQL, JWT lifecycle, role matrices, or financial concurrency. + +**Expected behavior** + +- Fast unit tests continue to cover business branches. +- Focused security integration tests prove filter-chain and method-authorization behavior. +- PostgreSQL-backed tests prove schema, locking, and transaction behavior. +- Security regression tests map to confirmed requirements. + +**Possibly affected files** + +- `src/test/java/**` +- `src/test/resources/**` +- `pom.xml` if a PostgreSQL integration-test dependency is approved +- CI workflows if an integration-test job is approved + +**Acceptance criteria** + +- [ ] Tests cover anonymous `401`, authenticated `USER` `403`, and authorized `ADMIN` success. +- [ ] Tests cover tampered, expired, wrong-type, revoked, and rotated JWTs. +- [ ] Tests cover account lockout and access-token rejection after state changes. +- [ ] Tests cover wallet ownership and transaction rollback/concurrency. +- [ ] CI publishes readable unit and integration test results. + +### TEST-002 — Create a functional Postman verification suite + +**Classification:** Confirmed requirement +**Priority:** P2 +**Depends on:** TEST-001 + +**Current problem** + +Postman is the intended local demonstration tool, but no versioned collection currently provides repeatable critical-flow verification. + +**Expected behavior** + +- A collection and local environment exercise the API in dependency order. +- Tests capture identifiers/tokens safely and assert HTTP status plus response contracts. +- Positive and negative scenarios are both represented. +- No real or reusable credentials are committed. + +**Possibly affected files** + +- New Postman collection under a documentation/testing directory +- Sanitized Postman environment template +- `README.md` +- API functional-test instructions + +**Acceptance criteria** + +- [ ] Collection covers register, login, refresh rotation, logout, and revoked-token failure. +- [ ] Collection covers `USER`/`ADMIN` authorization and wallet ownership. +- [ ] Collection covers deposit, withdrawal, transfer, insufficient funds, and history. +- [ ] Collection contains assertions for expected `2xx`, `400`, `401`, `403`, and non-enumerating `404` cases. +- [ ] Exported files contain no tokens, passwords, local paths, or private environment values. + +### DOC-001 — Keep security documentation synchronized with evidence + +**Classification:** Detected defect +**Priority:** P2 +**Depends on:** All completed tasks that change documented behavior + +**Current problem** + +Some documentation currently describes target controls—such as token rotation, lockout, TOTP, and limits—as fully implemented. + +**Expected behavior** + +- Documentation differentiates implemented, verified, partial, and planned capabilities. +- Architecture decisions remain documented even when implementation work is pending. +- OWASP mapping describes evidence and residual work rather than claiming certification. + +**Possibly affected files** + +- `README.md` +- `SECURITY.md` +- `CONTEXT.md` +- `DECISIONS.md` +- `database/README.md` + +**Acceptance criteria** + +- [ ] Every security claim links to implementation or test evidence. +- [ ] Planned controls are not described as active controls. +- [ ] Conflicting documents identify the implementation plan as the current status source. +- [ ] OWASP Top 10:2025 names and ordering match the official release. + +--- + +## P3 — Decisions pending confirmation + +### DESIGN-001 — Review TOTP-based two-factor authentication + +**Classification:** Pending decision +**Priority:** P3 +**Depends on:** Stable authentication, session, and administrative provisioning flows + +**Current problem** + +The model and architecture documents reference TOTP, but enrollment, secret protection, recovery, verification, reauthentication, and administrative policy are not fully designed or implemented. + +**Expected behavior if approved** + +- A written decision defines which operations require TOTP. +- Enrollment requires identity verification and protected secret handling. +- Recovery and reset flows are abuse-resistant and audited. +- Tests prove bypass resistance and recovery behavior. + +**Possibly affected files** + +- `DECISIONS.md` +- Authentication entities, DTOs, controllers, and services +- Encryption/key-management configuration +- Audit actions +- Security and functional tests + +**Acceptance criteria for the decision phase** + +- [ ] Threat model and protected operations are approved. +- [ ] Secret storage and recovery strategy are approved. +- [ ] UX and Postman demonstration flow are documented. +- [ ] Implementation is not started until the decision is accepted. + +### DESIGN-002 — Review configurable financial limits + +**Classification:** Pending decision +**Priority:** P3 +**Depends on:** TX-001 + +**Current problem** + +Configuration contains transaction and daily-limit values, but their semantics, currency handling, enforcement point, concurrency behavior, and administrative ownership are not fully defined. + +**Expected behavior if approved** + +- Limits have an explicit business meaning and currency strategy. +- Enforcement is atomic and cannot be bypassed with concurrent requests. +- Configuration ownership and administrative override rules are documented. +- Limit failures do not change balances and are audited. + +**Possibly affected files** + +- `DECISIONS.md` +- Transaction configuration properties +- `TransactionService.java` +- Transaction repositories/queries +- Database constraints or aggregation queries if selected +- Transaction tests and Postman collection + +**Acceptance criteria for the decision phase** + +- [ ] Per-transaction and daily-limit semantics are approved. +- [ ] Multi-currency handling is defined. +- [ ] Concurrency and reset-window behavior are defined. +- [ ] Implementation is not started until the decision is accepted. + +--- + +## Known decision conflicts + +These conflicts must be resolved explicitly during implementation rather than silently in documentation: + +| Conflict | Current sources | Required resolution | +|----------|-----------------|---------------------| +| Functional roles | PostgreSQL and `UserRole` currently include `MANAGER`; confirmed target business roles are `USER` and `ADMIN` | Decide whether `MANAGER` is removed through migration or retained as a non-functional reserved value | +| Login-attempt threshold | Database allows up to 3; application configuration and security documentation state 5 | Select one policy, migrate schema/configuration, and test it | +| Refresh-token storage | Session model stores SHA-256 hashes; user model stores the original token | Make the session model authoritative and remove legacy storage safely | +| Refresh behavior | Architecture requires rotation; current implementation reuses the refresh token | Define atomic rotation and reuse handling before implementation | +| Email verification | Model/documentation describe verification as required; authentication currently does not consistently enforce it | Confirm whether verification is required for this educational scope | +| Administrative provisioning | Public seed creates an administrator; confirmed architecture requires controlled bootstrap | Separate development data and implement one-time bootstrap | +| TOTP and limits | Architecture documents describe them as controls; current decision marks them pending | Keep them pending until separate design decisions are accepted | + +## Definition of project verification + +The project should not be described as fully verified until all applicable checks are satisfied: + +- [ ] Docker Compose starts a clean local environment without public default administrative credentials. +- [ ] Bootstrap, authentication, authorization, session, wallet, and transaction acceptance criteria pass. +- [ ] Unit, security integration, and PostgreSQL-backed tests pass. +- [ ] The Postman critical-flow collection passes from a clean local environment. +- [ ] README, security documentation, and architecture decisions describe the same behavior. +- [ ] No source, exported collection, logs, or artifacts contain secrets or reusable tokens. From 5b8c43a74937c1e1f32f9fa0fa397171ccbd8425 Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:22:07 -0500 Subject: [PATCH 04/13] delete script 07-seed-sql the administrator must create using bootstrap, task SEC-001 is being carried out --- database/07-seed.sql | 15 --------------- database/README.md | 7 ++++--- docs/IMPLEMENTATION-PLAN.md | 8 ++++---- 3 files changed, 8 insertions(+), 22 deletions(-) delete mode 100644 database/07-seed.sql diff --git a/database/07-seed.sql b/database/07-seed.sql deleted file mode 100644 index ffb300f..0000000 --- a/database/07-seed.sql +++ /dev/null @@ -1,15 +0,0 @@ --- TEST DATA (DEVELOPMENT ONLY) - --- Test user (password: Test1234!) -INSERT INTO users (email, password_hash, role, email_verified) VALUES -('admin@test.com', '$2a$10$N9qo8uLOickgx2ZMRZoMye1J8FRKp0LEzKd6KPXB5z.Q7.QN0lLBa', 'ADMIN', TRUE), -('user@test.com', '$2a$10$N9qo8uLOickgx2ZMRZoMye1J8FRKp0LEzKd6KPXB5z.Q7.QN0lLBa', 'USER', TRUE); - --- Test wallets -INSERT INTO wallets (user_id, balance, currency) VALUES -((SELECT id FROM users WHERE email = 'admin@test.com'), 1000.00, 'USD'), -((SELECT id FROM users WHERE email = 'user@test.com'), 500.00, 'USD'); - --- Test log -INSERT INTO audit_logs (user_id, action, severity_level, details) VALUES -((SELECT id FROM users WHERE email = 'admin@test.com'), 'USER_LOGIN', 'INFO', '{"ip": "127.0.0.1"}'); \ No newline at end of file diff --git a/database/README.md b/database/README.md index 80f5105..891c024 100644 --- a/database/README.md +++ b/database/README.md @@ -1,7 +1,8 @@ # Database — Secure Wallet API PostgreSQL 16 schema for a secure digital wallet system. -All scripts run automatically on first Docker startup via `docker-entrypoint-initdb.d`, in numbered order. +Schema scripts run automatically on first Docker startup via `docker-entrypoint-initdb.d`, in numbered order. +No development seed data is executed automatically. Test fixtures must be provisioned separately. --- @@ -15,7 +16,7 @@ All scripts run automatically on first Docker startup via `docker-entrypoint-ini | `04-index.sql` | 4th | Performance indexes (partial, composite, GIN) | | `05-triggers.sql` | 5th | Auto-update timestamps, balance validation, audit triggers | | `06-functions.sql` | 6th | ACID transaction processor, session cleanup | -| `07-seed.sql` | 7th | Development test data only | +| — | — | No seed data; administrators are provisioned by the application bootstrap procedure | --- @@ -172,4 +173,4 @@ docker compose --profile tools up -d # pgAdmin: http://localhost:5050 # PostgreSQL: localhost:5432 -``` \ No newline at end of file +``` diff --git a/docs/IMPLEMENTATION-PLAN.md b/docs/IMPLEMENTATION-PLAN.md index 7854d2a..c8fbf15 100644 --- a/docs/IMPLEMENTATION-PLAN.md +++ b/docs/IMPLEMENTATION-PLAN.md @@ -59,19 +59,19 @@ Postman critical-flow collection **Current problem** -`database/07-seed.sql` creates an administrator with publicly know credentials during PostgreSQL initialization. +The former `database/07-seed.sql` created an administrator with publicly known credentials during PostgreSQL initialization. It has been removed from the database initialization directory. **Expected behavior** -- Remove 07-seed.sql if no application or test dependency requirest it. +- Remove the seed from the repository's initialization directory; provision administrators through the controlled application bootstrap instead. - Configure Docker Compose to initialize only the required database schema. - Do not create default users or administrators during SQL initialization. - Keep Docker Compose as the primary way to start PostgreSQL and Spring Boot. **Acceptance criteria** -- [ ] No default administrator is created by SQL initialization. -- [ ] No public administrator password remains in the repository's current tracked files. +- [x] No default administrator is created by SQL initialization path. +- [x] No public administrator password remains in the repository's database scripts. - [ ] PostgreSQL initializes correctly using Docker Compose. - [ ] Existing tests continue to pass. From c4690953d13610c7a5d0a4cf82d87066e232be06 Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:31:32 -0500 Subject: [PATCH 05/13] The database container is checked to verify users and administrators --- docs/IMPLEMENTATION-PLAN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/IMPLEMENTATION-PLAN.md b/docs/IMPLEMENTATION-PLAN.md index c8fbf15..7bc9837 100644 --- a/docs/IMPLEMENTATION-PLAN.md +++ b/docs/IMPLEMENTATION-PLAN.md @@ -70,9 +70,9 @@ The former `database/07-seed.sql` created an administrator with publicly known c **Acceptance criteria** -- [x] No default administrator is created by SQL initialization path. +- [x] No default administrator is created by SQL initialization path (verified with a temporary clean PostgreSQL container). - [x] No public administrator password remains in the repository's database scripts. -- [ ] PostgreSQL initializes correctly using Docker Compose. +- [x] PostgreSQL initializes correctly using Docker Compose and the current schema scripts. - [ ] Existing tests continue to pass. ### SEC-002 — Add controlled one-time bootstrap for the first administrator From e01241ee8b0d2f623452725ad685bbcdef25dcd7 Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:36:47 -0500 Subject: [PATCH 06/13] feat(auth): add controlled first admin bootstrap --- .env.example | 8 +- .../config/AdminBootstrapProperties.java | 23 +++++ .../user/repository/UserRepository.java | 8 +- .../user/service/AdminBootstrapService.java | 75 +++++++++++++++ src/main/resources/application.yml | 13 ++- .../service/AdminBootstrapServiceTest.java | 91 +++++++++++++++++++ 6 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java create mode 100644 src/main/java/com/wallet/secure/user/service/AdminBootstrapService.java create mode 100644 src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java diff --git a/.env.example b/.env.example index 536781f..22ae5b4 100644 --- a/.env.example +++ b/.env.example @@ -20,8 +20,14 @@ SPRING_PROFILES_ACTIVE=dev JWT_SECRET=CHANGE_ME_GENERATE_WITH_OPENSSL_RAND_BASE64_64 JWT_EXPIRATION=86400000 +# FIRST ADMIN BOOTSTRAP (disabled by default) +# Enable only for the first controlled provisioning run. +ADMIN_BOOTSTRAP_ENABLED=false +ADMIN_BOOTSTRAP_EMAIL=admin@example.test +ADMIN_BOOTSTRAP_PASSWORD=CHANGE_ME_PRIVATE_BOOTSTRAP_PASSWORD + # Server SERVER_PORT=8080 # Volume (absolute path - adjust according to your system) -FILE_DATA_POSTGRES=/path/to/your/data/postgres_wallet \ No newline at end of file +FILE_DATA_POSTGRES=/path/to/your/data/postgres_wallet diff --git a/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java b/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java new file mode 100644 index 0000000..be890ac --- /dev/null +++ b/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java @@ -0,0 +1,23 @@ +package com.wallet.secure.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Private, opt-in input for provisioning the first business administrator. + * + * Environment variables map to these properties as: + * ADMIN_BOOTSTRAP_ENABLED, ADMIN_BOOTSTRAP_EMAIL and ADMIN_BOOTSTRAP_PASSWORD. + */ +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "admin.bootstrap") +public class AdminBootstrapProperties { + + private boolean enabled; + private String email; + private String password; +} diff --git a/src/main/java/com/wallet/secure/user/repository/UserRepository.java b/src/main/java/com/wallet/secure/user/repository/UserRepository.java index d41dcf0..c4607c4 100644 --- a/src/main/java/com/wallet/secure/user/repository/UserRepository.java +++ b/src/main/java/com/wallet/secure/user/repository/UserRepository.java @@ -47,6 +47,12 @@ public interface UserRepository extends JpaRepository { */ boolean existsByEmail(String email); + /** + * Used by the one-time admin bootstrap to enforce a single initial ADMIN. + * OWASP A01: privileged role provisioning is not exposed through public registration. + */ + boolean existsByRole(com.wallet.secure.common.enums.UserRole role); + // ─── Security Queries ───────────────────────────────────────────────────── /** @@ -136,4 +142,4 @@ void lockAccount( @Modifying @Query("UPDATE User u SET u.emailVerified = true WHERE u.id = :id") void verifyEmail(@Param("id") UUID id); -} \ No newline at end of file +} diff --git a/src/main/java/com/wallet/secure/user/service/AdminBootstrapService.java b/src/main/java/com/wallet/secure/user/service/AdminBootstrapService.java new file mode 100644 index 0000000..f2d0fc7 --- /dev/null +++ b/src/main/java/com/wallet/secure/user/service/AdminBootstrapService.java @@ -0,0 +1,75 @@ +package com.wallet.secure.user.service; + +import com.wallet.secure.common.enums.UserRole; +import com.wallet.secure.config.AdminBootstrapProperties; +import com.wallet.secure.user.entity.User; +import com.wallet.secure.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.regex.Pattern; + +/** + * Provisions only the first ADMIN, when explicitly enabled. + * + * OWASP A01: the public registration path remains USER-only. + * OWASP A02: the bootstrap password is BCrypt-hashed before persistence. + * OWASP A05: credentials are never written to logs or responses. + */ +@Component +@RequiredArgsConstructor +@Log4j2 +public class AdminBootstrapService implements ApplicationRunner { + + private static final Pattern EMAIL_PATTERN = + Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"); + + private final AdminBootstrapProperties properties; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Override + @Transactional + public void run(ApplicationArguments args) { + if (!properties.isEnabled()) { + log.info("Admin bootstrap disabled"); + return; + } + + validateInput(); + + if (userRepository.existsByRole(UserRole.ADMIN)) { + log.info("Admin bootstrap skipped: an administrator already exists"); + return; + } + + if (userRepository.existsByEmail(properties.getEmail())) { + throw new IllegalStateException("Admin bootstrap email is already registered"); + } + + User admin = User.builder() + .email(properties.getEmail()) + .passwordHash(passwordEncoder.encode(properties.getPassword())) + .role(UserRole.ADMIN) + .emailVerified(true) + .isActive(true) + .build(); + + userRepository.save(admin); + log.info("Initial administrator provisioned successfully"); + } + + private void validateInput() { + if (properties.getEmail() == null || !EMAIL_PATTERN.matcher(properties.getEmail()).matches()) { + throw new IllegalStateException("Admin bootstrap email is invalid"); + } + if (properties.getPassword() == null || properties.getPassword().length() < 8) { + throw new IllegalStateException("Admin bootstrap password does not meet minimum requirements"); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b223c15..fb19148 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -79,6 +79,17 @@ jwt: header: Authorization prefix: Bearer +# FIRST ADMIN BOOTSTRAP +# Disabled by default. Enable only through private environment variables. +# ADMIN_BOOTSTRAP_ENABLED=true +# ADMIN_BOOTSTRAP_EMAIL=admin@example.test +# ADMIN_BOOTSTRAP_PASSWORD= +admin: + bootstrap: + enabled: ${ADMIN_BOOTSTRAP_ENABLED:false} + email: ${ADMIN_BOOTSTRAP_EMAIL:} + password: ${ADMIN_BOOTSTRAP_PASSWORD:} + # LOGGING logging: level: @@ -146,4 +157,4 @@ app: display-request-duration: true # Show response time in UI try-it-out-enabled: true # Enable "Try it out" by default show-actuator: false # Don't expose actuator in Swagger - packages-to-scan: com.wallet.secure # Only scan our packages \ No newline at end of file + packages-to-scan: com.wallet.secure # Only scan our packages diff --git a/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java b/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java new file mode 100644 index 0000000..bcafae0 --- /dev/null +++ b/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java @@ -0,0 +1,91 @@ +package com.wallet.secure.user.service; + +import com.wallet.secure.common.enums.UserRole; +import com.wallet.secure.config.AdminBootstrapProperties; +import com.wallet.secure.user.entity.User; +import com.wallet.secure.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.DefaultApplicationArguments; +import org.springframework.security.crypto.password.PasswordEncoder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AdminBootstrapServiceTest { + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + private AdminBootstrapProperties properties; + private AdminBootstrapService service; + + @BeforeEach + void setUp() { + properties = new AdminBootstrapProperties(); + service = new AdminBootstrapService(properties, userRepository, passwordEncoder); + } + + @Test + void disabledBootstrapDoesNotQueryOrCreateUser() throws Exception { + service.run(new DefaultApplicationArguments()); + + verifyNoInteractions(userRepository, passwordEncoder); + } + + @Test + void validBootstrapCreatesVerifiedAdminWithEncodedPassword() throws Exception { + properties.setEnabled(true); + properties.setEmail("admin@example.test"); + properties.setPassword("StrongPass1!"); + when(userRepository.existsByRole(UserRole.ADMIN)).thenReturn(false); + when(userRepository.existsByEmail("admin@example.test")).thenReturn(false); + when(passwordEncoder.encode("StrongPass1!")).thenReturn("$2a$12$encoded"); + + service.run(new DefaultApplicationArguments()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + verify(userRepository).save(captor.capture()); + User saved = captor.getValue(); + assertThat(saved.getRole()).isEqualTo(UserRole.ADMIN); + assertThat(saved.getEmail()).isEqualTo("admin@example.test"); + assertThat(saved.getPasswordHash()).isEqualTo("$2a$12$encoded"); + assertThat(saved.getEmailVerified()).isTrue(); + verify(passwordEncoder).encode("StrongPass1!"); + } + + @Test + void existingAdminPreventsSecondAdmin() throws Exception { + properties.setEnabled(true); + properties.setEmail("admin@example.test"); + properties.setPassword("StrongPass1!"); + when(userRepository.existsByRole(UserRole.ADMIN)).thenReturn(true); + + service.run(new DefaultApplicationArguments()); + + verify(userRepository, never()).save(any(User.class)); + verifyNoInteractions(passwordEncoder); + } + + @Test + void invalidPasswordFailsWithoutPersistingOrLoggingSecret() { + properties.setEnabled(true); + properties.setEmail("admin@example.test"); + properties.setPassword("short"); + + assertThatThrownBy(() -> service.run(new DefaultApplicationArguments())) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Admin bootstrap password does not meet minimum requirements"); + verifyNoInteractions(userRepository, passwordEncoder); + } +} From 5ed10ffc3d9e1211cb4abd2f1ccf06167ac9ec63 Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:59:04 -0500 Subject: [PATCH 07/13] docs: add rule of git for agents --- AGENTS.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f2b0ac4..948d802 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -256,4 +256,10 @@ Before opening a PR, verify: - [ ] `@Transactional` on financial operations - [ ] OWASP comments on security decisions - [ ] `@Schema` on new DTOs for Swagger -- [ ] `@Tag` on new controllers for Swagger grouping \ No newline at end of file +- [ ] `@Tag` on new controllers for Swagger grouping + +## Git Safety Rules +- Do not create commits unless the user explicitly authorizes the commit in the current message. +- Do not run `git push`, `git merge`, rebase, revert, cherry-pick, or create pull requests unless explicitly. +- Reading git history, status, and diffs is allowed. +- Before any Git write operation, explain the exact command and wait for confirmation. From 3113ef9f5edf0654e8836b88a68406328cf002bf Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:44:23 -0500 Subject: [PATCH 08/13] feat(auht): add email validation using jakarta --- .../config/AdminBootstrapProperties.java | 14 ++++++++ .../user/service/AdminBootstrapService.java | 33 +++++++++++++------ src/main/resources/application.yml | 2 +- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java b/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java index be890ac..9ccee42 100644 --- a/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java +++ b/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java @@ -1,5 +1,9 @@ package com.wallet.secure.config; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -18,6 +22,16 @@ public class AdminBootstrapProperties { private boolean enabled; + + @Email(message = "Email no válido") + @NotBlank(message = "Email obligatorio") private String email; + + @NotBlank(message = "Contraseña obligatoria") + @Size(min = 12, max = 72, message = "La contraseña debe tener entre 12 y 72 caracteres") + @Pattern( + regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&_.#^])[A-Za-z\\d@$!%*?&_.#^]{12,72}$", + message = "La contraseña debe incluir mayúscula, minúscula, número y carácter especial" + ) private String password; } diff --git a/src/main/java/com/wallet/secure/user/service/AdminBootstrapService.java b/src/main/java/com/wallet/secure/user/service/AdminBootstrapService.java index f2d0fc7..0b5f5ff 100644 --- a/src/main/java/com/wallet/secure/user/service/AdminBootstrapService.java +++ b/src/main/java/com/wallet/secure/user/service/AdminBootstrapService.java @@ -4,6 +4,7 @@ import com.wallet.secure.config.AdminBootstrapProperties; import com.wallet.secure.user.entity.User; import com.wallet.secure.user.repository.UserRepository; +import jakarta.validation.Validator; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.boot.ApplicationArguments; @@ -12,8 +13,6 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.util.regex.Pattern; - /** * Provisions only the first ADMIN, when explicitly enabled. * @@ -26,13 +25,23 @@ @Log4j2 public class AdminBootstrapService implements ApplicationRunner { - private static final Pattern EMAIL_PATTERN = - Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"); - private final AdminBootstrapProperties properties; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final Validator validator; + /** + * Runs once after Spring has created the application context. + * + *

This is provisioning, not public registration: it is an operational + * entry point used to create the first business administrator. Keeping it + * as an {@link ApplicationRunner} means it is not reachable through HTTP + * or Postman.

+ * + *

The operation is idempotent. Re-starting the application with the + * same variables does not change the existing administrator because the + * role existence check happens before persistence.

+ */ @Override @Transactional public void run(ApplicationArguments args) { @@ -64,12 +73,16 @@ public void run(ApplicationArguments args) { log.info("Initial administrator provisioned successfully"); } + /** + * Validates only non-secret properties and password policy requirements. + * + *

Exception messages intentionally describe the violated rule without + * including the supplied password (OWASP A05/A09: avoid secret leakage in + * logs and startup diagnostics).

+ */ private void validateInput() { - if (properties.getEmail() == null || !EMAIL_PATTERN.matcher(properties.getEmail()).matches()) { - throw new IllegalStateException("Admin bootstrap email is invalid"); - } - if (properties.getPassword() == null || properties.getPassword().length() < 8) { - throw new IllegalStateException("Admin bootstrap password does not meet minimum requirements"); + if (!validator.validate(properties).isEmpty()) { + throw new IllegalStateException("Admin bootstrap configuration is invalid"); } } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fb19148..7a9fac2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -140,7 +140,7 @@ app: security: max-login-attempts: 5 # Login attempts before lockout lockout-duration-minutes: 30 # Lockout time - password-min-length: 8 + password-min-length: 12 password-require-uppercase: true password-require-lowercase: true password-require-digit: true From 263aadea856491104b8d2dc296e531142eac4630 Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:45:18 -0500 Subject: [PATCH 09/13] tests: add validation with jakarta --- .../service/AdminBootstrapServiceTest.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java b/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java index bcafae0..8a09715 100644 --- a/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java +++ b/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java @@ -12,12 +12,21 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.DefaultApplicationArguments; import org.springframework.security.crypto.password.PasswordEncoder; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +/** + * Unit tests for the first-admin provisioning boundary. + * + *

Mockito is used instead of a Spring context so these tests verify the + * service decisions in isolation: whether persistence and BCrypt are called, + * not Hibernate or PostgreSQL behavior.

+ */ @ExtendWith(MockitoExtension.class) class AdminBootstrapServiceTest { @@ -29,14 +38,17 @@ class AdminBootstrapServiceTest { private AdminBootstrapProperties properties; private AdminBootstrapService service; + private Validator validator; @BeforeEach void setUp() { properties = new AdminBootstrapProperties(); - service = new AdminBootstrapService(properties, userRepository, passwordEncoder); + validator = Validation.buildDefaultValidatorFactory().getValidator(); + service = new AdminBootstrapService(properties, userRepository, passwordEncoder, validator); } @Test + /** Disabled is the safe default for normal application starts. */ void disabledBootstrapDoesNotQueryOrCreateUser() throws Exception { service.run(new DefaultApplicationArguments()); @@ -44,6 +56,7 @@ void disabledBootstrapDoesNotQueryOrCreateUser() throws Exception { } @Test + /** A valid bootstrap creates a verified ADMIN and never stores plaintext. */ void validBootstrapCreatesVerifiedAdminWithEncodedPassword() throws Exception { properties.setEnabled(true); properties.setEmail("admin@example.test"); @@ -65,6 +78,7 @@ void validBootstrapCreatesVerifiedAdminWithEncodedPassword() throws Exception { } @Test + /** Idempotency prevents a restart from creating or replacing another ADMIN. */ void existingAdminPreventsSecondAdmin() throws Exception { properties.setEnabled(true); properties.setEmail("admin@example.test"); @@ -78,6 +92,7 @@ void existingAdminPreventsSecondAdmin() throws Exception { } @Test + /** Invalid input fails before repository or encoder interaction. */ void invalidPasswordFailsWithoutPersistingOrLoggingSecret() { properties.setEnabled(true); properties.setEmail("admin@example.test"); @@ -85,7 +100,7 @@ void invalidPasswordFailsWithoutPersistingOrLoggingSecret() { assertThatThrownBy(() -> service.run(new DefaultApplicationArguments())) .isInstanceOf(IllegalStateException.class) - .hasMessage("Admin bootstrap password does not meet minimum requirements"); + .hasMessage("Admin bootstrap configuration is invalid"); verifyNoInteractions(userRepository, passwordEncoder); } } From 669a5e9ea86c3ec38240f2a44492a45c74b0cc30 Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:46:40 -0500 Subject: [PATCH 10/13] feat(docker): add bootstrap variable configuration --- docker-compose.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6e1d0c0..69bd6e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,10 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # JWT from .env JWT_SECRET: ${JWT_SECRET} + # First ADMIN bootstrap; disabled unless explicitly enabled in .env. + ADMIN_BOOTSTRAP_ENABLED: ${ADMIN_BOOTSTRAP_ENABLED:-false} + ADMIN_BOOTSTRAP_EMAIL: ${ADMIN_BOOTSTRAP_EMAIL:-} + ADMIN_BOOTSTRAP_PASSWORD: ${ADMIN_BOOTSTRAP_PASSWORD:-} SERVER_PORT: ${SERVER_PORT:-8080} ports: - "${SERVER_PORT:-8080}:${SERVER_PORT:-8080}" @@ -82,4 +86,4 @@ services: networks: wallet-network: - driver: bridge \ No newline at end of file + driver: bridge From 679040307dd47fe5c871868f4753a39f04bcab9f Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:29:20 -0500 Subject: [PATCH 11/13] fix(persistence): map IP addresses as PostgreSQL INET --- src/main/java/com/wallet/secure/audit/entity/AuditLog.java | 1 + src/main/java/com/wallet/secure/auth/entity/Session.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/com/wallet/secure/audit/entity/AuditLog.java b/src/main/java/com/wallet/secure/audit/entity/AuditLog.java index 84e8d8f..0547ad8 100644 --- a/src/main/java/com/wallet/secure/audit/entity/AuditLog.java +++ b/src/main/java/com/wallet/secure/audit/entity/AuditLog.java @@ -108,6 +108,7 @@ public class AuditLog { * Supports both IPv4 and IPv6. * OWASP A09: essential for geographic anomaly detection and forensics. */ + @JdbcTypeCode(SqlTypes.INET) @Column(name = "ip_address") private String ipAddress; diff --git a/src/main/java/com/wallet/secure/auth/entity/Session.java b/src/main/java/com/wallet/secure/auth/entity/Session.java index cc938d8..1eaef65 100644 --- a/src/main/java/com/wallet/secure/auth/entity/Session.java +++ b/src/main/java/com/wallet/secure/auth/entity/Session.java @@ -3,6 +3,8 @@ import com.wallet.secure.user.entity.User; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -87,6 +89,7 @@ public class Session { * OWASP A07: enables geographic anomaly detection. * Example: session created in Colombia, refresh from Russia → suspicious. */ + @JdbcTypeCode(SqlTypes.INET) @Column(name = "ip_address") private String ipAddress; From 47e96ca3e7817bb5e12dc9ad9f79061a42636513 Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:41:06 -0500 Subject: [PATCH 12/13] fix(wallet): align updated timestamp mapping --- src/main/java/com/wallet/secure/wallet/entity/Wallet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wallet/secure/wallet/entity/Wallet.java b/src/main/java/com/wallet/secure/wallet/entity/Wallet.java index 8be3e24..5bb5d77 100644 --- a/src/main/java/com/wallet/secure/wallet/entity/Wallet.java +++ b/src/main/java/com/wallet/secure/wallet/entity/Wallet.java @@ -104,7 +104,7 @@ public class Wallet { private Instant createdAt; @LastModifiedDate - @Column(name = "update_at", nullable = false) + @Column(name = "updated_at", nullable = false) private Instant updateAt; // --- Business Methods From 1b84db0d31bb807b4ccac4edfed34be7f72c877d Mon Sep 17 00:00:00 2001 From: DJAngel973 <189556164+DJAngel973@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:01:22 -0500 Subject: [PATCH 13/13] feat: the regular expression is implemented with all characters --- .../wallet/secure/config/AdminBootstrapProperties.java | 3 ++- .../java/com/wallet/secure/user/dto/RegisterRequest.java | 3 ++- .../secure/user/service/AdminBootstrapServiceTest.java | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java b/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java index 9ccee42..163d6e0 100644 --- a/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java +++ b/src/main/java/com/wallet/secure/config/AdminBootstrapProperties.java @@ -30,7 +30,8 @@ public class AdminBootstrapProperties { @NotBlank(message = "Contraseña obligatoria") @Size(min = 12, max = 72, message = "La contraseña debe tener entre 12 y 72 caracteres") @Pattern( - regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&_.#^])[A-Za-z\\d@$!%*?&_.#^]{12,72}$", + // Do not whitelist symbols: passwords from password managers may contain /, +, =, etc. + regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z0-9\\s])\\S{12,72}$", message = "La contraseña debe incluir mayúscula, minúscula, número y carácter especial" ) private String password; diff --git a/src/main/java/com/wallet/secure/user/dto/RegisterRequest.java b/src/main/java/com/wallet/secure/user/dto/RegisterRequest.java index 9f7962b..81fa36f 100644 --- a/src/main/java/com/wallet/secure/user/dto/RegisterRequest.java +++ b/src/main/java/com/wallet/secure/user/dto/RegisterRequest.java @@ -33,7 +33,8 @@ public class RegisterRequest { @NotBlank(message = "Password is required") @Size(min = 12, max = 72, message = "Password must be between 12 and 72 characters") @Pattern( - regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&_.#^])[A-Za-z\\d@$!%*?&_.#^]{12,}$", + // OWASP: do not restrict the symbols a password manager may generate. + regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z0-9\\s])\\S{12,72}$", message = "Password must contain uppercase, lowercase, number and special character" ) private String password; diff --git a/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java b/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java index 8a09715..47378e9 100644 --- a/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java +++ b/src/test/java/com/wallet/secure/user/service/AdminBootstrapServiceTest.java @@ -60,10 +60,10 @@ void disabledBootstrapDoesNotQueryOrCreateUser() throws Exception { void validBootstrapCreatesVerifiedAdminWithEncodedPassword() throws Exception { properties.setEnabled(true); properties.setEmail("admin@example.test"); - properties.setPassword("StrongPass1!"); + properties.setPassword("Strong/Pass1!"); when(userRepository.existsByRole(UserRole.ADMIN)).thenReturn(false); when(userRepository.existsByEmail("admin@example.test")).thenReturn(false); - when(passwordEncoder.encode("StrongPass1!")).thenReturn("$2a$12$encoded"); + when(passwordEncoder.encode("Strong/Pass1!")).thenReturn("$2a$12$encoded"); service.run(new DefaultApplicationArguments()); @@ -74,7 +74,7 @@ void validBootstrapCreatesVerifiedAdminWithEncodedPassword() throws Exception { assertThat(saved.getEmail()).isEqualTo("admin@example.test"); assertThat(saved.getPasswordHash()).isEqualTo("$2a$12$encoded"); assertThat(saved.getEmailVerified()).isTrue(); - verify(passwordEncoder).encode("StrongPass1!"); + verify(passwordEncoder).encode("Strong/Pass1!"); } @Test @@ -82,7 +82,7 @@ void validBootstrapCreatesVerifiedAdminWithEncodedPassword() throws Exception { void existingAdminPreventsSecondAdmin() throws Exception { properties.setEnabled(true); properties.setEmail("admin@example.test"); - properties.setPassword("StrongPass1!"); + properties.setPassword("Strong/Pass1!"); when(userRepository.existsByRole(UserRole.ADMIN)).thenReturn(true); service.run(new DefaultApplicationArguments());