No description
Find a file
Justin Martz cb5f96baf1 Merge branch 'REPO-dev-env' into 'main'
REPO - Fix justinplayer's external_id to a stable UUID for local dev

See merge request guitartech/guitartech-auth-service!27
2026-08-16 20:01:41 +00:00
deploy Repo now this 2026-03-08 03:12:21 +00:00
gradle/wrapper Bootstrap Spring Boot authentication service 2026-01-03 16:30:05 -07:00
src REPO - Fix justinplayer's external_id to a stable UUID for local dev 2026-08-16 20:01:41 +00:00
.env.example Repo deployment 2026-03-07 21:39:20 +00:00
.gitattributes Bootstrap Spring Boot authentication service 2026-01-03 16:30:05 -07:00
.gitignore FEATURE - Order entitlements-in-roles by name alphabetically 2026-07-19 05:13:44 +00:00
.gitlab-ci.yml Increase test coverage 2026-03-15 00:11:03 +00:00
build.gradle REPO - Fix justinplayer's external_id to a stable UUID for local dev 2026-08-16 20:01:41 +00:00
CLAUDE.md REPO - Fix justinplayer's external_id to a stable UUID for local dev 2026-08-16 20:01:41 +00:00
docker-compose.yml Repo now this 2026-03-08 03:12:21 +00:00
Dockerfile Don't produce plain JAR at all 2026-03-06 23:09:35 -07:00
gradlew Bootstrap Spring Boot authentication service 2026-01-03 16:30:05 -07:00
gradlew.bat Bootstrap Spring Boot authentication service 2026-01-03 16:30:05 -07:00
guitartech-auth-service.code-workspace FEATURE - Roles and entitlements 2026-07-09 18:48:06 +00:00
README.md FEATURE - Roles and entitlements 2026-07-09 18:48:06 +00:00
settings.gradle Bootstrap Spring Boot authentication service 2026-01-03 16:30:05 -07:00

guitartech-auth-service

A Spring Boot 4.1.0 REST API for user authentication and management. Implements JWT-based authentication with email-verified user registration, login, profile management, and role/entitlement-based authorization.

Quick Start

Prerequisites

  • JDK 21+
  • PostgreSQL 16 (running locally or via Docker)
  • Gradle (included via wrapper: ./gradlew)

Local Development

  1. Set up environment variables:

    source .env.local
    

    This loads JWT_SECRET and DB_PASSWORD for local development.

  2. Start PostgreSQL:

    docker run -d \
      --name guitartech-postgres \
      -e POSTGRES_DB=guitartech_auth_service \
      -e POSTGRES_PASSWORD=postgres \
      -p 5432:5432 \
      postgres:16-alpine
    
  3. Run the application: Using the Spring Boot Dashboard extension in VS Code:

    • Source .env.local in your terminal: source .env.local
    • Click the run button in the Dashboard

    Or via CLI:

    source .env.local
    ./gradlew bootRun
    
  4. Verify it's running: Check the console for Started GuitartechAuthServiceApplication, then confirm the security filter chain responds (401 without a token is expected here):

    curl -i http://localhost:8080/roles
    

The application will automatically apply Liquibase database migrations on startup.

Dev Profile & Seed Data

For local development, run with the dev Spring profile instead of the default:

source .env.local
./gradlew bootRunDev

Entitlements and roles (ADMIN/PLAYER) are seeded in every environment, every startup, by PermissionsSeeder (config/PermissionsSeeder.java), which reads src/main/resources/permissions.yaml and upserts additively — it fills in whatever's missing but never overwrites a role's name/description or removes an entitlement association, so live edits made through the /roles API are never clobbered on restart.

This Gradle task (build.gradle) runs the app with spring.profiles.active=dev, which additionally activates DevEnvironmentInitializer (config/DevEnvironmentInitializer.java) — a CommandLineRunner that looks up the already-seeded ADMIN/PLAYER roles and seeds two ready-to-use demo accounts (safe to restart repeatedly without duplicating data), both with password Megadude#13:

  • justinadmin (bill@microsoft.com) — ADMIN role
  • justinplayer (sergey@google.com) — PLAYER role

Adding a new protected endpoint? Add its entitlement to permissions.yaml (and to the relevant role's entitlements list if it should be granted) — PermissionsSeeder picks it up on the next startup in every environment, no migration needed.

API Endpoints

Registration is a two-step, email-verified flow: POST /users creates an unregistered user and emails a 6-character verification code, then POST /tokens/verifications exchanges that code for a JWT and completes registration. Returning users authenticate with POST /tokens.

Public Endpoints

  • POST /users — Start registration (sends a verification code by email)

    {
      "username": "john_doe",
      "password": "SecurePass123!",
      "email": "john@example.com"
    }
    
  • POST /tokens/verifications — Complete registration with the emailed code, returns a JWT

    {
      "verification_code": "AB12CD"
    }
    
  • POST /tokens — Log in an existing user, returns a JWT

    {
      "username": "john_doe",
      "password": "SecurePass123!"
    }
    

Protected Endpoints (Require JWT Bearer Token)

All endpoints below require Authorization: Bearer <jwt-token> and are additionally gated by entitlement (e.g. guitartech-auth-service.role.read) via @PreAuthorize.

  • PUT /users/me/username — Update your own username
  • PUT /users/me/password — Update your own password
  • GET /roles — List all roles (HAL/hypermedia) — requires role.read
  • GET /roles/{id} — Get a single role — requires role.read
  • POST /roles — Create a role — requires role.create
    {
      "name": "BETA_TESTER",
      "description": "GuitarTech Beta Tester",
      "entitlement_ids": [2, 3, 4]
    }
    
  • PUT /roles/{id} — Update a role's name/description — requires role.update
  • DELETE /roles/{id} — Delete a role (blocked if any user still has it assigned) — requires role.delete
  • PUT /roles/{roleId}/entitlements/{entitlementId} — Add an entitlement to a role (idempotent) — requires role.update
  • DELETE /roles/{roleId}/entitlements/{entitlementId} — Remove an entitlement from a role (idempotent) — requires role.update
  • PUT /users/{externalId}/roles/{roleId} — Assign a role to a user (idempotent), returns the user's updated roles — requires role.update
  • DELETE /users/{externalId}/roles/{roleId} — Remove a role from a user (idempotent, 204) — requires role.update
  • GET /users — List all users (HAL/hypermedia, admin view incl. email and timestamps) — requires user.read
  • GET /users/{externalId} — Get a single user — requires user.read

Role names must be uppercase letters/underscores only (e.g. ADMIN, BETA_TESTER). Entitlement names follow a <service>.<resource>.<action> convention (e.g. guitartech-auth-service.role.create).

Building & Testing

./gradlew clean build          # Full clean build
./gradlew build -x test        # Build without tests
./gradlew test                 # Run all tests
./gradlew test --tests SomeTest # Run a specific test

Tests use Testcontainers with a real PostgreSQL database (containerized), not an in-memory DB.

macOS Note: For best performance with Testcontainers, use Orbstack instead of Docker Desktop. Orbstack is lightweight, faster, and handles container networking better on macOS.

Environment Variables

Create a .env.local file (git-ignored) with:

export JWT_SECRET="your-secret-min-64-chars"
export DB_PASSWORD="postgres"

Or set them directly:

JWT_SECRET=my-secret DB_PASSWORD=postgres ./gradlew bootRun

For Deployed Environments

The app reads these environment variables at runtime:

  • JWT_SECRET — JWT signing secret (required in production)
  • SPRING_DATASOURCE_URL — Database URL (defaults to localhost)
  • SPRING_DATASOURCE_USERNAME — Database user
  • SPRING_DATASOURCE_PASSWORD — Database password
  • SMTP_HOST / SMTP_ADDRESS / SMTP_TOKEN — SMTP host, from-address, and credential used to email registration verification codes (see EmailServiceImpl)

Database

Local Setup

The default application.properties connects to:

jdbc:postgresql://localhost:5432/guitartech_auth_service
username: postgres
password: postgres

Start PostgreSQL with the Docker command above, and migrations run automatically on app startup.

Resetting the Database Locally

# Connect to PostgreSQL
psql -U postgres -d guitartech_auth_service

# Drop tables to reset
DROP TABLE auth_user CASCADE;
DROP TABLE database_changelog CASCADE;
DROP TABLE database_changelog_lock CASCADE;

Then restart the app — migrations will re-apply.

Security

This project uses GitLab's free SAST (Static Application Security Testing) to automatically scan for vulnerabilities:

  • Semgrep — Scans Java source code for security issues and code quality problems
  • SpotBugs — Detects common Java bugs
  • Runs on all merge requests and branches before code can be merged
  • Results appear in the Merge Request pipeline UI

To view SAST reports:

  1. Go to your merge request in GitLab
  2. Click the Checks or Pipeline tab
  3. Look for SAST job details and any detected issues

Deployment

This service deploys to production only via GitLab CI/CD:

  • Production (guitartech.app) — Manual deployment to VPS (requires manual trigger in GitLab pipeline)

The pipeline automatically runs:

  1. Build — Compiles code
  2. Test — Runs tests + SAST security scanning
  3. Containerize — Builds Docker image
  4. Publish — Pushes image to registry (main branch only)
  5. Deploy — Manual production deployment (main branch only)

Local Development: Run locally with source .env.local && ./gradlew bootRun

Production Setup: Runs in Docker Compose on a 2GB VPS with Postgres. See docker-compose.yml for configuration.

Project Structure

src/main/java/tech/guitar/auth/
├── GuitartechAuthServiceApplication.java  # Entry point
├── config/                                # SecurityConfig, DevEnvironmentInitializer
├── controller/                            # REST endpoints (Role, Token, User)
├── facade/                                # RoleFacade — orchestrates across services
├── service/                               # Business logic + UserFacade/TokenFacade
├── repository/                            # Data access (JPA)
├── entity/                                # JPA entities
├── domain/                                # Domain models (Role, User, Entitlement, ...)
├── dto/                                   # Request/response DTOs
├── filter/                                # JWT validation filter
├── mapper/                                # Entity <-> domain converters
└── exception/                             # Custom exceptions + global handler

src/main/resources/
├── application.properties                 # Configuration
└── db/changelog/                          # Liquibase migrations

src/test/
├── AbstractIntegrationTest.java          # Base test class (Testcontainers)
├── repository/                           # Repository tests
├── service/                              # Service tests
├── facade/                                # Facade tests
├── controller/                            # Controller integration tests
└── testutil/                             # Test utilities

Key Features

  • JWT Authentication — HS512 algorithm, 24-hour expiration
  • Email-Verified Registration — registration completes only after the emailed verification code is submitted
  • Role & Entitlement Authorization@PreAuthorize-gated endpoints backed by roles and fine-grained entitlements
  • Password Security — BCrypt hashing (strength factor 13)
  • Input Validation — DTO-level with Jakarta validation annotations
  • Database Migrations — Liquibase with UTC timestamps
  • Global Exception Handling — Consistent error responses, including RFC 7807 ProblemDetail responses for role-related conflicts
  • Comprehensive Tests — Repository, service, facade, and controller layers, all against a real DB via Testcontainers

Development Workflow

  1. Create a feature branch off main
  2. Write tests first (repository, service, controller layers)
  3. Implement the feature
  4. Run all tests locally: ./gradlew test
  5. Open a merge request
  6. GitLab CI automatically runs:
    • Build: Compiles code
    • Test: Runs unit/integration tests + SAST security scanning
    • Containerize: Builds Docker image
    • All must pass before merge
  7. Merge to main once approved
  8. Publish: Image is pushed to GitLab Container Registry
  9. Manually trigger production deployment from the GitLab pipeline UI (only available for main branch)

Test Coverage

The project enforces 80% minimum code coverage on the main branch:

  • On main branch: Pipeline fails if coverage drops below 80%
  • On merge requests: Coverage below 80% is flagged as a warning (doesn't block merge)
  • View locally: ./gradlew jacocoTestReport to generate coverage report

This ensures code quality stays consistent while allowing flexibility during feature development on branches.

Useful Commands

# Format and check code
./gradlew spotlessApply      # Auto-format code

# View test coverage
./gradlew jacocoTestReport

# Clean up everything
./gradlew clean

# Run integration tests only
./gradlew test -i integration

Troubleshooting

"Could not connect to PostgreSQL"

  • Ensure PostgreSQL is running: docker ps | grep postgres
  • Check credentials match .env.local or application.properties

"JWT_SECRET is not set"

  • Run source .env.local before starting the app
  • Or set: export JWT_SECRET=<value>

"Liquibase migration failed"

  • Check database permissions and connectivity
  • Review migration files in src/main/resources/db/changelog/migrations/

Support

For issues or questions, refer to CLAUDE.md for detailed architecture and testing guidelines.