-
Notifications
You must be signed in to change notification settings - Fork 41
Add Password Policy Functionality #217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@devondragon review is needed for this PR. Thank you. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds comprehensive password policy functionality to the Spring User Framework, implementing configurable password validation rules including length, character requirements, similarity checks, dictionary-based rejection, and password history tracking.
- Implemented PasswordPolicyService with configurable validation rules using Passay library
- Added password history tracking to prevent password reuse
- Integrated password policy validation into user registration flow
Reviewed Changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| PasswordPolicyService.java | Core service implementing password validation with configurable rules |
| PasswordHistoryEntry.java | Entity for tracking password history |
| PasswordHistoryRepository.java | Repository for password history operations |
| UserService.java | Added password history tracking integration |
| UserAPI.java | Integrated password policy validation in registration endpoint |
| dsspringuserconfig.properties | Added password policy configuration properties |
| dsspringusermessages.properties | Added password validation error messages |
| additional-spring-configuration-metadata.json | Added metadata for password policy properties |
| UserServiceTest.java | Updated test setup with password history repository |
| PasswordPolicyServiceTest.java | Comprehensive test suite for password policy service |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
src/test/java/com/digitalsanctuary/spring/user/service/UserServiceTest.java
Show resolved
Hide resolved
src/test/java/com/digitalsanctuary/spring/user/service/UserServiceTest.java
Outdated
Show resolved
Hide resolved
src/test/java/com/digitalsanctuary/spring/user/service/UserServiceTest.java
Outdated
Show resolved
Hide resolved
src/main/java/com/digitalsanctuary/spring/user/service/UserService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/digitalsanctuary/spring/user/service/UserService.java
Outdated
Show resolved
Hide resolved
src/main/resources/META-INF/additional-spring-configuration-metadata.json
Outdated
Show resolved
Hide resolved
|
@devondragon I have made a new commit that apply all request changes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/main/java/com/digitalsanctuary/spring/user/service/UserService.java:397
- The changeUserPassword method lacks integration with the new password policy validation and password history saving. This method should validate the new password and save it to history.
public void changeUserPassword(final User user, final String password) {
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| @Value("classpath:common_passwords.txt") | ||
| private Resource commonPasswordsResource; |
Copilot
AI
Sep 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded classpath reference to 'common_passwords.txt' may cause runtime failures if the file doesn't exist. This should be made configurable or the initialization should handle missing files gracefully.
| // when(messages.getMessage(anyString(), any(), any(Locale.class))) | ||
| // .thenAnswer(inv -> inv.getArgument(0, String.class)); |
Copilot
AI
Sep 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove commented-out code to improve code cleanliness.
| // when(messages.getMessage(anyString(), any(), any(Locale.class))) | |
| // .thenAnswer(inv -> inv.getArgument(0, String.class)); |
- Extract password validation logic into separate private methods for better maintainability - Add checkPasswordHistory() method for cleaner password history validation - Add checkPasswordSimilarity() method for username/email similarity checks - Add validateWithPassay() method to encapsulate Passay validation logic - Add buildPassayRules() and createSpecialCharacterRule() helper methods - Implement early returns for history and similarity checks to improve performance - Add Optional return types for null-safe error handling - Improve error logging with critical severity for missing common passwords file - Add debug logging for password rejection reasons These changes improve code readability, testability, and follow single responsibility principle by breaking down the large validate() method into focused helper methods. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Add historyCount field with @value annotation for configuration - Implement cleanUpPasswordHistory() method to remove old password entries - Automatically clean up entries beyond configured history count after saving - Add password history tracking to changeUserPassword() method - Prevent database bloat by limiting stored password history entries This ensures password history doesn't grow unbounded and maintains only the configured number of recent passwords for reuse checking. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Remove unnecessary quotes around special-chars property value - Fix escaping for backslash and quotes in allowed special characters - Ensures proper parsing of special characters configuration The previous format with surrounding quotes could cause parsing issues and prevent certain special characters from being recognized. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Correct validate() method call to include both password and username parameters - Previously passing null for username prevented similarity check from executing - Now properly tests password-to-username similarity validation This ensures the similarity check test actually validates the intended behavior. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Add @column annotations with proper constraints for passwordHash and entryDate - Set passwordHash max length to 255 characters (sufficient for bcrypt hashes) - Mark both passwordHash and entryDate as non-nullable fields - Add database indexes on user_id and entry_date for query performance - Improve query performance for findByUserOrderByEntryDateDesc operations These changes ensure data integrity at the database level and optimize password history lookups which are performed on every password change. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Add mock for PasswordPolicyService dependency - Ensures UserAPI unit tests can run with password policy integration - Prevents NullPointerException when UserAPI references passwordPolicyService Required for testing UserAPI with the new password policy validation feature. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Create PasswordPolicyServiceIntegrationTest to verify property loading - Test that special characters are correctly parsed from configuration - Validate all configured special characters work as expected - Ensure property file escaping is handled properly - Test with actual Spring context and property loading This integration test ensures the configuration properties are correctly formatted and that the password policy works with real application context, catching any property parsing issues that unit tests might miss. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Fix password history off-by-one error in cleanUpPasswordHistory() - Keep historyCount + 1 entries to ensure proper password reuse prevention - With historyCount=3, now keeps [current, prev1, prev2, prev3] - Prevents immediate reuse of the Nth previous password - Fix UserAPIUnitTest NPE from unstubbed PasswordPolicyService mock - Add stub to return empty list (no validation errors) for all registration tests - Add Collections import for the stub return value - Ensures tests don't fail with NullPointerException on .isEmpty() call These changes address critical issues identified in code review: 1. Password history was only preventing reuse of historyCount-1 passwords 2. Unit tests were failing due to unstubbed mock returning null 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 12 out of 14 changed files in this pull request and generated 3 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| # Require at least one special character | ||
| user.security.password.require-special=true | ||
| # Allowed special characters | ||
| user.security.password.special-chars=~`!@#$%^&*()_-+={}[]|\\:;"'<>,.?/ |
Copilot
AI
Sep 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The backslash in the special characters string should be escaped properly. Consider using a double backslash (\\) to ensure proper parsing, or document the expected behavior for this configuration value.
| user.security.password.special-chars=~`!@#$%^&*()_-+={}[]|\\:;"'<>,.?/ | |
| user.security.password.special-chars=~`!@#$%^&*()_-+={}[]|\\\\:;"'<>,.?/ |
| log.error("CRITICAL: Failed to load common passwords file. " + | ||
| "Common password checking is DISABLED. This is a security risk!", e); |
Copilot
AI
Sep 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In production environments, failing to load the common passwords dictionary is a significant security risk. Consider failing fast (throwing an exception) or using a fallback mechanism instead of silently continuing with reduced security.
| // Keep historyCount + 1 entries: the current password plus historyCount previous passwords | ||
| // This ensures we actually prevent reuse of the last historyCount passwords | ||
| int maxEntries = historyCount + 1; |
Copilot
AI
Sep 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for calculating maxEntries may be confusing. If historyCount=3 means 'prevent reuse of last 3 passwords', then keeping historyCount + 1 entries means keeping 4 total entries (current + 3 previous). Consider clarifying this behavior in the configuration documentation or adjusting the logic to match user expectations.
| // Keep historyCount + 1 entries: the current password plus historyCount previous passwords | |
| // This ensures we actually prevent reuse of the last historyCount passwords | |
| int maxEntries = historyCount + 1; | |
| // Keep the current password plus the last 'historyCount' previous passwords. | |
| // This ensures we prevent reuse of the last 'historyCount' previous passwords (excluding the current password). | |
| int maxEntries = historyCount + 1; // current password + historyCount previous passwords |
PR for issue #158