feat(auth): Session-based authentication with JPA user/role model#66
Open
ebouchut wants to merge 32 commits into
Open
feat(auth): Session-based authentication with JPA user/role model#66ebouchut wants to merge 32 commits into
ebouchut wants to merge 32 commits into
Conversation
Hand-write Liquibase database migrations instead of usiing the useless MCD-generated DDL
…eSQL The smoke test now behaves the same locally and in CI . It no longer needs the dev environment running: - Run LearnDevApplicationTests against an ephemeral PostgreSQL started by Testcontainers. - Exclude the MongoDB auto-configuration.
Configure Spring Security for session-based form login with a BCrypt password encoder. Authentication endpoints are grouped under /auth, and that URL convention is documented in CONTRIBUTING.
Add AuthController serving the home, login and dashboard views and handling the registration form (display and submission). Duplicate username/email and validation errors are surfaced as field errors; a successful registration redirects to /auth/login?registered. Add the four Thymeleaf templates under the /auth/ URL prefix. Forms post through th:action, so Spring Security's CSRF token is injected automatically.
Set spring.thymeleaf.cache to false in the dev profile so template edits are picked up without restarting the app.
Record the decision to name every test *Test/*Tests and run the whole suite under Surefire in mvn test, rather than adding the Failsafe plugin with an *IT suffix. Prompted by an AuthFlowIT test that mvn test silently skipped because no Failsafe plugin is configured.
Drive the full auth journey through MockMvc against the shared Postgres container: an anonymous request to /dashboard redirects to login, a new account registers, a wrong password is rejected, and correct credentials authenticate and land on /dashboard. The register POST uses a CSRF token, proving CSRF protection is wired correctly.
Set the session cookie to HttpOnly so client-side JavaScript cannot read it (mitigates XSS session theft) and SameSite=Lax so it is withheld on cross-site requests (CSRF defense in depth). The Secure attribute is left commented out until the app is served over HTTPS.
Add three cross-linked reference docs: GLOSSARY.md (domain and technical term definitions), docs/tech-stacks.md (catalogue of tools and versions), and ARCHITECTURE.md (layers, request flow, auth, data, and testing). Each points to the others and to the ADRs, keeping what/how/why separate.
Add a run target that starts the Podman machine if the socket is unreachable, brings up the Postgres and Mongo containers, then runs the Spring Boot app in the foreground. Also list it in the help output.
Tick the completed steps for the JPA entities and session-auth plan. The manual browser smoke test (Task 11, Step 3) is left unchecked as it has not been performed yet.
Rewrite the run section (sdk env, podman guard, compose, mvnw) and add a note on why Podman needs a Linux VM on macOS and Windows. Add a Documentation section linking ARCHITECTURE, tech-stacks, GLOSSARY, and the ADRs. Use 'podman info' rather than 'podman machine info' for the machine-up check, since the latter succeeds even when the machine is down.
- Drop the frontend/npm mention (the backend uses Maven only) and the stale 'cd backend' step now that the app is at the project root. - Add a test naming-convention note (test classes end in Test) . - Reword the Podman Testcontainers setup.
Contributor
There was a problem hiding this comment.
Pull request overview
Introduces session-based authentication for the server-rendered Spring Boot app, backed by new JPA User/Role entities and repositories aligned with the existing Liquibase schema, plus a Testcontainers-based PostgreSQL test strategy to validate mappings and migrations against real Postgres.
Changes:
- Add JPA user/role model (entities + repositories) and seed base roles via Liquibase.
- Implement Spring Security session form-login with registration flow (DTO validation, controller, services, templates).
- Add/expand test coverage using a shared Testcontainers PostgreSQL base (slice tests + end-to-end MockMvc flow) and document architecture/testing conventions.
Reviewed changes
Copilot reviewed 39 out of 40 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/java/com/ericbouchut/learndev/user/repository/UserRepositoryTest.java | Adds a JPA slice test validating User persistence and repository queries against Postgres. |
| src/test/java/com/ericbouchut/learndev/support/AbstractPostgresIT.java | Provides a shared singleton Postgres Testcontainers base for tests. |
| src/test/java/com/ericbouchut/learndev/role/repository/RoleRepositoryTest.java | Adds a JPA slice test validating seeded roles are queryable. |
| src/test/java/com/ericbouchut/learndev/LearnDevApplicationTests.java | Updates context smoke test to run against Testcontainers Postgres and excludes Mongo auto-config. |
| src/test/java/com/ericbouchut/learndev/auth/RegistrationServiceTest.java | Adds unit tests for registration hashing, default role assignment, and duplicate email rejection. |
| src/test/java/com/ericbouchut/learndev/auth/CustomUserDetailsServiceTest.java | Adds unit tests for mapping roles to Spring Security authorities and unknown-user behavior. |
| src/test/java/com/ericbouchut/learndev/auth/AuthFlowTest.java | Adds end-to-end MockMvc test covering register → login → protected page flow. |
| src/main/resources/templates/register.html | Adds Thymeleaf registration page with validation error rendering. |
| src/main/resources/templates/login.html | Adds Thymeleaf login page with registered/logout/error messages. |
| src/main/resources/templates/home.html | Adds a basic home page with auth navigation links. |
| src/main/resources/templates/dashboard.html | Adds a protected dashboard view showing authenticated username and logout form. |
| src/main/resources/db/changelog/changes/V20260623105538-seed-roles.sql | Seeds baseline roles (STUDENT, INSTRUCTOR, ADMIN) via Liquibase formatted SQL. |
| src/main/resources/application.yaml | Hardens session cookie settings (HttpOnly, SameSite). |
| src/main/resources/application-dev.yaml | Adds dev Thymeleaf caching override and clarifies Liquibase sectioning. |
| src/main/java/com/ericbouchut/learndev/user/repository/UserRepository.java | Introduces Spring Data JPA repository for User. |
| src/main/java/com/ericbouchut/learndev/user/entity/User.java | Introduces User JPA entity with UUID PK and role association. |
| src/main/java/com/ericbouchut/learndev/role/repository/RoleRepository.java | Introduces Spring Data JPA repository for Role. |
| src/main/java/com/ericbouchut/learndev/role/entity/Role.java | Introduces Role JPA entity. |
| src/main/java/com/ericbouchut/learndev/common/config/SecurityConfig.java | Adds Spring Security filter chain configuration and BCrypt password encoder. |
| src/main/java/com/ericbouchut/learndev/auth/RegistrationService.java | Implements registration business logic (uniqueness checks, hashing, default role). |
| src/main/java/com/ericbouchut/learndev/auth/exception/DuplicateUsernameException.java | Adds domain exception for duplicate usernames. |
| src/main/java/com/ericbouchut/learndev/auth/exception/DuplicateEmailException.java | Adds domain exception for duplicate emails. |
| src/main/java/com/ericbouchut/learndev/auth/dto/RegisterForm.java | Adds validated form-backing DTO for registration. |
| src/main/java/com/ericbouchut/learndev/auth/CustomUserDetailsService.java | Loads users/roles into Spring Security UserDetails authorities. |
| src/main/java/com/ericbouchut/learndev/auth/AuthController.java | Adds MVC endpoints for home/login/dashboard and registration form + submission. |
| README.md | Updates run instructions and expands documentation/contributing pointers. |
| pom.xml | Adds validation + Testcontainers dependencies. |
| Makefile | Adds test/run targets and Podman-aware Testcontainers wiring. |
| GLOSSARY.md | Adds shared domain/technical terminology for the project. |
| docs/tech-stacks.md | Documents the technology stack and versions used. |
| docs/plans/2026-06-16-jpa-entities-session-auth.md | Adds a detailed implementation plan/record for the feature. |
| docs/adr/README.md | Expands ADR index with newly added decisions. |
| docs/adr/0009-run-tests-under-surefire-not-failsafe.md | Records decision to run all tests under Surefire using *Test suffix. |
| docs/adr/0008-share-singleton-testcontainers-postgres.md | Records decision to use singleton static container vs @Container lifecycle. |
| docs/adr/0007-use-postgresql-over-mysql.md | Records database choice ADR. |
| docs/adr/0006-test-against-real-postgres-testcontainers.md | Records decision to use real Postgres via Testcontainers for tests. |
| docs/adr/0005-handwrite-liquibase-migrations-over-mcd-ddl.md | Records decision to hand-write Liquibase migrations. |
| CONTRIBUTING.md | Updates contribution guidance, routing/testing conventions, and how to run tests with Podman/Testcontainers. |
| ARCHITECTURE.md | Adds architecture overview, request flow, auth model, and testing strategy description. |
| .gitignore | Removes ignored generated DDL artifacts under docs/database/ddl. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+28
to
+34
| @SOCK=$$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}' 2>/dev/null); \ | ||
| if [ -n "$$SOCK" ]; then \ | ||
| echo "Podman detected, socket: $$SOCK"; \ | ||
| DOCKER_HOST="unix://$$SOCK" TESTCONTAINERS_RYUK_DISABLED=true ./mvnw test; \ | ||
| else \ | ||
| ./mvnw test; \ | ||
| fi |
Comment on lines
+40
to
+58
| @Transactional | ||
| public User register(RegisterForm form) { | ||
| if (users.existsByUsername(form.username())) { | ||
| throw new DuplicateUsernameException(form.username()); | ||
| } | ||
| if (users.existsByEmail(form.email())) { | ||
| throw new DuplicateEmailException(form.email()); | ||
| } | ||
| Role student = roles.findByRoleName("STUDENT") | ||
| .orElseThrow(() -> new IllegalStateException("STUDENT role not seeded")); | ||
|
|
||
| User user = new User(); | ||
| user.setUsername(form.username()); | ||
| user.setEmail(form.email()); | ||
| user.setPassword(encoder.encode(form.password())); | ||
| user.getRoles().add(student); | ||
|
|
||
| return users.save(user); | ||
| } |
Comment on lines
+69
to
+76
| // Plain many-to-many: Hibernate inserts (user_id, role_id); the extra | ||
| // user_roles columns (assigned_at) are populated by their DB defaults. | ||
| @ManyToMany(fetch = FetchType.EAGER) | ||
| @JoinTable( | ||
| name = "user_roles", | ||
| joinColumns = @JoinColumn(name = "user_id"), | ||
| inverseJoinColumns = @JoinColumn(name = "role_id")) | ||
| private Set<Role> roles = new HashSet<>(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds:
Note
Authentication uses server-side sessions with Spring Security form login which suits the Thymeleaf server-rendered UI.
It does not use JWT, which is planned for v2 when switching to a React frontend.
See ADR-0001.
STUDENT,INSTRUCTOR,ADMINare seeded.SUPERADMINrole is deferred under YAGNI (feat(role): Add the SUPERADMIN role #65).New Features
Authentication and authorization
/auth/URL prefix(
/auth/login,/auth/register,/auth/logout); everything else requiresauthentication.
RegisterForm(Bean Validation) →AuthController→RegistrationService, which enforces unique username/email, hashes thepassword with BCrypt, and assigns the default
STUDENTrole.CustomUserDetailsServiceROLE_-prefixed authorities,is_active(disabled) andis_locked(locked) account flags.HttpOnlySameSite=LaxSecuredocumented for HTTPS).Data model and migrations
Userentity and its Spring Data repositoryUUIDprimary key to mitigate IDOR attacks, see ADR-0003)Roleentity and its Spring Data repositorySTUDENT,INSTRUCTOR, andADMINroles.Frontend
home,login,register, anddashboard, wired to the/auth/endpoints.th:action, so Spring Security's CSRF token is injected automatically.Testing
@DataJpaTest) for the repositories.AuthFlowTest(MockMvc:register => reject-anonymous => reject-wrong-password => login => dashboard,make testwires Testcontainers to Podman automatically.Documentation and tooling
GLOSSARY.md,ARCHITECTURE.md,docs/tech-stacks.md.make runtarget to start the databases and the app (container-engineagnostic).
Testing done
All tests passed:
register=>login=>/dashboard=>logout.