How giovanni104/secure-otp-service works
1. What this repository is
- A single-service REST API for generating and validating one-time passwords (OTPs).
- The runtime is a Spring Boot application written in Java 25, with Redis as the only persistent external dependency.
- The app is designed as a small, layered backend: HTTP boundary, application/service logic, Redis-backed state, and centralized error handling.
2. Top-level runtime boundary
- OtpApplication is the Spring Boot entry point.
- The application is packaged and run either directly with Maven or as a Docker container.
- docker-compose.yml defines the deployment boundary: one container for the API and one for Redis.
3. HTTP API layer
- OtpController is the REST façade.
- It exposes two main operations:
- generate an OTP for an identifier
- validate an OTP for an identifier
- Request/response shapes are isolated in DTOs under src/main/java/com/example/otp/dto, which keeps the API contract separate from internal service logic.
4. Application/service layer
- OtpService is the core business layer.
- This is where OTP generation, validation, expiration semantics, and state transitions are handled.
- The service sits between the controller and Redis, so the controller does not contain OTP logic.
5. Security-sensitive OTP processing
- OTPs are not stored in plaintext.
- MessageDigestSupport and the security approach described in the README indicate HMAC-SHA256 hashing plus constant-time comparison via MessageDigest.isEqual.
- The service uses SecureRandom for code generation, which is important because this is security-sensitive data rather than a normal random token.
6. Redis as the state store
- RedisConfig wires Spring Data Redis connectivity.
- Redis is used as a temporary, TTL-based store for OTP lifecycle state rather than as a long-term database.
- The README describes three logical key types:
- active OTP hash
- request marker for distinguishing expired vs nonexistent
- used marker for distinguishing already-used vs expired/nonexistent
- This means the architecture relies on Redis expiration as part of the business rules, not just as storage cleanup.
7. Configuration boundary
- OtpProperties and ApplicationConfig hold externalized settings.
- Important runtime parameters include OTP length, TTL, used-marker TTL, HMAC secret, Redis host/port/password, and whether the raw OTP is returned in responses.
- This makes the service environment-driven and suitable for local, containerized, or production deployments with different secrets and TTLs.
8. Validation and API contracts
- The DTOs are split by use case:
- GenerateOtpRequest / GenerateOtpResponse
- ValidateOtpRequest / ValidateOtpResponse
- Jakarta Validation is part of the stack, so request validation likely happens at the controller boundary before service logic is invoked.
- This keeps invalid input from reaching Redis or the crypto logic.
9. Error handling subsystem
- The exception package defines domain-specific failure modes:
- OtpNotFoundException
- OtpExpiredException
- InvalidOtpException
- OtpAlreadyUsedException
- GlobalExceptionHandler centralizes translation from domain exceptions to API error responses via ApiErrorResponse.
- The architecture therefore has a clean separation between business failures and HTTP status mapping.
10. End-to-end generate flow
- Client calls POST /api/otp/generate.
- Controller validates input and delegates to OtpService.
- Service generates a numeric OTP, hashes it, stores it in Redis with TTL, and returns metadata.
- If enabled by config, the actual OTP is included in the response; otherwise it is meant to be delivered through an external channel such as SMS/email.
11. End-to-end validate flow
- Client calls POST /api/otp/validate.
- Controller passes identifier and OTP to OtpService.
- Service looks up Redis state, distinguishes between not found / expired / already used, hashes the supplied OTP, compares securely, and deletes or marks state on success.
- Validation is therefore stateful and depends on Redis key lifecycle, not just a stateless cryptographic check.
12. Operational and deployment boundaries
- The app exposes Spring Boot Actuator health endpoints for readiness/liveness style checks.
- Docker Compose is the operational entry point for local execution and integration testing.
- The README’s production guidance also implies the system expects Redis network isolation, secret management, and rate limiting outside the service itself.
13. Testing footprint
- The repository currently shows a minimal Spring Boot test class under src/test.
- That suggests the repo is oriented around integration/bootstrapping verification rather than a large internal test suite, at least in the current tree.
14. How to draw the architecture
- The simplest accurate diagram is:
- external REST clients on one side
- Spring Boot API container in the middle, split into controller, service, config, and exception handling
- Redis as the only backing state service on the other side
- The key architectural idea is that OTP security and lifecycle are enforced by the service layer plus Redis TTL/markers, not by the HTTP layer.
Main entry points: OtpService.java OTP service · OtpApplication.java OTP app · OtpController.java OTP controller · docker-compose.yml Compose stack
Components 17
Clients
External callers and deployment entry points
- REST client · Calls the OTP API
Spring Boot API
HTTP boundary, service logic, and error translation
- OTP app · Spring Boot application startup
src/main/java/com/example/otp/OtpApplication.java - OTP controller · HTTP façade for generate and validate
src/main/java/com/example/otp/controller/OtpController.java - OTP service · Generates, validates, and advances OTP state
src/main/java/com/example/otp/service/OtpService.java - Digest support · HMAC and constant-time comparison support
src/main/java/com/example/otp/service/MessageDigestSupport.java - Gen request · Request payload for OTP generation
src/main/java/com/example/otp/dto/GenerateOtpRequest.java - Gen response · Response payload for OTP generation
src/main/java/com/example/otp/dto/GenerateOtpResponse.java - Val request · Request payload for OTP validation
src/main/java/com/example/otp/dto/ValidateOtpRequest.java - Val response · Response payload for OTP validation
src/main/java/com/example/otp/dto/ValidateOtpResponse.java - Exception handler · Maps domain failures to API errors
src/main/java/com/example/otp/exception/GlobalExceptionHandler.java - API error · Standard error response body
src/main/java/com/example/otp/exception/ApiErrorResponse.java
Configuration
Externalized settings and infrastructure wiring
- OTP props · OTP length, TTL, secret, and response mode
src/main/java/com/example/otp/config/OtpProperties.java - App config · Wires application-level beans and settings
src/main/java/com/example/otp/config/ApplicationConfig.java - Redis config · Connects Spring Data Redis
src/main/java/com/example/otp/config/RedisConfig.java
Redis State
Temporary OTP lifecycle storage
- Redis · TTL-backed OTP hashes and markers
Operations
Container and runtime packaging
- Compose stack · Starts API and Redis containers
docker-compose.yml - API image · Builds the Spring Boot runtime image
Dockerfile
Connections 18
- REST client → OTP controller: POST /api/otp/* · Client invokes generate or validate endpoints
- Compose stack → API image: build/run · Compose runs the API container image
- Compose stack → Redis: starts · Compose provides Redis as the backing service
- API image → OTP app: packages · Container starts the Spring Boot entrypoint
- OTP app → App config: loads · Bootstraps application configuration
- OTP app → Redis config: loads · Initializes Redis connectivity
- OTP app → Exception handler: registers · Enables centralized exception mapping
- OTP controller → Gen request: binds · Binds generate input JSON
- OTP controller → Val request: binds · Binds validate input JSON
- OTP controller → OTP service: delegates · Hands off OTP business operations
- OTP service → Digest support: hash/compare · Uses HMAC and constant-time equality
- OTP service → OTP props: reads · Consumes TTL, length, secret, and flags
- OTP service → Redis: stores/loads · Persists OTP hashes and lifecycle markers
- OTP service → Gen response: returns · Builds generation result payloads
- OTP service → Val response: returns · Builds validation result payloads
- Exception handler → API error: emits · Serializes structured API errors
- Redis config → Redis: connects · Binds Spring Data Redis to the external store
- App config → OTP props: binds · Maps externalized OTP settings