Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ricardomb-tech/auth-service/llms.txt

Use this file to discover all available pages before exploring further.

Auth Service is structured around Clean Architecture with a strict, unidirectional dependency rule: infrastructure → application → domain. This means that the core domain logic — account lifecycle, token semantics, and business invariants — has zero knowledge of Spring, JPA, HTTP, or any external SDK. Downstream concerns (persistence, email, OAuth2, REST controllers) are adapters that implement interfaces defined by the domain. The dependency rule is not a convention enforced by code review; it is mechanically verified by an ArchUnit test that runs in every CI build and breaks the pipeline if violated.

Layer Overview

The codebase is organized under com.auth_service.auth into four top-level packages:
com.auth_service.auth/
├── domain/         # Pure Java — models, value objects, ports, domain events, exceptions
├── application/    # Use cases — orchestrate domain through port interfaces; @Transactional lives here
├── infrastructure/ # Adapters — REST controllers, JPA repositories, email, OAuth2 handlers, Kafka
└── config/         # Spring wiring — SecurityFilterChain, @ConfigurationProperties, bean composition

domain/ — the core

The domain layer contains the Account aggregate root, value objects (Email, HashedPassword, AccountId), the AccountStatus and Role enumerations, domain events (AccountRegistered, AccountVerified, TokenFamilyRevoked, …), domain exceptions, and port interfaces (e.g. AccountRepository, RefreshTokenRepository, EmailSender). This layer imports nothing from Spring, JPA, or any external SDK — it is plain Java 21.
// AccountStatus — transitions enforced by application/usecase only (AD-6, AD-13)
public enum AccountStatus {
    PENDING_VERIFICATION,
    ACTIVE,
    LOCKED,
    DISABLED
}

// Role — stored in JWT claims as a string array
public enum Role {
    USER,
    ADMIN
}

application/ — use cases

Each PRD capability maps to a single use case class in application/usecase/ (e.g. RegisterAccountUseCase, LoginUseCase, RefreshTokenUseCase, FederatedLoginUseCase). Use cases are the only layer annotated with @Transactional, and the only layer that orchestrates domain objects through port interfaces. Controllers hold no business logic; domain objects hold no transaction management.

infrastructure/ — adapters

Infrastructure adapters implement the domain port interfaces:
  • adapters/postgresql/ — JPA entities and Spring Data repositories; maps between JPA entities and domain models.
  • adapters/email/SmtpEmailSender (prod, behind a Resilience4j circuit breaker) and LoggingEmailSender (dev/MailHog).
  • adapters/oauth/OAuth2AuthenticationSuccessHandler, OAuth2AuthenticationFailureHandler, CookieOAuth2AuthorizationRequestRepository, and GitHubOAuth2UserService. All delegate into FederatedLoginUseCase.
  • adapters/security/JwtAuthenticationFilter intercepts every request, validates the JWT, and populates the SecurityContext.
  • adapters/messaging/ — transactional outbox poller and Kafka event publisher (staged for Epic 6).
  • controller/AuthController (public auth endpoints) and UserController (protected profile endpoint), each delegating entirely to use cases.

Layer Dependency Diagram

The config/ package sits outside this graph — it wires adapters to use cases through dependency injection and is the only place where Spring beans are composed across layers.

Architectural Decision Records

AD-2 — Single token issuance point (TokenIssuer)

All AccessToken + RefreshToken pairs — whether produced by credential login, refresh rotation, or OAuth2 federation — are emitted exclusively by TokenIssuer in application/usecase. No adapter or controller constructs JWTs directly. This guarantees that the credential login and the OAuth2 success handler can never diverge in claims format, TTL, or signing algorithm.

AD-3 — JWT HS256, fixed claim set, 15-minute TTL

This is a security contract that downstream services depend on. Any change to the claim set or signing algorithm is a breaking change for all JWT consumers.
Access tokens are signed JWTs using HS256 with a secret of at least 256 bits. The fixed claim set is:
ClaimValue
subAccount UUID (stable identifier for downstream authorization)
emailVerified email address
rolesJSON array of role strings, e.g. ["USER"]
iatIssued-at timestamp (Unix seconds)
expExpiry timestamp (Unix seconds); TTL = 15 minutes
iss"auth-service"
Tokens are never persisted. Individual revocation is not supported — token expiry (15 minutes) is the sole lifecycle boundary. The short TTL limits the blast radius of a leaked token.

AD-4 — Opaque refresh tokens, SHA-256 hash storage, family-based rotation with reuse detection

Presenting a refresh token that has already been used triggers revocation of the entire token family — all sessions for the affected account are invalidated. This is the intended defense against token theft.
Refresh tokens are cryptographically random opaque values (not JWTs). The plaintext token is sent to the client exactly once — at issuance — and never stored. The database stores only the SHA-256 hash of the token alongside:
  • family_id — groups all tokens in a rotation chain together
  • expires_at — 7-day TTL from issuance
  • used_at — set when the token is exchanged for a new pair
  • revoked_at — set when the family is invalidated
On POST /auth/refresh:
  1. The presented token hash is looked up in the database.
  2. If used_at or revoked_at is non-null → entire family is revoked; client must log in again.
  3. If valid → used_at is stamped, a new token is issued in the same family, and the new pair is returned.
Logout, password reset (FR-7), and account deactivation (FR-11) revoke all families for the account.

AD-5 — Credential and one-time token hardening

  • Passwords are stored as BCrypt hashes via Spring Security’s DelegatingPasswordEncoder. Plaintext passwords are never logged.
  • Verification and password-reset tokens are random values sent by email; only their SHA-256 hash is stored in the database. Each token is single-use (stamped with consumed_at on redemption) with a TTL of 24 hours (email verification) or 1 hour (password reset).

AD-8 — RFC 9457 Problem Details for all errors

There are two distinct code paths that produce error responses. Both produce identical application/problem+json shapes so that API consumers can handle them uniformly.
Every error response — domain validation failures, authentication errors, authorization failures — is returned as application/problem+json (RFC 9457 / formerly RFC 7807). There are two response paths:
  1. MVC layer — a @RestControllerAdvice (GlobalExceptionHandler) handles domain and application exceptions and builds ProblemDetail objects.
  2. Security filter layerJwtAuthenticationFilter failures (missing or invalid JWT) never reach the MVC dispatcher. SecurityConfig registers a custom AuthenticationEntryPoint and AccessDeniedHandler that write application/problem+json directly to the servlet response using the same ProblemDetail structure.
An example error response for an unauthenticated request:
{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authentication required.",
  "instance": "/api/v1/users/me"
}

AD-11 — Deny-all security; explicit public allowlist

Every new endpoint is protected by default. Adding a route to the public allowlist in SecurityConfig is a deliberate, reviewable decision — not the default.
SecurityConfig terminates the SecurityFilterChain with anyRequest().authenticated(). The explicit public allowlist is:
private static final String[] PUBLIC_ENDPOINTS = {
    "/error",
    "/swagger-ui.html",
    "/swagger-ui/**",
    "/v3/api-docs",
    "/v3/api-docs/**",
    "/auth/**",
    "/oauth2/**",
    "/login/oauth2/**"
};
The session strategy is SessionCreationPolicy.STATELESS — no server-side session is ever created. CSRF protection is disabled because the API uses no session cookies. The OAuth2 authorization request state is stored in a short-lived cookie (CookieOAuth2AuthorizationRequestRepository) rather than the HTTP session to remain compatible with stateless operation. Actuator endpoints (/actuator/**) are never in the public allowlist and must be exposed on a separate management port or behind infrastructure-level authentication.

AD-12 — ArchUnit enforces layer dependencies in CI

The dependency rule (infrastructure → application → domain) is verified by an ArchitectureRulesTest using ArchUnit 1.4.2 on every build. The test fails — and therefore blocks the CI pipeline — if:
  • Any type in domain/ imports from application/ or infrastructure/.
  • Any type in application/ imports from infrastructure/.
  • Any cyclic dependency between packages is detected.
This makes AD-1, AD-6, and AD-13 mechanical constraints, not style conventions that erode over time.

Database Schema Overview

The following entity-relationship diagram shows the core tables managed by Flyway: Hibernate is configured with spring.jpa.hibernate.ddl-auto=validate across all profiles — it validates the schema against the entity mappings but never generates DDL. All schema changes are made through versioned Flyway migrations (db/migration/V<n>__<description>.sql).

System Context

The following diagram shows how Auth Service fits within a broader system: Downstream services validate JWTs locally using the shared JWT_SECRET_CURRENT. They never call Auth Service for token introspection — the stateless JWT design means verification is a local operation at the cost of a 15-minute revocation lag.

Capability to Architecture Mapping

CapabilityUse CaseGoverning ADs
Registration + email verificationRegisterAccountUseCase, VerifyAccountUseCaseAD-5, AD-6, AD-8, AD-9, AD-13, AD-14
Credential loginLoginUseCaseTokenIssuerAD-2, AD-3, AD-8, AD-13
Refresh token rotationRefreshTokenUseCaseAD-4, AD-13
LogoutLogoutUseCaseAD-4, AD-13
OAuth2 social loginadapters/oauthFederatedLoginUseCaseTokenIssuerAD-2, AD-6, AD-13, AD-17
Password resetRequestPasswordResetUseCase, ResetPasswordUseCaseAD-4, AD-5, AD-8, AD-9, AD-13
Own profileUserControllerGetOwnProfileUseCaseAD-3, AD-11, AD-18
Admin account managementAdminControllerManageAccountUseCaseAD-4, AD-6, AD-11, AD-18, AD-20
Layer rule enforcementArchitectureRulesTestAD-12, AD-21

Deferred Decisions

The following decisions are intentionally deferred and documented rather than left implicit:
  • RS256 + JWKS endpoint — HS256 with dual-secret rotation is sufficient for the current single-consumer model. RS256 with a public JWKS endpoint will be revisited when multiple independent consumer teams exist.
  • Token introspection endpoint — Not implemented; no consumer has requested it.
  • Concrete secret store (Vault / AWS Secrets Manager / Kubernetes Secrets) — AD-19 defines the contract (active + previous key pair injected from outside the process), not the tool.
  • Rate limiting and brute-force lockout — Story 3.2; currently no rate limiting on /auth/login or /auth/forgot-password.
  • Transactional outbox publishing — Redpanda is already started by docker-compose.yml; active consumption begins in Epic 6.

Build docs developers (and LLMs) love