Repository navigation
Conversation
Carries the email address to which a one-time password reset link will be sent. The email field is validated with @notblank and @Email so an invalid address is rejected at the HTTP layer before reaching the service, keeping the error response uniform with other endpoints.
Carries the new password and its confirmation for the reset-password endpoint. Token is intentionally absent: it travels as a @RequestParam in the URL (see AuthController) so the frontend can copy it directly from the reset-link query string without wrapping it in a JSON body. @PasswordsMatch and @validpassword mirror the constraints already on RegisterRequest.confirmPassword, ensuring the same complexity rules apply whether a user sets a password for the first time or resets it. toString() masks both password fields to prevent them from leaking into logs via LoggingAspect (same pattern as RegisterRequest).
…ions Defines the three lifecycle endpoints that depend on the token services introduced on this branch: activate (ActivationToken), forgotPassword and resetPassword (PasswordResetToken). Keeping these in a dedicated interface rather than mixing them into AuthService follows the same single-responsibility split already in place between AuthService and the individual token services.
Accounts start disabled by default (@Builder.Default = false). AuthServiceImpl.register() will set it to false explicitly and send an activation email; UserServiceImpl.activate() flips it to true once the token is validated.
Adds the boolean flag that controls whether a registered account can log in. Default FALSE so every new row starts disabled until the user clicks the activation link sent by email.
Accounts are now created with enabled=false so they cannot log in until the user clicks the activation link. register() generates an ActivationToken and delegates the email to EmailService. This wires the token infrastructure from this branch into the registration flow, completing the activation lifecycle.
Passes disabled(!appUser.isEnabled()) to the User builder so Spring Security's AuthenticationManager rejects login attempts on accounts that have not yet been activated via the email link.
…d reset activate() validates the ActivationToken, flips enabled=true, and returns the updated user profile. forgotPassword() generates a PasswordResetToken and sends the reset email only if the email matches a known account (no information leakage on unknown emails). resetPassword() validates and consumes the PasswordResetToken, then re-encodes and saves the new password.
…ints
GET /auth/activate?token={token} - activates the account
POST /auth/forgot-password - sends a reset link
POST /auth/reset-password?token={...} - confirms the new password
The reset token travels as a @RequestParam so the frontend can copy
it straight from the email link URL without embedding it in a JSON
body. ResetPasswordRequest carries the new password and its
confirmation only; @PasswordsMatch validates both fields match and
@validpassword enforces complexity rules.
…word
_10_: activate with valid token returns 200 and user profile.
_11_: forgot-password with valid email returns 200 and delegates to UserService.
_12_: forgot-password with invalid email format returns 400.
_13_: reset-password with valid token and matching passwords returns 200,
token passed as @RequestParam (not in body).
_14_: reset-password with weak new password returns 400 (@validpassword).
_15_: reset-password with non-matching passwords returns 400 (@PasswordsMatch).
Also adds @MockitoBean UserService so the @WebMvcTest slice can
construct AuthController, which now depends on this interface.
_01_: activate() sets enabled=true and returns user profile. _02_: forgotPassword() sends reset email when email matches a known account. _03_: forgotPassword() sends nothing when email is unknown (no info leakage). _04_: resetPassword() encodes and saves the new password.
…ints These three endpoints must be accessible without a JWT: activate is called from an email link (no session), and forgot/reset are used precisely when the user cannot log in. Without this permit, Spring Security returns 401 on all three.
register() now creates accounts with enabled=false, so a direct login attempt after registration fails with DisabledException. Tests _14_ and _16_ inject AppUserRepository and call enableUser() between the register and login steps, simulating what the activation link does in production.
AuthServiceImpl.register() now injects ActivationTokenService and EmailService. @Injectmocks requires all constructor dependencies to be present as @mock; without them Mockito leaves them null and the test fails with NullPointerException on the first call. Also stubs activationTokenService.generate() and verifies that sendActivationEmail() is called, and asserts enabled=false on the saved user.
Without this annotation the full application context loads with the default profile, which tries to build EmailServiceImpl with a real JavaMailSender bean. That bean is only declared under the "test" profile (application-test.yml), causing context load failure.
…swordsMatch into user-management Merge feature/token-management into feature/user-management, resolving 13 conflicts. Strategy: take HEAD (user-management) as the base for all files since it is the more evolved branch (JPA Role entity, ManyToMany roles, accountLocked, full UserService interface); only adopt the incoming changes where they represent targeted improvements. Key resolutions: - ResetPasswordRequest: take token-management version entirely (token moved from request body to @RequestParam, @PasswordsMatch added, confirmPassword field added, toString() masks both fields). - AuthController.resetPassword: adopt @RequestParam String token signature; remove request.token() call. - AuthServiceImpl: remove duplicate activationTokenService/emailService fields introduced by both sides independently; keep HEAD's register() that uses JPA Role entity (.roles(Set.of(userRole))). - V10 migration dropped: V7 already adds the enabled column on this branch; applying V10 would cause a "column already exists" failure. - AuthControllerTest: clean up duplicate imports, replace _12_/_13_ (token-in-body form) with @RequestParam versions, add _16_/_17_ for @validpassword and @PasswordsMatch coverage on reset-password. - SecurityAuthorizationTest: take HEAD's inline enableUser logic (no helper method needed).
…abledException (403), and ExpiredJwtException (401) LockedException and DisabledException both extend AuthenticationException, so they would previously fall into the generic handleAuthentication handler and return 401. That conflated two distinct conditions: a locked account (should be 423 Locked) and an unactivated account (should be 403 Forbidden - the user is known but not yet permitted). Spring resolves @ExceptionHandler by most-specific type first, so placing these two handlers before the generic AuthenticationException handler is enough. ExpiredJwtException (io.jsonwebtoken) is not an AuthenticationException - it is thrown directly when a controller method calls JwtService.extractAllClaims() with an expired token (e.g. the token-info endpoint). Without a specific handler it would fall through to the catch-all and return 500.
… 403, and ExpiredJwtException 401 handlers Each new handler gets its own numbered test (_07_, _08_, _09_) following the existing convention: throw the exception directly at the handler method and assert status + message, with no Spring context overhead.
…token support Adds the jwt.refresh-expiration-ms property binding alongside the existing jwt.secret and jwt.expiration-ms. JwtService will use this value to set a longer TTL (7 days) on refresh tokens, keeping it separate from the short-lived access token expiry.
604800000 ms = 7 days. JwtService will read this value via JwtProperties to issue refresh tokens with a significantly longer TTL than the 1-hour access token.
…laims to JwtService generateRefreshToken(String username) issues a JWT with token_type="refresh" and the longer refreshExpirationMs TTL from JwtProperties. The token_type claim lets the application distinguish refresh tokens from access tokens programmatically. isRefreshToken(String token) verifies the token_type claim, used by AuthServiceImpl.refresh() (feature/token-management) to reject access tokens submitted to the refresh endpoint. extractAllClaims(String token) exposes the full Claims map, used by the new GET /auth/token-info endpoint so authenticated users can inspect their own JWT.
… methods JwtProperties record now has a third field (refreshExpirationMs), so all constructor calls are updated. Added tests: - _06_: refresh token encodes the subject correctly - _07_: isRefreshToken() returns true only for a refresh token (access token returns false), distinguishing the two by the token_type claim - _08_: extractAllClaims() returns a map containing the sub claim
…tion Authenticated users can call GET /api/v1/auth/token-info to inspect the claims embedded in their current bearer token (sub, iat, exp, and any extra claims such as roles or jti added by later branches). The endpoint reads the token from the Authorization header, delegates to JwtService.extractAllClaims(), and wraps the result in ApiResponse<Map<String,Object>>. If the token is expired, JwtService.extractAllClaims() throws ExpiredJwtException, which the new handler in GlobalExceptionHandler now maps to 401 instead of 500.
_09_: authenticated request with a fake token - jwtService.extractAllClaims() is mocked to return a minimal claims map, asserting that the response contains the sub and token_type fields. _10_: unauthenticated request returns 401, confirming the endpoint is behind the security filter chain.
…token-info into roles-permissions Conflict resolutions: - GlobalExceptionHandler: kept HEAD's handlers (InvalidTokenException 400, BadCredentialsException 401, AccessDeniedException 403, ObjectOptimisticLocking 409) and applied feature/auth changes: LockedException now returns 423, DisabledException now returns 403 (was 401 in both cases on this branch), ExpiredJwtException added. - GlobalExceptionHandlerTest: updated _06_ and _07_ assertions to expect 423 and 403 respectively; added _12_ for ExpiredJwtException; kept all HEAD-specific tests.
…ice, token-info into token-management Conflict resolutions: - JwtService.java: import conflict only - kept both java.util.UUID (HEAD, for jti) and java.util.Map (feature/roles-permissions, for extractAllClaims). - AuthController.java: kept all three fields (authService, userService, jwtService) and all endpoints (logout, token-info, activate, forgot-password, reset-password). Removed @Operation/@ApiResponses from token-info to match this branch's style. - JwtServiceTest.java: kept HEAD's _04_ (JwtException on tampered token, since isTokenValid was removed in SST adaptation); discarded feature/roles-permissions' _04_ (uses isTokenValid) and collapsed _05_ into the existing _04_. - AuthControllerTest.java: kept all HEAD tests (_08_ to _15_); renumbered token-info tests from _09_/_10_ to _16_/_17_ to avoid duplicate method names.
…erarchy All three existing token entities (ActivationToken, BlacklistedToken, PasswordResetToken) repeated the same id, user, token, and createdAt declarations. Extracting them into AuditableToken - a @MappedSuperclass - removes the duplication and gives every future token type (RefreshToken) the same baseline fields for free. Concretely: id (@id + @GeneratedValue), user (@manytoone lazy to AppUser), token (VARCHAR 768, covers JWTs), createdAt. Each subclass keeps only the fields that make it specific. @EqualsAndHashCode(of = "id") lives on the superclass so identity is always PK-based regardless of subclass.
Removes id, user, token, createdAt (now inherited). Switches @builder to @SuperBuilder so Lombok's builder chain works across the inheritance. @EqualsAndHashCode delegates to the superclass via callSuper = true.
Removes id, user, token, createdAt (now inherited). Switches @builder to @SuperBuilder. The token column keeps its VARCHAR 768 length via the superclass mapping, covering full JWT strings.
Removes id, user, token (now inherited). Adds createdAt via inheritance; the matching column is added in V11__add_created_at_to_password_reset_tokens. Keeps expiryDate (renamed from expiresAt at schema creation time) untouched.
… set user.setRoles(roles) discarded every existing role not listed in the request. user.getRoles().addAll(roles) preserves roles the user already has and only adds the new ones. To replace the full set, callers should call assignRoles for the desired set and use removeRoleFromUser for individual removals.
…les test Replaced .roles(Set.of(userRole)) with .build() + getRoles().add(userRole) so the roles collection is mutable. This is required by the additive assignRoles fix (B3): calling addAll on an immutable Set.of() would throw UnsupportedOperationException at test time (Hibernate always provides a mutable collection at runtime, so production code is unaffected). Added _04b_ to verify that assignRoles preserves existing roles and only adds new ones, contrasting with the previous replace-all behavior.
… _15_
LoginRequest.email is annotated @Email, so passing a bare username
("hopper", "babbage") triggered Bean Validation and returned 400 before
the AuthenticationManager was ever reached. The fix passes the full email
address, matching AuthServiceImpl.login() which looks up accounts by email.
…token-management
Conflict in SecurityAuthorizationTest._14_: HEAD already had enableUser()
and "Password1@"; incoming feature/auth only added the email fix
("hopper" -> "hopper@example.com") but kept the old password. Kept HEAD
(email already correct on this branch, password and enableUser preserved).
DisabledException now maps to 403 (added this session). Registering a user leaves enabled=false, so attempting login on a disabled account throws DisabledException before the password is ever checked. Added enableUser() so the authentication reaches BadCredentialsException -> 401 as the test expects.
V11 migration added created_at NOT NULL to password_reset_tokens. PasswordResetToken now extends AuditableToken which carries that column, but the repository test built tokens without setting createdAt, triggering a NOT NULL constraint violation at save time.
Brings in enableUser("babbage") fix for _15_ (DisabledException->403 now
fires before bad-password check on disabled accounts) and createdAt in
PasswordResetTokenRepositoryTest builders (V11 NOT NULL constraint).
No code conflicts: both files auto-merged cleanly.
The merge from feature/token-management brought in enableUser("babbage")
inside _18_ShouldReturn401_WhenPasswordIsWrong, but that helper method
exists only on the token-management version of the file. On
feature/user-management the same pattern is done inline (see _14_, _19_,
_20_). Replaced with the equivalent appUserRepository lambda so the class
compiles.
The setUp() creates appUser with enabled=false (intentional, for _08_ which tests account activation). The forgotPassword() guard added in item 05 skips disabled accounts, so _09_ never reached the email send. Fixed by setting enabled=true at the top of _09_ only.
…Response Renames the JSON field from "token" to "access_token" (OAuth 2.0 naming convention) and removes the username field. The username is already embedded as the "sub" claim inside the JWT, so returning it separately is redundant. The field is renamed accessToken in Java to match, with an explicit @JsonProperty("access_token") to keep the JSON contract.
…cess_token JSON path
Resolves 5 conflicts. AuthResponse on this branch gains the access_token rename and loses username (from feature/auth) while keeping refreshToken (introduced here). AuthServiceImpl.login() and refresh() now construct the 3-field record. All test assertions updated accordingly.
Adds expiresIn (JSON: expires_in) as the access token lifetime in seconds. The client uses this value to schedule a proactive token refresh before the access token expires, following the OAuth 2.0 token response convention. The value is computed by AuthServiceImpl from JwtService.getExpirationMs().
… in AuthServiceImplTest
…uthControllerTest
…_in and getExpirationMs() Resolves 2 conflicts. AuthServiceImpl.login() and refresh() now construct the 4-field AuthResponse (accessToken, tokenType, refreshToken, expiresIn) using generateToken(AppUser) from this branch and getExpirationMs()/1000 from feature/token-management. AuthServiceImplTest stubs getExpirationMs() and asserts the 4-field record. JwtService, AuthControllerTest and SecurityAuthorizationTest auto-merged cleanly.
Test _20_ (locked account) was still reading .path("data").path("token")
after the AuthResponse field was renamed to accessToken / access_token
(JSON). The token extracted was therefore empty, making the first
authenticated request return 401 instead of 200, failing the assertion
at line 241. Same fix already applied to _16_ on feature/token-management.
feat(user-management): refresh token system, security hardening, and role management
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.
Summary
Integrates all feature branches completed since the last master merge:
Test plan