A learning project built to explore real-world security practices in the context of a digital savings wallet REST API.
Built with Java 17, Spring Boot 3.4.2, PostgreSQL 16 and Docker.
Security is a first-class concern here β not an afterthought. Every architectural decision is documented, every security rule references OWASP, and every sensitive operation is audited.
This is an academic/portfolio project designed to answer a specific question:
How would a financial API look if security, auditability, and correctness were treated as requirements from day one β before a single line of business code is written?
The result is a digital savings wallet where users can deposit, withdraw, and transfer money between accounts β built with the same security discipline you'd expect in a real fintech product.
Core priorities (in order):
- Security β OWASP Top 10 (2025) compliance throughout
- Correctness β ACID financial transactions, no money lost
- Auditability β every sensitive operation logged and traceable
- Maintainability β clean, documented, testable code
- Register, update profile, and soft-delete accounts
- Role-based access control:
USER,ADMIN,MANAGER - Account lockout after 5 failed login attempts (30-minute cooldown)
- Two-Factor Authentication (2FA / TOTP β RFC 6238, Google Authenticator compatible)
- Email verification flag and login tracking
- One wallet per user per currency
- Multi-currency support: USD, EUR, COP, MXN, ARS
- Real-time balance with ACID-guaranteed updates
- Wallet states:
ACTIVE,SUSPENDED,CLOSED
| Operation | Description |
|---|---|
| Deposit | Add funds from an external source into a wallet |
| Withdrawal | Move funds from a wallet to an external destination |
| Transfer | Move funds between any two wallets in the system |
| History | Full state-change log per transaction |
- 2FA required for operations above $100
- Transaction limit: $10,000 | Daily limit: $50,000
- Pessimistic locking (
FOR UPDATE) on all balance reads - Unique reference code per transaction
- JSONB metadata (IP, device, geolocation) for fraud detection
- Dual-token JWT: Access Token (15 min) + Refresh Token (7 days)
- Refresh Token rotation and revocation stored in the database
- SHA-256 for token hashing β BCrypt only for passwords
- UUID primary keys on all entities (prevents enumeration)
- Stack traces never exposed to the client
- OWASP Dependency Check in CI β fails build on CVSS β₯ 7
- CodeQL static analysis on every PR
- Independent
audit/domain β auditing is a business requirement, not a cross-cutting concern - Every sensitive operation generates an
audit_logsentry - IP address, User-Agent, timestamp, and JSONB details per event
- PostgreSQL triggers for automatic state-change auditing
Pattern: Hybrid Domain-Driven Design β layers organized by domain, never by class type.
com.wallet.secure/
βββ config/ # SecurityConfig, OpenApiConfig, AuditConfig, JwtConfig
βββ auth/ # Authentication: JWT, 2FA, sessions
β βββ controller/ # AuthController, SessionController
β βββ dto/ # LoginRequest, AuthResponse, RefreshTokenRequest
β βββ entity/ # Session
β βββ repository/ # SessionRepository
β βββ security/ # JwtAuthFilter, JwtService, UserDetailsServiceImpl
β βββ service/ # AuthService, SessionService
βββ user/ # Users and profiles
βββ wallet/ # Wallets and balances
βββ transaction/ # Transactions + history (business core)
βββ audit/ # Security audit trail
βββ common/ # ApiResponse<T>, exceptions, enums, validators
Architecture rule: NEVER create root-level folders by type (controllers/, services/). Each domain owns its own layers.
π
CONTEXT.mdβ Single source of truth: full architecture, stack, conventions, and rules. πDECISIONS.mdβ 8 Architecture Decision Records with full reasoning for every major choice.
A wallet API without a solid auth model is not a wallet β it's an open bank account.
This project enforces two distinct layers:
Authentication β Who are you? Dual-token JWT ensures a stolen Access Token is useless after 15 minutes. The Refresh Token is stored as a SHA-256 hash in the database, enabling real revocation on logout. Sessions are tracked per device.
Authorization β What are you allowed to do?
userId is always extracted from the JWT β never from the request body or path parameters. This means a user cannot access another user's wallet by simply changing an ID in the URL. Resources respond with 404 (not 403) to prevent enumeration.
POST /auth/login
β Validate credentials (BCrypt password check)
β Check account not locked
β If operation requires 2FA β POST /auth/2fa/verify
β Issue Access Token (15 min) + Refresh Token (7 days)
β Register session in DB
β Audit log event
β Return tokens
POST /auth/refresh
β Validate Refresh Token (signature + expiry + exists in DB)
β Rotate token (invalidate old, issue new)
β Return new Access Token + new Refresh Token
POST /auth/logout
β Revoke Refresh Token in DB
β Audit log event
β Access Token expires naturally within 15 min
| Parameter | Value | Reason |
|---|---|---|
| Access Token TTL | 15 minutes | Limits exposure if stolen |
| Refresh Token TTL | 7 days | Security/UX balance |
| Max inactivity | 30 minutes | Banking standard |
| Concurrent sessions | Allowed | Tracked per device in DB |
| 2FA threshold | $100 | Protects everyday transactions, not just large ones |
OWASP Top 10 2025 was published on November 6, 2025 at the Global AppSec Conference in Washington D.C.
| # | Risk | Status | How it is addressed |
|---|---|---|---|
| A01 | Broken Access Control (includes SSRF) | β | userId from JWT only β never from request. 404 on ownership mismatch. RBAC via Spring Security. API makes no outbound calls (SSRF N/A) |
| A02 | Cryptographic Failures | β | BCrypt strength 12 for passwords. SHA-256 for token hashing. JWT signed with HS256. HTTPS required in production. DECIMAL(19,4) for money β no float precision errors |
| A03 | Software Supply Chain Failures π | β | OWASP Dependency Check in CI (failOnCVSS β₯ 7). All dependency versions pinned in pom.xml |
| A04 | Injection | β | Hibernate prepared statements on all queries. Bean Validation (@Valid) on every endpoint input |
| A05 | Security Misconfiguration | β | Error messages never expose internals. Stack traces never sent to client. No default credentials. Secure HTTP headers. Limited Actuator exposure |
| A06 | Insecure Design | β | Documented threat model. Transaction limits. 2FA threshold at $100. Pessimistic locking for concurrent balance reads |
| A07 | Identification & Authentication Failures | β | Dual-token JWT (15 min / 7 days). TOTP 2FA (RFC 6238). Account lockout. 30-min inactivity. Refresh Token rotation and revocation |
| A08 | Software and Data Integrity Failures | β | CodeQL static analysis in GitHub Actions. SQL schema versioned in Git. Hibernate ddl-auto: validate |
| A09 | Security Logging & Monitoring Failures | β | Every security event logged to audit_logs. Log4j2 async logging. No passwords or tokens ever written to logs |
| A10 | Mishandling of Exceptional Conditions π | β | Centralized GlobalExceptionHandler β never "fail open". No sensitive data in error responses |
PostgreSQL 16 with schema managed exclusively by versioned SQL scripts. Hibernate only validates β never creates or modifies tables.
| # | Script | Purpose |
|---|---|---|
| 01 | extensions.sql |
PostgreSQL extensions: pgcrypto, uuid-ossp |
| 02 | types.sql |
7 custom ENUMs (roles, currencies, statuses) |
| 03 | tables.sql |
6 core tables with constraints and FK rules |
| 04 | index.sql |
Partial, composite, and GIN indexes |
| 05 | triggers.sql |
Auto-update timestamps, balance validation, state-change audit |
| 06 | functions.sql |
process_transaction() ACID, cleanup_expired_sessions() |
| 07 | seed.sql |
Development test data only |
| 08 | migrations.sql |
Incremental schema changes |
π
database/README.mdβ Full ER diagram, FK rules, and DB security design decisions.
| Component | Technology | Version |
|---|---|---|
| Language | Java | 17 |
| Framework | Spring Boot | 3.4.2 |
| Security | Spring Security + JWT | jjwt 0.12.6 |
| Database | PostgreSQL | 16-alpine |
| ORM | Spring Data JPA / Hibernate | β |
| Logging | Log4j2 | 2.25.3 |
| Validation | Spring Bean Validation | β |
| Boilerplate | Lombok | 1.18.36 |
| API Docs | SpringDoc OpenAPI | 2.8.8 |
| Tests | JUnit 5 + Mockito + AssertJ | β |
| Containers | Docker + Docker Compose | β |
| CI/CD | GitHub Actions | β |
| Static Analysis | CodeQL | β |
| Dependency Scanning | OWASP Dependency Check | 12.2.0 |
- Java 17+
- Docker & Docker Compose
- Maven 3.9+ (Maven wrapper
./mvnwincluded)
# 1. Clone the repository
git clone https://github.com/DJAngel973/Secure-Wallet-API.git
cd Secure-Wallet-API
# 2. Set up environment variables
cp .env.example .env
# Edit .env with your local values (DB credentials, JWT secret, ports)
# 3. Start everything (automated script)
./script/dev-start.sh
# Or step by step:
docker compose up -d # PostgreSQL only
docker compose --profile tools up -d # PostgreSQL + pgAdmin at http://localhost:5050
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev./script/dev-stop.sh./mvnw test # All unit tests (H2 in-memory, no DB needed)
./mvnw test -Dtest=TransactionServiceTest # Single test classOnce the application is running, the full interactive API documentation is available at:
http://localhost:8080/swagger-ui.html
All endpoints return a standard ApiResponse<T> envelope:
{
"success": true,
"message": "Operation completed",
"data": { }
}POST /auth/register
Content-Type: application/json
{
"email": "alice@example.com",
"password": "SecureP@ss1",
"firstName": "Alice",
"lastName": "Smith"
}POST /auth/login
Content-Type: application/json
{
"email": "alice@example.com",
"password": "ExampleSecureP@ss1"
}{
"success": true,
"message": "Login successful",
"data": {
"accessToken": "ExampleekeypracticeyJhbGc...",
"refreshToken": "ExampleekeypracticeyJhbGcNiJ9...",
"tokenType": "Bearer",
"expiresIn": 900
}
}Use the
accessTokenas a Bearer token in theAuthorizationheader for all protected requests.
GET /users/me
Authorization: Bearer <accessToken>POST /wallets
Authorization: Bearer <accessToken>
Content-Type: application/json
{
"currency": "USD"
}POST /transactions/deposit
Authorization: Bearer <accessToken>
Content-Type: application/json
{
"walletId": "ExampleekeypracticeyJhbGcafa6",
"amount": 500.00,
"description": "Initial deposit"
}Amounts above $100 require a valid TOTP code. Add the
X-2FA-Codeheader with the 6-digit code from your authenticator app.
POST /transactions/transfer
Authorization: Bearer <accessToken>
X-2FA-Code: 123456
Content-Type: application/json
{
"sourceWalletId": "ExampleekeypracticeyJhbGc-2c963f66afa6",
"targetWalletId": "ExampleekeypracticeyJhbGc-3d074g77bgb7",
"amount": 150.00,
"description": "Splitting dinner"
}GET /transactions/wallet/ExampleekeypracticeyJhbGcafa6
Authorization: Bearer <accessToken>POST /auth/refresh
Content-Type: application/json
{
"refreshToken": "ExampleekeypracticeyJhbGcNiJ9..."
}POST /auth/logout
Authorization: Bearer <accessToken>
Content-Type: application/json
{
"refreshToken": "ExampleekeypracticeyJhbGcNiJ9..."
}| Job | Trigger | Description |
|---|---|---|
| Build & Test | Every PR / push | Compile + unit tests (JUnit 5, test profile with H2) |
| OWASP Dependency Check | Every PR / push | CVE scan β fails on CVSS β₯ 7 |
| CodeQL Analysis | Every PR / push | Static security analysis |
| Package JAR | Merge to main |
Builds production artifact |
| Document | Description |
|---|---|
CONTEXT.md |
Single source of truth: architecture, stack, conventions, and rules |
DECISIONS.md |
8 Architecture Decision Records with full reasoning |
SECURITY.md |
Threat model, OWASP Top 10 (2025) full coverage, unbreakable rules |
AGENTS.md |
Instructions for AI agents working on this codebase |
database/README.md |
ER diagram, FK rules, DB security design decisions |
CONTRIBUTING.md |
How to contribute |
CODE_OF_CONDUCT.md |
Community standards |
This is an academic/portfolio project.
If you find a vulnerability, please open an issue with the security label.
MIT Β© 2025β2026 DJAngel973