├── .github └── ISSUE_TEMPLATE │ └── issue_report.md ├── .gitignore ├── .project ├── CONTRIBUTING.md ├── License.md ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── baeldung │ │ ├── Application.java │ │ ├── captcha │ │ ├── AbstractCaptchaService.java │ │ ├── CaptchaService.java │ │ ├── CaptchaServiceV3.java │ │ ├── CaptchaSettings.java │ │ ├── GoogleResponse.java │ │ ├── ICaptchaService.java │ │ └── ReCaptchaAttemptService.java │ │ ├── persistence │ │ ├── dao │ │ │ ├── DeviceMetadataRepository.java │ │ │ ├── NewLocationTokenRepository.java │ │ │ ├── PasswordResetTokenRepository.java │ │ │ ├── PrivilegeRepository.java │ │ │ ├── RoleRepository.java │ │ │ ├── UserLocationRepository.java │ │ │ ├── UserRepository.java │ │ │ └── VerificationTokenRepository.java │ │ └── model │ │ │ ├── DeviceMetadata.java │ │ │ ├── NewLocationToken.java │ │ │ ├── PasswordResetToken.java │ │ │ ├── Privilege.java │ │ │ ├── Role.java │ │ │ ├── User.java │ │ │ ├── UserLocation.java │ │ │ └── VerificationToken.java │ │ ├── registration │ │ ├── OnRegistrationCompleteEvent.java │ │ └── listener │ │ │ └── RegistrationListener.java │ │ ├── security │ │ ├── ActiveUserStore.java │ │ ├── AuthenticationFailureListener.java │ │ ├── CustomAuthenticationFailureHandler.java │ │ ├── CustomRememberMeServices.java │ │ ├── ISecurityUserService.java │ │ ├── LoggedUser.java │ │ ├── LoginAttemptService.java │ │ ├── MyCustomLoginAuthenticationSuccessHandler.java │ │ ├── MyLogoutSuccessHandler.java │ │ ├── MySimpleUrlAuthenticationSuccessHandler.java │ │ ├── MyUserDetailsService.java │ │ ├── UserSecurityService.java │ │ ├── google2fa │ │ │ ├── CustomAuthenticationProvider.java │ │ │ ├── CustomWebAuthenticationDetails.java │ │ │ └── CustomWebAuthenticationDetailsSource.java │ │ └── location │ │ │ ├── DifferentLocationChecker.java │ │ │ ├── DifferentLocationLoginListener.java │ │ │ └── OnDifferentLocationLoginEvent.java │ │ ├── service │ │ ├── DeviceService.java │ │ ├── IUserService.java │ │ └── UserService.java │ │ ├── spring │ │ ├── AppConfig.java │ │ ├── CaptchaConfig.java │ │ ├── LoginNotificationConfig.java │ │ ├── MvcConfig.java │ │ ├── PersistenceJPAConfig.java │ │ ├── SecSecurityConfig.java │ │ ├── ServiceConfig.java │ │ ├── SetupDataLoader.java │ │ └── SpringTaskConfig.java │ │ ├── task │ │ └── TokensPurgeTask.java │ │ ├── validation │ │ ├── EmailExistsException.java │ │ ├── EmailValidator.java │ │ ├── PasswordConstraintValidator.java │ │ ├── PasswordMatches.java │ │ ├── PasswordMatchesValidator.java │ │ ├── UserValidator.java │ │ ├── ValidEmail.java │ │ └── ValidPassword.java │ │ └── web │ │ ├── controller │ │ ├── OldRegistrationController.java │ │ ├── RegistrationCaptchaController.java │ │ ├── RegistrationController.java │ │ ├── RegistrationRestController.java │ │ ├── RoleHierarchyController.java │ │ └── UserController.java │ │ ├── dto │ │ ├── PasswordDto.java │ │ └── UserDto.java │ │ ├── error │ │ ├── InvalidOldPasswordException.java │ │ ├── ReCaptchaInvalidException.java │ │ ├── ReCaptchaUnavailableException.java │ │ ├── RestResponseEntityExceptionHandler.java │ │ ├── UnusualLocationException.java │ │ ├── UserAlreadyExistException.java │ │ └── UserNotFoundException.java │ │ └── util │ │ └── GenericResponse.java ├── resources │ ├── .gitignore │ ├── application-dev.properties │ ├── application.properties │ ├── logback.xml │ ├── maxmind │ │ ├── COPYRIGHT.txt │ │ ├── GeoLite2-City.mmdb │ │ ├── GeoLite2-Country.mmdb │ │ └── LICENSE.txt │ ├── messages_en.properties │ ├── messages_es_ES.properties │ ├── templates │ │ ├── admin.html │ │ ├── badUser.html │ │ ├── changePassword.html │ │ ├── console.html │ │ ├── customLogin.html │ │ ├── emailError.html │ │ ├── expiredAccount.html │ │ ├── forgetPassword.html │ │ ├── home.html │ │ ├── homepage.html │ │ ├── invalidSession.html │ │ ├── login.html │ │ ├── loginRememberMe.html │ │ ├── logout.html │ │ ├── qrcode.html │ │ ├── registration.html │ │ ├── registrationCaptcha.html │ │ ├── registrationConfirm.html │ │ ├── registrationReCaptchaV3.html │ │ ├── roleHierarchy.html │ │ ├── successRegister.html │ │ ├── updatePassword.html │ │ └── users.html │ ├── webSecurityConfig-basic.xml │ └── webSecurityConfig.xml └── webapp │ └── resources │ ├── bootstrap.css │ └── pwstrength.js └── test ├── java └── com │ └── baeldung │ ├── spring │ ├── ConfigTest.java │ ├── TestDbConfig.java │ ├── TestIntegrationConfig.java │ └── TestTaskConfig.java │ └── test │ ├── ChangePasswordIntegrationTest.java │ ├── DeviceServiceIntegrationTest.java │ ├── GetLoggedUsersIntegrationTest.java │ ├── IntegrationSuite.java │ ├── LocalizationIntegrationTest.java │ ├── LockAccountAfterSeveralTriesIntegrationTest.java │ ├── RegistrationControllerIntegrationTest.java │ ├── RegistrationPasswordLiveTest.java │ ├── SpringSecurityRolesIntegrationTest.java │ ├── TokenExpirationIntegrationTest.java │ ├── UserIntegrationTest.java │ └── UserServiceIntegrationTest.java └── resources ├── logback-test.xml └── maxmind └── GeoLite2-City.mmdb /.github/ISSUE_TEMPLATE/issue_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue Report 3 | about: Report an issue to help us improve 4 | title: '[ISSUE] ' 5 | --- 6 | 7 | **Article and Module Links** 8 | A link to the affected article and the affected module. You can find the link to the module in the Conclusion section in the "on Github" standard phase. 9 | 10 | **Describe the Issue** 11 | A clear and concise description of what the issue is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected Behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional Context** 32 | Add any other context about the issue here. 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mobile Tools for Java (J2ME) 2 | .mtj.tmp/ 3 | 4 | # Package Files # 5 | *.jar 6 | *.war 7 | *.ear 8 | 9 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 10 | hs_err_pid* 11 | 12 | target/ 13 | 14 | # Eclipse 15 | .settings/ 16 | *.project 17 | *.classpath 18 | .prefs 19 | *.prefs 20 | .metadata/ 21 | .factorypath 22 | 23 | # Intellij 24 | .idea/ 25 | *.iml 26 | *.iws 27 | /.apt_generated_tests/ 28 | 29 | **/META-INF/maven 30 | **/META-INF/MANIFEST.MF 31 | 32 | *.log -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | spring-security-login-and-registration 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.wst.jsdt.core.javascriptValidator 10 | 11 | 12 | 13 | 14 | org.eclipse.jdt.core.javabuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.wst.common.project.facet.core.builder 20 | 21 | 22 | 23 | 24 | org.springframework.ide.eclipse.core.springbuilder 25 | 26 | 27 | 28 | 29 | org.eclipse.wst.validation.validationbuilder 30 | 31 | 32 | 33 | 34 | org.eclipse.m2e.core.maven2Builder 35 | 36 | 37 | 38 | 39 | 40 | org.eclipse.jem.workbench.JavaEMFNature 41 | org.eclipse.wst.common.modulecore.ModuleCoreNature 42 | org.springframework.ide.eclipse.core.springnature 43 | org.eclipse.jdt.core.javanature 44 | org.eclipse.m2e.core.maven2Nature 45 | org.eclipse.wst.common.project.facet.core.nature 46 | org.eclipse.wst.jsdt.core.jsNature 47 | 48 | 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Baeldung Tutorials 2 | First off, thank you for considering contributing to Baeldung Tutorials. 3 | 4 | ## Reporting Issues 5 | Before you submit an issue, please review the guidelines below: 6 | 7 | 1. **No Custom Modifications:** If your issue arises from any custom modifications you've made to the code in the repository, we won't be able to assist. We can only help if the issue is reproducible with the untouched codebase from this repo. If you're working with a modified version, consider asking for help on StackOverflow or other relevant forums. 8 | 2. **Use a clear and descriptive title** for the issue to identify the problem. 9 | 3. **Include a link to the article** you're having issues with. 10 | 4. **Describe the exact steps which reproduce the problem** in as many details as possible. 11 | 5. **Additional Details:** Offer any other context or descriptions that could be useful. Screenshots, error messages, copy/pasteable snippets, or logs can be immensely helpful. -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Baeldung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Login and Registration Example Project with Spring Security 2 | If you're already a student of Learn Spring Security, you can get started diving deeper into registration with Module 2
3 | If you're not yet a student, you can get access to the course here: https://bit.ly/github-lss 4 |


5 | 6 | 7 | ### Relevant Articles: 8 | - [Spring Security Registration Series](https://www.baeldung.com/spring-security-registration) 9 | - [The Registration Process With Spring Security](https://www.baeldung.com/registration-with-spring-mvc-and-spring-security) 10 | - [Registration – Activate a New Account by Email](https://www.baeldung.com/registration-verify-user-by-email) 11 | - [Registration with Spring Security – Password Encoding](https://www.baeldung.com/spring-security-registration-password-encoding-bcrypt) 12 | - [Spring Security – Roles and Privileges](https://www.baeldung.com/role-and-privilege-for-spring-security-registration) 13 | - [Prevent Brute Force Authentication Attempts with Spring Security](https://www.baeldung.com/spring-security-block-brute-force-authentication-attempts) 14 | - [Spring Security – Reset Your Password](https://www.baeldung.com/spring-security-registration-i-forgot-my-password) 15 | - [Spring Security Registration – Resend Verification Email](https://www.baeldung.com/spring-security-registration-verification-email) 16 | - [The Registration API becomes RESTful](https://www.baeldung.com/registration-restful-api) 17 | - [Registration – Password Strength and Rules](https://www.baeldung.com/registration-password-strength-and-rules) 18 | - [Updating Your Password](https://www.baeldung.com/updating-your-password) 19 | - [Two Factor Auth with Spring Security](https://www.baeldung.com/spring-security-two-factor-authentication-with-soft-token) 20 | - [Registration with Spring – Integrate reCAPTCHA](https://www.baeldung.com/spring-security-registration-captcha) 21 | - [Purging Expired Tokens Generated by the Registration](https://www.baeldung.com/registration-token-cleanup) 22 | - [Custom Login Page for Returning User](https://www.baeldung.com/custom-login-page-for-returning-user) 23 | - [Allow Authentication from Accepted Locations Only with Spring Security](https://www.baeldung.com/spring-security-restrict-authentication-by-geography) 24 | - [Spring Security – Auto Login User After Registration](https://www.baeldung.com/spring-security-auto-login-user-after-registration) 25 | - [Keep Track of Logged in Users With Spring Security](https://www.baeldung.com/spring-security-track-logged-in-users) 26 | - [Login for a Spring Web App – Error Handling and Localization](https://www.baeldung.com/spring-security-login-error-handling-localization) 27 | - [Notify User of Login From New Device or Location](https://www.baeldung.com/spring-security-login-new-device-location) 28 | - [Preventing Username Enumeration Attacks with Spring Security](https://www.baeldung.com/spring-security-enumeration-attacks) 29 | 30 | 31 | ### Build and Deploy the Project 32 | ``` 33 | mvn clean install 34 | ``` 35 | 36 | This is a Spring Boot project, so you can deploy it by simply using the main class: `Application.java` 37 | 38 | Once deployed, you can access the app at: 39 | 40 | https://localhost:8081 41 | 42 | 43 | ### Set up MySQL 44 | By default, the project is configured to use the embedded H2 database. 45 | If you want to use the MySQL instead, you need to uncomment relevant section in the [application.properties](src/main/resources/application.properties) and create the db user as shown below: 46 | ``` 47 | mysql -u root -p 48 | > CREATE USER 'tutorialuser'@'localhost' IDENTIFIED BY 'tutorialmy5ql'; 49 | > GRANT ALL PRIVILEGES ON *.* TO 'tutorialuser'@'localhost'; 50 | > FLUSH PRIVILEGES; 51 | ``` 52 | 53 | 54 | ### Set up Email 55 | 56 | You need to configure the email by providing your own username and password in application.properties 57 | You also need to use your own host, you can use Amazon or Google for example. 58 | 59 | ### AuthenticationSuccessHandler configuration for Custom Login Page article 60 | If you want to activate the configuration for the article [Custom Login Page for Returning User](https://www.baeldung.com/custom-login-page-for-returning-user), then you need to comment the @Component("myAuthenticationSuccessHandler") annotation in the MySimpleUrlAuthenticationSuccessHandler and uncomment the same in MyCustomLoginAuthenticationSuccessHandler. 61 | 62 | ### Feature toggle for Geo IP Lib 63 | The geolocation checks do not work for the IP addresses 127.0.0.1 and 0.0.0.0, 64 | which can be a problem when running the application locally or in a test environment. 65 | To enable/disable the check on the geolocation, set the property `geo.ip.lib.enabled` to true/false; this is false by default. 66 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/Application.java: -------------------------------------------------------------------------------- 1 | package com.baeldung; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.web.context.request.RequestContextListener; 8 | 9 | @SpringBootApplication 10 | public class Application extends SpringBootServletInitializer { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(Application.class, args); 14 | } 15 | 16 | @Bean 17 | public RequestContextListener requestContextListener() { 18 | return new RequestContextListener(); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/captcha/AbstractCaptchaService.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.captcha; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import jakarta.servlet.http.HttpServletRequest; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.util.StringUtils; 11 | import org.springframework.web.client.RestOperations; 12 | 13 | import com.baeldung.web.error.ReCaptchaInvalidException; 14 | 15 | public abstract class AbstractCaptchaService implements ICaptchaService{ 16 | 17 | private final static Logger LOGGER = LoggerFactory.getLogger(AbstractCaptchaService.class); 18 | 19 | @Autowired 20 | protected HttpServletRequest request; 21 | 22 | @Autowired 23 | protected CaptchaSettings captchaSettings; 24 | 25 | @Autowired 26 | protected ReCaptchaAttemptService reCaptchaAttemptService; 27 | 28 | @Autowired 29 | protected RestOperations restTemplate; 30 | 31 | protected static final Pattern RESPONSE_PATTERN = Pattern.compile("[A-Za-z0-9_-]+"); 32 | 33 | protected static final String RECAPTCHA_URL_TEMPLATE = "https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s"; 34 | 35 | @Override 36 | public String getReCaptchaSite() { 37 | return captchaSettings.getSite(); 38 | } 39 | 40 | @Override 41 | public String getReCaptchaSecret() { 42 | return captchaSettings.getSecret(); 43 | } 44 | 45 | 46 | protected void securityCheck(final String response) { 47 | LOGGER.debug("Attempting to validate response {}", response); 48 | 49 | if (reCaptchaAttemptService.isBlocked(getClientIP())) { 50 | throw new ReCaptchaInvalidException("Client exceeded maximum number of failed attempts"); 51 | } 52 | 53 | if (!responseSanityCheck(response)) { 54 | throw new ReCaptchaInvalidException("Response contains invalid characters"); 55 | } 56 | } 57 | 58 | protected boolean responseSanityCheck(final String response) { 59 | return StringUtils.hasLength(response) && RESPONSE_PATTERN.matcher(response).matches(); 60 | } 61 | 62 | protected String getClientIP() { 63 | final String xfHeader = request.getHeader("X-Forwarded-For"); 64 | if (xfHeader == null || xfHeader.isEmpty() || !xfHeader.contains(request.getRemoteAddr())) { 65 | return request.getRemoteAddr(); 66 | } 67 | return xfHeader.split(",")[0]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/captcha/CaptchaService.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.captcha; 2 | 3 | import java.net.URI; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.client.RestClientException; 9 | 10 | import com.baeldung.web.error.ReCaptchaInvalidException; 11 | import com.baeldung.web.error.ReCaptchaUnavailableException; 12 | 13 | @Service("captchaService") 14 | public class CaptchaService extends AbstractCaptchaService { 15 | 16 | private final static Logger LOGGER = LoggerFactory.getLogger(CaptchaService.class); 17 | 18 | @Override 19 | public void processResponse(final String response) { 20 | securityCheck(response); 21 | 22 | final URI verifyUri = URI.create(String.format(RECAPTCHA_URL_TEMPLATE, getReCaptchaSecret(), response, getClientIP())); 23 | try { 24 | final GoogleResponse googleResponse = restTemplate.getForObject(verifyUri, GoogleResponse.class); 25 | LOGGER.debug("Google's response: {} ", googleResponse.toString()); 26 | 27 | if (!googleResponse.isSuccess()) { 28 | if (googleResponse.hasClientError()) { 29 | reCaptchaAttemptService.reCaptchaFailed(getClientIP()); 30 | } 31 | throw new ReCaptchaInvalidException("reCaptcha was not successfully validated"); 32 | } 33 | } catch (RestClientException rce) { 34 | throw new ReCaptchaUnavailableException("Registration unavailable at this time. Please try again later.", rce); 35 | } 36 | reCaptchaAttemptService.reCaptchaSucceeded(getClientIP()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/captcha/CaptchaServiceV3.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.captcha; 2 | 3 | import java.net.URI; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.client.RestClientException; 9 | 10 | import com.baeldung.web.error.ReCaptchaInvalidException; 11 | import com.baeldung.web.error.ReCaptchaUnavailableException; 12 | 13 | @Service("captchaServiceV3") 14 | public class CaptchaServiceV3 extends AbstractCaptchaService { 15 | 16 | private final static Logger LOGGER = LoggerFactory.getLogger(CaptchaServiceV3.class); 17 | 18 | public static final String REGISTER_ACTION = "register"; 19 | 20 | @Override 21 | public void processResponse(String response, final String action) throws ReCaptchaInvalidException { 22 | securityCheck(response); 23 | 24 | final URI verifyUri = URI.create(String.format(RECAPTCHA_URL_TEMPLATE, getReCaptchaSecret(), response, getClientIP())); 25 | try { 26 | final GoogleResponse googleResponse = restTemplate.getForObject(verifyUri, GoogleResponse.class); 27 | LOGGER.debug("Google's response: {} ", googleResponse.toString()); 28 | 29 | if (!googleResponse.isSuccess() || !googleResponse.getAction().equals(action) || googleResponse.getScore() < captchaSettings.getThreshold()) { 30 | if (googleResponse.hasClientError()) { 31 | reCaptchaAttemptService.reCaptchaFailed(getClientIP()); 32 | } 33 | throw new ReCaptchaInvalidException("reCaptcha was not successfully validated"); 34 | } 35 | } catch (RestClientException rce) { 36 | throw new ReCaptchaUnavailableException("Registration unavailable at this time. Please try again later.", rce); 37 | } 38 | reCaptchaAttemptService.reCaptchaSucceeded(getClientIP()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/captcha/CaptchaSettings.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.captcha; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | @ConfigurationProperties(prefix = "google.recaptcha.key") 8 | public class CaptchaSettings { 9 | 10 | private String site; 11 | private String secret; 12 | 13 | //reCAPTCHA V3 14 | private String siteV3; 15 | private String secretV3; 16 | private float threshold; 17 | 18 | public CaptchaSettings() { 19 | } 20 | 21 | public String getSite() { 22 | return site; 23 | } 24 | 25 | public void setSite(String site) { 26 | this.site = site; 27 | } 28 | 29 | public String getSecret() { 30 | return secret; 31 | } 32 | 33 | public void setSecret(String secret) { 34 | this.secret = secret; 35 | } 36 | 37 | public String getSiteV3() { 38 | return siteV3; 39 | } 40 | 41 | public void setSiteV3(String siteV3) { 42 | this.siteV3 = siteV3; 43 | } 44 | 45 | public String getSecretV3() { 46 | return secretV3; 47 | } 48 | 49 | public void setSecretV3(String secretV3) { 50 | this.secretV3 = secretV3; 51 | } 52 | 53 | public float getThreshold() { 54 | return threshold; 55 | } 56 | 57 | public void setThreshold(float threshold) { 58 | this.threshold = threshold; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/captcha/GoogleResponse.java: -------------------------------------------------------------------------------- 1 | 2 | package com.baeldung.captcha; 3 | 4 | import com.fasterxml.jackson.annotation.*; 5 | 6 | import java.util.Arrays; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | @JsonPropertyOrder({ "success", "score", "action","challenge_ts", "hostname", "error-codes" }) 13 | public class GoogleResponse { 14 | 15 | @JsonProperty("success") 16 | private boolean success; 17 | @JsonProperty("challenge_ts") 18 | private String challengeTimeStamp; 19 | @JsonProperty("hostname") 20 | private String hostname; 21 | @JsonProperty("score") 22 | private float score; 23 | @JsonProperty("action") 24 | private String action; 25 | @JsonProperty("error-codes") 26 | private ErrorCode[] errorCodes; 27 | 28 | 29 | enum ErrorCode { 30 | MissingSecret, InvalidSecret, MissingResponse, InvalidResponse, BadRequest, TimeoutOrDuplicate; 31 | 32 | private static Map errorsMap = new HashMap<>(6); 33 | 34 | static { 35 | errorsMap.put("missing-input-secret", MissingSecret); 36 | errorsMap.put("invalid-input-secret", InvalidSecret); 37 | errorsMap.put("missing-input-response", MissingResponse); 38 | errorsMap.put("invalid-input-response", InvalidResponse); 39 | errorsMap.put("bad-request", BadRequest); 40 | errorsMap.put("timeout-or-duplicate", TimeoutOrDuplicate); 41 | } 42 | 43 | @JsonCreator 44 | public static ErrorCode forValue(final String value) { 45 | return errorsMap.get(value.toLowerCase()); 46 | } 47 | } 48 | 49 | @JsonProperty("success") 50 | public boolean isSuccess() { 51 | return success; 52 | } 53 | 54 | @JsonProperty("success") 55 | public void setSuccess(boolean success) { 56 | this.success = success; 57 | } 58 | 59 | @JsonProperty("challenge_ts") 60 | public String getChallengeTimeStamp() { 61 | return challengeTimeStamp; 62 | } 63 | 64 | @JsonProperty("challenge_ts") 65 | public void setChallengeTimeStamp(String challengeTimeStamp) { 66 | this.challengeTimeStamp = challengeTimeStamp; 67 | } 68 | 69 | @JsonProperty("hostname") 70 | public String getHostname() { 71 | return hostname; 72 | } 73 | 74 | @JsonProperty("hostname") 75 | public void setHostname(String hostname) { 76 | this.hostname = hostname; 77 | } 78 | 79 | @JsonProperty("error-codes") 80 | public void setErrorCodes(ErrorCode[] errorCodes) { 81 | this.errorCodes = errorCodes; 82 | } 83 | 84 | @JsonProperty("error-codes") 85 | public ErrorCode[] getErrorCodes() { 86 | return errorCodes; 87 | } 88 | 89 | @JsonProperty("score") 90 | public float getScore() { 91 | return score; 92 | } 93 | 94 | @JsonProperty("score") 95 | public void setScore(float score) { 96 | this.score = score; 97 | } 98 | 99 | @JsonProperty("action") 100 | public String getAction() { 101 | return action; 102 | } 103 | 104 | @JsonProperty("action") 105 | public void setAction(String action) { 106 | this.action = action; 107 | } 108 | 109 | @JsonIgnore 110 | public boolean hasClientError() { 111 | final ErrorCode[] errors = getErrorCodes(); 112 | if (errors == null) { 113 | return false; 114 | } 115 | for (final ErrorCode error : errors) { 116 | switch (error) { 117 | case InvalidResponse: 118 | case MissingResponse: 119 | case BadRequest: 120 | return true; 121 | default: 122 | break; 123 | } 124 | } 125 | return false; 126 | } 127 | 128 | @Override 129 | public String toString() { 130 | return "GoogleResponse{" + "success=" + success + ", challengeTs='" + challengeTimeStamp + '\'' + ", hostname='" + hostname + '\''+ ", score='" + score + '\''+ ", action='" + action+ '\'' + ", errorCodes=" + Arrays.toString(errorCodes) + '}'; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/captcha/ICaptchaService.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.captcha; 2 | 3 | import com.baeldung.web.error.ReCaptchaInvalidException; 4 | 5 | public interface ICaptchaService { 6 | 7 | default void processResponse(final String response) throws ReCaptchaInvalidException {} 8 | 9 | default void processResponse(final String response, String action) throws ReCaptchaInvalidException {} 10 | 11 | String getReCaptchaSite(); 12 | 13 | String getReCaptchaSecret(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/captcha/ReCaptchaAttemptService.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.captcha; 2 | 3 | import com.google.common.cache.CacheBuilder; 4 | import com.google.common.cache.CacheLoader; 5 | import com.google.common.cache.LoadingCache; 6 | import org.springframework.stereotype.Service; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | @Service("reCaptchaAttemptService") 10 | public class ReCaptchaAttemptService { 11 | private final int MAX_ATTEMPT = 4; 12 | private LoadingCache attemptsCache; 13 | 14 | public ReCaptchaAttemptService() { 15 | attemptsCache = CacheBuilder.newBuilder().expireAfterWrite(4, TimeUnit.HOURS).build(new CacheLoader() { 16 | @Override 17 | public Integer load(final String key) { 18 | return 0; 19 | } 20 | }); 21 | } 22 | 23 | public void reCaptchaSucceeded(final String key) { 24 | attemptsCache.invalidate(key); 25 | } 26 | 27 | public void reCaptchaFailed(final String key) { 28 | int attempts = attemptsCache.getUnchecked(key); 29 | attempts++; 30 | attemptsCache.put(key, attempts); 31 | } 32 | 33 | public boolean isBlocked(final String key) { 34 | return attemptsCache.getUnchecked(key) >= MAX_ATTEMPT; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/dao/DeviceMetadataRepository.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.dao; 2 | 3 | import com.baeldung.persistence.model.DeviceMetadata; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface DeviceMetadataRepository extends JpaRepository { 9 | 10 | List findByUserId(Long userId); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/dao/NewLocationTokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.dao; 2 | 3 | import com.baeldung.persistence.model.NewLocationToken; 4 | import com.baeldung.persistence.model.UserLocation; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface NewLocationTokenRepository extends JpaRepository { 8 | 9 | NewLocationToken findByToken(String token); 10 | 11 | NewLocationToken findByUserLocation(UserLocation userLocation); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/dao/PasswordResetTokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.dao; 2 | 3 | import com.baeldung.persistence.model.PasswordResetToken; 4 | import com.baeldung.persistence.model.User; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Modifying; 7 | import org.springframework.data.jpa.repository.Query; 8 | 9 | import java.util.Date; 10 | import java.util.stream.Stream; 11 | 12 | public interface PasswordResetTokenRepository extends JpaRepository { 13 | 14 | PasswordResetToken findByToken(String token); 15 | 16 | PasswordResetToken findByUser(User user); 17 | 18 | Stream findAllByExpiryDateLessThan(Date now); 19 | 20 | void deleteByExpiryDateLessThan(Date now); 21 | 22 | @Modifying 23 | @Query("delete from PasswordResetToken t where t.expiryDate <= ?1") 24 | void deleteAllExpiredSince(Date now); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/dao/PrivilegeRepository.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.dao; 2 | 3 | import com.baeldung.persistence.model.Privilege; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface PrivilegeRepository extends JpaRepository { 7 | 8 | Privilege findByName(String name); 9 | 10 | @Override 11 | void delete(Privilege privilege); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/dao/RoleRepository.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.dao; 2 | 3 | import com.baeldung.persistence.model.Role; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface RoleRepository extends JpaRepository { 7 | 8 | Role findByName(String name); 9 | 10 | @Override 11 | void delete(Role role); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/dao/UserLocationRepository.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.dao; 2 | 3 | import com.baeldung.persistence.model.User; 4 | import com.baeldung.persistence.model.UserLocation; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface UserLocationRepository extends JpaRepository { 8 | UserLocation findByCountryAndUser(String country, User user); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/dao/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.dao; 2 | 3 | import com.baeldung.persistence.model.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface UserRepository extends JpaRepository { 7 | User findByEmail(String email); 8 | 9 | @Override 10 | void delete(User user); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/dao/VerificationTokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.dao; 2 | 3 | import com.baeldung.persistence.model.VerificationToken; 4 | import com.baeldung.persistence.model.User; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Modifying; 7 | import org.springframework.data.jpa.repository.Query; 8 | 9 | import java.util.Date; 10 | import java.util.stream.Stream; 11 | 12 | public interface VerificationTokenRepository extends JpaRepository { 13 | 14 | VerificationToken findByToken(String token); 15 | 16 | VerificationToken findByUser(User user); 17 | 18 | Stream findAllByExpiryDateLessThan(Date now); 19 | 20 | void deleteByExpiryDateLessThan(Date now); 21 | 22 | @Modifying 23 | @Query("delete from VerificationToken t where t.expiryDate <= ?1") 24 | void deleteAllExpiredSince(Date now); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/model/DeviceMetadata.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.model; 2 | 3 | import jakarta.persistence.*; 4 | import java.util.Date; 5 | import java.util.Objects; 6 | 7 | @Entity 8 | public class DeviceMetadata { 9 | 10 | @Id 11 | @GeneratedValue(strategy = GenerationType.AUTO) 12 | private Long id; 13 | private Long userId; 14 | private String deviceDetails; 15 | private String location; 16 | private Date lastLoggedIn; 17 | 18 | public Long getId() { 19 | return id; 20 | } 21 | 22 | public void setId(Long id) { 23 | this.id = id; 24 | } 25 | 26 | public Long getUserId() { 27 | return userId; 28 | } 29 | 30 | public void setUserId(Long userId) { 31 | this.userId = userId; 32 | } 33 | 34 | public String getDeviceDetails() { 35 | return deviceDetails; 36 | } 37 | 38 | public void setDeviceDetails(String deviceDetails) { 39 | this.deviceDetails = deviceDetails; 40 | } 41 | 42 | public String getLocation() { 43 | return location; 44 | } 45 | 46 | public void setLocation(String location) { 47 | this.location = location; 48 | } 49 | 50 | public Date getLastLoggedIn() { 51 | return lastLoggedIn; 52 | } 53 | 54 | public void setLastLoggedIn(Date lastLoggedIn) { 55 | this.lastLoggedIn = lastLoggedIn; 56 | } 57 | 58 | @Override 59 | public boolean equals(Object o) { 60 | if (this == o) return true; 61 | if (o == null || getClass() != o.getClass()) return false; 62 | DeviceMetadata that = (DeviceMetadata) o; 63 | return Objects.equals(getId(), that.getId()) && 64 | Objects.equals(getUserId(), that.getUserId()) && 65 | Objects.equals(getDeviceDetails(), that.getDeviceDetails()) && 66 | Objects.equals(getLocation(), that.getLocation()) && 67 | Objects.equals(getLastLoggedIn(), that.getLastLoggedIn()); 68 | } 69 | 70 | @Override 71 | public int hashCode() { 72 | return Objects.hash(getId(), getUserId(), getDeviceDetails(), getLocation(), getLastLoggedIn()); 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | final StringBuilder sb = new StringBuilder("DeviceMetadata{"); 78 | sb.append("id=").append(id); 79 | sb.append(", userId=").append(userId); 80 | sb.append(", deviceDetails='").append(deviceDetails).append('\''); 81 | sb.append(", location='").append(location).append('\''); 82 | sb.append(", lastLoggedIn=").append(lastLoggedIn); 83 | sb.append('}'); 84 | return sb.toString(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/model/NewLocationToken.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.model; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.FetchType; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.JoinColumn; 9 | import jakarta.persistence.OneToOne; 10 | 11 | @Entity 12 | public class NewLocationToken { 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.AUTO) 15 | private Long id; 16 | 17 | private String token; 18 | 19 | @OneToOne(targetEntity = UserLocation.class, fetch = FetchType.EAGER) 20 | @JoinColumn(nullable = false, name = "user_location_id") 21 | private UserLocation userLocation; 22 | 23 | // 24 | 25 | public NewLocationToken() { 26 | super(); 27 | } 28 | 29 | public NewLocationToken(final String token) { 30 | super(); 31 | this.token = token; 32 | } 33 | 34 | public NewLocationToken(final String token, final UserLocation userLocation) { 35 | super(); 36 | this.token = token; 37 | this.userLocation = userLocation; 38 | } 39 | 40 | // 41 | 42 | public Long getId() { 43 | return id; 44 | } 45 | 46 | public void setId(Long id) { 47 | this.id = id; 48 | } 49 | 50 | public String getToken() { 51 | return token; 52 | } 53 | 54 | public void setToken(String token) { 55 | this.token = token; 56 | } 57 | 58 | public UserLocation getUserLocation() { 59 | return userLocation; 60 | } 61 | 62 | public void setUserLocation(UserLocation userLocation) { 63 | this.userLocation = userLocation; 64 | } 65 | 66 | @Override 67 | public int hashCode() { 68 | final int prime = 31; 69 | int result = 1; 70 | result = (prime * result) + ((getId() == null) ? 0 : getId().hashCode()); 71 | result = (prime * result) + ((getToken() == null) ? 0 : getToken().hashCode()); 72 | result = (prime * result) + ((getUserLocation() == null) ? 0 : getUserLocation().hashCode()); 73 | return result; 74 | } 75 | 76 | @Override 77 | public boolean equals(Object obj) { 78 | if (this == obj) { 79 | return true; 80 | } 81 | if (obj == null) { 82 | return false; 83 | } 84 | if (getClass() != obj.getClass()) { 85 | return false; 86 | } 87 | final NewLocationToken other = (NewLocationToken) obj; 88 | if (getId() == null) { 89 | if (other.getId() != null) { 90 | return false; 91 | } 92 | } else if (!getId().equals(other.getId())) { 93 | return false; 94 | } 95 | if (getToken() == null) { 96 | if (other.getToken() != null) { 97 | return false; 98 | } 99 | } else if (!getToken().equals(other.getToken())) { 100 | return false; 101 | } 102 | if (getUserLocation() == null) { 103 | if (other.getUserLocation() != null) { 104 | return false; 105 | } 106 | } else if (!getUserLocation().equals(other.getUserLocation())) { 107 | return false; 108 | } 109 | return true; 110 | } 111 | 112 | @Override 113 | public String toString() { 114 | return "NewLocationToken [id=" + id + ", token=" + token + ", userLocation=" + userLocation + "]"; 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/model/PasswordResetToken.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.model; 2 | 3 | import java.util.Calendar; 4 | import java.util.Date; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.FetchType; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.JoinColumn; 12 | import jakarta.persistence.OneToOne; 13 | 14 | @Entity 15 | public class PasswordResetToken { 16 | 17 | private static final int EXPIRATION = 60 * 24; 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.AUTO) 21 | private Long id; 22 | 23 | private String token; 24 | 25 | @OneToOne(targetEntity = User.class, fetch = FetchType.EAGER) 26 | @JoinColumn(nullable = false, name = "user_id") 27 | private User user; 28 | 29 | private Date expiryDate; 30 | 31 | public PasswordResetToken() { 32 | super(); 33 | } 34 | 35 | public PasswordResetToken(final String token) { 36 | super(); 37 | 38 | this.token = token; 39 | this.expiryDate = calculateExpiryDate(EXPIRATION); 40 | } 41 | 42 | public PasswordResetToken(final String token, final User user) { 43 | super(); 44 | 45 | this.token = token; 46 | this.user = user; 47 | this.expiryDate = calculateExpiryDate(EXPIRATION); 48 | } 49 | 50 | // 51 | public Long getId() { 52 | return id; 53 | } 54 | 55 | public String getToken() { 56 | return token; 57 | } 58 | 59 | public void setToken(final String token) { 60 | this.token = token; 61 | } 62 | 63 | public User getUser() { 64 | return user; 65 | } 66 | 67 | public void setUser(final User user) { 68 | this.user = user; 69 | } 70 | 71 | public Date getExpiryDate() { 72 | return expiryDate; 73 | } 74 | 75 | public void setExpiryDate(final Date expiryDate) { 76 | this.expiryDate = expiryDate; 77 | } 78 | 79 | private Date calculateExpiryDate(final int expiryTimeInMinutes) { 80 | final Calendar cal = Calendar.getInstance(); 81 | cal.setTimeInMillis(new Date().getTime()); 82 | cal.add(Calendar.MINUTE, expiryTimeInMinutes); 83 | return new Date(cal.getTime().getTime()); 84 | } 85 | 86 | public void updateToken(final String token) { 87 | this.token = token; 88 | this.expiryDate = calculateExpiryDate(EXPIRATION); 89 | } 90 | 91 | // 92 | 93 | @Override 94 | public int hashCode() { 95 | final int prime = 31; 96 | int result = 1; 97 | result = prime * result + ((getExpiryDate() == null) ? 0 : getExpiryDate().hashCode()); 98 | result = prime * result + ((getToken() == null) ? 0 : getToken().hashCode()); 99 | result = prime * result + ((getUser() == null) ? 0 : getUser().hashCode()); 100 | return result; 101 | } 102 | 103 | @Override 104 | public boolean equals(final Object obj) { 105 | if (this == obj) { 106 | return true; 107 | } 108 | if (obj == null) { 109 | return false; 110 | } 111 | if (getClass() != obj.getClass()) { 112 | return false; 113 | } 114 | final PasswordResetToken other = (PasswordResetToken) obj; 115 | if (getExpiryDate() == null) { 116 | if (other.getExpiryDate() != null) { 117 | return false; 118 | } 119 | } else if (!getExpiryDate().equals(other.getExpiryDate())) { 120 | return false; 121 | } 122 | if (getToken() == null) { 123 | if (other.getToken() != null) { 124 | return false; 125 | } 126 | } else if (!getToken().equals(other.getToken())) { 127 | return false; 128 | } 129 | if (getUser() == null) { 130 | if (other.getUser() != null) { 131 | return false; 132 | } 133 | } else if (!getUser().equals(other.getUser())) { 134 | return false; 135 | } 136 | return true; 137 | } 138 | 139 | @Override 140 | public String toString() { 141 | final StringBuilder builder = new StringBuilder(); 142 | builder.append("Token [String=").append(token).append("]").append("[Expires").append(expiryDate).append("]"); 143 | return builder.toString(); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/model/Privilege.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.model; 2 | 3 | import java.util.Collection; 4 | 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.ManyToMany; 10 | 11 | @Entity 12 | public class Privilege { 13 | 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.AUTO) 16 | private Long id; 17 | 18 | private String name; 19 | 20 | @ManyToMany(mappedBy = "privileges") 21 | private Collection roles; 22 | 23 | public Privilege() { 24 | super(); 25 | } 26 | 27 | public Privilege(final String name) { 28 | super(); 29 | this.name = name; 30 | } 31 | 32 | // 33 | 34 | public Long getId() { 35 | return id; 36 | } 37 | 38 | public void setId(final Long id) { 39 | this.id = id; 40 | } 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | public void setName(final String name) { 47 | this.name = name; 48 | } 49 | 50 | public Collection getRoles() { 51 | return roles; 52 | } 53 | 54 | public void setRoles(final Collection roles) { 55 | this.roles = roles; 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | final int prime = 31; 61 | int result = 1; 62 | result = prime * result + ((getName() == null) ? 0 : getName().hashCode()); 63 | return result; 64 | } 65 | 66 | @Override 67 | public boolean equals(Object obj) { 68 | if (this == obj) 69 | return true; 70 | if (obj == null) 71 | return false; 72 | if (getClass() != obj.getClass()) 73 | return false; 74 | Privilege other = (Privilege) obj; 75 | if (getName() == null) { 76 | if (other.getName() != null) 77 | return false; 78 | } else if (!getName().equals(other.getName())) 79 | return false; 80 | return true; 81 | } 82 | 83 | @Override 84 | public String toString() { 85 | final StringBuilder builder = new StringBuilder(); 86 | builder.append("Privilege [name=").append(name).append("]").append("[id=").append(id).append("]"); 87 | return builder.toString(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/model/Role.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.model; 2 | 3 | import java.util.Collection; 4 | 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.JoinTable; 11 | import jakarta.persistence.ManyToMany; 12 | 13 | @Entity 14 | public class Role { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.AUTO) 18 | private Long id; 19 | 20 | @ManyToMany(mappedBy = "roles") 21 | private Collection users; 22 | 23 | @ManyToMany 24 | @JoinTable(name = "roles_privileges", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "privilege_id", referencedColumnName = "id")) 25 | private Collection privileges; 26 | 27 | private String name; 28 | 29 | public Role() { 30 | super(); 31 | } 32 | 33 | public Role(final String name) { 34 | super(); 35 | this.name = name; 36 | } 37 | 38 | // 39 | 40 | public Long getId() { 41 | return id; 42 | } 43 | 44 | public void setId(final Long id) { 45 | this.id = id; 46 | } 47 | 48 | public String getName() { 49 | return name; 50 | } 51 | 52 | public void setName(final String name) { 53 | this.name = name; 54 | } 55 | 56 | public Collection getUsers() { 57 | return users; 58 | } 59 | 60 | public void setUsers(final Collection users) { 61 | this.users = users; 62 | } 63 | 64 | public Collection getPrivileges() { 65 | return privileges; 66 | } 67 | 68 | public void setPrivileges(final Collection privileges) { 69 | this.privileges = privileges; 70 | } 71 | 72 | @Override 73 | public int hashCode() { 74 | final int prime = 31; 75 | int result = 1; 76 | result = prime * result + ((getName() == null) ? 0 : getName().hashCode()); 77 | return result; 78 | } 79 | 80 | @Override 81 | public boolean equals(final Object obj) { 82 | if (this == obj) { 83 | return true; 84 | } 85 | if (obj == null) { 86 | return false; 87 | } 88 | if (getClass() != obj.getClass()) { 89 | return false; 90 | } 91 | final Role role = (Role) obj; 92 | if (!getName().equals(role.getName())) { 93 | return false; 94 | } 95 | return true; 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | final StringBuilder builder = new StringBuilder(); 101 | builder.append("Role [name=").append(name).append("]").append("[id=").append(id).append("]"); 102 | return builder.toString(); 103 | } 104 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/model/User.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.model; 2 | 3 | import java.util.Collection; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.FetchType; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.JoinColumn; 12 | import jakarta.persistence.JoinTable; 13 | import jakarta.persistence.ManyToMany; 14 | import jakarta.persistence.Table; 15 | 16 | import org.jboss.aerogear.security.otp.api.Base32; 17 | 18 | @Entity 19 | @Table(name = "user_account") 20 | public class User { 21 | 22 | @Id 23 | @Column(unique = true, nullable = false) 24 | @GeneratedValue(strategy = GenerationType.AUTO) 25 | private Long id; 26 | 27 | private String firstName; 28 | 29 | private String lastName; 30 | 31 | private String email; 32 | 33 | @Column(length = 60) 34 | private String password; 35 | 36 | private boolean enabled; 37 | 38 | private boolean isUsing2FA; 39 | 40 | private String secret; 41 | 42 | // 43 | 44 | @ManyToMany(fetch = FetchType.EAGER) 45 | @JoinTable(name = "users_roles", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")) 46 | private Collection roles; 47 | 48 | public User() { 49 | super(); 50 | this.secret = Base32.random(); 51 | this.enabled = false; 52 | } 53 | 54 | public Long getId() { 55 | return id; 56 | } 57 | 58 | public void setId(final Long id) { 59 | this.id = id; 60 | } 61 | 62 | public String getFirstName() { 63 | return firstName; 64 | } 65 | 66 | public void setFirstName(final String firstName) { 67 | this.firstName = firstName; 68 | } 69 | 70 | public String getLastName() { 71 | return lastName; 72 | } 73 | 74 | public void setLastName(final String lastName) { 75 | this.lastName = lastName; 76 | } 77 | 78 | public String getEmail() { 79 | return email; 80 | } 81 | 82 | public void setEmail(final String username) { 83 | this.email = username; 84 | } 85 | 86 | public String getPassword() { 87 | return password; 88 | } 89 | 90 | public void setPassword(final String password) { 91 | this.password = password; 92 | } 93 | 94 | public Collection getRoles() { 95 | return roles; 96 | } 97 | 98 | public void setRoles(final Collection roles) { 99 | this.roles = roles; 100 | } 101 | 102 | public boolean isEnabled() { 103 | return enabled; 104 | } 105 | 106 | public void setEnabled(final boolean enabled) { 107 | this.enabled = enabled; 108 | } 109 | 110 | public boolean isUsing2FA() { 111 | return isUsing2FA; 112 | } 113 | 114 | public void setUsing2FA(boolean isUsing2FA) { 115 | this.isUsing2FA = isUsing2FA; 116 | } 117 | 118 | public String getSecret() { 119 | return secret; 120 | } 121 | 122 | public void setSecret(String secret) { 123 | this.secret = secret; 124 | } 125 | 126 | @Override 127 | public int hashCode() { 128 | final int prime = 31; 129 | int result = 1; 130 | result = (prime * result) + ((getEmail() == null) ? 0 : getEmail().hashCode()); 131 | return result; 132 | } 133 | 134 | @Override 135 | public boolean equals(final Object obj) { 136 | if (this == obj) { 137 | return true; 138 | } 139 | if (obj == null) { 140 | return false; 141 | } 142 | if (getClass() != obj.getClass()) { 143 | return false; 144 | } 145 | final User user = (User) obj; 146 | if (!getEmail().equals(user.getEmail())) { 147 | return false; 148 | } 149 | return true; 150 | } 151 | 152 | @Override 153 | public String toString() { 154 | final StringBuilder builder = new StringBuilder(); 155 | builder.append("User [id=") 156 | .append(id) 157 | .append(", firstName=").append(firstName) 158 | .append(", lastName=").append(lastName) 159 | .append(", email=").append(email) 160 | .append(", enabled=").append(enabled) 161 | .append(", isUsing2FA=").append(isUsing2FA) 162 | .append(", secret=").append(secret) 163 | .append(", roles=").append(roles) 164 | .append("]"); 165 | return builder.toString(); 166 | } 167 | 168 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/model/UserLocation.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.model; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.FetchType; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.JoinColumn; 9 | import jakarta.persistence.ManyToOne; 10 | 11 | @Entity 12 | public class UserLocation { 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.AUTO) 15 | private Long id; 16 | 17 | private String country; 18 | 19 | private boolean enabled; 20 | 21 | @ManyToOne(targetEntity = User.class, fetch = FetchType.EAGER) 22 | @JoinColumn(nullable = false, name = "user_id") 23 | private User user; 24 | 25 | public UserLocation() { 26 | super(); 27 | enabled = false; 28 | } 29 | 30 | public UserLocation(String country, User user) { 31 | super(); 32 | this.country = country; 33 | this.user = user; 34 | enabled = false; 35 | } 36 | 37 | public Long getId() { 38 | return id; 39 | } 40 | 41 | public void setId(Long id) { 42 | this.id = id; 43 | } 44 | 45 | public String getCountry() { 46 | return country; 47 | } 48 | 49 | public void setCountry(String country) { 50 | this.country = country; 51 | } 52 | 53 | public User getUser() { 54 | return user; 55 | } 56 | 57 | public void setUser(User user) { 58 | this.user = user; 59 | } 60 | 61 | public boolean isEnabled() { 62 | return enabled; 63 | } 64 | 65 | public void setEnabled(boolean enabled) { 66 | this.enabled = enabled; 67 | } 68 | 69 | @Override 70 | public int hashCode() { 71 | final int prime = 31; 72 | int result = 1; 73 | result = (prime * result) + ((getCountry() == null) ? 0 : getCountry().hashCode()); 74 | result = (prime * result) + (isEnabled() ? 1231 : 1237); 75 | result = (prime * result) + ((getId() == null) ? 0 : getId().hashCode()); 76 | result = (prime * result) + ((getUser() == null) ? 0 : getUser().hashCode()); 77 | return result; 78 | } 79 | 80 | @Override 81 | public boolean equals(Object obj) { 82 | if (this == obj) { 83 | return true; 84 | } 85 | if (obj == null) { 86 | return false; 87 | } 88 | if (getClass() != obj.getClass()) { 89 | return false; 90 | } 91 | final UserLocation other = (UserLocation) obj; 92 | if (getCountry() == null) { 93 | if (other.getCountry() != null) { 94 | return false; 95 | } 96 | } else if (!getCountry().equals(other.getCountry())) { 97 | return false; 98 | } 99 | if (isEnabled() != other.isEnabled()) { 100 | return false; 101 | } 102 | if (getId() == null) { 103 | if (other.getId() != null) { 104 | return false; 105 | } 106 | } else if (!getId().equals(other.getId())) { 107 | return false; 108 | } 109 | if (getUser() == null) { 110 | if (other.getUser() != null) { 111 | return false; 112 | } 113 | } else if (!getUser().equals(other.getUser())) { 114 | return false; 115 | } 116 | return true; 117 | } 118 | 119 | @Override 120 | public String toString() { 121 | return "UserLocation [id=" + id + ", country=" + country + ", enabled=" + enabled + ", user=" + user + "]"; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/persistence/model/VerificationToken.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.persistence.model; 2 | 3 | import java.util.Calendar; 4 | import java.util.Date; 5 | 6 | import jakarta.persistence.*; 7 | 8 | @Entity 9 | public class VerificationToken { 10 | 11 | private static final int EXPIRATION = 60 * 24; 12 | 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.AUTO) 15 | private Long id; 16 | 17 | private String token; 18 | 19 | @OneToOne(targetEntity = User.class, fetch = FetchType.EAGER) 20 | @JoinColumn(nullable = false, name = "user_id", foreignKey = @ForeignKey(name = "FK_VERIFY_USER")) 21 | private User user; 22 | 23 | private Date expiryDate; 24 | 25 | public VerificationToken() { 26 | super(); 27 | } 28 | 29 | public VerificationToken(final String token) { 30 | super(); 31 | 32 | this.token = token; 33 | this.expiryDate = calculateExpiryDate(EXPIRATION); 34 | } 35 | 36 | public VerificationToken(final String token, final User user) { 37 | super(); 38 | 39 | this.token = token; 40 | this.user = user; 41 | this.expiryDate = calculateExpiryDate(EXPIRATION); 42 | } 43 | 44 | public Long getId() { 45 | return id; 46 | } 47 | 48 | public String getToken() { 49 | return token; 50 | } 51 | 52 | public void setToken(final String token) { 53 | this.token = token; 54 | } 55 | 56 | public User getUser() { 57 | return user; 58 | } 59 | 60 | public void setUser(final User user) { 61 | this.user = user; 62 | } 63 | 64 | public Date getExpiryDate() { 65 | return expiryDate; 66 | } 67 | 68 | public void setExpiryDate(final Date expiryDate) { 69 | this.expiryDate = expiryDate; 70 | } 71 | 72 | private Date calculateExpiryDate(final int expiryTimeInMinutes) { 73 | final Calendar cal = Calendar.getInstance(); 74 | cal.setTimeInMillis(new Date().getTime()); 75 | cal.add(Calendar.MINUTE, expiryTimeInMinutes); 76 | return new Date(cal.getTime().getTime()); 77 | } 78 | 79 | public void updateToken(final String token) { 80 | this.token = token; 81 | this.expiryDate = calculateExpiryDate(EXPIRATION); 82 | } 83 | 84 | // 85 | 86 | @Override 87 | public int hashCode() { 88 | final int prime = 31; 89 | int result = 1; 90 | result = prime * result + ((getExpiryDate() == null) ? 0 : getExpiryDate().hashCode()); 91 | result = prime * result + ((getToken() == null) ? 0 : getToken().hashCode()); 92 | result = prime * result + ((getUser() == null) ? 0 : getUser().hashCode()); 93 | return result; 94 | } 95 | 96 | @Override 97 | public boolean equals(final Object obj) { 98 | if (this == obj) { 99 | return true; 100 | } 101 | if (obj == null) { 102 | return false; 103 | } 104 | if (getClass() != obj.getClass()) { 105 | return false; 106 | } 107 | final VerificationToken other = (VerificationToken) obj; 108 | if (getExpiryDate() == null) { 109 | if (other.getExpiryDate() != null) { 110 | return false; 111 | } 112 | } else if (!getExpiryDate().equals(other.getExpiryDate())) { 113 | return false; 114 | } 115 | if (getToken() == null) { 116 | if (other.getToken() != null) { 117 | return false; 118 | } 119 | } else if (!getToken().equals(other.getToken())) { 120 | return false; 121 | } 122 | if (getUser() == null) { 123 | if (other.getUser() != null) { 124 | return false; 125 | } 126 | } else if (!getUser().equals(other.getUser())) { 127 | return false; 128 | } 129 | return true; 130 | } 131 | 132 | @Override 133 | public String toString() { 134 | final StringBuilder builder = new StringBuilder(); 135 | builder.append("Token [String=").append(token).append("]").append("[Expires").append(expiryDate).append("]"); 136 | return builder.toString(); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/registration/OnRegistrationCompleteEvent.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.registration; 2 | 3 | import java.util.Locale; 4 | 5 | import com.baeldung.persistence.model.User; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | @SuppressWarnings("serial") 9 | public class OnRegistrationCompleteEvent extends ApplicationEvent { 10 | 11 | private final String appUrl; 12 | private final Locale locale; 13 | private final User user; 14 | 15 | public OnRegistrationCompleteEvent(final User user, final Locale locale, final String appUrl) { 16 | super(user); 17 | this.user = user; 18 | this.locale = locale; 19 | this.appUrl = appUrl; 20 | } 21 | 22 | // 23 | 24 | public String getAppUrl() { 25 | return appUrl; 26 | } 27 | 28 | public Locale getLocale() { 29 | return locale; 30 | } 31 | 32 | public User getUser() { 33 | return user; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/registration/listener/RegistrationListener.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.registration.listener; 2 | 3 | import java.util.UUID; 4 | 5 | import com.baeldung.service.IUserService; 6 | import com.baeldung.persistence.model.User; 7 | import com.baeldung.registration.OnRegistrationCompleteEvent; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.ApplicationListener; 10 | import org.springframework.context.MessageSource; 11 | import org.springframework.core.env.Environment; 12 | import org.springframework.mail.SimpleMailMessage; 13 | import org.springframework.mail.javamail.JavaMailSender; 14 | import org.springframework.stereotype.Component; 15 | 16 | @Component 17 | public class RegistrationListener implements ApplicationListener { 18 | @Autowired 19 | private IUserService service; 20 | 21 | @Autowired 22 | private MessageSource messages; 23 | 24 | @Autowired 25 | private JavaMailSender mailSender; 26 | 27 | @Autowired 28 | private Environment env; 29 | 30 | // API 31 | 32 | @Override 33 | public void onApplicationEvent(final OnRegistrationCompleteEvent event) { 34 | this.confirmRegistration(event); 35 | } 36 | 37 | private void confirmRegistration(final OnRegistrationCompleteEvent event) { 38 | final User user = event.getUser(); 39 | final String token = UUID.randomUUID().toString(); 40 | service.createVerificationTokenForUser(user, token); 41 | 42 | final SimpleMailMessage email = constructEmailMessage(event, user, token); 43 | mailSender.send(email); 44 | } 45 | 46 | // 47 | 48 | private SimpleMailMessage constructEmailMessage(final OnRegistrationCompleteEvent event, final User user, final String token) { 49 | final String recipientAddress = user.getEmail(); 50 | final String subject = "Registration Confirmation"; 51 | final String confirmationUrl = event.getAppUrl() + "/registrationConfirm?token=" + token; 52 | final String message = messages.getMessage("message.regSuccLink", null, "You registered successfully. To confirm your registration, please click on the below link.", event.getLocale()); 53 | final SimpleMailMessage email = new SimpleMailMessage(); 54 | email.setTo(recipientAddress); 55 | email.setSubject(subject); 56 | email.setText(message + " \r\n" + confirmationUrl); 57 | email.setFrom(env.getProperty("support.email")); 58 | return email; 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/ActiveUserStore.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class ActiveUserStore { 7 | 8 | public List users; 9 | 10 | public ActiveUserStore() { 11 | users = new ArrayList<>(); 12 | } 13 | 14 | public List getUsers() { 15 | return users; 16 | } 17 | 18 | public void setUsers(List users) { 19 | this.users = users; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/AuthenticationFailureListener.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.ApplicationListener; 7 | import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class AuthenticationFailureListener implements ApplicationListener { 12 | 13 | @Autowired 14 | private HttpServletRequest request; 15 | 16 | @Autowired 17 | private LoginAttemptService loginAttemptService; 18 | 19 | @Override 20 | public void onApplicationEvent(final AuthenticationFailureBadCredentialsEvent e) { 21 | final String xfHeader = request.getHeader("X-Forwarded-For"); 22 | if (xfHeader == null || xfHeader.isEmpty() || !xfHeader.contains(request.getRemoteAddr())) { 23 | loginAttemptService.loginFailed(request.getRemoteAddr()); 24 | } else { 25 | loginAttemptService.loginFailed(xfHeader.split(",")[0]); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/CustomAuthenticationFailureHandler.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import java.io.IOException; 4 | import java.util.Locale; 5 | 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.context.MessageSource; 12 | import org.springframework.security.core.AuthenticationException; 13 | import org.springframework.security.web.WebAttributes; 14 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.servlet.LocaleResolver; 17 | 18 | @Component("authenticationFailureHandler") 19 | public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { 20 | 21 | @Autowired 22 | private MessageSource messages; 23 | 24 | @Autowired 25 | private LocaleResolver localeResolver; 26 | 27 | @Autowired 28 | private HttpServletRequest request; 29 | 30 | @Autowired 31 | private LoginAttemptService loginAttemptService; 32 | 33 | @Override 34 | public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception) throws IOException, ServletException { 35 | setDefaultFailureUrl("/login?error=true"); 36 | 37 | super.onAuthenticationFailure(request, response, exception); 38 | 39 | final Locale locale = localeResolver.resolveLocale(request); 40 | 41 | String errorMessage = messages.getMessage("message.badCredentials", null, locale); 42 | 43 | if (loginAttemptService.isBlocked()) { 44 | errorMessage = messages.getMessage("auth.message.blocked", null, locale); 45 | } 46 | 47 | if (exception.getMessage() 48 | .equalsIgnoreCase("User is disabled")) { 49 | errorMessage = messages.getMessage("auth.message.disabled", null, locale); 50 | } else if (exception.getMessage() 51 | .equalsIgnoreCase("User account has expired")) { 52 | errorMessage = messages.getMessage("auth.message.expired", null, locale); 53 | } else if (exception.getMessage() 54 | .equalsIgnoreCase("blocked")) { 55 | errorMessage = messages.getMessage("auth.message.blocked", null, locale); 56 | } else if (exception.getMessage() 57 | .equalsIgnoreCase("unusual location")) { 58 | errorMessage = messages.getMessage("auth.message.unusual.location", null, locale); 59 | } 60 | 61 | request.getSession() 62 | .setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, errorMessage); 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/CustomRememberMeServices.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import java.util.Date; 4 | 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | 8 | import com.baeldung.persistence.dao.UserRepository; 9 | import com.baeldung.persistence.model.User; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.security.authentication.AuthenticationDetailsSource; 12 | import org.springframework.security.authentication.RememberMeAuthenticationToken; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; 15 | import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; 16 | import org.springframework.security.core.userdetails.UserDetails; 17 | import org.springframework.security.core.userdetails.UserDetailsService; 18 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 19 | import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken; 20 | import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices; 21 | import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; 22 | 23 | public class CustomRememberMeServices extends PersistentTokenBasedRememberMeServices { 24 | 25 | @Autowired 26 | private UserRepository userRepository; 27 | 28 | private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); 29 | private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); 30 | private PersistentTokenRepository tokenRepository; 31 | private String key; 32 | 33 | public CustomRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) { 34 | super(key, userDetailsService, tokenRepository); 35 | this.tokenRepository = tokenRepository; 36 | this.key = key; 37 | } 38 | 39 | @Override 40 | protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { 41 | String username = ((User) successfulAuthentication.getPrincipal()).getEmail(); 42 | logger.debug("Creating new persistent login for user " + username); 43 | PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date()); 44 | try { 45 | tokenRepository.createNewToken(persistentToken); 46 | addCookie(persistentToken, request, response); 47 | } catch (Exception e) { 48 | logger.error("Failed to save persistent token ", e); 49 | } 50 | } 51 | 52 | @Override 53 | protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails user) { 54 | User auser = userRepository.findByEmail(user.getUsername()); 55 | RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(key, auser, authoritiesMapper.mapAuthorities(user.getAuthorities())); 56 | auth.setDetails(authenticationDetailsSource.buildDetails(request)); 57 | return auth; 58 | } 59 | 60 | private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) { 61 | setCookie(new String[] { token.getSeries(), token.getTokenValue() }, getTokenValiditySeconds(), request, response); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/ISecurityUserService.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | public interface ISecurityUserService { 4 | 5 | String validatePasswordResetToken(String token); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/LoggedUser.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | import jakarta.servlet.http.HttpSessionBindingEvent; 7 | import jakarta.servlet.http.HttpSessionBindingListener; 8 | 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class LoggedUser implements HttpSessionBindingListener, Serializable { 13 | private static final long serialVersionUID = 1L; 14 | 15 | private String username; 16 | private ActiveUserStore activeUserStore; 17 | 18 | public LoggedUser(String username, ActiveUserStore activeUserStore) { 19 | this.username = username; 20 | this.activeUserStore = activeUserStore; 21 | } 22 | 23 | public LoggedUser() { 24 | } 25 | 26 | @Override 27 | public void valueBound(HttpSessionBindingEvent event) { 28 | List users = activeUserStore.getUsers(); 29 | LoggedUser user = (LoggedUser) event.getValue(); 30 | if (!users.contains(user.getUsername())) { 31 | users.add(user.getUsername()); 32 | } 33 | } 34 | 35 | @Override 36 | public void valueUnbound(HttpSessionBindingEvent event) { 37 | List users = activeUserStore.getUsers(); 38 | LoggedUser user = (LoggedUser) event.getValue(); 39 | users.remove(user.getUsername()); 40 | } 41 | 42 | public String getUsername() { 43 | return username; 44 | } 45 | 46 | public void setUsername(String username) { 47 | this.username = username; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/LoginAttemptService.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import java.util.concurrent.ExecutionException; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import jakarta.servlet.http.HttpServletRequest; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import com.google.common.cache.CacheBuilder; 12 | import com.google.common.cache.CacheLoader; 13 | import com.google.common.cache.LoadingCache; 14 | 15 | @Service 16 | public class LoginAttemptService { 17 | 18 | public static final int MAX_ATTEMPT = 10; 19 | private LoadingCache attemptsCache; 20 | 21 | @Autowired 22 | private HttpServletRequest request; 23 | 24 | public LoginAttemptService() { 25 | super(); 26 | attemptsCache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader() { 27 | @Override 28 | public Integer load(final String key) { 29 | return 0; 30 | } 31 | }); 32 | } 33 | 34 | public void loginFailed(final String key) { 35 | int attempts; 36 | try { 37 | attempts = attemptsCache.get(key); 38 | } catch (final ExecutionException e) { 39 | attempts = 0; 40 | } 41 | attempts++; 42 | attemptsCache.put(key, attempts); 43 | } 44 | 45 | public boolean isBlocked() { 46 | try { 47 | return attemptsCache.get(getClientIP()) >= MAX_ATTEMPT; 48 | } catch (final ExecutionException e) { 49 | return false; 50 | } 51 | } 52 | 53 | private String getClientIP() { 54 | final String xfHeader = request.getHeader("X-Forwarded-For"); 55 | if (xfHeader != null) { 56 | return xfHeader.split(",")[0]; 57 | } 58 | return request.getRemoteAddr(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/MyCustomLoginAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import java.io.IOException; 4 | 5 | import jakarta.servlet.http.Cookie; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import jakarta.servlet.http.HttpSession; 9 | 10 | import com.baeldung.persistence.model.User; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.security.core.Authentication; 13 | import org.springframework.security.web.DefaultRedirectStrategy; 14 | import org.springframework.security.web.RedirectStrategy; 15 | import org.springframework.security.web.WebAttributes; 16 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 17 | 18 | //@Component("myAuthenticationSuccessHandler") 19 | public class MyCustomLoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler { 20 | 21 | private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); 22 | 23 | @Autowired 24 | ActiveUserStore activeUserStore; 25 | 26 | @Override 27 | public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException { 28 | addWelcomeCookie(gerUserName(authentication), response); 29 | redirectStrategy.sendRedirect(request, response, "/homepage.html?user=" + authentication.getName()); 30 | 31 | final HttpSession session = request.getSession(false); 32 | if (session != null) { 33 | session.setMaxInactiveInterval(30 * 60); 34 | String username; 35 | if (authentication.getPrincipal() instanceof User) { 36 | username = ((User)authentication.getPrincipal()).getEmail(); 37 | } 38 | else { 39 | username = authentication.getName(); 40 | } 41 | 42 | LoggedUser user = new LoggedUser(username, activeUserStore); 43 | session.setAttribute("user", user); 44 | } 45 | clearAuthenticationAttributes(request); 46 | } 47 | 48 | private String gerUserName(final Authentication authentication) { 49 | return ((User) authentication.getPrincipal()).getFirstName(); 50 | } 51 | 52 | private void addWelcomeCookie(final String user, final HttpServletResponse response) { 53 | Cookie welcomeCookie = getWelcomeCookie(user); 54 | response.addCookie(welcomeCookie); 55 | } 56 | 57 | private Cookie getWelcomeCookie(final String user) { 58 | Cookie welcomeCookie = new Cookie("welcome", user); 59 | welcomeCookie.setMaxAge(60 * 60 * 24 * 30); // 30 days 60 | return welcomeCookie; 61 | } 62 | 63 | protected void clearAuthenticationAttributes(final HttpServletRequest request) { 64 | final HttpSession session = request.getSession(false); 65 | if (session == null) { 66 | return; 67 | } 68 | session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); 69 | } 70 | 71 | public void setRedirectStrategy(final RedirectStrategy redirectStrategy) { 72 | this.redirectStrategy = redirectStrategy; 73 | } 74 | 75 | protected RedirectStrategy getRedirectStrategy() { 76 | return redirectStrategy; 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/MyLogoutSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import java.io.IOException; 4 | 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import jakarta.servlet.http.HttpSession; 9 | 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Component("myLogoutSuccessHandler") 15 | public class MyLogoutSuccessHandler implements LogoutSuccessHandler { 16 | 17 | @Override 18 | public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 19 | final HttpSession session = request.getSession(); 20 | if (session != null) { 21 | session.removeAttribute("user"); 22 | } 23 | 24 | response.sendRedirect("/logout.html?logSucc=true"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/MyUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import com.baeldung.persistence.dao.UserRepository; 4 | import com.baeldung.persistence.model.Privilege; 5 | import com.baeldung.persistence.model.Role; 6 | import com.baeldung.persistence.model.User; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.security.core.GrantedAuthority; 9 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | import org.springframework.security.core.userdetails.UserDetailsService; 12 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.List; 19 | 20 | @Service("userDetailsService") 21 | @Transactional 22 | public class MyUserDetailsService implements UserDetailsService { 23 | 24 | @Autowired 25 | private UserRepository userRepository; 26 | 27 | @Autowired 28 | private LoginAttemptService loginAttemptService; 29 | 30 | public MyUserDetailsService() { 31 | super(); 32 | } 33 | 34 | // API 35 | 36 | @Override 37 | public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException { 38 | if (loginAttemptService.isBlocked()) { 39 | throw new RuntimeException("blocked"); 40 | } 41 | 42 | try { 43 | final User user = userRepository.findByEmail(email); 44 | if (user == null) { 45 | throw new UsernameNotFoundException("No user found with username: " + email); 46 | } 47 | 48 | return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, getAuthorities(user.getRoles())); 49 | } catch (final Exception e) { 50 | throw new RuntimeException(e); 51 | } 52 | } 53 | 54 | // UTIL 55 | 56 | private Collection getAuthorities(final Collection roles) { 57 | return getGrantedAuthorities(getPrivileges(roles)); 58 | } 59 | 60 | private List getPrivileges(final Collection roles) { 61 | final List privileges = new ArrayList<>(); 62 | final List collection = new ArrayList<>(); 63 | for (final Role role : roles) { 64 | privileges.add(role.getName()); 65 | collection.addAll(role.getPrivileges()); 66 | } 67 | for (final Privilege item : collection) { 68 | privileges.add(item.getName()); 69 | } 70 | 71 | return privileges; 72 | } 73 | 74 | private List getGrantedAuthorities(final List privileges) { 75 | final List authorities = new ArrayList<>(); 76 | for (final String privilege : privileges) { 77 | authorities.add(new SimpleGrantedAuthority(privilege)); 78 | } 79 | return authorities; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/UserSecurityService.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security; 2 | 3 | import com.baeldung.persistence.dao.PasswordResetTokenRepository; 4 | import com.baeldung.persistence.model.PasswordResetToken; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import jakarta.transaction.Transactional; 9 | import java.util.Calendar; 10 | 11 | @Service 12 | @Transactional 13 | public class UserSecurityService implements ISecurityUserService { 14 | 15 | @Autowired 16 | private PasswordResetTokenRepository passwordTokenRepository; 17 | 18 | @Override 19 | public String validatePasswordResetToken(String token) { 20 | final PasswordResetToken passToken = passwordTokenRepository.findByToken(token); 21 | 22 | return !isTokenFound(passToken) ? "invalidToken" 23 | : isTokenExpired(passToken) ? "expired" 24 | : null; 25 | } 26 | 27 | private boolean isTokenFound(PasswordResetToken passToken) { 28 | return passToken != null; 29 | } 30 | 31 | private boolean isTokenExpired(PasswordResetToken passToken) { 32 | final Calendar cal = Calendar.getInstance(); 33 | return passToken.getExpiryDate().before(cal.getTime()); 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/google2fa/CustomAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security.google2fa; 2 | 3 | import com.baeldung.persistence.dao.UserRepository; 4 | import com.baeldung.persistence.model.User; 5 | import org.jboss.aerogear.security.otp.Totp; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.authentication.BadCredentialsException; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.AuthenticationException; 12 | 13 | //@Component 14 | public class CustomAuthenticationProvider extends DaoAuthenticationProvider { 15 | 16 | @Autowired 17 | private UserRepository userRepository; 18 | 19 | @Override 20 | public Authentication authenticate(Authentication auth) throws AuthenticationException { 21 | final User user = userRepository.findByEmail(auth.getName()); 22 | if ((user == null)) { 23 | throw new BadCredentialsException("Invalid username or password"); 24 | } 25 | // to verify verification code 26 | if (user.isUsing2FA()) { 27 | final String verificationCode = ((CustomWebAuthenticationDetails) auth.getDetails()).getVerificationCode(); 28 | final Totp totp = new Totp(user.getSecret()); 29 | if (!isValidLong(verificationCode) || !totp.verify(verificationCode)) { 30 | throw new BadCredentialsException("Invalid verification code"); 31 | } 32 | 33 | } 34 | final Authentication result = super.authenticate(auth); 35 | return new UsernamePasswordAuthenticationToken(user, result.getCredentials(), result.getAuthorities()); 36 | } 37 | 38 | private boolean isValidLong(String code) { 39 | try { 40 | Long.parseLong(code); 41 | } catch (final NumberFormatException e) { 42 | return false; 43 | } 44 | return true; 45 | } 46 | 47 | @Override 48 | public boolean supports(Class authentication) { 49 | return authentication.equals(UsernamePasswordAuthenticationToken.class); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/google2fa/CustomWebAuthenticationDetails.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security.google2fa; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | 5 | import org.springframework.security.web.authentication.WebAuthenticationDetails; 6 | 7 | public class CustomWebAuthenticationDetails extends WebAuthenticationDetails { 8 | 9 | private static final long serialVersionUID = 1L; 10 | 11 | private final String verificationCode; 12 | 13 | public CustomWebAuthenticationDetails(HttpServletRequest request) { 14 | super(request); 15 | verificationCode = request.getParameter("code"); 16 | } 17 | 18 | public String getVerificationCode() { 19 | return verificationCode; 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/google2fa/CustomWebAuthenticationDetailsSource.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security.google2fa; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | 5 | import org.springframework.security.authentication.AuthenticationDetailsSource; 6 | import org.springframework.security.web.authentication.WebAuthenticationDetails; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class CustomWebAuthenticationDetailsSource implements AuthenticationDetailsSource { 11 | @Override 12 | public WebAuthenticationDetails buildDetails(HttpServletRequest context) { 13 | return new CustomWebAuthenticationDetails(context); 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/location/DifferentLocationChecker.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security.location; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | 5 | import com.baeldung.persistence.model.NewLocationToken; 6 | import com.baeldung.service.IUserService; 7 | import com.baeldung.web.error.UnusualLocationException; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.ApplicationEventPublisher; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | import org.springframework.security.core.userdetails.UserDetailsChecker; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Component 15 | public class DifferentLocationChecker implements UserDetailsChecker { 16 | 17 | @Autowired 18 | private IUserService userService; 19 | 20 | @Autowired 21 | private HttpServletRequest request; 22 | 23 | @Autowired 24 | private ApplicationEventPublisher eventPublisher; 25 | 26 | @Override 27 | public void check(UserDetails userDetails) { 28 | final String ip = getClientIP(); 29 | final NewLocationToken token = userService.isNewLoginLocation(userDetails.getUsername(), ip); 30 | if (token != null) { 31 | final String appUrl = "http://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath(); 32 | eventPublisher.publishEvent(new OnDifferentLocationLoginEvent(request.getLocale(), userDetails.getUsername(), ip, token, appUrl)); 33 | throw new UnusualLocationException("unusual location"); 34 | } 35 | } 36 | 37 | private String getClientIP() { 38 | final String xfHeader = request.getHeader("X-Forwarded-For"); 39 | if (xfHeader == null || xfHeader.isEmpty() || !xfHeader.contains(request.getRemoteAddr())) { 40 | return request.getRemoteAddr(); 41 | } 42 | return xfHeader.split(",")[0]; 43 | // return "128.101.101.101"; // for testing United States 44 | // return "41.238.0.198"; // for testing Egypt 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/location/DifferentLocationLoginListener.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security.location; 2 | 3 | import java.util.Date; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.ApplicationListener; 7 | import org.springframework.context.MessageSource; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.mail.SimpleMailMessage; 10 | import org.springframework.mail.javamail.JavaMailSender; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class DifferentLocationLoginListener implements ApplicationListener { 15 | 16 | @Autowired 17 | private MessageSource messages; 18 | 19 | @Autowired 20 | private JavaMailSender mailSender; 21 | 22 | @Autowired 23 | private Environment env; 24 | 25 | @Override 26 | public void onApplicationEvent(final OnDifferentLocationLoginEvent event) { 27 | final String enableLocUri = event.getAppUrl() + "/user/enableNewLoc?token=" + event.getToken() 28 | .getToken(); 29 | final String changePassUri = event.getAppUrl() + "/changePassword.html"; 30 | final String recipientAddress = event.getUsername(); 31 | final String subject = "Login attempt from different location"; 32 | final String message = messages.getMessage("message.differentLocation", new Object[] { new Date().toString(), event.getToken() 33 | .getUserLocation() 34 | .getCountry(), event.getIp(), enableLocUri, changePassUri }, event.getLocale()); 35 | 36 | final SimpleMailMessage email = new SimpleMailMessage(); 37 | email.setTo(recipientAddress); 38 | email.setSubject(subject); 39 | email.setText(message); 40 | email.setFrom(env.getProperty("support.email")); 41 | System.out.println(message); 42 | mailSender.send(email); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/security/location/OnDifferentLocationLoginEvent.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.security.location; 2 | 3 | import java.util.Locale; 4 | 5 | import com.baeldung.persistence.model.NewLocationToken; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | @SuppressWarnings("serial") 9 | public class OnDifferentLocationLoginEvent extends ApplicationEvent { 10 | 11 | private final Locale locale; 12 | private final String username; 13 | private final String ip; 14 | private final NewLocationToken token; 15 | private final String appUrl; 16 | 17 | // 18 | 19 | public OnDifferentLocationLoginEvent(Locale locale, String username, String ip, NewLocationToken token, String appUrl) { 20 | super(token); 21 | this.locale = locale; 22 | this.username = username; 23 | this.ip = ip; 24 | this.token = token; 25 | this.appUrl = appUrl; 26 | } 27 | 28 | // 29 | public Locale getLocale() { 30 | return locale; 31 | } 32 | 33 | public String getUsername() { 34 | return username; 35 | } 36 | 37 | public String getIp() { 38 | return ip; 39 | } 40 | 41 | public NewLocationToken getToken() { 42 | return token; 43 | } 44 | 45 | public String getAppUrl() { 46 | return appUrl; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/service/IUserService.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.service; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | import com.baeldung.web.dto.UserDto; 8 | import com.baeldung.persistence.model.PasswordResetToken; 9 | import com.baeldung.persistence.model.User; 10 | import com.baeldung.persistence.model.VerificationToken; 11 | import com.baeldung.persistence.model.NewLocationToken; 12 | 13 | public interface IUserService { 14 | 15 | User registerNewUserAccount(UserDto accountDto); 16 | 17 | User getUser(String verificationToken); 18 | 19 | void saveRegisteredUser(User user); 20 | 21 | void deleteUser(User user); 22 | 23 | void createVerificationTokenForUser(User user, String token); 24 | 25 | VerificationToken getVerificationToken(String VerificationToken); 26 | 27 | VerificationToken generateNewVerificationToken(String token); 28 | 29 | void createPasswordResetTokenForUser(User user, String token); 30 | 31 | User findUserByEmail(String email); 32 | 33 | PasswordResetToken getPasswordResetToken(String token); 34 | 35 | Optional getUserByPasswordResetToken(String token); 36 | 37 | Optional getUserByID(long id); 38 | 39 | void changeUserPassword(User user, String password); 40 | 41 | boolean checkIfValidOldPassword(User user, String password); 42 | 43 | String validateVerificationToken(String token); 44 | 45 | String generateQRUrl(User user) throws UnsupportedEncodingException; 46 | 47 | User updateUser2FA(boolean use2FA); 48 | 49 | List getUsersFromSessionRegistry(); 50 | 51 | NewLocationToken isNewLoginLocation(String username, String ip); 52 | 53 | String isValidNewLocationToken(String token); 54 | 55 | void addUserLocation(User user, String ip); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/spring/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import com.baeldung.security.ActiveUserStore; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | public class AppConfig { 9 | // beans 10 | 11 | @Bean 12 | public ActiveUserStore activeUserStore() { 13 | return new ActiveUserStore(); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/spring/CaptchaConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.http.client.ClientHttpRequestFactory; 7 | import org.springframework.http.client.SimpleClientHttpRequestFactory; 8 | import org.springframework.web.client.RestOperations; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | @Configuration 12 | @ComponentScan(basePackages = { "com.baeldung.captcha" }) 13 | public class CaptchaConfig { 14 | @Bean 15 | public ClientHttpRequestFactory clientHttpRequestFactory() { 16 | SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); 17 | factory.setConnectTimeout(3 * 1000); 18 | factory.setReadTimeout(7 * 1000); 19 | return factory; 20 | } 21 | 22 | @Bean 23 | public RestOperations restTemplate() { 24 | RestTemplate restTemplate = new RestTemplate(this.clientHttpRequestFactory()); 25 | return restTemplate; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/spring/LoginNotificationConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import com.maxmind.geoip2.DatabaseReader; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.util.ResourceUtils; 7 | import ua_parser.Parser; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | 12 | @Configuration 13 | public class LoginNotificationConfig { 14 | 15 | @Bean 16 | public Parser uaParser() throws IOException { 17 | return new Parser(); 18 | } 19 | 20 | @Bean(name="GeoIPCity") 21 | public DatabaseReader databaseReader() throws IOException { 22 | File database = ResourceUtils 23 | .getFile("classpath:maxmind/GeoLite2-City.mmdb"); 24 | return new DatabaseReader.Builder(database) 25 | .build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/spring/MvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import java.util.Locale; 4 | 5 | import com.baeldung.validation.EmailValidator; 6 | import com.baeldung.validation.PasswordMatchesValidator; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 9 | import org.springframework.boot.web.server.WebServerFactoryCustomizer; 10 | import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; 11 | import org.springframework.context.MessageSource; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.ComponentScan; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.validation.Validator; 16 | import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; 17 | import org.springframework.web.context.request.RequestContextListener; 18 | import org.springframework.web.servlet.LocaleResolver; 19 | import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; 20 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 21 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 22 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 23 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 24 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 25 | import org.springframework.web.servlet.i18n.CookieLocaleResolver; 26 | import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; 27 | 28 | @Configuration 29 | @ComponentScan(basePackages = { "com.baeldung.web" }) 30 | @EnableWebMvc 31 | public class MvcConfig implements WebMvcConfigurer { 32 | 33 | public MvcConfig() { 34 | super(); 35 | } 36 | 37 | @Autowired 38 | private MessageSource messageSource; 39 | 40 | @Override 41 | public void addViewControllers(final ViewControllerRegistry registry) { 42 | registry.addViewController("/").setViewName("forward:/login"); 43 | registry.addViewController("/loginRememberMe"); 44 | registry.addViewController("/customLogin"); 45 | registry.addViewController("/registration.html"); 46 | registry.addViewController("/registrationCaptcha.html"); 47 | registry.addViewController("/registrationReCaptchaV3.html"); 48 | registry.addViewController("/logout.html"); 49 | registry.addViewController("/homepage.html"); 50 | registry.addViewController("/expiredAccount.html"); 51 | registry.addViewController("/emailError.html"); 52 | registry.addViewController("/home.html"); 53 | registry.addViewController("/invalidSession.html"); 54 | registry.addViewController("/admin.html"); 55 | registry.addViewController("/successRegister.html"); 56 | registry.addViewController("/forgetPassword.html"); 57 | registry.addViewController("/updatePassword.html"); 58 | registry.addViewController("/changePassword.html"); 59 | registry.addViewController("/users.html"); 60 | registry.addViewController("/qrcode.html"); 61 | } 62 | 63 | @Override 64 | public void configureDefaultServletHandling(final DefaultServletHandlerConfigurer configurer) { 65 | configurer.enable(); 66 | } 67 | 68 | @Override 69 | public void addResourceHandlers(final ResourceHandlerRegistry registry) { 70 | registry.addResourceHandler("/resources/**").addResourceLocations("/", "/resources/"); 71 | } 72 | 73 | @Override 74 | public void addInterceptors(final InterceptorRegistry registry) { 75 | final LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); 76 | localeChangeInterceptor.setParamName("lang"); 77 | registry.addInterceptor(localeChangeInterceptor); 78 | } 79 | 80 | // beans 81 | 82 | @Bean 83 | public LocaleResolver localeResolver() { 84 | final CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver(); 85 | cookieLocaleResolver.setDefaultLocale(Locale.ENGLISH); 86 | return cookieLocaleResolver; 87 | } 88 | 89 | @Bean 90 | public EmailValidator usernameValidator() { 91 | return new EmailValidator(); 92 | } 93 | 94 | @Bean 95 | public PasswordMatchesValidator passwordMatchesValidator() { 96 | return new PasswordMatchesValidator(); 97 | } 98 | 99 | @Bean 100 | @ConditionalOnMissingBean(RequestContextListener.class) 101 | public RequestContextListener requestContextListener() { 102 | return new RequestContextListener(); 103 | } 104 | 105 | @Override 106 | public Validator getValidator() { 107 | LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); 108 | validator.setValidationMessageSource(messageSource); 109 | return validator; 110 | } 111 | 112 | @Bean 113 | WebServerFactoryCustomizer enableDefaultServlet() { 114 | return (factory) -> factory.setRegisterDefaultServlet(true); 115 | } 116 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/spring/PersistenceJPAConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.transaction.annotation.EnableTransactionManagement; 5 | 6 | @Configuration 7 | @EnableTransactionManagement 8 | public class PersistenceJPAConfig {} -------------------------------------------------------------------------------- /src/main/java/com/baeldung/spring/ServiceConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @ComponentScan({ "com.baeldung.service" }) 8 | public class ServiceConfig { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/spring/SetupDataLoader.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collection; 6 | import java.util.List; 7 | 8 | import com.baeldung.persistence.dao.PrivilegeRepository; 9 | import com.baeldung.persistence.dao.RoleRepository; 10 | import com.baeldung.persistence.dao.UserRepository; 11 | import com.baeldung.persistence.model.Privilege; 12 | import com.baeldung.persistence.model.Role; 13 | import com.baeldung.persistence.model.User; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.context.ApplicationListener; 16 | import org.springframework.context.event.ContextRefreshedEvent; 17 | import org.springframework.security.crypto.password.PasswordEncoder; 18 | import org.springframework.stereotype.Component; 19 | import org.springframework.transaction.annotation.Transactional; 20 | 21 | @Component 22 | public class SetupDataLoader implements ApplicationListener { 23 | 24 | private boolean alreadySetup = false; 25 | 26 | @Autowired 27 | private UserRepository userRepository; 28 | 29 | @Autowired 30 | private RoleRepository roleRepository; 31 | 32 | @Autowired 33 | private PrivilegeRepository privilegeRepository; 34 | 35 | @Autowired 36 | private PasswordEncoder passwordEncoder; 37 | 38 | // API 39 | 40 | @Override 41 | @Transactional 42 | public void onApplicationEvent(final ContextRefreshedEvent event) { 43 | if (alreadySetup) { 44 | return; 45 | } 46 | 47 | // == create initial privileges 48 | final Privilege readPrivilege = createPrivilegeIfNotFound("READ_PRIVILEGE"); 49 | final Privilege writePrivilege = createPrivilegeIfNotFound("WRITE_PRIVILEGE"); 50 | final Privilege passwordPrivilege = createPrivilegeIfNotFound("CHANGE_PASSWORD_PRIVILEGE"); 51 | 52 | // == create initial roles 53 | final List adminPrivileges = new ArrayList<>(Arrays.asList(readPrivilege, writePrivilege, passwordPrivilege)); 54 | final List userPrivileges = new ArrayList<>(Arrays.asList(readPrivilege, passwordPrivilege)); 55 | final Role adminRole = createRoleIfNotFound("ROLE_ADMIN", adminPrivileges); 56 | createRoleIfNotFound("ROLE_USER", userPrivileges); 57 | 58 | // == create initial user 59 | createUserIfNotFound("test@test.com", "Test", "Test", "test", new ArrayList<>(Arrays.asList(adminRole))); 60 | 61 | alreadySetup = true; 62 | } 63 | 64 | @Transactional 65 | public Privilege createPrivilegeIfNotFound(final String name) { 66 | Privilege privilege = privilegeRepository.findByName(name); 67 | if (privilege == null) { 68 | privilege = new Privilege(name); 69 | privilege = privilegeRepository.save(privilege); 70 | } 71 | return privilege; 72 | } 73 | 74 | @Transactional 75 | public Role createRoleIfNotFound(final String name, final Collection privileges) { 76 | Role role = roleRepository.findByName(name); 77 | if (role == null) { 78 | role = new Role(name); 79 | } 80 | role.setPrivileges(privileges); 81 | role = roleRepository.save(role); 82 | return role; 83 | } 84 | 85 | @Transactional 86 | public User createUserIfNotFound(final String email, final String firstName, final String lastName, final String password, final Collection roles) { 87 | User user = userRepository.findByEmail(email); 88 | if (user == null) { 89 | user = new User(); 90 | user.setFirstName(firstName); 91 | user.setLastName(lastName); 92 | user.setPassword(passwordEncoder.encode(password)); 93 | user.setEmail(email); 94 | user.setEnabled(true); 95 | } 96 | user.setRoles(roles); 97 | user = userRepository.save(user); 98 | return user; 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /src/main/java/com/baeldung/spring/SpringTaskConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @Configuration 8 | @EnableScheduling 9 | @ComponentScan({ "com.baeldung.task" }) 10 | public class SpringTaskConfig { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/task/TokensPurgeTask.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.task; 2 | 3 | import com.baeldung.persistence.dao.PasswordResetTokenRepository; 4 | import com.baeldung.persistence.dao.VerificationTokenRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.scheduling.annotation.Scheduled; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import java.time.Instant; 11 | import java.util.Date; 12 | 13 | @Service 14 | @Transactional 15 | public class TokensPurgeTask { 16 | 17 | @Autowired 18 | VerificationTokenRepository tokenRepository; 19 | 20 | @Autowired 21 | PasswordResetTokenRepository passwordTokenRepository; 22 | 23 | @Scheduled(cron = "${purge.cron.expression}") 24 | public void purgeExpired() { 25 | 26 | Date now = Date.from(Instant.now()); 27 | 28 | passwordTokenRepository.deleteAllExpiredSince(now); 29 | tokenRepository.deleteAllExpiredSince(now); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/validation/EmailExistsException.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.validation; 2 | 3 | @SuppressWarnings("serial") 4 | public class EmailExistsException extends Throwable { 5 | 6 | public EmailExistsException(final String message) { 7 | super(message); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/validation/EmailValidator.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.validation; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | import jakarta.validation.ConstraintValidator; 7 | import jakarta.validation.ConstraintValidatorContext; 8 | 9 | public class EmailValidator implements ConstraintValidator { 10 | private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; 11 | private static final Pattern PATTERN = Pattern.compile(EMAIL_PATTERN); 12 | 13 | @Override 14 | public boolean isValid(final String username, final ConstraintValidatorContext context) { 15 | return (validateEmail(username)); 16 | } 17 | 18 | private boolean validateEmail(final String email) { 19 | Matcher matcher = PATTERN.matcher(email); 20 | return matcher.matches(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/validation/PasswordConstraintValidator.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.validation; 2 | 3 | import java.util.Arrays; 4 | 5 | import jakarta.validation.ConstraintValidator; 6 | import jakarta.validation.ConstraintValidatorContext; 7 | 8 | import org.passay.AlphabeticalSequenceRule; 9 | import org.passay.DigitCharacterRule; 10 | import org.passay.LengthRule; 11 | import org.passay.NumericalSequenceRule; 12 | import org.passay.PasswordData; 13 | import org.passay.PasswordValidator; 14 | import org.passay.QwertySequenceRule; 15 | import org.passay.RuleResult; 16 | import org.passay.SpecialCharacterRule; 17 | import org.passay.UppercaseCharacterRule; 18 | import org.passay.WhitespaceRule; 19 | 20 | import com.google.common.base.Joiner; 21 | 22 | public class PasswordConstraintValidator implements ConstraintValidator { 23 | 24 | @Override 25 | public void initialize(final ValidPassword arg0) { 26 | 27 | } 28 | 29 | @Override 30 | public boolean isValid(final String password, final ConstraintValidatorContext context) { 31 | // @formatter:off 32 | final PasswordValidator validator = new PasswordValidator(Arrays.asList( 33 | new LengthRule(8, 30), 34 | new UppercaseCharacterRule(1), 35 | new DigitCharacterRule(1), 36 | new SpecialCharacterRule(1), 37 | new NumericalSequenceRule(3,false), 38 | new AlphabeticalSequenceRule(3,false), 39 | new QwertySequenceRule(3,false), 40 | new WhitespaceRule())); 41 | final RuleResult result = validator.validate(new PasswordData(password)); 42 | if (result.isValid()) { 43 | return true; 44 | } 45 | context.disableDefaultConstraintViolation(); 46 | context.buildConstraintViolationWithTemplate(Joiner.on(",").join(validator.getMessages(result))).addConstraintViolation(); 47 | return false; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/validation/PasswordMatches.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.validation; 2 | 3 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 4 | import static java.lang.annotation.ElementType.TYPE; 5 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.Target; 10 | 11 | import jakarta.validation.Constraint; 12 | import jakarta.validation.Payload; 13 | 14 | @Target({ TYPE, ANNOTATION_TYPE }) 15 | @Retention(RUNTIME) 16 | @Constraint(validatedBy = PasswordMatchesValidator.class) 17 | @Documented 18 | public @interface PasswordMatches { 19 | 20 | String message() default "Passwords don't match"; 21 | 22 | Class[] groups() default {}; 23 | 24 | Class[] payload() default {}; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/validation/PasswordMatchesValidator.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.validation; 2 | 3 | import jakarta.validation.ConstraintValidator; 4 | import jakarta.validation.ConstraintValidatorContext; 5 | 6 | import com.baeldung.web.dto.UserDto; 7 | 8 | public class PasswordMatchesValidator implements ConstraintValidator { 9 | 10 | @Override 11 | public void initialize(final PasswordMatches constraintAnnotation) { 12 | // 13 | } 14 | 15 | @Override 16 | public boolean isValid(final Object obj, final ConstraintValidatorContext context) { 17 | final UserDto user = (UserDto) obj; 18 | return user.getPassword().equals(user.getMatchingPassword()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/validation/UserValidator.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.validation; 2 | 3 | import com.baeldung.web.dto.UserDto; 4 | import org.springframework.validation.Errors; 5 | import org.springframework.validation.ValidationUtils; 6 | import org.springframework.validation.Validator; 7 | 8 | public class UserValidator implements Validator { 9 | 10 | @Override 11 | public boolean supports(final Class clazz) { 12 | return UserDto.class.isAssignableFrom(clazz); 13 | } 14 | 15 | @Override 16 | public void validate(final Object obj, final Errors errors) { 17 | ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "message.firstName", "First Name is required."); 18 | ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "message.lastName", "Last Name is required."); 19 | ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "message.password", "Password is required."); 20 | ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "message.username", "UserName is required."); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/validation/ValidEmail.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.validation; 2 | 3 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 4 | import static java.lang.annotation.ElementType.FIELD; 5 | import static java.lang.annotation.ElementType.TYPE; 6 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 7 | 8 | import java.lang.annotation.Documented; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import jakarta.validation.Constraint; 13 | import jakarta.validation.Payload; 14 | 15 | @Target({ TYPE, FIELD, ANNOTATION_TYPE }) 16 | @Retention(RUNTIME) 17 | @Constraint(validatedBy = EmailValidator.class) 18 | @Documented 19 | public @interface ValidEmail { 20 | 21 | String message() default "Invalid Email"; 22 | 23 | Class[] groups() default {}; 24 | 25 | Class[] payload() default {}; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/validation/ValidPassword.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.validation; 2 | 3 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 4 | import static java.lang.annotation.ElementType.FIELD; 5 | import static java.lang.annotation.ElementType.TYPE; 6 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 7 | 8 | import java.lang.annotation.Documented; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import jakarta.validation.Constraint; 13 | import jakarta.validation.Payload; 14 | 15 | @Documented 16 | @Constraint(validatedBy = PasswordConstraintValidator.class) 17 | @Target({ TYPE, FIELD, ANNOTATION_TYPE }) 18 | @Retention(RUNTIME) 19 | public @interface ValidPassword { 20 | 21 | String message() default "Invalid Password"; 22 | 23 | Class[] groups() default {}; 24 | 25 | Class[] payload() default {}; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/controller/RegistrationCaptchaController.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.controller; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.validation.Valid; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.ApplicationEventPublisher; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import com.baeldung.captcha.CaptchaServiceV3; 14 | import com.baeldung.captcha.ICaptchaService; 15 | import com.baeldung.persistence.model.User; 16 | import com.baeldung.registration.OnRegistrationCompleteEvent; 17 | import com.baeldung.service.IUserService; 18 | import com.baeldung.web.dto.UserDto; 19 | import com.baeldung.web.util.GenericResponse; 20 | 21 | @RestController 22 | public class RegistrationCaptchaController { 23 | private final Logger LOGGER = LoggerFactory.getLogger(getClass()); 24 | 25 | @Autowired 26 | private IUserService userService; 27 | 28 | @Autowired 29 | private ICaptchaService captchaService; 30 | 31 | @Autowired 32 | private ICaptchaService captchaServiceV3; 33 | 34 | @Autowired 35 | private ApplicationEventPublisher eventPublisher; 36 | 37 | public RegistrationCaptchaController() { 38 | super(); 39 | } 40 | 41 | // Registration 42 | @PostMapping("/user/registrationCaptcha") 43 | public GenericResponse captchaRegisterUserAccount(@Valid final UserDto accountDto, final HttpServletRequest request) { 44 | 45 | final String response = request.getParameter("g-recaptcha-response"); 46 | captchaService.processResponse(response); 47 | 48 | return registerNewUserHandler(accountDto, request); 49 | } 50 | 51 | 52 | // Registration reCaptchaV3 53 | @PostMapping("/user/registrationCaptchaV3") 54 | public GenericResponse captchaV3RegisterUserAccount(@Valid final UserDto accountDto, final HttpServletRequest request) { 55 | 56 | final String response = request.getParameter("response"); 57 | captchaServiceV3.processResponse(response, CaptchaServiceV3.REGISTER_ACTION); 58 | 59 | return registerNewUserHandler(accountDto, request); 60 | } 61 | 62 | private GenericResponse registerNewUserHandler(final UserDto accountDto, final HttpServletRequest request) { 63 | LOGGER.debug("Registering user account with information: {}", accountDto); 64 | 65 | final User registered = userService.registerNewUserAccount(accountDto); 66 | eventPublisher.publishEvent(new OnRegistrationCompleteEvent(registered, request.getLocale(), getAppUrl(request))); 67 | return new GenericResponse("success"); 68 | } 69 | 70 | 71 | private String getAppUrl(HttpServletRequest request) { 72 | return "http://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath(); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/controller/RoleHierarchyController.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.servlet.ModelAndView; 6 | 7 | @Controller 8 | public class RoleHierarchyController { 9 | 10 | @GetMapping("/roleHierarchy") 11 | public ModelAndView roleHierarcy() { 12 | ModelAndView model = new ModelAndView(); 13 | model.addObject("adminMessage","Admin content available"); 14 | model.addObject("staffMessage","Staff content available"); 15 | model.addObject("userMessage","User content available"); 16 | model.setViewName("roleHierarchy"); 17 | return model; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.controller; 2 | 3 | import java.util.Locale; 4 | 5 | import com.baeldung.security.ActiveUserStore; 6 | import com.baeldung.service.IUserService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | 12 | @Controller 13 | public class UserController { 14 | 15 | @Autowired 16 | ActiveUserStore activeUserStore; 17 | 18 | @Autowired 19 | IUserService userService; 20 | 21 | @GetMapping("/loggedUsers") 22 | public String getLoggedUsers(final Locale locale, final Model model) { 23 | model.addAttribute("users", activeUserStore.getUsers()); 24 | return "users"; 25 | } 26 | 27 | @GetMapping("/loggedUsersFromSessionRegistry") 28 | public String getLoggedUsersFromSessionRegistry(final Locale locale, final Model model) { 29 | model.addAttribute("users", userService.getUsersFromSessionRegistry()); 30 | return "users"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/dto/PasswordDto.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.dto; 2 | 3 | import com.baeldung.validation.ValidPassword; 4 | 5 | public class PasswordDto { 6 | 7 | private String oldPassword; 8 | 9 | private String token; 10 | 11 | @ValidPassword 12 | private String newPassword; 13 | 14 | public String getOldPassword() { 15 | return oldPassword; 16 | } 17 | 18 | public void setOldPassword(String oldPassword) { 19 | this.oldPassword = oldPassword; 20 | } 21 | 22 | public String getNewPassword() { 23 | return newPassword; 24 | } 25 | 26 | public void setNewPassword(String newPassword) { 27 | this.newPassword = newPassword; 28 | } 29 | 30 | public String getToken() { 31 | return token; 32 | } 33 | 34 | public void setToken(String token) { 35 | this.token = token; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/dto/UserDto.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.dto; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import jakarta.validation.constraints.Size; 5 | 6 | import com.baeldung.validation.PasswordMatches; 7 | import com.baeldung.validation.ValidEmail; 8 | import com.baeldung.validation.ValidPassword; 9 | 10 | @PasswordMatches 11 | public class UserDto { 12 | @NotNull 13 | @Size(min = 1, message = "{Size.userDto.firstName}") 14 | private String firstName; 15 | 16 | @NotNull 17 | @Size(min = 1, message = "{Size.userDto.lastName}") 18 | private String lastName; 19 | 20 | @ValidPassword 21 | private String password; 22 | 23 | @NotNull 24 | @Size(min = 1) 25 | private String matchingPassword; 26 | 27 | @ValidEmail 28 | @NotNull 29 | @Size(min = 1, message = "{Size.userDto.email}") 30 | private String email; 31 | 32 | private boolean isUsing2FA; 33 | 34 | public String getEmail() { 35 | return email; 36 | } 37 | 38 | public void setEmail(final String email) { 39 | this.email = email; 40 | } 41 | 42 | private Integer role; 43 | 44 | public Integer getRole() { 45 | return role; 46 | } 47 | 48 | public void setRole(final Integer role) { 49 | this.role = role; 50 | } 51 | 52 | public String getFirstName() { 53 | return firstName; 54 | } 55 | 56 | public void setFirstName(final String firstName) { 57 | this.firstName = firstName; 58 | } 59 | 60 | public String getLastName() { 61 | return lastName; 62 | } 63 | 64 | public void setLastName(final String lastName) { 65 | this.lastName = lastName; 66 | } 67 | 68 | public String getPassword() { 69 | return password; 70 | } 71 | 72 | public void setPassword(final String password) { 73 | this.password = password; 74 | } 75 | 76 | public String getMatchingPassword() { 77 | return matchingPassword; 78 | } 79 | 80 | public void setMatchingPassword(final String matchingPassword) { 81 | this.matchingPassword = matchingPassword; 82 | } 83 | 84 | public boolean isUsing2FA() { 85 | return isUsing2FA; 86 | } 87 | 88 | public void setUsing2FA(boolean isUsing2FA) { 89 | this.isUsing2FA = isUsing2FA; 90 | } 91 | 92 | @Override 93 | public String toString() { 94 | final StringBuilder builder = new StringBuilder(); 95 | builder.append("UserDto [firstName=") 96 | .append(firstName) 97 | .append(", lastName=") 98 | .append(lastName) 99 | .append(", email=") 100 | .append(email) 101 | .append(", isUsing2FA=") 102 | .append(isUsing2FA) 103 | .append(", role=") 104 | .append(role).append("]"); 105 | return builder.toString(); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/error/InvalidOldPasswordException.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.error; 2 | 3 | public final class InvalidOldPasswordException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 5861310537366287163L; 6 | 7 | public InvalidOldPasswordException() { 8 | super(); 9 | } 10 | 11 | public InvalidOldPasswordException(final String message, final Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public InvalidOldPasswordException(final String message) { 16 | super(message); 17 | } 18 | 19 | public InvalidOldPasswordException(final Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/error/ReCaptchaInvalidException.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.error; 2 | 3 | public final class ReCaptchaInvalidException extends RuntimeException { 4 | 5 | public ReCaptchaInvalidException() { 6 | super(); 7 | } 8 | 9 | public ReCaptchaInvalidException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | public ReCaptchaInvalidException(final String message) { 14 | super(message); 15 | } 16 | 17 | public ReCaptchaInvalidException(final Throwable cause) { 18 | super(cause); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/error/ReCaptchaUnavailableException.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.error; 2 | 3 | public final class ReCaptchaUnavailableException extends RuntimeException { 4 | 5 | public ReCaptchaUnavailableException() { 6 | super(); 7 | } 8 | 9 | public ReCaptchaUnavailableException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | public ReCaptchaUnavailableException(final String message) { 14 | super(message); 15 | } 16 | 17 | public ReCaptchaUnavailableException(final Throwable cause) { 18 | super(cause); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/error/UnusualLocationException.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.error; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | 5 | public final class UnusualLocationException extends AuthenticationException { 6 | 7 | private static final long serialVersionUID = 5861310537366287163L; 8 | 9 | public UnusualLocationException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | public UnusualLocationException(final String message) { 14 | super(message); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/error/UserAlreadyExistException.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.error; 2 | 3 | public final class UserAlreadyExistException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 5861310537366287163L; 6 | 7 | public UserAlreadyExistException() { 8 | super(); 9 | } 10 | 11 | public UserAlreadyExistException(final String message, final Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public UserAlreadyExistException(final String message) { 16 | super(message); 17 | } 18 | 19 | public UserAlreadyExistException(final Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/error/UserNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.error; 2 | 3 | public final class UserNotFoundException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 5861310537366287163L; 6 | 7 | public UserNotFoundException() { 8 | super(); 9 | } 10 | 11 | public UserNotFoundException(final String message, final Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public UserNotFoundException(final String message) { 16 | super(message); 17 | } 18 | 19 | public UserNotFoundException(final Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/baeldung/web/util/GenericResponse.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.web.util; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import org.springframework.validation.FieldError; 7 | import org.springframework.validation.ObjectError; 8 | 9 | public class GenericResponse { 10 | private String message; 11 | private String error; 12 | 13 | public GenericResponse(final String message) { 14 | super(); 15 | this.message = message; 16 | } 17 | 18 | public GenericResponse(final String message, final String error) { 19 | super(); 20 | this.message = message; 21 | this.error = error; 22 | } 23 | 24 | public GenericResponse(List allErrors, String error) { 25 | this.error = error; 26 | String temp = allErrors.stream().map(e -> { 27 | if (e instanceof FieldError) { 28 | return "{\"field\":\"" + ((FieldError) e).getField() + "\",\"defaultMessage\":\"" + e.getDefaultMessage() + "\"}"; 29 | } else { 30 | return "{\"object\":\"" + e.getObjectName() + "\",\"defaultMessage\":\"" + e.getDefaultMessage() + "\"}"; 31 | } 32 | }).collect(Collectors.joining(",")); 33 | this.message = "[" + temp + "]"; 34 | } 35 | 36 | public String getMessage() { 37 | return message; 38 | } 39 | 40 | public void setMessage(final String message) { 41 | this.message = message; 42 | } 43 | 44 | public String getError() { 45 | return error; 46 | } 47 | 48 | public void setError(final String error) { 49 | this.error = error; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/resources/.gitignore: -------------------------------------------------------------------------------- 1 | email.properties -------------------------------------------------------------------------------- /src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | ################### H2 Console Configuration ########################## 2 | spring.h2.console.enabled=true 3 | spring.h2.console.path=/h2 4 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.thymeleaf.cache=false 2 | # 5am every day 3 | purge.cron.expression=0 0 5 * * ? 4 | 5 | server.port=8081 6 | 7 | #Feature toggle for geo ip lib 8 | geo.ip.lib.enabled=false 9 | 10 | # Uncomment the keys to enable captcha 11 | #google.recaptcha.key.site=6LfaHiITAAAAAAgZBHl4ZUZAYk5RlOYTr6m2N34X 12 | #google.recaptcha.key.secret=6LfaHiITAAAAANpDTA_Zflwib95IhDqg2SNRLt4U 13 | 14 | # recaptcha v3 keys 15 | google.recaptcha.key.site=6LefKOAUAAAAAE9c8M_Das3vwhlMPmkFTAzvxokN 16 | google.recaptcha.key.secret=6LefKOAUAAAAAGs0hOsCoOBu14TKDGu100LkpnVo 17 | google.recaptcha.key.threshold=0.5 18 | 19 | ################### JavaMail Configuration ########################## 20 | support.email=USERNAME@gmail.com 21 | spring.mail.host=smtp.gmail.com 22 | spring.mail.port=465 23 | spring.mail.protocol=smtps 24 | spring.mail.username=USERNAME@gmail.com 25 | spring.mail.password=PASSWORD 26 | spring.mail.properties.mail.transport.protocol=smtps 27 | spring.mail.properties.mail.smtps.auth=true 28 | spring.mail.properties.mail.smtps.starttls.enable=true 29 | spring.mail.properties.mail.smtps.timeout=8000 30 | 31 | # uncomment this property to see the SQL statements generated 32 | #logging.level.org.hibernate.SQL=DEBUG 33 | 34 | spring.main.allow-bean-definition-overriding=true 35 | 36 | ##### MySQL 37 | #################### DataSource Configuration ########################## 38 | #spring.datasource.url=jdbc:mysql://localhost:3306/registration_02?createDatabaseIfNotExist=true 39 | #spring.datasource.username=tutorialuser 40 | #spring.datasource.password=tutorialmy5ql 41 | #################### Hibernate Configuration ########################## 42 | #spring.jpa.show-sql=false 43 | #spring.jpa.hibernate.ddl-auto=update 44 | 45 | ####### H2 46 | ################### DataSource Configuration ########################## 47 | spring.datasource.url=jdbc:h2:mem:registration_02;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE 48 | spring.datasource.username=sa 49 | spring.datasource.password= 50 | ################### Hibernate Configuration ########################## 51 | spring.jpa.show-sql=false 52 | spring.jpa.hibernate.ddl-auto=update 53 | 54 | ##### activate dev profile 55 | spring.profiles.active=dev 56 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | web - %date [%thread] %-5level %logger{5} - %message%n%stack{5,1} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/maxmind/COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Database and Contents Copyright (c) 2020 MaxMind, Inc. 2 | -------------------------------------------------------------------------------- /src/main/resources/maxmind/GeoLite2-City.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baeldung/spring-security-registration/ddfba5867b5eba89a5e0b717b2ff247456b87a80/src/main/resources/maxmind/GeoLite2-City.mmdb -------------------------------------------------------------------------------- /src/main/resources/maxmind/GeoLite2-Country.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baeldung/spring-security-registration/ddfba5867b5eba89a5e0b717b2ff247456b87a80/src/main/resources/maxmind/GeoLite2-Country.mmdb -------------------------------------------------------------------------------- /src/main/resources/maxmind/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Use of this MaxMind product is governed by MaxMind's GeoLite2 End User License Agreement, 2 | which can be viewed at https://www.maxmind.com/en/geolite2/eula. 3 | 4 | This database incorporates GeoNames [https://www.geonames.org] geographical data, 5 | which is made available under the Creative Commons Attribution 4.0 License. 6 | To view a copy of this license, visit https://creativecommons.org/licenses/by/4.0/. 7 | -------------------------------------------------------------------------------- /src/main/resources/messages_en.properties: -------------------------------------------------------------------------------- 1 | message.username=Username required 2 | message.password=Password required 3 | message.unauth=Unauthorized Access !! 4 | message.badCredentials=Invalid Credentials 5 | message.sessionExpired=Session Timed Out 6 | message.logoutError=Sorry, error logging out 7 | message.logoutSucc=You logged out successfully 8 | message.regSucc=You registered successfully. We will send you a confirmation message to your email account. 9 | message.regSuccLink=You registered successfully. To confirm your registration, please click on the below link. 10 | message.regSuccConfirmed=Your registration is confirmed. 11 | message.regError=An account for that username/email already exists. Please enter a different username. 12 | message.lastName=Last name is required 13 | message.firstName=First name required 14 | message.badEmail=Invalid email address 15 | message.email.config.error=Error in java mail configuration 16 | token.message=Your token is: 17 | auth.message.disabled=Your account is disabled please check your mail and click on the confirmation link 18 | auth.message.expired=Your registration token has expired. Please register again. 19 | auth.message.invalidUser=This username is invalid, or does not exist. 20 | auth.message.invalidToken=Invalid token. 21 | label.user.email=Email: 22 | label.user.firstName=First name: 23 | label.user.lastName=Last name: 24 | label.user.password=Password: 25 | label.user.confirmPass=Confirm password 26 | label.user.2fa=Use Two step verification 27 | label.form.submit=Submit 28 | label.form.title=Registration Form 29 | label.form.loginLink=Back to login 30 | label.login=Login here 31 | label.form.loginTitle=Login 32 | label.form.loginEmail=Email 33 | label.form.loginPass=Password 34 | label.form.login2fa=Google Authenticator Verification Code 35 | label.form.loginEnglish=English 36 | label.form.loginSpanish=Spanish 37 | label.form.loginSignUp=Sign up 38 | label.form.loginSignUpCaptcha=Sign up with Captcha 39 | label.form.loginSignUpReCaptchaV3=Sign up with reCAPTCHA v3 40 | label.form.rememberMe=Remember Me 41 | label.pages.logout=Logout 42 | label.pages.admin=Administrator 43 | label.pages.home.title=Home 44 | label.pages.home.message=Welcome Home 45 | label.pages.users.message=View Logged In Users 46 | label.pages.users.sessionregistry.message=View Logged In Users from SessionRegistry 47 | label.pages.admin.message=Welcome Admin 48 | label.pages.user.message=Welcome User 49 | label.successRegister.title=Registration Success 50 | label.badUser.title=Invalid Link 51 | ValidEmail.user.email=Invalid email address! 52 | UniqueUsername.user.username=An account with that username/email already exists 53 | Size.userDto.firstName=Length must be greater than {min} 54 | Size.userDto.lastName=Length must be greater than {min} 55 | Size.userDto.email=Length must be greater than {min} 56 | NotNull.user.firstName=First name required 57 | NotEmpty.user.firstName=First name required 58 | NotNull.user.lastName=Last name required 59 | NotEmpty.user.lastName=Last name required 60 | NotNull.user.username=Username(Email) required 61 | NotEmpty.user.username=Username(Email) required 62 | NotNull.user.password=Password required 63 | NotEmpty.user.password=Password required 64 | NotNull.user.matchingPassword=Required 65 | NotEmpty.user.matchingPassword=Required 66 | PasswordMatches.user:Password does not match! 67 | Email.user.email=Invalid Username (Email) 68 | label.form.resendRegistrationToken=Re-send Token 69 | message.resendToken=We will send an email with a new registration token to your email account 70 | message.forgetPassword=Forget Password 71 | message.resetPassword=Reset Password 72 | message.updatePassword=Update Password 73 | message.userNotFound=User Not Found 74 | auth.message.blocked=This ip is blocked for 24 hours 75 | message.accountVerified=Your account verified successfully 76 | message.resetPasswordSuc=Password reset successfully 77 | message.resetYourPassword=Reset your password 78 | message.resetPasswordEmail=You should receive an Password Reset Email shortly 79 | message.error=Error Occurred 80 | message.updatePasswordSuc=Password updated successfully 81 | message.changePassword=Change Password 82 | message.invalidOldPassword=Invalid Old Password 83 | message.invalidReCaptcha=Invalid reCaptcha 84 | message.unavailableReCaptcha=Registration is unavailable at this time. Please try again later. 85 | label.user.newPassword=New Password 86 | label.user.oldPassword=Old Password 87 | error.wordLength=Your password is too short 88 | error.wordNotEmail=Do not use your email as your password 89 | error.wordSequences=Your password contains sequences 90 | error.wordLowercase=Use lower case characters 91 | error.wordUppercase=Use upper case characters 92 | error.wordOneNumber=Use numbers 93 | error.wordOneSpecialChar=Use special characters 94 | message.differentLocation=There was a login attempt from unusual location at {0}, we blocked the connection which was from {1} with IP address {2} \r\n If that was you, please enable this new location at {3} \r\n If it was not you, then you need to change your password {4} 95 | message.newLoc.enabled = New Location {0} is now enabled 96 | auth.message.unusual.location = You are trying to login from unusual location, check your email for more details 97 | message.login.notification.deviceDetails=Device details: 98 | message.login.notification.location=Location: 99 | message.login.notification.ip=IP Address: -------------------------------------------------------------------------------- /src/main/resources/templates/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 19 |
20 |

unauth

21 | 22 |

admin

23 |
24 | 25 | -------------------------------------------------------------------------------- /src/main/resources/templates/badUser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bad user 5 | 6 | 7 |
8 |

error

9 |
10 | signup 11 | 12 |
13 |
14 |

resend

15 | 16 | 17 | 18 | 19 | 42 | 43 |
44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/resources/templates/changePassword.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | change password 14 | 15 | 16 | 26 |
27 |
28 | 29 |

change password

30 |
31 |
32 | 33 | 34 | 35 | 36 |

37 | 38 | 39 |
40 | 41 |

42 | 43 |
44 | 45 | 46 | 47 |
48 |

49 | 51 |
52 |
53 | 54 |
55 |
56 | 57 | 58 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/main/resources/templates/console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 |
18 |
message
19 | 20 |

This is the landing page for the admin

21 | 22 |
This text is only visible to a user
23 | 24 |
This text is only visible to an admin
25 | 26 | admin 27 |
28 | change password 29 |
30 |
31 |
32 | 33 |
34 | You are using Two-step authentication Disable 2FA 35 |
36 | 37 |
38 | You are not using Two-step authentication Enable 2FA 39 |
40 | 41 |
42 | 48 |
49 | 50 | 51 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/resources/templates/customLogin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Custom Login 6 | 40 | 44 | 45 | 46 |
message
47 | 48 | 49 |
error
50 | 51 |
52 |
53 |

Header

54 |

55 | 56 |
57 |   58 | 59 |

60 | 61 | 62 | 63 |

64 | 65 | 66 | 67 | 68 |

69 | 70 | 71 |
72 |
73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/resources/templates/emailError.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | home 5 | 6 | 7 |
8 |

error

9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/templates/expiredAccount.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | home 5 | 6 | 7 |
8 |

expired

9 |
10 | signup 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/templates/forgetPassword.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | reset 7 | 8 | 9 |
10 |

reset

11 |
12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 |
21 | registration 22 |

23 | login 24 | 25 |
26 | 27 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/main/resources/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | home 6 | 7 | 8 | 18 |
19 |

home

20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/resources/templates/homepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | home 5 | 6 | 7 | 17 | 18 |
19 |
user
20 | 21 |
22 | 23 |

user

24 | 25 | admin 26 |

27 | View logged in users 28 |
29 | View logged in users 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/resources/templates/invalidSession.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | home 5 | 6 | 7 |
8 |

expired

9 | login 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 26 | 30 | 31 | 32 |
message
33 | 34 | 35 |
error
36 | 37 |
38 |
39 |

Header

40 | login | 41 | login 42 |

43 | 44 |
45 |   46 | 47 |

48 | 49 | 50 | 51 |

52 | 53 | 54 | 55 |

56 | 57 | 58 | 59 |

60 | 61 | 62 |
63 |
Current Locale :

64 | signup 65 |
66 | captcha signup 67 |

68 | recaptcha v3 signup 69 |

70 | reset 71 |
72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/resources/templates/loginRememberMe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 26 | 30 | 31 | 32 |
message
33 | 34 | 35 |
error
36 | 37 |
38 |
39 |

Header

40 | login | 41 | login 42 |

43 | 44 |
45 |   46 | 47 |

48 | 49 | 50 | 51 |

52 | 53 | 54 | 55 |

56 | 57 | 58 | 59 |
60 |
61 | 62 | 63 | 64 |

65 | 66 | 67 |
68 |
Current Locale :

69 | signup 70 |
71 | captcha signup 72 |

73 | reset 74 |
75 |
76 | 77 | 78 | -------------------------------------------------------------------------------- /src/main/resources/templates/logout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | home 7 | 8 | 9 | 10 |
11 |

error

12 | 13 |

success

14 |


15 | login 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/templates/qrcode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | QR code 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Scan this Barcode using Google Authenticator app on your phone to use it later in login 14 | Android and 15 | iPhone 16 |

17 |
18 | 19 |
20 | 21 | 22 | Go to login page 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/templates/registrationConfirm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | home 6 | 7 | 8 |
9 | 10 |

success

11 |
12 | token 13 | token 14 |
15 | login 16 |
17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/templates/roleHierarchy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 19 |
20 |

Role Hierarchy

21 |

Access to ADMIN section

22 |

Access to STAFF section

23 |

Access to USER section

24 |
25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/templates/successRegister.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | home 7 | 8 | 9 |
10 |

success

11 | login 12 |
13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/templates/updatePassword.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | update password 14 | 15 | 16 |
17 |
18 |
19 |

reset

20 |
21 |
22 | 23 | 24 |
25 |

26 | 27 | 28 |
29 |

30 | 31 | 32 | 33 | 34 |
35 |

36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 | 108 |
109 | 110 | 111 | -------------------------------------------------------------------------------- /src/main/resources/templates/users.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | home 7 | 8 | 9 | 19 |
20 |

Currently logged in users

21 |
22 |

user

23 |
24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/webSecurityConfig-basic.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 29 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/resources/webSecurityConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/spring/ConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | @Configuration 10 | // @ComponentScan("com.baeldung.test") 11 | public class ConfigTest implements WebMvcConfigurer { 12 | 13 | public ConfigTest() { 14 | super(); 15 | } 16 | 17 | // API 18 | @Bean 19 | public PasswordEncoder encoder() { 20 | return new BCryptPasswordEncoder(11); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/test/java/com/baeldung/spring/TestDbConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.boot.autoconfigure.domain.EntityScan; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 8 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 9 | import org.springframework.security.crypto.password.PasswordEncoder; 10 | 11 | @Configuration 12 | @EnableJpaRepositories("com.baeldung.persistence.dao") 13 | @EntityScan("com.baeldung.persistence.model") 14 | @EnableAutoConfiguration 15 | public class TestDbConfig { 16 | 17 | @Bean 18 | public PasswordEncoder encoder() { 19 | return new BCryptPasswordEncoder(11); 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/java/com/baeldung/spring/TestIntegrationConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import com.maxmind.geoip2.DatabaseReader; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 5 | import org.springframework.boot.test.mock.mockito.MockBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.mail.javamail.JavaMailSender; 9 | import org.springframework.security.core.session.SessionRegistry; 10 | import org.springframework.security.core.session.SessionRegistryImpl; 11 | import org.springframework.web.context.request.RequestContextListener; 12 | 13 | @Configuration 14 | public class TestIntegrationConfig { 15 | 16 | @Bean 17 | @ConditionalOnMissingBean(RequestContextListener.class) 18 | public RequestContextListener requestContextListener() { 19 | 20 | return new RequestContextListener(); 21 | } 22 | 23 | @Bean 24 | public SessionRegistry sessionRegistry() { 25 | return new SessionRegistryImpl(); 26 | } 27 | 28 | @MockBean 29 | private JavaMailSender javaMailSender; 30 | 31 | @MockBean(name = "GeoIPCountry") 32 | private DatabaseReader databaseReader; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/spring/TestTaskConfig.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.spring; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @ComponentScan({ "com.baeldung.task" }) 8 | public class TestTaskConfig { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/test/GetLoggedUsersIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.test; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 16 | import org.springframework.security.crypto.password.PasswordEncoder; 17 | import org.springframework.test.context.junit.jupiter.SpringExtension; 18 | 19 | import com.baeldung.Application; 20 | import com.baeldung.persistence.dao.UserRepository; 21 | import com.baeldung.persistence.model.User; 22 | import com.baeldung.spring.TestDbConfig; 23 | import com.baeldung.spring.TestIntegrationConfig; 24 | 25 | import io.restassured.RestAssured; 26 | import io.restassured.authentication.FormAuthConfig; 27 | import io.restassured.response.Response; 28 | import io.restassured.specification.RequestSpecification; 29 | 30 | 31 | @ExtendWith(SpringExtension.class) 32 | @SpringBootTest(classes = { Application.class, TestDbConfig.class, TestIntegrationConfig.class }, webEnvironment = WebEnvironment.RANDOM_PORT) 33 | public class GetLoggedUsersIntegrationTest { 34 | 35 | @Autowired 36 | private UserRepository userRepository; 37 | 38 | @Autowired 39 | private PasswordEncoder passwordEncoder; 40 | 41 | @Value("${local.server.port}") 42 | int port; 43 | 44 | private FormAuthConfig formConfig; 45 | private String LOGGED_USERS_URL, SESSION_REGISTRY_LOGGED_USERS_URL; 46 | 47 | // 48 | 49 | @BeforeEach 50 | public void init() { 51 | User user = userRepository.findByEmail("test@test.com"); 52 | if (user == null) { 53 | user = new User(); 54 | user.setFirstName("Test"); 55 | user.setLastName("Test"); 56 | user.setPassword(passwordEncoder.encode("test")); 57 | user.setEmail("test@test.com"); 58 | user.setEnabled(true); 59 | userRepository.save(user); 60 | } else { 61 | user.setPassword(passwordEncoder.encode("test")); 62 | userRepository.save(user); 63 | } 64 | 65 | RestAssured.port = port; 66 | RestAssured.baseURI = "http://localhost"; 67 | LOGGED_USERS_URL = "/loggedUsers"; 68 | SESSION_REGISTRY_LOGGED_USERS_URL = "/loggedUsersFromSessionRegistry"; 69 | formConfig = new FormAuthConfig("/login", "username", "password"); 70 | } 71 | 72 | @Test 73 | public void givenLoggedInUser_whenGettingLoggedUsersFromActiveUserStore_thenResponseContainsUser() { 74 | final RequestSpecification request = RestAssured.given().auth().form("test@test.com", "test", formConfig); 75 | 76 | final Map params = new HashMap<>(); 77 | params.put("password", "test"); 78 | 79 | final Response response = request.with().params(params).get(LOGGED_USERS_URL); 80 | 81 | assertEquals(200, response.statusCode()); 82 | assertTrue(response.body().asString().contains("test@test.com")); 83 | } 84 | 85 | @Test 86 | public void givenLoggedInUser_whenGettingLoggedUsersFromSessionRegistry_thenResponseContainsUser() { 87 | final RequestSpecification request = RestAssured.given().auth().form("test@test.com", "test", formConfig); 88 | 89 | final Map params = new HashMap<>(); 90 | params.put("password", "test"); 91 | 92 | final Response response = request.with().params(params).get(SESSION_REGISTRY_LOGGED_USERS_URL); 93 | 94 | assertEquals(200, response.statusCode()); 95 | assertTrue(response.body().asString().contains("test@test.com")); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/test/IntegrationSuite.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.test; 2 | 3 | import org.junit.platform.runner.JUnitPlatform; 4 | import org.junit.platform.suite.api.SelectClasses; 5 | import org.junit.runner.RunWith; 6 | 7 | @RunWith(JUnitPlatform.class) 8 | @SelectClasses({ // @formatter:off 9 | ChangePasswordIntegrationTest.class, 10 | DeviceServiceIntegrationTest.class, 11 | TokenExpirationIntegrationTest.class, 12 | RegistrationControllerIntegrationTest.class, 13 | GetLoggedUsersIntegrationTest.class, 14 | UserServiceIntegrationTest.class, 15 | UserIntegrationTest.class, 16 | SpringSecurityRolesIntegrationTest.class, 17 | LocalizationIntegrationTest.class 18 | })// @formatter:on 19 | public class IntegrationSuite { 20 | // 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/test/LocalizationIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.test; 2 | 3 | import static org.hamcrest.Matchers.containsString; 4 | 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.context.junit.jupiter.SpringExtension; 11 | import com.baeldung.Application; 12 | import com.baeldung.spring.TestIntegrationConfig; 13 | import io.restassured.RestAssured; 14 | import io.restassured.specification.RequestSpecification; 15 | 16 | @ExtendWith(SpringExtension.class) 17 | @SpringBootTest(classes = { Application.class, TestIntegrationConfig.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 18 | public class LocalizationIntegrationTest { 19 | 20 | @Value("${local.server.port}") 21 | int port; 22 | 23 | @BeforeEach 24 | public void init() { 25 | RestAssured.port = port; 26 | RestAssured.baseURI = "http://localhost"; 27 | } 28 | 29 | @Test 30 | public void given_theLanuageParamterIsEnglish_then_the_title_of_the_log_page_is_Login() { 31 | final RequestSpecification request = RestAssured.given().param("lang", "en"); 32 | request.when().get("/login").then().assertThat().statusCode(200).and().body(containsString("

Login

")); 33 | } 34 | 35 | @Test 36 | public void given_theLanuageParamterIsSpanish_then_the_title_of_the_log_page_is_Ingreso() { 37 | final RequestSpecification request = RestAssured.given().param("lang", "es_ES"); 38 | request.when().get("/login").then().assertThat().statusCode(200).and().body(containsString("

Ingreso

")); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/test/LockAccountAfterSeveralTriesIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.test; 2 | 3 | import static com.baeldung.security.LoginAttemptService.MAX_ATTEMPT; 4 | import static org.hamcrest.Matchers.containsString; 5 | import static org.hamcrest.Matchers.not; 6 | 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | import org.springframework.test.context.junit.jupiter.SpringExtension; 15 | 16 | import com.baeldung.Application; 17 | import com.baeldung.persistence.dao.UserRepository; 18 | import com.baeldung.persistence.model.User; 19 | import com.baeldung.spring.TestDbConfig; 20 | import com.baeldung.spring.TestIntegrationConfig; 21 | 22 | import io.restassured.RestAssured; 23 | import io.restassured.authentication.FormAuthConfig; 24 | import io.restassured.specification.RequestSpecification; 25 | 26 | /** 27 | * Test class for the case to see that the user is blocked after several tries 28 | */ 29 | @ExtendWith(SpringExtension.class) 30 | @SpringBootTest(classes = { Application.class, TestDbConfig.class, TestIntegrationConfig.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 31 | public class LockAccountAfterSeveralTriesIntegrationTest { 32 | 33 | @Autowired 34 | private UserRepository userRepository; 35 | 36 | @Autowired 37 | private PasswordEncoder passwordEncoder; 38 | 39 | @Value("${local.server.port}") 40 | int port; 41 | 42 | private FormAuthConfig formConfig; 43 | 44 | @BeforeEach 45 | public void init() { 46 | User user = userRepository.findByEmail("test@test.com"); 47 | if (user == null) { 48 | user = new User(); 49 | user.setFirstName("Test"); 50 | user.setLastName("Test"); 51 | user.setPassword(passwordEncoder.encode("test")); 52 | user.setEmail("test@test.com"); 53 | user.setEnabled(true); 54 | userRepository.save(user); 55 | } else { 56 | user.setPassword(passwordEncoder.encode("test")); 57 | userRepository.save(user); 58 | } 59 | 60 | RestAssured.port = port; 61 | RestAssured.baseURI = "http://localhost"; 62 | formConfig = new FormAuthConfig("/login", "username", "password"); 63 | } 64 | 65 | @Test 66 | public void givenLoggedInUser_whenUsernameOrPasswordIsIncorrectAfterMaxAttempt_thenUserBlockFor24Hours() { 67 | //first request where a user tries several incorrect credential 68 | for (int i = 0; i < MAX_ATTEMPT - 2; i++) { 69 | final RequestSpecification requestIncorrect = RestAssured.given().auth().form("testtesefsdt.com" + i, "tesfsdft", formConfig); 70 | 71 | requestIncorrect.when().get("/console").then().assertThat().statusCode(200).and().body(not(containsString("home"))); 72 | } 73 | 74 | //then user tries a correct user 75 | final RequestSpecification request = RestAssured.given().auth().form("test@test.com", "test", formConfig); 76 | 77 | request.when().get("/console").then().assertThat().statusCode(200).and().body(containsString("home")); 78 | 79 | for (int i = 0; i < 3; i++) { 80 | final RequestSpecification requestSecond = RestAssured.given().auth().form("testtesefsdt.com", "tesfsdft", formConfig); 81 | 82 | requestSecond.when().get("/console").then().assertThat().statusCode(200).and().body(not(containsString("home"))); 83 | } 84 | 85 | //the third request where we can see that the user is blocked even if he previously entered a correct credential 86 | final RequestSpecification requestCorrect = RestAssured.given().auth().form("test@test.com", "test", formConfig); 87 | 88 | requestCorrect.when().get("/console").then().assertThat().statusCode(200).and().body(not(containsString("home"))); 89 | } 90 | } -------------------------------------------------------------------------------- /src/test/java/com/baeldung/test/RegistrationControllerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.test; 2 | 3 | import static org.hamcrest.Matchers.containsString; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 6 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 8 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 12 | 13 | import java.time.Instant; 14 | import java.time.temporal.ChronoUnit; 15 | import java.util.Date; 16 | import java.util.UUID; 17 | 18 | import jakarta.persistence.EntityManager; 19 | import jakarta.persistence.PersistenceContext; 20 | 21 | import org.junit.jupiter.api.BeforeEach; 22 | import org.junit.jupiter.api.Test; 23 | import org.junit.jupiter.api.extension.ExtendWith; 24 | import org.springframework.beans.factory.annotation.Autowired; 25 | import org.springframework.boot.test.context.SpringBootTest; 26 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 27 | import org.springframework.http.MediaType; 28 | import org.springframework.test.context.junit.jupiter.SpringExtension; 29 | import org.springframework.test.web.servlet.MockMvc; 30 | import org.springframework.test.web.servlet.ResultActions; 31 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 32 | import org.springframework.transaction.annotation.Transactional; 33 | import org.springframework.util.LinkedMultiValueMap; 34 | import org.springframework.util.MultiValueMap; 35 | import org.springframework.web.context.WebApplicationContext; 36 | 37 | import com.baeldung.Application; 38 | import com.baeldung.persistence.model.User; 39 | import com.baeldung.persistence.model.VerificationToken; 40 | import com.baeldung.spring.TestDbConfig; 41 | import com.baeldung.spring.TestIntegrationConfig; 42 | 43 | @ExtendWith(SpringExtension.class) 44 | @SpringBootTest(classes = { Application.class, TestDbConfig.class, TestIntegrationConfig.class }, webEnvironment = WebEnvironment.RANDOM_PORT) 45 | @Transactional 46 | class RegistrationControllerIntegrationTest { 47 | 48 | @Autowired 49 | private WebApplicationContext webApplicationContext; 50 | 51 | @PersistenceContext 52 | private EntityManager entityManager; 53 | 54 | private MockMvc mockMvc; 55 | private String token; 56 | 57 | @BeforeEach 58 | public void setUp() { 59 | mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 60 | 61 | User user = new User(); 62 | user.setEmail(UUID.randomUUID().toString() + "@example.com"); 63 | user.setPassword(UUID.randomUUID().toString()); 64 | user.setFirstName("First"); 65 | user.setLastName("Last"); 66 | 67 | entityManager.persist(user); 68 | token = UUID.randomUUID().toString(); 69 | VerificationToken verificationToken = new VerificationToken(token, user); 70 | verificationToken.setExpiryDate(Date.from(Instant.now().plus(2, ChronoUnit.DAYS))); 71 | 72 | entityManager.persist(verificationToken); 73 | 74 | /* 75 | flush managed entities to the database to populate identifier field 76 | */ 77 | entityManager.flush(); 78 | entityManager.clear(); 79 | } 80 | 81 | @Test 82 | void testRegistrationConfirm() throws Exception { 83 | ResultActions resultActions = this.mockMvc.perform(get("/registrationConfirm?token=" + token)); 84 | resultActions.andExpect(status().is3xxRedirection()); 85 | resultActions.andExpect(model().attribute("messageKey", "message.accountVerified")); 86 | resultActions.andExpect(view().name("redirect:/console")); 87 | } 88 | 89 | @Test 90 | void testRegistrationValidation() throws Exception { 91 | 92 | final MultiValueMap param = new LinkedMultiValueMap<>(); 93 | param.add("firstName", ""); 94 | param.add("lastName", ""); 95 | param.add("email", ""); 96 | param.add("password", ""); 97 | param.add("matchingPassword", ""); 98 | 99 | ResultActions resultActions = this.mockMvc.perform(post("/user/registration").params(param)); 100 | resultActions.andExpect(status().is(400)); 101 | resultActions.andExpect(content().contentType(MediaType.APPLICATION_JSON)).andExpect(jsonPath("$.error", is("InvaliduserDto"))) 102 | .andExpect(jsonPath("$.message", containsString("{\"field\":\"lastName\",\"defaultMessage\":\"Length must be greater than 1\"}"))); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/test/RegistrationPasswordLiveTest.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.test; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.UUID; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | 13 | import io.restassured.RestAssured; 14 | import io.restassured.response.Response; 15 | 16 | public class RegistrationPasswordLiveTest { 17 | private final String BASE_URI = "http://localhost:8081/"; 18 | 19 | @Test 20 | public void givenInvalidPassword_thenBadRequest() { 21 | // too short 22 | assertEquals(HttpStatus.BAD_REQUEST.value(), getResponseForPassword("123")); 23 | 24 | // no special character 25 | assertEquals(HttpStatus.BAD_REQUEST.value(), getResponseForPassword("1abZRplYU")); 26 | 27 | // no upper case letter 28 | assertEquals(HttpStatus.BAD_REQUEST.value(), getResponseForPassword("1_abidpsvl")); 29 | 30 | // no number 31 | assertEquals(HttpStatus.BAD_REQUEST.value(), getResponseForPassword("abZRYUpl")); 32 | 33 | // alphabet sequence 34 | assertEquals(HttpStatus.BAD_REQUEST.value(), getResponseForPassword("1_abcZRYU")); 35 | 36 | // qwerty sequence 37 | assertEquals(HttpStatus.BAD_REQUEST.value(), getResponseForPassword("1_abZRTYU")); 38 | 39 | // numeric sequence 40 | assertEquals(HttpStatus.BAD_REQUEST.value(), getResponseForPassword("123_zqrtU")); 41 | 42 | // valid password 43 | assertEquals(HttpStatus.OK.value(), getResponseForPassword("12_zwRHIPKA")); 44 | } 45 | 46 | private int getResponseForPassword(String pass) { 47 | final Map param = new HashMap<>(); 48 | final String randomName = UUID.randomUUID().toString(); 49 | param.put("firstName", randomName); 50 | param.put("lastName", "Doe"); 51 | param.put("email", randomName + "@x.com"); 52 | param.put("password", pass); 53 | param.put("matchingPassword", pass); 54 | 55 | final Response response = RestAssured.given().formParams(param).accept(MediaType.APPLICATION_JSON_VALUE).post(BASE_URI + "user/registration"); 56 | return response.getStatusCode(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/test/SpringSecurityRolesIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.test; 2 | 3 | import static org.junit.Assert.assertNull; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | import org.springframework.test.context.junit.jupiter.SpringExtension; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | import com.baeldung.persistence.dao.PrivilegeRepository; 18 | import com.baeldung.persistence.dao.RoleRepository; 19 | import com.baeldung.persistence.dao.UserRepository; 20 | import com.baeldung.persistence.model.Privilege; 21 | import com.baeldung.persistence.model.Role; 22 | import com.baeldung.persistence.model.User; 23 | import com.baeldung.spring.TestDbConfig; 24 | 25 | @ExtendWith(SpringExtension.class) 26 | @SpringBootTest(classes = TestDbConfig.class) 27 | @Transactional 28 | public class SpringSecurityRolesIntegrationTest { 29 | 30 | @Autowired 31 | private UserRepository userRepository; 32 | 33 | @Autowired 34 | private RoleRepository roleRepository; 35 | 36 | @Autowired 37 | private PrivilegeRepository privilegeRepository; 38 | 39 | @Autowired 40 | private PasswordEncoder passwordEncoder; 41 | 42 | private User user; 43 | private Role role; 44 | private Privilege privilege; 45 | 46 | // tests 47 | 48 | @Test 49 | public void testDeleteUser() { 50 | role = new Role("TEST_ROLE"); 51 | roleRepository.save(role); 52 | 53 | user = new User(); 54 | user.setFirstName("John"); 55 | user.setLastName("Doe"); 56 | user.setPassword(passwordEncoder.encode("123")); 57 | user.setEmail("john@doe.com"); 58 | user.setRoles(Arrays.asList(role)); 59 | user.setEnabled(true); 60 | userRepository.save(user); 61 | 62 | assertNotNull(userRepository.findByEmail(user.getEmail())); 63 | assertNotNull(roleRepository.findByName(role.getName())); 64 | user.setRoles(null); 65 | userRepository.delete(user); 66 | 67 | assertNull(userRepository.findByEmail(user.getEmail())); 68 | assertNotNull(roleRepository.findByName(role.getName())); 69 | } 70 | 71 | @Test 72 | public void testDeleteRole() { 73 | privilege = new Privilege("TEST_PRIVILEGE"); 74 | privilegeRepository.save(privilege); 75 | 76 | role = new Role("TEST_ROLE"); 77 | role.setPrivileges(Arrays.asList(privilege)); 78 | roleRepository.save(role); 79 | 80 | user = new User(); 81 | user.setFirstName("John"); 82 | user.setLastName("Doe"); 83 | user.setPassword(passwordEncoder.encode("123")); 84 | user.setEmail("john@doe.com"); 85 | user.setRoles(Arrays.asList(role)); 86 | user.setEnabled(true); 87 | userRepository.save(user); 88 | 89 | assertNotNull(privilegeRepository.findByName(privilege.getName())); 90 | assertNotNull(userRepository.findByEmail(user.getEmail())); 91 | assertNotNull(roleRepository.findByName(role.getName())); 92 | 93 | user.setRoles(new ArrayList<>()); 94 | role.setPrivileges(new ArrayList<>()); 95 | roleRepository.delete(role); 96 | 97 | assertNull(roleRepository.findByName(role.getName())); 98 | assertNotNull(privilegeRepository.findByName(privilege.getName())); 99 | assertNotNull(userRepository.findByEmail(user.getEmail())); 100 | } 101 | 102 | @Test 103 | public void testDeletePrivilege() { 104 | privilege = new Privilege("TEST_PRIVILEGE"); 105 | privilegeRepository.save(privilege); 106 | 107 | role = new Role("TEST_ROLE"); 108 | role.setPrivileges(Arrays.asList(privilege)); 109 | roleRepository.save(role); 110 | 111 | assertNotNull(roleRepository.findByName(role.getName())); 112 | assertNotNull(privilegeRepository.findByName(privilege.getName())); 113 | 114 | role.setPrivileges(new ArrayList<>()); 115 | privilegeRepository.delete(privilege); 116 | 117 | assertNull(privilegeRepository.findByName(privilege.getName())); 118 | assertNotNull(roleRepository.findByName(role.getName())); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/test/TokenExpirationIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.test; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertNotNull; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | import java.time.Instant; 9 | import java.time.temporal.ChronoUnit; 10 | import java.util.Date; 11 | import java.util.Optional; 12 | import java.util.UUID; 13 | 14 | import jakarta.persistence.EntityManager; 15 | import jakarta.persistence.PersistenceContext; 16 | 17 | import org.junit.jupiter.api.AfterEach; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.extension.ExtendWith; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.boot.test.context.SpringBootTest; 23 | import org.springframework.test.context.junit.jupiter.SpringExtension; 24 | import org.springframework.transaction.annotation.Transactional; 25 | 26 | import com.baeldung.persistence.dao.UserRepository; 27 | import com.baeldung.persistence.dao.VerificationTokenRepository; 28 | import com.baeldung.persistence.model.User; 29 | import com.baeldung.persistence.model.VerificationToken; 30 | import com.baeldung.spring.TestDbConfig; 31 | import com.baeldung.spring.TestTaskConfig; 32 | import com.baeldung.task.TokensPurgeTask; 33 | 34 | @ExtendWith(SpringExtension.class) 35 | @SpringBootTest(classes = { TestDbConfig.class, TestTaskConfig.class }) 36 | @Transactional 37 | class TokenExpirationIntegrationTest { 38 | 39 | @Autowired 40 | private UserRepository userRepository; 41 | 42 | @Autowired 43 | private VerificationTokenRepository tokenRepository; 44 | 45 | @Autowired 46 | private TokensPurgeTask tokensPurgeTask; 47 | 48 | @PersistenceContext 49 | private EntityManager entityManager; 50 | 51 | private Long token_id; 52 | private Long user_id; 53 | 54 | // 55 | 56 | @BeforeEach 57 | public void givenUserWithExpiredToken() { 58 | 59 | // we need a clear token repository 60 | tokenRepository.deleteAll(); 61 | 62 | User user = new User(); 63 | user.setEmail(UUID.randomUUID().toString() + "@example.com"); 64 | user.setPassword(UUID.randomUUID().toString()); 65 | user.setFirstName("First"); 66 | user.setLastName("Last"); 67 | 68 | entityManager.persist(user); 69 | String token = UUID.randomUUID().toString(); 70 | VerificationToken verificationToken = new VerificationToken(token, user); 71 | verificationToken.setExpiryDate(Date.from(Instant.now().minus(2, ChronoUnit.DAYS))); 72 | 73 | entityManager.persist(verificationToken); 74 | 75 | /* 76 | flush managed entities to the database to populate identifier field 77 | */ 78 | entityManager.flush(); 79 | 80 | /* 81 | remove managed entities from the persistence context 82 | so that subsequent SQL queries hit the database 83 | */ 84 | entityManager.clear(); 85 | 86 | token_id = verificationToken.getId(); 87 | user_id = user.getId(); 88 | } 89 | 90 | @Test 91 | void whenContextLoad_thenCorrect() { 92 | assertNotNull(user_id); 93 | assertNotNull(token_id); 94 | assertTrue(userRepository.findById(user_id).isPresent()); 95 | 96 | Optional verificationToken = tokenRepository.findById(token_id); 97 | assertTrue(verificationToken.isPresent()); 98 | assertTrue(tokenRepository.findAllByExpiryDateLessThan(Date.from(Instant.now())).anyMatch((token) -> token.equals(verificationToken.get()))); 99 | } 100 | 101 | @AfterEach 102 | public void flushAfter() { 103 | entityManager.flush(); 104 | } 105 | 106 | @Test 107 | void whenRemoveByGeneratedQuery_thenCorrect() { 108 | tokenRepository.deleteByExpiryDateLessThan(Date.from(Instant.now())); 109 | assertEquals(0, tokenRepository.count()); 110 | } 111 | 112 | @Test 113 | void whenRemoveByJPQLQuery_thenCorrect() { 114 | tokenRepository.deleteAllExpiredSince(Date.from(Instant.now())); 115 | assertEquals(0, tokenRepository.count()); 116 | } 117 | 118 | @Test 119 | void whenPurgeTokenTask_thenCorrect() { 120 | tokensPurgeTask.purgeExpired(); 121 | assertFalse(tokenRepository.findById(token_id).isPresent()); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/com/baeldung/test/UserIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.baeldung.test; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertTrue; 4 | 5 | import java.util.UUID; 6 | 7 | import jakarta.persistence.EntityManager; 8 | import jakarta.persistence.PersistenceContext; 9 | 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Disabled; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.test.context.junit.jupiter.SpringExtension; 18 | import org.springframework.transaction.annotation.Transactional; 19 | 20 | import com.baeldung.persistence.dao.UserRepository; 21 | import com.baeldung.persistence.dao.VerificationTokenRepository; 22 | import com.baeldung.persistence.model.User; 23 | import com.baeldung.persistence.model.VerificationToken; 24 | import com.baeldung.spring.LoginNotificationConfig; 25 | import com.baeldung.spring.ServiceConfig; 26 | import com.baeldung.spring.TestDbConfig; 27 | import com.baeldung.spring.TestIntegrationConfig; 28 | import com.baeldung.validation.EmailExistsException; 29 | 30 | @ExtendWith(SpringExtension.class) 31 | @SpringBootTest(classes = { TestDbConfig.class, ServiceConfig.class, TestIntegrationConfig.class, LoginNotificationConfig.class}) 32 | @Transactional 33 | class UserIntegrationTest { 34 | 35 | @Autowired 36 | private VerificationTokenRepository tokenRepository; 37 | 38 | @Autowired 39 | private UserRepository userRepository; 40 | 41 | @PersistenceContext 42 | private EntityManager entityManager; 43 | 44 | private Long tokenId; 45 | private Long userId; 46 | 47 | // 48 | 49 | @BeforeEach 50 | public void givenUserAndVerificationToken() throws EmailExistsException { 51 | User user = new User(); 52 | user.setEmail("test@example.com"); 53 | user.setPassword("SecretPassword"); 54 | user.setFirstName("First"); 55 | user.setLastName("Last"); 56 | entityManager.persist(user); 57 | 58 | String token = UUID.randomUUID().toString(); 59 | VerificationToken verificationToken = new VerificationToken(token, user); 60 | entityManager.persist(verificationToken); 61 | 62 | entityManager.flush(); 63 | entityManager.clear(); 64 | 65 | tokenId = verificationToken.getId(); 66 | userId = user.getId(); 67 | } 68 | 69 | @AfterEach 70 | public void flushAfter() { 71 | entityManager.flush(); 72 | entityManager.clear(); 73 | } 74 | 75 | // 76 | 77 | @Test 78 | void whenContextLoad_thenCorrect() { 79 | assertTrue(userRepository.count() > 0); 80 | assertTrue(tokenRepository.count() > 0); 81 | } 82 | 83 | // @Test(expected = Exception.class) 84 | @Test 85 | @Disabled("needs to go through the service and get transactional semantics") 86 | void whenRemovingUser_thenFkViolationException() { 87 | userRepository.deleteById(userId); 88 | } 89 | 90 | @Test 91 | void whenRemovingTokenThenUser_thenCorrect() { 92 | tokenRepository.deleteById(tokenId); 93 | userRepository.deleteById(userId); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | web - %date [%thread] %-5level %logger{5} - %message%n%stack{5,1} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/test/resources/maxmind/GeoLite2-City.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baeldung/spring-security-registration/ddfba5867b5eba89a5e0b717b2ff247456b87a80/src/test/resources/maxmind/GeoLite2-City.mmdb --------------------------------------------------------------------------------