├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── demo │ │ └── springcustomizedstarterexample │ │ ├── SpringCustomizedStarterExampleApplication.java │ │ ├── config │ │ ├── AppBeanConfiguration.java │ │ ├── AppConfig.java │ │ ├── AppProperties.java │ │ ├── WebMvcConfig.java │ │ └── WebSecurityConfig.java │ │ ├── controller │ │ ├── AuthenticationController.java │ │ ├── ExceptionHandlerController.java │ │ ├── TestController.java │ │ └── UserController.java │ │ ├── entities │ │ ├── UserEntity.java │ │ ├── common │ │ │ ├── AbstractGenericPKAuditableEntity.java │ │ │ ├── AbstractGenericPrimaryKey.java │ │ │ └── AuditorAwareUserImpl.java │ │ └── example │ │ │ ├── BooleanConverter.java │ │ │ └── ExampleEntity.java │ │ ├── repository │ │ └── UserRepository.java │ │ ├── security │ │ ├── AppSecurityUtils.java │ │ ├── CustomAuthenticationEntryPoint.java │ │ ├── CustomUserDetails.java │ │ ├── CustomUserDetailsService.java │ │ ├── JWTAuthenticationFilter.java │ │ ├── JWTTokenProvider.java │ │ └── oauth │ │ │ ├── CustomOAuth2UserService.java │ │ │ ├── OAuth2AuthenticationFailureHandler.java │ │ │ ├── OAuth2AuthenticationSuccessHandler.java │ │ │ └── common │ │ │ ├── CustomAbstractOAuth2UserInfo.java │ │ │ ├── FacebookCustomAbstractOAuth2UserInfo.java │ │ │ ├── GithubCustomAbstractOAuth2UserInfo.java │ │ │ ├── GoogleCustomAbstractOAuth2UserInfo.java │ │ │ ├── HttpCookieOAuth2AuthorizationRequestRepository.java │ │ │ ├── OAuth2Util.java │ │ │ └── SecurityEnums.java │ │ ├── services │ │ ├── auth │ │ │ ├── AuthenticationService.java │ │ │ ├── AuthenticationServiceImpl.java │ │ │ └── dtos │ │ │ │ ├── AuthResponseDTO.java │ │ │ │ ├── LoginRequestDTO.java │ │ │ │ └── RegisterUserRequestDTO.java │ │ ├── common │ │ │ ├── GenericMapper.java │ │ │ └── GenericResponseDTO.java │ │ ├── mail │ │ │ ├── AbstractDefaultEmailService.java │ │ │ ├── EmailService.java │ │ │ └── MessageTemplateCodeUtil.java │ │ └── webapp │ │ │ └── user │ │ │ ├── UserMapper.java │ │ │ ├── UserService.java │ │ │ ├── UserServiceImpl.java │ │ │ └── dto │ │ │ ├── ForgotPasswordRequestDTO.java │ │ │ ├── ResetPasswordRequestDTO.java │ │ │ ├── UpdatePasswordRequestDTO.java │ │ │ ├── UserDTO.java │ │ │ └── VerifyEmailRequestDTO.java │ │ └── utils │ │ ├── AppUtils.java │ │ ├── AppWebUtils.java │ │ ├── StringToEnumConverter.java │ │ └── exceptions │ │ ├── AppExceptionConstants.java │ │ ├── BadRequestException.java │ │ ├── CustomAppException.java │ │ └── ResourceNotFoundException.java └── resources │ ├── application-security-example.yml │ ├── application.yml │ └── mail-templates │ ├── reset-password.ftlh │ ├── verification-code.ftlh │ └── welcome.ftlh └── test └── java └── com └── demo └── springcustomizedstarterexample └── SpringCustomizedStarterExampleApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrawan/Spring-Security-OAuth-Example/d344199ef9cc32d3f11cbbc7f68a376f54b8fc84/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Spring Security with OAuth2, and Custom JWT 2 | - OAuth2 with (Google, Facebook, Github), (additional example.yml included) 3 | - Additional Custom JWT (register and Login) 4 | - Email based - register/login, freemarker-templates, registration-verification and password-reset 5 | 6 | Link: 7 | - [x] Viewing Backend: https://github.com/ashrawan/Spring-Security-OAuth-Example 8 | - [ ] Frontend: https://github.com/ashrawan/Angular-Security-OAuthJWT-Example 9 | 10 | ## Flow Overview: 11 | 12 | [ __OAUTH2 CONFIGURATION PROPERTIES__ @see: resources -> _application.yml_ ] 13 | 14 | ```yaml 15 | 16 | # By default, popular provider details isn't required @See org.springframework.security.config.oauth2.client.CommonOAuth2Provider 17 | # To achieve same through java configuration, @See org.springframework.security.oauth2.client.registration.ClientRegistrationRepository that register ClientRegistration Object 18 | 19 | security: 20 | oauth2: 21 | client: 22 | registration: 23 | google: 24 | clientId: ${GOOGLE_CLIENT_ID} 25 | clientSecret: ${GOOGLE_CLIENT_SECRET} 26 | redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" 27 | scope: email, profile 28 | 29 | facebook: 30 | clientId: ${FACEBOOK_CLIENT_ID} 31 | clientSecret: ${FACEBOOK_CLIENT_SECRET} 32 | redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" 33 | scope: email, public_profile 34 | 35 | ``` 36 | 37 | [ __MAIN WEB SECURITY CONFIGURATION__ @see: config -> _WebSecurityConfig.java_ ] 38 | 39 | ```java 40 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 41 | 42 | @Override 43 | protected void configure(HttpSecurity http) throws Exception { 44 | http 45 | // withDefaults() uses a Bean by the name of CorsConfigurationSource 46 | .cors(withDefaults()) 47 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 48 | .and() 49 | .csrf().disable() 50 | .formLogin().disable() 51 | .httpBasic().disable() 52 | .exceptionHandling(e -> e 53 | .authenticationEntryPoint(customAuthenticationEntryPoint)) 54 | .authorizeRequests(a -> a 55 | .antMatchers("/auth/**", "/oauth2/**").permitAll() 56 | .anyRequest().authenticated()) 57 | .oauth2Login(o -> o 58 | .authorizationEndpoint().baseUri("/oauth2/authorize") 59 | .authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository) 60 | .and() 61 | .redirectionEndpoint().baseUri("/oauth2/callback/*") 62 | .and() 63 | .userInfoEndpoint().userService(customOAuth2UserService) 64 | .and() 65 | .successHandler(oAuth2AuthenticationSuccessHandler) 66 | .failureHandler(oAuth2AuthenticationFailureHandler)); 67 | 68 | // Add our custom Token based authentication filter 69 | http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); 70 | } 71 | } 72 | ``` 73 | 74 | --- 75 | 76 | ## #. Run using maven wrapper/executable or via your preferred IDE 77 | 78 | - __Note__: 79 | - To run: Change datasource properties @See application.yml -> ```spring: datasource: ``` (create database "test") 80 | - To view flow: Change logging level as needed @See application.yml -> ```logging:``` 81 | 82 | ```cmd 83 | ./mvnw spring-boot:run // (using maven-wrapper) 84 | 85 | OR 86 | 87 | mvn spring-boot:run // ( using env mvn executable) 88 | ``` 89 | --- 90 | 91 | ### 1. CUSTOM REGISTRATION AND OAUTH2 REGISTRATION 92 | 93 | - __CUSTOM__: Registration is done via custom PUBLIC endpoint -> "/auth/register" { email/username, password, address etc. } 94 | - __OAUTH2__: Registration requires setting up `_CustomOAuth2UserService.java_ which extends DefaultOAuth2UserService` 95 | - we carry authentication with OAuth2 providers. After authentication, we will receive email or some form of identification, 96 | - And then we check whether the specified email belonging to that provider already exists in our system/database or not 97 | - If its new user, we registered that user directly, 98 | - OPTIONAL: to promote users for new Sign up confirmation: we can structure flow e.g. setting up flag in db "userApprovedRegistration: false" or 99 | emailVerified, with verification code - if user already exists, flow continue to LOG IN / SIGN IN OAUTH2 process (see below) 100 | 101 | ### 2. CUSTOM / OAUTH2 - LOGIN OR SIGN IN 102 | 103 | - __CUSTOM__: JWT token are created via separate custom PUBLIC endpoint -> "/auth/login" { email/username, password } 104 | - __OAUTH2__: In case of OAuth2, authentication via OAuth2 providers is done with _CustomOAuth2UserService.java_ 105 | - on SUCCESSFUL authentication custom class `_OAuth2AuthenticationSuccessHandler.java_ extends SimpleUrlAuthenticationSuccessHandler` triggers 106 | - This class handles custom jwt token creation and respond back to redirect_uri 107 | 108 | ``` 109 | String redirect_uri="http://my-ui-app.com/login" 110 | String targetUrl = UriComponentsBuilder.fromUriString(redirect_uri) 111 | .queryParam("token", createdCustomJwtToken) 112 | .build().toUriString(); 113 | 114 | // e.g. targetUrl - http://my-ui-app.com/login?token=created-custom-jwt-token 115 | redirectStrategy.sendRedirect(request, response, targetUrl); 116 | ``` 117 | 118 | ```text 119 | # 1. Requesting OAuth2 Sign Up / Sign In 120 | e.g. http://localhost:8080/oauth2/authorize/google?redirect_uri=http://localhost:8080/oauth2/redirect 121 | http://localhost:8080/oauth2/authorize/google?redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth2%2Fredirect 122 | 123 | # 2. After clicking on consent screen 124 | For all OAuth2 providers, On Successful OAuth2User Authentication Request process OAuth2UserRequest (CustomOAuth2UserService) 125 | - Determine is this [ New Sign Up ] or [ Existing Sign In ] 126 | - Create Principle Object that will set Authentication object 127 | - Continue flow to either OAuth2AuthenticationSuccessHandler OR OAuth2AuthenticationFailureHandler 128 | 129 | # 3. Flow, After Success/Failure Authentication 130 | - OAuth2AuthenticationSuccessHandler.java 131 | - We create ( Custom JWT Token OR use OAuthProvider JWT token ) and respond back to redirect_uri , e.g. http://my-ui-app.com/oauth2/redirectPage?token=jwtToken ) 132 | - http://localhost:8080/oauth2/redirect?token=jwtToken ) 133 | 134 | - OAuth2AuthenticationFailureHandler.java 135 | - We send authentication error response to the redirect_uri , e.g. http://my-ui-app.com/oauth2/redirectPage?error=Sorry-Couldnt-retrieve-your-email-from-Provider ) 136 | - http://localhost:8080/oauth2/redirect?error=authenticationException-OR-localizedMessage-OR-custom-message ) 137 | ``` 138 | 139 | ### 3. ACCESSING RESOURCES ( ACCESS CONTROL MECHANISM ) 140 | 141 | - If user requests __PUBLIC__ endpoints 142 | - PUBLIC endpoints are freely available and skips Security filter chain 143 | 144 | - If user requests into __SECURED__ endpoints e.g. "/api/userProfile" 145 | - Requesting WITHOUT token i.e. "Authorization" header 146 | - SecurityContext is empty, ` (path request matcher or anyRequest() ) -> .authenticated()` will catch, or will fail from `globalMethodSecurity` expression if defined 147 | - And forward it to _CustomAuthenticationEntryPoint.java_ for __SC_UNAUTHORIZED__ response 148 | 149 | - Requesting WITH token i.e. "Authorization" header has jwt token 150 | - _JWTAuthenticationFilter.java_ Validates Token and set SecurityContext -> Authentication Object 151 | - Since, Authentication Object is available, spring security filter continues the flow 152 | - Further permission checks are done in spring security Expression-Based Access Control or any configured Access Control mechanisms 153 | 154 | `jwtAuthenticationFilter`: A custom filter to validate and parse jwtToken and Set Authentication Object 155 | 156 | - Checks "Authorization" header is present 157 | - Not Present: Continues the filterChain 158 | - If present: Validates Token and set SecurityContext -> Authentication Object 159 | 160 | ## Structure 161 | 162 | - Properties defined in `application.yml` are initialized into `AppProperties.java`, default values are initialized in same class 163 | - For Multiple properties usage from `AppProperties appProperties`, values are initialized in `@PostConstruct` to avoid code clutter 164 | - Uses Lombok `@Getter @Setter` for getter/setter generation 165 | - By Default, config classes uses Field Injection `@Autowired`, other class uses constructor injection 166 | - Exception thrown are handled from `@ControllerAdvice` 167 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.demo 8 | spring-customized-starter-example 9 | 0.0.1-SNAPSHOT 10 | spring-customized-starter-example 11 | Demo project a Simple CRUD app with ( Spring Security, OAuth2, JWT ) in Spring Boot 12 | 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.5.5 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 11 24 | 25 | 0.9.1 26 | 1.18.2 27 | 1.4.2.Final 28 | 29 | 3.8.1 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-jpa 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-security 43 | 44 | 45 | org.springframework.security 46 | spring-security-oauth2-client 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-web 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-devtools 55 | runtime 56 | true 57 | 58 | 59 | 60 | 61 | mysql 62 | mysql-connector-java 63 | runtime 64 | 65 | 66 | org.projectlombok 67 | lombok 68 | ${org.projectlombok.version} 69 | true 70 | 71 | 72 | io.jsonwebtoken 73 | jjwt 74 | ${io.jsonwebtoken.version} 75 | 76 | 77 | org.mapstruct 78 | mapstruct 79 | ${org.mapstruct.version} 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-starter-test 86 | test 87 | 88 | 89 | org.springframework.security 90 | spring-security-test 91 | test 92 | 93 | 94 | 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-starter-freemarker 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-starter-mail 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.springframework.boot 112 | spring-boot-maven-plugin 113 | ${project.parent.version} 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-compiler-plugin 119 | ${org.apache.maven.plugins.version} 120 | 121 | ${java.version} 122 | ${java.version} 123 | 124 | 125 | org.projectlombok 126 | lombok 127 | ${org.projectlombok.version} 128 | 129 | 130 | org.mapstruct 131 | mapstruct-processor 132 | ${org.mapstruct.version} 133 | 134 | 135 | 136 | -Amapstruct.suppressGeneratorTimestamp=true 137 | -Amapstruct.defaultComponentModel=spring 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/SpringCustomizedStarterExampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample; 2 | 3 | import com.demo.springcustomizedstarterexample.config.AppProperties; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 8 | 9 | import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; 10 | import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; 11 | 12 | @SpringBootApplication 13 | @EnableJpaAuditing(auditorAwareRef = "auditorAwareUserImpl") 14 | @EnableConfigurationProperties(AppProperties.class) 15 | public class SpringCustomizedStarterExampleApplication { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(SpringCustomizedStarterExampleApplication.class, args); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/config/AppBeanConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.config; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.json.JsonMapper; 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | import org.springframework.web.cors.CorsConfiguration; 13 | import org.springframework.web.cors.CorsConfigurationSource; 14 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 15 | 16 | import java.util.Arrays; 17 | 18 | @Configuration 19 | public class AppBeanConfiguration { 20 | 21 | @Autowired 22 | private AppProperties appProperties; 23 | 24 | @Bean 25 | public ObjectMapper objectMapper() { 26 | ObjectMapper mapper = JsonMapper.builder() 27 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 28 | .addModule(new JavaTimeModule()) 29 | .build(); 30 | return mapper; 31 | } 32 | 33 | /** 34 | * Setting up Cors Configuration 35 | * `http.cors(withDefaults())` uses a Bean by the name of CorsConfigurationSource 36 | * 37 | * @return CorsConfigurationSource 38 | */ 39 | @Bean 40 | CorsConfigurationSource corsConfigurationSource() { 41 | CorsConfiguration configuration = new CorsConfiguration(); 42 | configuration.setAllowedOrigins(Arrays.asList(appProperties.getCors().getAllowedOrigins())); 43 | configuration.setAllowedMethods(Arrays.asList(appProperties.getCors().getAllowedMethods())); 44 | configuration.setAllowedHeaders(Arrays.asList(appProperties.getCors().getAllowedHeaders())); 45 | configuration.setExposedHeaders(Arrays.asList(appProperties.getCors().getExposedHeaders())); 46 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 47 | source.registerCorsConfiguration("/**", configuration); 48 | return source; 49 | } 50 | 51 | /** 52 | * Setting up Password encoder 53 | * 54 | * @return PasswordEncoder 55 | */ 56 | @Bean 57 | public PasswordEncoder getPasswordEncoder() { 58 | return new BCryptPasswordEncoder(); 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.config; 2 | 3 | import com.demo.springcustomizedstarterexample.utils.AppUtils; 4 | import com.demo.springcustomizedstarterexample.utils.AppWebUtils; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.databind.json.JsonMapper; 7 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | import org.springframework.web.cors.CorsConfiguration; 13 | import org.springframework.web.cors.CorsConfigurationSource; 14 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 15 | 16 | import java.util.Arrays; 17 | 18 | @Configuration 19 | public class AppConfig { 20 | 21 | private final AppProperties appProperties; 22 | private final ObjectMapper objectMapper; 23 | 24 | public AppConfig(AppProperties appProperties, 25 | ObjectMapper objectMapper) { 26 | this.appProperties = appProperties; 27 | this.objectMapper = objectMapper; 28 | } 29 | 30 | @Bean 31 | public AppUtils appUtils() { 32 | return new AppUtils(objectMapper); 33 | } 34 | 35 | @Bean 36 | public AppWebUtils webUtils() { 37 | int cookieExpireSeconds = appProperties.getOAuth2().getCookieExpireSeconds(); 38 | return new AppWebUtils(cookieExpireSeconds); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/config/AppProperties.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.config; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.validation.annotation.Validated; 8 | 9 | @Validated 10 | @ConfigurationProperties(prefix = "myapp") 11 | @Slf4j 12 | @Getter 13 | @Setter 14 | public class AppProperties { 15 | 16 | public AppProperties() { 17 | log.info("Application Properties Initialized"); 18 | } 19 | 20 | private String appName = "My Stater App"; 21 | 22 | private String officialCompanyName = ""; 23 | 24 | private String officialCompanyDomain = ""; 25 | 26 | // Mail config 27 | private Mail mail = new Mail(); 28 | 29 | // CORS configuration 30 | private Cors cors = new Cors(); 31 | 32 | // JWT token generation related properties 33 | private Jwt jwt = new Jwt(); 34 | 35 | // Custom specific OAuth2 Properties 36 | private OAuth2 oAuth2 = new OAuth2(); 37 | 38 | // Custom Defaults App/Web/Rest/Misc Properties 39 | private Defaults defaults = new Defaults(); 40 | 41 | @Getter 42 | @Setter 43 | public static class Mail { 44 | private String defaultEmailAddress; 45 | private long verificationCodeExpirationSeconds = 600; // 10 minutes 46 | } 47 | 48 | @Getter 49 | @Setter 50 | public static class Cors { 51 | 52 | private String[] allowedOrigins; 53 | private String[] allowedMethods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"}; 54 | private String[] allowedHeaders = {"*"}; 55 | private String[] exposedHeaders = {"*"}; 56 | private long maxAge = 3600L; 57 | } 58 | 59 | @Getter 60 | @Setter 61 | public static class Jwt { 62 | 63 | private String secretKey; 64 | private boolean isSecretKeyBase64Encoded = false; 65 | private long expirationMillis = 864000000L; // 10 days 66 | // For short-lived tokens and cookies 67 | private int shortLivedMillis = 120000; // Two minutes 68 | } 69 | 70 | @Getter 71 | @Setter 72 | public static class OAuth2 { 73 | private String[] authorizedRedirectOrigins; 74 | private int cookieExpireSeconds = 120; // Two minutes 75 | } 76 | 77 | @Getter 78 | @Setter 79 | public static class Defaults { 80 | private int defaultPageStart = 0; 81 | private int defaultPageSize = 50; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.config; 2 | 3 | import com.demo.springcustomizedstarterexample.utils.StringToEnumConverter; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.domain.PageRequest; 7 | import org.springframework.data.web.PageableHandlerMethodArgumentResolver; 8 | import org.springframework.format.FormatterRegistry; 9 | import org.springframework.http.converter.HttpMessageConverter; 10 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 11 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 12 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 13 | 14 | import java.text.SimpleDateFormat; 15 | import java.util.List; 16 | 17 | @Configuration 18 | public class WebMvcConfig implements WebMvcConfigurer { 19 | 20 | @Autowired 21 | private AppProperties appProperties; 22 | 23 | @Override 24 | public void addArgumentResolvers(List resolvers) { 25 | PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); 26 | resolver.setFallbackPageable(PageRequest.of(appProperties.getDefaults().getDefaultPageStart(), appProperties.getDefaults().getDefaultPageSize())); 27 | resolvers.add(resolver); 28 | WebMvcConfigurer.super.addArgumentResolvers(resolvers); 29 | } 30 | 31 | @Override 32 | public void addFormatters(FormatterRegistry registry) { 33 | registry.addConverterFactory(new StringToEnumConverter()); 34 | WebMvcConfigurer.super.addFormatters(registry); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.config; 2 | 3 | import com.demo.springcustomizedstarterexample.security.CustomAuthenticationEntryPoint; 4 | import com.demo.springcustomizedstarterexample.security.CustomUserDetailsService; 5 | import com.demo.springcustomizedstarterexample.security.JWTAuthenticationFilter; 6 | import com.demo.springcustomizedstarterexample.security.oauth.CustomOAuth2UserService; 7 | import com.demo.springcustomizedstarterexample.security.oauth.OAuth2AuthenticationFailureHandler; 8 | import com.demo.springcustomizedstarterexample.security.oauth.OAuth2AuthenticationSuccessHandler; 9 | import com.demo.springcustomizedstarterexample.security.oauth.common.HttpCookieOAuth2AuthorizationRequestRepository; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.security.authentication.AuthenticationManager; 13 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 14 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 15 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 16 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 17 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 18 | import org.springframework.security.config.http.SessionCreationPolicy; 19 | import org.springframework.security.crypto.password.PasswordEncoder; 20 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 21 | 22 | import static org.springframework.security.config.Customizer.withDefaults; 23 | 24 | @Configuration 25 | @EnableWebSecurity 26 | @EnableGlobalMethodSecurity( 27 | securedEnabled = true, 28 | jsr250Enabled = true, 29 | prePostEnabled = true 30 | ) 31 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 32 | 33 | // CustomUserDetailsService - To process custom user SignUp/SignIn request 34 | // CustomOAuth2UserService - To process OAuth user SignUp/SignIn request 35 | private final CustomUserDetailsService customUserDetailsService; 36 | private final CustomOAuth2UserService customOAuth2UserService; 37 | private final PasswordEncoder passwordEncoder; 38 | 39 | // CustomAuthenticationEntryPoint - Unauthorized Access handler 40 | // JWTAuthenticationFilter - Retrieves request JWT token and, validate and set Authentication 41 | private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; 42 | private final JWTAuthenticationFilter jwtAuthenticationFilter; 43 | 44 | // Cookie based repository, OAuth2 Success and Failure Handler 45 | private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; 46 | private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; 47 | private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; 48 | 49 | public WebSecurityConfig(CustomUserDetailsService customUserDetailsService, 50 | CustomOAuth2UserService customOAuth2UserService, 51 | PasswordEncoder passwordEncoder, 52 | CustomAuthenticationEntryPoint customAuthenticationEntryPoint, 53 | JWTAuthenticationFilter jwtAuthenticationFilter, 54 | HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository, 55 | OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler, 56 | OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler) { 57 | this.customUserDetailsService = customUserDetailsService; 58 | this.customOAuth2UserService = customOAuth2UserService; 59 | this.passwordEncoder = passwordEncoder; 60 | this.customAuthenticationEntryPoint = customAuthenticationEntryPoint; 61 | this.jwtAuthenticationFilter = jwtAuthenticationFilter; 62 | this.httpCookieOAuth2AuthorizationRequestRepository = httpCookieOAuth2AuthorizationRequestRepository; 63 | this.oAuth2AuthenticationSuccessHandler = oAuth2AuthenticationSuccessHandler; 64 | this.oAuth2AuthenticationFailureHandler = oAuth2AuthenticationFailureHandler; 65 | } 66 | 67 | 68 | @Override 69 | protected void configure(HttpSecurity http) throws Exception { 70 | http 71 | // withDefaults() uses a Bean by the name of CorsConfigurationSource 72 | .cors(withDefaults()) 73 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 74 | .and() 75 | .csrf().disable() 76 | // CookieCsrfTokenRepository config - all put,post,delete request (form curl or browser or any) requires csrf token 77 | // .csrf(c -> c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) 78 | .formLogin().disable() 79 | .httpBasic().disable() 80 | .exceptionHandling(e -> e 81 | .authenticationEntryPoint(customAuthenticationEntryPoint)) 82 | .authorizeRequests(a -> a 83 | .antMatchers("/auth/**", "/oauth2/**").permitAll() 84 | .anyRequest().authenticated()) 85 | .oauth2Login(o -> o 86 | .authorizationEndpoint().baseUri("/oauth2/authorize") 87 | .authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository) 88 | .and() 89 | .redirectionEndpoint().baseUri("/oauth2/callback/*") 90 | .and() 91 | .userInfoEndpoint().userService(customOAuth2UserService) 92 | .and() 93 | .successHandler(oAuth2AuthenticationSuccessHandler) 94 | .failureHandler(oAuth2AuthenticationFailureHandler)); 95 | 96 | // Add our custom Token based authentication filter 97 | http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); 98 | } 99 | 100 | @Bean 101 | @Override 102 | public AuthenticationManager authenticationManagerBean() throws Exception { 103 | return super.authenticationManagerBean(); 104 | } 105 | 106 | @Override 107 | public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { 108 | authenticationManagerBuilder 109 | .userDetailsService(customUserDetailsService) 110 | .passwordEncoder(passwordEncoder); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/controller/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.controller; 2 | 3 | import com.demo.springcustomizedstarterexample.services.auth.AuthenticationService; 4 | import com.demo.springcustomizedstarterexample.services.auth.dtos.AuthResponseDTO; 5 | import com.demo.springcustomizedstarterexample.services.auth.dtos.LoginRequestDTO; 6 | import com.demo.springcustomizedstarterexample.services.auth.dtos.RegisterUserRequestDTO; 7 | import com.demo.springcustomizedstarterexample.services.common.GenericResponseDTO; 8 | import com.demo.springcustomizedstarterexample.services.webapp.user.UserService; 9 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.ForgotPasswordRequestDTO; 10 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.ResetPasswordRequestDTO; 11 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.UserDTO; 12 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.VerifyEmailRequestDTO; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | @Slf4j 19 | @RestController 20 | @RequestMapping("/auth") 21 | public class AuthenticationController { 22 | 23 | private final AuthenticationService authenticationService; 24 | private final UserService userService; 25 | 26 | public AuthenticationController(AuthenticationService authenticationService, 27 | UserService userService) { 28 | this.authenticationService = authenticationService; 29 | this.userService = userService; 30 | } 31 | 32 | @PostMapping("/login") 33 | public ResponseEntity loginUser(@RequestBody LoginRequestDTO loginRequest) { 34 | log.info("Authentication API: loginUser: ", loginRequest.getEmail()); 35 | AuthResponseDTO authResponseDTO = authenticationService.loginUser(loginRequest); 36 | return new ResponseEntity<>(authResponseDTO, HttpStatus.OK); 37 | } 38 | 39 | @PostMapping("/register") 40 | public ResponseEntity registerUser(@RequestBody RegisterUserRequestDTO registerUserRequestDTO) { 41 | log.info("Authentication API: registerUser: ", registerUserRequestDTO.getEmail()); 42 | UserDTO userDTO = authenticationService.registerUser(registerUserRequestDTO); 43 | return new ResponseEntity<>(userDTO, HttpStatus.OK); 44 | } 45 | 46 | @GetMapping("/resend-verification-email") 47 | public ResponseEntity resendVerificationEmail(@RequestParam("email") String email) { 48 | log.info("Authentication API: resendVerificationEmail: ", email); 49 | GenericResponseDTO resendVerificationEmailStatus = userService.sendVerificationEmail(email); 50 | return new ResponseEntity<>(resendVerificationEmailStatus, HttpStatus.OK); 51 | } 52 | 53 | @PostMapping("/check-verification-code") 54 | public ResponseEntity checkVerificationCode(@RequestBody VerifyEmailRequestDTO verifyEmailRequestDTO) { 55 | log.info("Authentication API: checkVerificationCode: ", verifyEmailRequestDTO.getEmail()); 56 | GenericResponseDTO checkVerificationCodeStatus = userService.verifyEmailAddress(verifyEmailRequestDTO); 57 | return new ResponseEntity<>(checkVerificationCodeStatus, HttpStatus.OK); 58 | } 59 | 60 | @PostMapping("/send-forgot-password") 61 | public ResponseEntity sendResetPasswordEmail(@RequestBody ForgotPasswordRequestDTO forgotPasswordRequestDTO) { 62 | log.info("Authentication API: sendResetPasswordEmail: ", forgotPasswordRequestDTO.getEmail()); 63 | GenericResponseDTO resendVerificationEmailStatus = userService.sendResetPasswordEmail(forgotPasswordRequestDTO); 64 | return new ResponseEntity<>(resendVerificationEmailStatus, HttpStatus.OK); 65 | } 66 | 67 | @PostMapping("/process-password-reset") 68 | public ResponseEntity verifyAndProcessPasswordResetRequest(@RequestBody ResetPasswordRequestDTO resetPasswordRequestDTO) { 69 | log.info("Authentication API: verifyAndProcessPasswordResetRequest: ", resetPasswordRequestDTO.getEmail()); 70 | GenericResponseDTO checkVerificationCodeStatus = userService.verifyAndProcessPasswordResetRequest(resetPasswordRequestDTO); 71 | return new ResponseEntity<>(checkVerificationCodeStatus, HttpStatus.OK); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/controller/ExceptionHandlerController.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.controller; 2 | 3 | import com.demo.springcustomizedstarterexample.services.common.GenericResponseDTO; 4 | import com.demo.springcustomizedstarterexample.utils.exceptions.CustomAppException; 5 | import com.demo.springcustomizedstarterexample.utils.exceptions.ResourceNotFoundException; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.security.authentication.BadCredentialsException; 10 | import org.springframework.web.bind.annotation.ControllerAdvice; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 13 | 14 | import javax.servlet.http.HttpServletRequest; 15 | 16 | @Slf4j 17 | @ControllerAdvice 18 | public class ExceptionHandlerController { 19 | 20 | @ExceptionHandler(ResourceNotFoundException.class) 21 | public ResponseEntity resourceNotFoundException(final ResourceNotFoundException ex, 22 | final HttpServletRequest request) { 23 | 24 | log.info("DataNotFoundException handled {} ", ex.getMessage()); 25 | GenericResponseDTO genericResponseDTO = new GenericResponseDTO<>(ex.getMessage(), null); 26 | return new ResponseEntity<>(genericResponseDTO, HttpStatus.NOT_FOUND); 27 | } 28 | 29 | @ExceptionHandler(MethodArgumentTypeMismatchException.class) 30 | public ResponseEntity methodArgumentTypeMismatchException(final MethodArgumentTypeMismatchException ex, 31 | final HttpServletRequest request) { 32 | 33 | log.info("MethodArgumentTypeMismatchException handled {} ", ex.getMessage()); 34 | GenericResponseDTO genericResponseDTO = new GenericResponseDTO<>(ex.getMessage(), null); 35 | return new ResponseEntity<>(genericResponseDTO, HttpStatus.BAD_REQUEST); 36 | } 37 | 38 | @ExceptionHandler(BadCredentialsException.class) 39 | public ResponseEntity badCredentialsException(final BadCredentialsException ex, 40 | final HttpServletRequest request) { 41 | 42 | log.info("badCredentialsException handled {} ", ex.getMessage()); 43 | GenericResponseDTO genericResponseDTO = new GenericResponseDTO<>(ex.getMessage(), null); 44 | return new ResponseEntity<>(genericResponseDTO, HttpStatus.BAD_REQUEST); 45 | } 46 | 47 | @ExceptionHandler(CustomAppException.class) 48 | public ResponseEntity globalAppException(final CustomAppException ex, 49 | final HttpServletRequest request) { 50 | 51 | log.info("CustomAppException handled {}", ex.getMessage()); 52 | GenericResponseDTO genericResponseDTO = new GenericResponseDTO<>(ex.getMessage(), null); 53 | return new ResponseEntity<>(genericResponseDTO, HttpStatus.INTERNAL_SERVER_ERROR); 54 | } 55 | 56 | @ExceptionHandler(RuntimeException.class) 57 | public ResponseEntity globalAppException(final RuntimeException ex, 58 | final HttpServletRequest request) { 59 | 60 | log.info("Runtime Exception occurred {} ", ex.getMessage()); 61 | ex.printStackTrace(); 62 | GenericResponseDTO genericResponseDTO = new GenericResponseDTO<>(ex.getMessage(), null); 63 | return new ResponseEntity<>(genericResponseDTO, HttpStatus.INTERNAL_SERVER_ERROR); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.ResponseBody; 6 | 7 | @Controller 8 | public class TestController { 9 | 10 | @GetMapping("/") 11 | @ResponseBody 12 | public String hello() { 13 | return "Hello! World"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.controller; 2 | 3 | import com.demo.springcustomizedstarterexample.security.AppSecurityUtils; 4 | import com.demo.springcustomizedstarterexample.services.common.GenericResponseDTO; 5 | import com.demo.springcustomizedstarterexample.services.webapp.user.UserService; 6 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.UpdatePasswordRequestDTO; 7 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.UserDTO; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.data.domain.Pageable; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.security.access.prepost.PreAuthorize; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import java.util.List; 16 | import java.util.Optional; 17 | 18 | @Slf4j 19 | @RestController 20 | @RequestMapping("users") 21 | //@PreAuthorize("isAuthenticated()") 22 | public class UserController { 23 | 24 | private final UserService userService; 25 | 26 | public UserController(UserService userService) { 27 | this.userService = userService; 28 | } 29 | 30 | @GetMapping 31 | public ResponseEntity getAllUser(Pageable pageable) { 32 | log.info("User API: get all user"); 33 | List userDTOList = userService.getAllUsers(pageable); 34 | return new ResponseEntity<>(userDTOList, HttpStatus.OK); 35 | } 36 | 37 | @GetMapping("/{id}") 38 | public ResponseEntity getUserById(@PathVariable Long id) { 39 | log.info("User API: get user by id: ", id); 40 | UserDTO userDTO = userService.getUserById(id); 41 | return new ResponseEntity<>(userDTO, HttpStatus.OK); 42 | } 43 | 44 | // TODO Remove this Method: Registration Functionality Provided from public endpoint in AuthenticationController 45 | // @PostMapping 46 | // public ResponseEntity createUser(@RequestBody UserDTO userDTO) { 47 | // log.info("User API: create user"); 48 | // UserDTO returnedUserDTO = userService.createUser(userDTO); 49 | // return new ResponseEntity<>(returnedUserDTO, HttpStatus.OK); 50 | // } 51 | 52 | @PutMapping 53 | public ResponseEntity updateUser(@RequestBody UserDTO userDTO) { 54 | log.info("User API: update user"); 55 | UserDTO returnedUserDTO = userService.updateUser(userDTO); 56 | return new ResponseEntity<>(returnedUserDTO, HttpStatus.OK); 57 | } 58 | 59 | @PutMapping("/update-password") 60 | public ResponseEntity updatePassword(@RequestBody UpdatePasswordRequestDTO updatePasswordRequest) { 61 | log.info("User API: processing password update for userId: "); 62 | GenericResponseDTO genericResponse = userService.updatePassword(updatePasswordRequest); 63 | return new ResponseEntity<>(genericResponse, HttpStatus.OK); 64 | } 65 | 66 | @GetMapping("/me") 67 | public ResponseEntity retrieveAuthenticatedUser() { 68 | Optional currentUserId = AppSecurityUtils.getCurrentUserId(); 69 | log.info("User API: retrieve authenticated user details for userId: ", currentUserId.get()); 70 | UserDTO genericResponse = userService.getUserById(currentUserId.get()); 71 | return new ResponseEntity<>(genericResponse, HttpStatus.OK); 72 | } 73 | 74 | @GetMapping("/email-exists") 75 | public ResponseEntity exists(@RequestParam("email") String email) { 76 | GenericResponseDTO genericResponseDTO = userService.userEmailExists(email); 77 | return new ResponseEntity<>(genericResponseDTO, HttpStatus.OK); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/entities/UserEntity.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.entities; 2 | 3 | import com.demo.springcustomizedstarterexample.entities.common.AbstractGenericPKAuditableEntity; 4 | import com.demo.springcustomizedstarterexample.security.oauth.common.SecurityEnums; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import javax.persistence.*; 11 | import java.time.Instant; 12 | import java.util.HashSet; 13 | import java.util.Set; 14 | 15 | @Entity 16 | @Table(name = "users", uniqueConstraints = { 17 | @UniqueConstraint(columnNames = "email") 18 | }) 19 | @Data 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public class UserEntity extends AbstractGenericPKAuditableEntity { 23 | 24 | @Column(name = "full_name", nullable = false) 25 | private String fullName; 26 | 27 | // TODO @Email Validation 28 | @Column(name = "email", nullable = false) 29 | private String email; 30 | 31 | @Column(name = "email_verified") 32 | private boolean emailVerified; 33 | 34 | @JsonProperty(value = "password", access = JsonProperty.Access.WRITE_ONLY) 35 | @Column(name = "password") 36 | private String password; 37 | 38 | @Column(name = "image_url") 39 | private String imageUrl; 40 | 41 | @ElementCollection(fetch = FetchType.LAZY) 42 | @CollectionTable( 43 | name = "users_roles", 44 | joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id") 45 | ) 46 | @Column(name = "role") 47 | protected Set roles = new HashSet<>(); 48 | 49 | @Column(name = "phone_number") 50 | private String phoneNumber; 51 | 52 | @Column(name = "registered_provider_name") 53 | @Enumerated(EnumType.STRING) 54 | private SecurityEnums.AuthProviderId registeredProviderName; 55 | 56 | @Column(name = "registered_provider_id") 57 | private String registeredProviderId; 58 | 59 | // Will be using same verificationCode and verificationCodeExpiresAt for both (email-verification and password reset) 60 | @Column(name = "verification_code") 61 | private String verificationCode; 62 | 63 | @Column(name = "verification_code_expires_at") 64 | private Instant verificationCodeExpiresAt; 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/entities/common/AbstractGenericPKAuditableEntity.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.entities.common; 2 | 3 | import com.demo.springcustomizedstarterexample.entities.UserEntity; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.data.annotation.CreatedBy; 7 | import org.springframework.data.annotation.CreatedDate; 8 | import org.springframework.data.annotation.LastModifiedBy; 9 | import org.springframework.data.annotation.LastModifiedDate; 10 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 11 | import org.springframework.lang.Nullable; 12 | 13 | import javax.persistence.*; 14 | import java.io.Serializable; 15 | import java.time.LocalDateTime; 16 | 17 | @MappedSuperclass 18 | @EntityListeners(AuditingEntityListener.class) 19 | @Getter 20 | @Setter 21 | public abstract class AbstractGenericPKAuditableEntity extends AbstractGenericPrimaryKey { 22 | 23 | @Nullable 24 | @CreatedBy 25 | @ManyToOne 26 | @JoinColumn(name = "created_by_id") 27 | private UserEntity createdBy; 28 | 29 | @Nullable 30 | @CreatedDate 31 | @Column(name = "created_date", columnDefinition = "TIMESTAMP") 32 | private LocalDateTime createdDate; 33 | 34 | @Nullable 35 | @LastModifiedBy 36 | @ManyToOne 37 | @JoinColumn(name = "last_modified_by_id") 38 | private UserEntity lastModifiedBy; 39 | 40 | @Nullable 41 | @LastModifiedDate 42 | @Column(name = "last_modified_date", columnDefinition = "TIMESTAMP") 43 | private LocalDateTime lastModifiedDate; 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/entities/common/AbstractGenericPrimaryKey.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.entities.common; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.lang.Nullable; 6 | 7 | import javax.persistence.*; 8 | 9 | @MappedSuperclass 10 | @Getter 11 | @Setter 12 | public abstract class AbstractGenericPrimaryKey { 13 | 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | @Nullable 17 | @Column(name = "id", nullable = false) 18 | private PK id; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/entities/common/AuditorAwareUserImpl.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.entities.common; 2 | 3 | import com.demo.springcustomizedstarterexample.entities.UserEntity; 4 | import com.demo.springcustomizedstarterexample.repository.UserRepository; 5 | import com.demo.springcustomizedstarterexample.security.AppSecurityUtils; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.domain.AuditorAware; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Optional; 11 | 12 | @Component("auditorAwareUserImpl") 13 | public class AuditorAwareUserImpl implements AuditorAware { 14 | 15 | @Autowired 16 | private UserRepository userRepository; 17 | 18 | @Override 19 | public Optional getCurrentAuditor() { 20 | Optional optionalUserId = Optional 21 | .ofNullable(AppSecurityUtils.getCurrentUserPrinciple()) 22 | .map(e -> e.getUserEntity().getId()); 23 | Optional userEntity = optionalUserId.map(userId -> userRepository.getById(userId)); 24 | return userEntity; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/entities/example/BooleanConverter.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.entities.example; 2 | 3 | import javax.persistence.AttributeConverter; 4 | import javax.persistence.Converter; 5 | 6 | @Converter(autoApply = true) 7 | public class BooleanConverter implements AttributeConverter { 8 | 9 | private static final Integer ZERO = 0; 10 | private static final Integer ONE = 1; 11 | 12 | public BooleanConverter() { 13 | } 14 | 15 | @Override 16 | public Integer convertToDatabaseColumn(Boolean attribute) { 17 | if (null == attribute) { 18 | return null; 19 | } 20 | if (Boolean.TRUE.equals(attribute)) { 21 | return ONE; 22 | } else { 23 | return ZERO; 24 | } 25 | } 26 | 27 | @Override 28 | public Boolean convertToEntityAttribute(Integer dbData) { 29 | if (null == dbData) { 30 | return null; 31 | } 32 | return ONE.equals(dbData); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/entities/example/ExampleEntity.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.entities.example; 2 | 3 | import org.hibernate.annotations.GenericGenerator; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Convert; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | 10 | public class ExampleEntity { 11 | 12 | @Id 13 | @GeneratedValue(generator = "uuid") 14 | @GenericGenerator(name = "uuid", strategy = "uuid2") 15 | @Column(name = "ID") 16 | private String id; 17 | 18 | @Column(name = "is_flying_bird", columnDefinition = "NUMBER(1,0)") 19 | @Convert(converter = BooleanConverter.class) 20 | private boolean isFlyingBird; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.repository; 2 | 3 | import com.demo.springcustomizedstarterexample.entities.UserEntity; 4 | import com.demo.springcustomizedstarterexample.security.oauth.common.SecurityEnums; 5 | import org.springframework.data.jpa.repository.EntityGraph; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | import org.springframework.stereotype.Repository; 11 | 12 | import java.util.Optional; 13 | 14 | @Repository 15 | public interface UserRepository extends JpaRepository, JpaSpecificationExecutor { 16 | 17 | @EntityGraph(attributePaths = "roles", type = EntityGraph.EntityGraphType.LOAD) 18 | Optional findByEmail(String email); 19 | 20 | boolean existsByEmail(String email); 21 | 22 | @Query("SELECT u FROM UserEntity u WHERE " + 23 | "u.email = :email and u.registeredProviderName = :registeredProviderName " + 24 | "and u.verificationCodeExpiresAt >= UTC_TIMESTAMP and u.verificationCode = :verificationCode") 25 | Optional verifyAndRetrieveEmailVerificationRequestUser(@Param("email") String email, 26 | @Param("registeredProviderName") SecurityEnums.AuthProviderId registeredProviderName, 27 | @Param("verificationCode") String verificationCode); 28 | 29 | @Query("SELECT u FROM UserEntity u WHERE " + 30 | "u.email = :email and u.registeredProviderName = :validProviderName " + 31 | "and u.verificationCodeExpiresAt >= UTC_TIMESTAMP and u.verificationCode = :verificationCode") 32 | Optional verifyAndRetrieveForgotPasswordRequestUser(@Param("email") String email, 33 | @Param("validProviderName") SecurityEnums.AuthProviderId validProviderName, 34 | @Param("verificationCode") String verificationCode); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/AppSecurityUtils.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.authority.AuthorityUtils; 6 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | 9 | import java.util.Collection; 10 | import java.util.HashSet; 11 | import java.util.Optional; 12 | import java.util.Set; 13 | 14 | public class AppSecurityUtils { 15 | 16 | public static final String ROLE_DEFAULT = "ROLE_DEFAULT"; 17 | 18 | /** 19 | * Converts list of roles into Collection of GrantedAuthority 20 | * 21 | * @param roles 22 | * @return Collection 23 | */ 24 | public static Collection convertRolesSetToGrantedAuthorityList(Set roles) { 25 | Collection authorities = new HashSet<>(); 26 | for (String role : roles) { 27 | GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role); 28 | authorities.add(grantedAuthority); 29 | } 30 | return authorities; 31 | } 32 | 33 | /** 34 | * Converts Collection of GrantedAuthority into list of roles 35 | * 36 | * @param grantedAuthorities 37 | * @return Set 38 | */ 39 | public static Set convertGrantedAuthorityListToRolesSet(Collection grantedAuthorities) { 40 | Set roles = AuthorityUtils.authorityListToSet(grantedAuthorities); 41 | return roles; 42 | } 43 | 44 | /** 45 | * Get Authentication object from SecurityContextHolder 46 | * 47 | * @return Authentication object 48 | */ 49 | public static Authentication getAuthenticationObject() { 50 | return SecurityContextHolder.getContext().getAuthentication(); 51 | } 52 | 53 | /** 54 | * Get current user principle 55 | * 56 | * @return CustomUserDetails - principle object 57 | */ 58 | public static CustomUserDetails getCurrentUserPrinciple() { 59 | Authentication authentication = getAuthenticationObject(); 60 | if (authentication != null) { 61 | Object principal = authentication.getPrincipal(); 62 | if (principal instanceof CustomUserDetails) { 63 | return ((CustomUserDetails) principal); 64 | } 65 | } 66 | return null; 67 | } 68 | 69 | /** 70 | * Get current user id 71 | * 72 | * @return Long - user id 73 | */ 74 | public static Optional getCurrentUserId() { 75 | Optional optionalUserId = Optional.ofNullable(getCurrentUserPrinciple()) 76 | .map(customUserDetails -> customUserDetails.getUserEntity()) 77 | .map(userEntity -> userEntity.getId()); 78 | return optionalUserId; 79 | } 80 | 81 | 82 | /** 83 | * Check if user is Authenticated 84 | * 85 | * @return true - if user is Authenticated 86 | */ 87 | public static boolean isAuthenticated() { 88 | Authentication authentication = getAuthenticationObject(); 89 | if (authentication != null) { 90 | return authentication.getAuthorities().stream() 91 | .noneMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(AppSecurityUtils.ROLE_DEFAULT)); 92 | } 93 | return false; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/CustomAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | import org.springframework.security.web.AuthenticationEntryPoint; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | 12 | @Component 13 | public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { 14 | 15 | @Override 16 | public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { 17 | httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getLocalizedMessage()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/CustomUserDetails.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security; 2 | 3 | import com.demo.springcustomizedstarterexample.entities.UserEntity; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.oauth2.core.user.OAuth2User; 9 | 10 | import java.util.Collection; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | @Getter 15 | @Setter 16 | // TODO Refactor implementation to support OAuth2 and OIDC 17 | public class CustomUserDetails implements OAuth2User, UserDetails { 18 | 19 | private String email; 20 | private String password; 21 | 22 | private UserEntity userEntity; 23 | // refers to UserEntity -> Authorities, Usually defines roles (ROLE_USER, ROLE_ADMIN) 24 | private Collection authorities; 25 | // permissions or combination of Scope:Permissions e.g. users:full, users:read, profile:full, profile:edit 26 | // private Map permissions; 27 | // OAuth2 Provider attributes or custom Attributes 28 | private Map attributes; 29 | // ================================================= 30 | 31 | public CustomUserDetails(String email, 32 | String password, 33 | UserEntity userEntity, 34 | Collection authorities, 35 | Map attributes) { 36 | this.email = email; 37 | this.password = password; 38 | this.userEntity = userEntity; 39 | this.authorities = authorities; 40 | this.attributes = attributes; 41 | } 42 | 43 | public static CustomUserDetails buildFromUserEntity(UserEntity userEntity) { 44 | 45 | Collection grantedAuthorities = AppSecurityUtils 46 | .convertRolesSetToGrantedAuthorityList(userEntity.getRoles()); 47 | return new CustomUserDetails( 48 | userEntity.getEmail(), 49 | userEntity.getPassword(), 50 | userEntity, 51 | grantedAuthorities, 52 | new HashMap<>() 53 | ); 54 | } 55 | 56 | 57 | public static CustomUserDetails buildWithAuthAttributesAndAuthorities(UserEntity userEntity, 58 | Collection authorities, 59 | Map attributes) { 60 | 61 | CustomUserDetails customUserDetails = CustomUserDetails.buildFromUserEntity(userEntity); 62 | customUserDetails.setAuthorities(authorities); 63 | customUserDetails.setAttributes(attributes); 64 | return customUserDetails; 65 | } 66 | 67 | 68 | // UserDetails fields 69 | @Override 70 | public String getPassword() { 71 | return this.password; 72 | } 73 | 74 | @Override 75 | public String getUsername() { 76 | return this.email; 77 | } 78 | 79 | @Override 80 | public boolean isAccountNonExpired() { 81 | return true; 82 | } 83 | 84 | @Override 85 | public boolean isAccountNonLocked() { 86 | return true; 87 | } 88 | 89 | @Override 90 | public boolean isCredentialsNonExpired() { 91 | return true; 92 | } 93 | 94 | @Override 95 | public boolean isEnabled() { 96 | return this.userEntity.isEmailVerified(); 97 | } 98 | 99 | // Oauth2User fields 100 | @Override 101 | public A getAttribute(String name) { 102 | return OAuth2User.super.getAttribute(name); 103 | } 104 | 105 | @Override 106 | public Map getAttributes() { 107 | return this.attributes; 108 | } 109 | 110 | @Override 111 | public Collection getAuthorities() { 112 | return this.authorities; 113 | } 114 | 115 | @Override 116 | public String getName() { 117 | return String.valueOf(this.getEmail()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security; 2 | 3 | import com.demo.springcustomizedstarterexample.entities.UserEntity; 4 | import com.demo.springcustomizedstarterexample.repository.UserRepository; 5 | import com.demo.springcustomizedstarterexample.utils.exceptions.AppExceptionConstants; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.core.userdetails.UserDetailsService; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | import org.springframework.stereotype.Service; 11 | 12 | @Service 13 | public class CustomUserDetailsService implements UserDetailsService { 14 | 15 | @Autowired 16 | private UserRepository userRepository; 17 | 18 | @Override 19 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 20 | UserEntity userEntity = userRepository.findByEmail(username) 21 | .orElseThrow(() -> new UsernameNotFoundException(AppExceptionConstants.BAD_LOGIN_CREDENTIALS)); 22 | return CustomUserDetails.buildFromUserEntity(userEntity); 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/JWTAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security; 2 | 3 | import io.jsonwebtoken.ExpiredJwtException; 4 | import io.jsonwebtoken.MalformedJwtException; 5 | import io.jsonwebtoken.SignatureException; 6 | import io.jsonwebtoken.UnsupportedJwtException; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.context.SecurityContextHolder; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.util.StringUtils; 14 | import org.springframework.web.filter.OncePerRequestFilter; 15 | 16 | import javax.servlet.FilterChain; 17 | import javax.servlet.ServletException; 18 | import javax.servlet.http.HttpServletRequest; 19 | import javax.servlet.http.HttpServletResponse; 20 | import java.io.IOException; 21 | 22 | /** 23 | * 24 | * ` http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); ` 25 | * See: WebSecurityConfig for complete configuration 26 | * 27 | * This filter just checks if the token is present in request Header, "Authorization" key 28 | * - If present => Validates Token AND set Authentication Object into SecurityContextHolder 29 | * - Else => Continue filter chain 30 | * 31 | */ 32 | @Slf4j 33 | @Service 34 | public class JWTAuthenticationFilter extends OncePerRequestFilter { 35 | 36 | @Autowired 37 | private JWTTokenProvider jwtTokenProvider; 38 | 39 | @Override 40 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 41 | try { 42 | String jwt = jwtTokenProvider.getBearerTokenFromRequestHeader(request); 43 | if (StringUtils.hasText(jwt) && this.jwtTokenProvider.validateJWTToken(jwt)) { 44 | Authentication authentication = this.jwtTokenProvider.getAuthenticationFromToken(jwt); 45 | SecurityContextHolder.getContext().setAuthentication(authentication); 46 | } 47 | filterChain.doFilter(request, response); 48 | } catch (ExpiredJwtException ex) { 49 | log.info("Security exception Expired JWT token for user {} - {}", ex.getClaims().getSubject(), ex.getMessage()); 50 | response.sendError(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value(), "Expired JWT token"); 51 | } catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException ex) { 52 | log.info("Security exception {} ", ex.getMessage()); 53 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 54 | } 55 | 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/JWTTokenProvider.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security; 2 | 3 | import com.demo.springcustomizedstarterexample.config.AppProperties; 4 | import com.demo.springcustomizedstarterexample.entities.UserEntity; 5 | import com.demo.springcustomizedstarterexample.services.webapp.user.UserMapper; 6 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.UserDTO; 7 | import com.demo.springcustomizedstarterexample.utils.AppUtils; 8 | import io.jsonwebtoken.*; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.HttpHeaders; 12 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.core.GrantedAuthority; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.util.StringUtils; 17 | 18 | import javax.annotation.PostConstruct; 19 | import javax.servlet.http.HttpServletRequest; 20 | import java.util.*; 21 | 22 | /** 23 | * Util based class that generates JWT token, create Authentication Object from token string and Validates token string 24 | */ 25 | @Component 26 | @Slf4j 27 | public class JWTTokenProvider { 28 | 29 | private static final String HEADER_AUTHORIZATION = HttpHeaders.AUTHORIZATION; 30 | private static final String BEARER_TOKEN_START = "Bearer "; 31 | 32 | // Initialized from configuration properties 33 | private String secretKey; 34 | private long validityInMilliseconds; 35 | 36 | @Autowired 37 | private AppProperties appProperties; 38 | 39 | @Autowired 40 | private UserMapper userMapper; 41 | 42 | @PostConstruct 43 | protected void init() { 44 | if (appProperties.getJwt().isSecretKeyBase64Encoded()) { 45 | secretKey = appProperties.getJwt().getSecretKey(); 46 | } else { 47 | secretKey = Base64.getEncoder().encodeToString(appProperties.getJwt().getSecretKey().getBytes()); 48 | } 49 | validityInMilliseconds = appProperties.getJwt().getExpirationMillis(); 50 | } 51 | 52 | public String createJWTToken(Authentication authentication) { 53 | CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); 54 | Set authoritiesSet = AppSecurityUtils.convertGrantedAuthorityListToRolesSet(customUserDetails.getAuthorities()); 55 | 56 | String authoritiesJsonValue = AppUtils.toJson(authoritiesSet); 57 | String attributesJsonValue = AppUtils.toJson(customUserDetails.getAttributes()); 58 | String userJsonValue = AppUtils.toJson(userMapper.toDto(customUserDetails.getUserEntity())); 59 | 60 | Claims claims = Jwts.claims().setSubject(customUserDetails.getEmail()); 61 | Map claimsMap = new HashMap<>(); 62 | claimsMap.put("email", customUserDetails.getEmail()); 63 | claimsMap.put("user", userJsonValue); 64 | claimsMap.put("authorities", authoritiesJsonValue); 65 | claimsMap.put("attributes", attributesJsonValue); 66 | claims.putAll(claimsMap); 67 | 68 | Date now = new Date(); 69 | Date validity = new Date(now.getTime() + validityInMilliseconds); 70 | 71 | return Jwts.builder() 72 | .setSubject(customUserDetails.getEmail()) 73 | .setClaims(claims) 74 | .setIssuedAt(now) 75 | .setExpiration(validity) 76 | .signWith(SignatureAlgorithm.HS512, secretKey) 77 | .compact(); 78 | } 79 | 80 | public Authentication getAuthenticationFromToken(String token) { 81 | Claims body = Jwts.parser() 82 | .setSigningKey(secretKey) 83 | .parseClaimsJws(token) 84 | .getBody(); 85 | 86 | // Parsing Claims Data 87 | String email = (String) body.get("email"); 88 | UserDTO userDTO = AppUtils.fromJson(body.get("user").toString(), UserDTO.class); 89 | UserEntity userEntity = userMapper.toEntity(userDTO); 90 | Set authoritiesSet = AppUtils.fromJson(body.get("authorities").toString(), (Class>) ((Class) Set.class)); 91 | Collection grantedAuthorities = AppSecurityUtils.convertRolesSetToGrantedAuthorityList(authoritiesSet); 92 | Map attributes = AppUtils.fromJson(body.get("attributes").toString(), (Class>) (Class) Map.class); 93 | 94 | // Setting Principle Object 95 | 96 | CustomUserDetails customUserDetails = CustomUserDetails.buildWithAuthAttributesAndAuthorities(userEntity, grantedAuthorities, attributes); 97 | customUserDetails.setAttributes(attributes); 98 | return new UsernamePasswordAuthenticationToken(customUserDetails, "", customUserDetails.getAuthorities()); 99 | } 100 | 101 | public String getBearerTokenFromRequestHeader(HttpServletRequest request) { 102 | String bearerToken = request.getHeader(HEADER_AUTHORIZATION); 103 | if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_TOKEN_START)) { 104 | return bearerToken.substring(7, bearerToken.length()); 105 | } 106 | return null; 107 | } 108 | 109 | public boolean validateJWTToken(String token) { 110 | try { 111 | Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); 112 | if (claims.getBody().getExpiration().before(new Date())) { 113 | return false; 114 | } 115 | return true; 116 | } catch (SignatureException e) { 117 | log.info("Invalid JWT signature."); 118 | log.trace("Invalid JWT signature trace: {}", e); 119 | } catch (MalformedJwtException e) { 120 | log.info("Invalid JWT token."); 121 | log.trace("Invalid JWT token trace: {}", e); 122 | } catch (ExpiredJwtException e) { 123 | log.info("Expired JWT token."); 124 | log.trace("Expired JWT token trace: {}", e); 125 | } catch (UnsupportedJwtException e) { 126 | log.info("Unsupported JWT token."); 127 | log.trace("Unsupported JWT token trace: {}", e); 128 | } catch (IllegalArgumentException e) { 129 | log.info("JWT token compact of handler are invalid."); 130 | log.trace("JWT token compact of handler are invalid trace: {}", e); 131 | } 132 | return false; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/CustomOAuth2UserService.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth; 2 | 3 | import com.demo.springcustomizedstarterexample.entities.UserEntity; 4 | import com.demo.springcustomizedstarterexample.security.AppSecurityUtils; 5 | import com.demo.springcustomizedstarterexample.security.CustomUserDetails; 6 | import com.demo.springcustomizedstarterexample.security.oauth.common.CustomAbstractOAuth2UserInfo; 7 | import com.demo.springcustomizedstarterexample.security.oauth.common.OAuth2Util; 8 | import com.demo.springcustomizedstarterexample.security.oauth.common.SecurityEnums; 9 | import com.demo.springcustomizedstarterexample.services.webapp.user.UserMapper; 10 | import com.demo.springcustomizedstarterexample.services.webapp.user.UserService; 11 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.UserDTO; 12 | import org.springframework.beans.BeanUtils; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.security.authentication.InternalAuthenticationServiceException; 15 | import org.springframework.security.core.AuthenticationException; 16 | import org.springframework.security.core.GrantedAuthority; 17 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 18 | import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; 19 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 20 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 21 | import org.springframework.security.oauth2.core.user.OAuth2User; 22 | import org.springframework.stereotype.Service; 23 | import org.springframework.util.StringUtils; 24 | 25 | import java.util.List; 26 | import java.util.Optional; 27 | import java.util.Set; 28 | import java.util.stream.Collectors; 29 | 30 | /** 31 | * 1. After Users Agrees by clicking on consent screen (To allow our app to access users allowed resources) 32 | * - loadUser will trigger for all OAuth2 provider - (GitHub, Google, Facebook, Custom Auth Provider etc.) 33 | *

34 | * 2. Retrieve attributes, from security.oauth2.core.user.OAuth2User which consists of { name, email, imageUrl and other attributes } 35 | * - Each registrationId will have their own attributes key (eg. google: picture, github: avatar_url etc), 36 | * - And Map this attributes specific to OAuth2 provider with-respect-to abstract CustomAbstractOAuth2UserInfo 37 | *

38 | * 3. Determine is this [ New Sign Up ] or [ Existing Sign In ] 39 | * - Sign In (email will be present in our database) OR 40 | * - Sign Up ( if don't have user email, we need to register user, and save email into db) 41 | *

42 | * 4. Create Principle Object i.e. CustomUserDetails implements OAuth2User 43 | * - return security.oauth2.core.user.OAuth2User that will set Authentication object, ( similar to CustomUserDetailsService - method loadUserByUsername ) 44 | *

45 | * 5. On completion "processOAuth2User()" Flow Jumps to either OAuth2AuthenticationSuccessHandler or OAuth2AuthenticationFailureHandler 46 | */ 47 | @Service 48 | public class CustomOAuth2UserService extends DefaultOAuth2UserService { 49 | 50 | @Autowired 51 | private UserService userService; 52 | 53 | @Autowired 54 | private UserMapper userMapper; 55 | 56 | @Override 57 | public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException { 58 | OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest); 59 | 60 | try { 61 | return processOAuth2User(oAuth2UserRequest, oAuth2User); 62 | } catch (AuthenticationException ex) { 63 | throw ex; 64 | } catch (Exception ex) { 65 | throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause()); 66 | } 67 | } 68 | 69 | private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, 70 | OAuth2User oAuth2User) { 71 | // Mapped OAuth2User to specific CustomAbstractOAuth2UserInfo for that registration id 72 | // clientRegistrationId - (google, facebook, gitHub, or Custom Auth Provider - ( keyClock, okta, authServer etc.) 73 | String clientRegistrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId(); 74 | CustomAbstractOAuth2UserInfo customAbstractOAuth2UserInfo = OAuth2Util.getOAuth2UserInfo(clientRegistrationId, oAuth2User.getAttributes()); 75 | 76 | // Check if the email is provided by the OAuthProvider 77 | SecurityEnums.AuthProviderId registeredProviderId = SecurityEnums.AuthProviderId.valueOf(clientRegistrationId); 78 | String userEmail = customAbstractOAuth2UserInfo.getEmail(); 79 | if (!StringUtils.hasText(userEmail)) { 80 | throw new InternalAuthenticationServiceException("Sorry, Couldn't retrieve your email from Provider " + clientRegistrationId + ". Email not available or Private by default"); 81 | } 82 | 83 | // Determine is this [ Login ] or [ New Sign up ] 84 | // Sign In (email will be present in our database) OR Sign Up ( if don't have user email, we need to register user, and save email into db) 85 | Optional optionalUserByEmail = userService.findOptionalUserByEmail(userEmail); 86 | if (optionalUserByEmail.isEmpty()) { 87 | optionalUserByEmail = Optional.of(registerNewOAuthUser(oAuth2UserRequest, customAbstractOAuth2UserInfo)); 88 | } 89 | UserDTO userDTO = optionalUserByEmail.get(); 90 | if (userDTO.getRegisteredProviderName().equals(registeredProviderId)) { 91 | updateExistingOAuthUser(userDTO, customAbstractOAuth2UserInfo); 92 | } else { 93 | String incorrectProviderChoice = "Sorry, this email is linked with \"" + userDTO.getRegisteredProviderName() + "\" account. " + 94 | "Please use your \"" + userDTO.getRegisteredProviderName() + "\" account to login."; 95 | throw new InternalAuthenticationServiceException(incorrectProviderChoice); 96 | } 97 | 98 | 99 | List grantedAuthorities = oAuth2User.getAuthorities().stream().collect(Collectors.toList()); 100 | grantedAuthorities.add(new SimpleGrantedAuthority(AppSecurityUtils.ROLE_DEFAULT)); 101 | UserEntity userEntity = userMapper.toEntity(userDTO); 102 | return CustomUserDetails.buildWithAuthAttributesAndAuthorities(userEntity, grantedAuthorities, oAuth2User.getAttributes()); 103 | } 104 | 105 | private UserDTO registerNewOAuthUser(OAuth2UserRequest oAuth2UserRequest, 106 | CustomAbstractOAuth2UserInfo customAbstractOAuth2UserInfo) { 107 | UserDTO userDTO = new UserDTO(); 108 | userDTO.setFullName(customAbstractOAuth2UserInfo.getName()); 109 | userDTO.setEmail(customAbstractOAuth2UserInfo.getEmail()); 110 | userDTO.setRegisteredProviderName(SecurityEnums.AuthProviderId.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId())); 111 | userDTO.setRegisteredProviderId(customAbstractOAuth2UserInfo.getId()); 112 | userDTO.setRoles(Set.of(AppSecurityUtils.ROLE_DEFAULT)); 113 | userDTO.setEmailVerified(true); 114 | UserDTO returnedUserDTO = userService.createUser(userDTO); 115 | return returnedUserDTO; 116 | } 117 | 118 | private void updateExistingOAuthUser(UserDTO existingUserDTO, 119 | CustomAbstractOAuth2UserInfo customAbstractOAuth2UserInfo) { 120 | existingUserDTO.setFullName(customAbstractOAuth2UserInfo.getName()); 121 | existingUserDTO.setImageUrl(customAbstractOAuth2UserInfo.getImageUrl()); 122 | UserDTO updatedUserDTO = userService.updateUser(existingUserDTO); 123 | BeanUtils.copyProperties(updatedUserDTO, existingUserDTO); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/OAuth2AuthenticationFailureHandler.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth; 2 | 3 | import com.demo.springcustomizedstarterexample.security.oauth.common.HttpCookieOAuth2AuthorizationRequestRepository; 4 | import com.demo.springcustomizedstarterexample.utils.AppWebUtils; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.core.AuthenticationException; 7 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.util.UriComponentsBuilder; 10 | 11 | import javax.servlet.ServletException; 12 | import javax.servlet.http.Cookie; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.io.IOException; 16 | 17 | import static com.demo.springcustomizedstarterexample.security.oauth.common.OAuth2Util.REDIRECT_URI_PARAM_COOKIE_NAME; 18 | 19 | /** 20 | * 1. Flow comes here "onAuthenticationFailure()", If OAuth2 Authentication Fails from - CustomOAuth2UserService 21 | * - We send authentication error response to the redirect_uri ( e.g. http://my-ui-app.com/oauth2/redirectPage?error=authenticationException-localizedMessage-or-custom-message ) 22 | * - Since its failure response, (we aren't sending any tokens or data ) so, we don't need to validate the redirect_uri for security measures 23 | * 24 | * 2. By default, OAuth2 uses Session based AuthorizationRequestRepository, since we are using Cookie based AuthorizationRequestRepository 25 | * - We clear authorizationRequest stored in our cookie, before sending redirect response 26 | */ 27 | @Component 28 | public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { 29 | 30 | @Autowired 31 | HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; 32 | 33 | @Override 34 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { 35 | String targetUrl = AppWebUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) 36 | .map(Cookie::getValue) 37 | .orElse(("/")); 38 | 39 | targetUrl = UriComponentsBuilder.fromUriString(targetUrl) 40 | .queryParam("error", exception.getLocalizedMessage()) 41 | .build().toUriString(); 42 | 43 | httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); 44 | getRedirectStrategy().sendRedirect(request, response, targetUrl); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/OAuth2AuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth; 2 | 3 | import com.demo.springcustomizedstarterexample.config.AppProperties; 4 | import com.demo.springcustomizedstarterexample.security.JWTTokenProvider; 5 | import com.demo.springcustomizedstarterexample.security.oauth.common.HttpCookieOAuth2AuthorizationRequestRepository; 6 | import com.demo.springcustomizedstarterexample.utils.AppWebUtils; 7 | import com.demo.springcustomizedstarterexample.utils.exceptions.BadRequestException; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.web.util.UriComponentsBuilder; 13 | 14 | import javax.servlet.ServletException; 15 | import javax.servlet.http.Cookie; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.io.IOException; 19 | import java.net.URI; 20 | import java.util.Arrays; 21 | import java.util.Optional; 22 | 23 | import static com.demo.springcustomizedstarterexample.security.oauth.common.OAuth2Util.ORIGINAL_REQUEST_URI_PARAM_COOKIE_NAME; 24 | import static com.demo.springcustomizedstarterexample.security.oauth.common.OAuth2Util.REDIRECT_URI_PARAM_COOKIE_NAME; 25 | 26 | /** 27 | * 1. Flow comes here "onAuthenticationSuccess()", After successful OAuth2 Authentication (see: CustomOAuth2UserService ) 28 | * - We create Custom JWT Token and respond back to redirect_uri ( e.g. http://my-ui-app.com/oauth2/redirectPage?token=generatedJwtToken ) 29 | * - We validate the redirect_uri for security measures, to send token to only our authorized redirect origins 30 | *

31 | * 2. By default, OAuth2 uses Session based AuthorizationRequestRepository, since we are using Cookie based AuthorizationRequestRepository 32 | * - We clear authorizationRequest stored in our cookie, before sending redirect response 33 | */ 34 | @Service 35 | public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { 36 | 37 | @Autowired 38 | private JWTTokenProvider jwtTokenProvider; 39 | 40 | @Autowired 41 | private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; 42 | 43 | @Autowired 44 | private AppProperties appProperties; 45 | 46 | @Override 47 | public void onAuthenticationSuccess(HttpServletRequest request, 48 | HttpServletResponse response, 49 | Authentication authentication) throws IOException, ServletException { 50 | String targetUrl = determineTargetUrl(request, response, authentication); 51 | 52 | if (response.isCommitted()) { 53 | logger.debug("Response has already been committed. Unable to redirect to " + targetUrl); 54 | return; 55 | } 56 | 57 | clearAuthenticationAttributes(request, response); 58 | getRedirectStrategy().sendRedirect(request, response, targetUrl); 59 | } 60 | 61 | protected String determineTargetUrl(HttpServletRequest request, 62 | HttpServletResponse response, 63 | Authentication authentication) { 64 | Optional redirectUri = AppWebUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) 65 | .map(Cookie::getValue); 66 | Optional originalRequestUri = AppWebUtils.getCookie(request, ORIGINAL_REQUEST_URI_PARAM_COOKIE_NAME) 67 | .map(Cookie::getValue); 68 | 69 | if (redirectUri.isPresent() && !isRedirectOriginAuthorized(redirectUri.get())) { 70 | throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication"); 71 | } 72 | 73 | String targetUrl = redirectUri.orElse(getDefaultTargetUrl()); 74 | 75 | String token = jwtTokenProvider.createJWTToken(authentication); 76 | 77 | return UriComponentsBuilder.fromUriString(targetUrl) 78 | .queryParam("token", token) 79 | .queryParam(ORIGINAL_REQUEST_URI_PARAM_COOKIE_NAME, originalRequestUri) 80 | .build().toUriString(); 81 | } 82 | 83 | protected void clearAuthenticationAttributes(HttpServletRequest request, 84 | HttpServletResponse response) { 85 | super.clearAuthenticationAttributes(request); 86 | httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); 87 | } 88 | 89 | private boolean isRedirectOriginAuthorized(String uri) { 90 | URI clientRedirectUri = URI.create(uri); 91 | 92 | return Arrays.stream(appProperties.getOAuth2().getAuthorizedRedirectOrigins()) 93 | .anyMatch(authorizedRedirectOrigin -> { 94 | URI authorizedURI = URI.create(authorizedRedirectOrigin); 95 | if (authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost()) 96 | && authorizedURI.getPort() == clientRedirectUri.getPort()) { 97 | return true; 98 | } 99 | return false; 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/common/CustomAbstractOAuth2UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth.common; 2 | 3 | import java.util.Map; 4 | 5 | public abstract class CustomAbstractOAuth2UserInfo { 6 | protected Map attributes; 7 | 8 | public CustomAbstractOAuth2UserInfo(Map attributes) { 9 | this.attributes = attributes; 10 | } 11 | 12 | public Map getAttributes() { 13 | return attributes; 14 | } 15 | 16 | public abstract String getId(); 17 | 18 | public abstract String getName(); 19 | 20 | public abstract String getEmail(); 21 | 22 | public abstract String getImageUrl(); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/common/FacebookCustomAbstractOAuth2UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth.common; 2 | 3 | import java.util.Map; 4 | 5 | public class FacebookCustomAbstractOAuth2UserInfo extends CustomAbstractOAuth2UserInfo { 6 | public FacebookCustomAbstractOAuth2UserInfo(Map attributes) { 7 | super(attributes); 8 | } 9 | 10 | @Override 11 | public String getId() { 12 | return (String) attributes.get("id"); 13 | } 14 | 15 | @Override 16 | public String getName() { 17 | return (String) attributes.get("name"); 18 | } 19 | 20 | @Override 21 | public String getEmail() { 22 | return (String) attributes.get("email"); 23 | } 24 | 25 | @Override 26 | public String getImageUrl() { 27 | if(attributes.containsKey("picture")) { 28 | Map pictureObj = (Map) attributes.get("picture"); 29 | if(pictureObj.containsKey("data")) { 30 | Map dataObj = (Map) pictureObj.get("data"); 31 | if(dataObj.containsKey("url")) { 32 | return (String) dataObj.get("url"); 33 | } 34 | } 35 | } 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/common/GithubCustomAbstractOAuth2UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth.common; 2 | 3 | import java.util.Map; 4 | 5 | public class GithubCustomAbstractOAuth2UserInfo extends CustomAbstractOAuth2UserInfo { 6 | 7 | public GithubCustomAbstractOAuth2UserInfo(Map attributes) { 8 | super(attributes); 9 | } 10 | 11 | @Override 12 | public String getId() { 13 | return ((Integer) attributes.get("id")).toString(); 14 | } 15 | 16 | @Override 17 | public String getName() { 18 | return (String) attributes.get("name"); 19 | } 20 | 21 | @Override 22 | public String getEmail() { 23 | return (String) attributes.get("email"); 24 | } 25 | 26 | @Override 27 | public String getImageUrl() { 28 | return (String) attributes.get("avatar_url"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/common/GoogleCustomAbstractOAuth2UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth.common; 2 | 3 | import java.util.Map; 4 | 5 | public class GoogleCustomAbstractOAuth2UserInfo extends CustomAbstractOAuth2UserInfo { 6 | 7 | public GoogleCustomAbstractOAuth2UserInfo(Map attributes) { 8 | super(attributes); 9 | } 10 | 11 | @Override 12 | public String getId() { 13 | return (String) attributes.get("sub"); 14 | } 15 | 16 | @Override 17 | public String getName() { 18 | return (String) attributes.get("name"); 19 | } 20 | 21 | @Override 22 | public String getEmail() { 23 | return (String) attributes.get("email"); 24 | } 25 | 26 | @Override 27 | public String getImageUrl() { 28 | return (String) attributes.get("picture"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/common/HttpCookieOAuth2AuthorizationRequestRepository.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth.common; 2 | 3 | import com.demo.springcustomizedstarterexample.utils.AppUtils; 4 | import com.demo.springcustomizedstarterexample.utils.AppWebUtils; 5 | import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; 6 | import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.util.Assert; 9 | 10 | import javax.servlet.http.Cookie; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | import static com.demo.springcustomizedstarterexample.security.oauth.common.OAuth2Util.*; 15 | 16 | /** 17 | * Cookie based repository for storing Authorization requests 18 | *

19 | * By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save 20 | * the authorization request. But, since our service is stateless, we can't save it in the session. 21 | * We'll use cookie instead. 22 | */ 23 | @Component 24 | public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository { 25 | 26 | /** 27 | * Load authorization request from cookie 28 | */ 29 | @Override 30 | public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { 31 | 32 | Assert.notNull(request, "request cannot be null"); 33 | 34 | return AppWebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME) 35 | .map(cookie -> deserializeCookie(cookie)) 36 | .orElse(null); 37 | } 38 | 39 | /** 40 | * Save authorization request in cookie 41 | */ 42 | @Override 43 | public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, 44 | HttpServletRequest request, 45 | HttpServletResponse response) { 46 | 47 | Assert.notNull(request, "request cannot be null"); 48 | Assert.notNull(response, "response cannot be null"); 49 | 50 | if (authorizationRequest == null) { 51 | 52 | AppWebUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); 53 | AppWebUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); 54 | AppWebUtils.deleteCookie(request, response, ORIGINAL_REQUEST_URI_PARAM_COOKIE_NAME); 55 | return; 56 | } 57 | 58 | // Setting up authorizationRequest COOKIE, redirectUri COOKIE and originalRequestUri COOKIE 59 | String redirectUri = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME); 60 | String originalRequestUri = request.getParameter(ORIGINAL_REQUEST_URI_PARAM_COOKIE_NAME); 61 | AppWebUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, AppUtils.serialize(authorizationRequest)); 62 | AppWebUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUri); 63 | AppWebUtils.addCookie(response, ORIGINAL_REQUEST_URI_PARAM_COOKIE_NAME, originalRequestUri); 64 | } 65 | 66 | @Override 67 | public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, 68 | HttpServletResponse response) { 69 | 70 | OAuth2AuthorizationRequest originalRequest = loadAuthorizationRequest(request); 71 | AppWebUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); 72 | return originalRequest; 73 | } 74 | 75 | @Deprecated 76 | @Override 77 | public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { 78 | throw new UnsupportedOperationException("Spring Security shouldn't have called the deprecated removeAuthorizationRequest(request)"); 79 | } 80 | 81 | 82 | private OAuth2AuthorizationRequest deserializeCookie(Cookie cookie) { 83 | return AppUtils.deserialize(cookie.getValue()); 84 | } 85 | 86 | public void removeAuthorizationRequestCookies(HttpServletRequest request, 87 | HttpServletResponse response) { 88 | AppWebUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); 89 | AppWebUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); 90 | AppWebUtils.deleteCookie(request, response, ORIGINAL_REQUEST_URI_PARAM_COOKIE_NAME); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/common/OAuth2Util.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth.common; 2 | 3 | import org.springframework.security.authentication.InternalAuthenticationServiceException; 4 | 5 | import java.util.Map; 6 | 7 | public class OAuth2Util { 8 | 9 | public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; 10 | 11 | // UI-App/Web-Client will use this param to redirect flow to appropriate page 12 | public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri"; 13 | public static final String ORIGINAL_REQUEST_URI_PARAM_COOKIE_NAME = "original_request_uri"; 14 | 15 | /** 16 | * Populate CustomAbstractOAuth2UserInfo for specific OAuthProvider 17 | */ 18 | public static CustomAbstractOAuth2UserInfo getOAuth2UserInfo(String registrationId, 19 | Map attributes) { 20 | if (registrationId.equalsIgnoreCase(SecurityEnums.AuthProviderId.google.toString())) { 21 | return new GoogleCustomAbstractOAuth2UserInfo(attributes); 22 | } else if (registrationId.equalsIgnoreCase(SecurityEnums.AuthProviderId.facebook.toString())) { 23 | return new FacebookCustomAbstractOAuth2UserInfo(attributes); 24 | } else if (registrationId.equalsIgnoreCase(SecurityEnums.AuthProviderId.github.toString())) { 25 | return new GithubCustomAbstractOAuth2UserInfo(attributes); 26 | } else { 27 | throw new InternalAuthenticationServiceException("Sorry! Login with " + registrationId + " is not supported yet."); 28 | } 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/security/oauth/common/SecurityEnums.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.security.oauth.common; 2 | 3 | public class SecurityEnums { 4 | 5 | public enum AuthProviderId { 6 | app_custom_authentication, google, facebook, github, 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/auth/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.auth; 2 | 3 | import com.demo.springcustomizedstarterexample.services.auth.dtos.AuthResponseDTO; 4 | import com.demo.springcustomizedstarterexample.services.auth.dtos.LoginRequestDTO; 5 | import com.demo.springcustomizedstarterexample.services.auth.dtos.RegisterUserRequestDTO; 6 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.UserDTO; 7 | 8 | public interface AuthenticationService { 9 | 10 | AuthResponseDTO loginUser(LoginRequestDTO loginRequest); 11 | 12 | UserDTO registerUser(RegisterUserRequestDTO registerUserRequestDTO); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/auth/AuthenticationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.auth; 2 | 3 | import com.demo.springcustomizedstarterexample.security.JWTTokenProvider; 4 | import com.demo.springcustomizedstarterexample.security.oauth.common.SecurityEnums; 5 | import com.demo.springcustomizedstarterexample.services.auth.dtos.AuthResponseDTO; 6 | import com.demo.springcustomizedstarterexample.services.auth.dtos.LoginRequestDTO; 7 | import com.demo.springcustomizedstarterexample.services.auth.dtos.RegisterUserRequestDTO; 8 | import com.demo.springcustomizedstarterexample.services.webapp.user.UserService; 9 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.UserDTO; 10 | import com.demo.springcustomizedstarterexample.utils.exceptions.AppExceptionConstants; 11 | import org.springframework.security.authentication.AuthenticationManager; 12 | import org.springframework.security.authentication.BadCredentialsException; 13 | import org.springframework.security.authentication.DisabledException; 14 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 15 | import org.springframework.security.core.Authentication; 16 | import org.springframework.security.core.AuthenticationException; 17 | import org.springframework.stereotype.Service; 18 | 19 | @Service 20 | public class AuthenticationServiceImpl implements AuthenticationService { 21 | 22 | private final AuthenticationManager authenticationManager; 23 | private final UserService userService; 24 | private final JWTTokenProvider jwtTokenProvider; 25 | 26 | public AuthenticationServiceImpl(AuthenticationManager authenticationManager, 27 | UserService userService, 28 | JWTTokenProvider jwtTokenProvider) { 29 | this.authenticationManager = authenticationManager; 30 | this.userService = userService; 31 | this.jwtTokenProvider = jwtTokenProvider; 32 | } 33 | 34 | 35 | @Override 36 | public AuthResponseDTO loginUser(LoginRequestDTO loginRequest) { 37 | try { 38 | Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); 39 | String token = jwtTokenProvider.createJWTToken(authentication); 40 | AuthResponseDTO authResponseDTO = new AuthResponseDTO(); 41 | authResponseDTO.setToken(token); 42 | return authResponseDTO; 43 | } catch (AuthenticationException e) { 44 | if (e instanceof DisabledException) { 45 | throw new BadCredentialsException(AppExceptionConstants.ACCOUNT_NOT_ACTIVATED); 46 | } 47 | throw new BadCredentialsException(e.getMessage()); 48 | } 49 | } 50 | 51 | @Override 52 | public UserDTO registerUser(RegisterUserRequestDTO registerUserRequestDTO) { 53 | UserDTO userDTO = new UserDTO(); 54 | userDTO.setEmail(registerUserRequestDTO.getEmail()); 55 | userDTO.setPassword(registerUserRequestDTO.getPassword()); 56 | userDTO.setFullName(registerUserRequestDTO.getFullName()); 57 | userDTO.setRegisteredProviderName(SecurityEnums.AuthProviderId.app_custom_authentication); 58 | UserDTO user = userService.createUser(userDTO); 59 | return user; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/auth/dtos/AuthResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.auth.dtos; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | public class AuthResponseDTO { 11 | 12 | private String token; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/auth/dtos/LoginRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.auth.dtos; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class LoginRequestDTO { 7 | 8 | private String email; 9 | 10 | private String password; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/auth/dtos/RegisterUserRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.auth.dtos; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class RegisterUserRequestDTO { 7 | 8 | private String fullName; 9 | 10 | private String email; 11 | 12 | private String password; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/common/GenericMapper.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.common; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | public interface GenericMapper { 7 | 8 | E toEntity(D dto); 9 | 10 | D toDto(E entity); 11 | 12 | default List toEntityList(final List dtos) { 13 | if (dtos != null) { 14 | return dtos.stream() 15 | .map(this::toEntity) 16 | .collect(Collectors.toList()); 17 | } 18 | return null; 19 | } 20 | 21 | default List toDtoList(final List entitys) { 22 | if (entitys != null) { 23 | return entitys.stream() 24 | .map(this::toDto) 25 | .collect(Collectors.toList()); 26 | } 27 | return null; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/common/GenericResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.common; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @Builder 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class GenericResponseDTO { 13 | 14 | private T response; 15 | 16 | private String messageCode; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/mail/AbstractDefaultEmailService.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.mail; 2 | 3 | import freemarker.template.Template; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.core.io.FileSystemResource; 6 | import org.springframework.mail.MailException; 7 | import org.springframework.mail.SimpleMailMessage; 8 | import org.springframework.mail.javamail.JavaMailSender; 9 | import org.springframework.mail.javamail.MimeMessageHelper; 10 | import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; 11 | import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; 12 | 13 | import javax.mail.MessagingException; 14 | import javax.mail.internet.MimeMessage; 15 | import java.io.File; 16 | import java.nio.charset.StandardCharsets; 17 | import java.util.Map; 18 | 19 | @Slf4j 20 | public abstract class AbstractDefaultEmailService { 21 | 22 | private final JavaMailSender javaMailSender; 23 | private final FreeMarkerConfigurer freemarkerConfigurer; 24 | private final String defaultSourceEmailAddress; 25 | 26 | protected AbstractDefaultEmailService(JavaMailSender javaMailSender, 27 | FreeMarkerConfigurer freemarkerConfigurer, 28 | String defaultSourceEmailAddress) { 29 | this.javaMailSender = javaMailSender; 30 | this.freemarkerConfigurer = freemarkerConfigurer; 31 | this.defaultSourceEmailAddress = defaultSourceEmailAddress; 32 | } 33 | 34 | public void sendSimpleMessage(String destinationEmail, 35 | String subject, 36 | String text) { 37 | try { 38 | SimpleMailMessage message = new SimpleMailMessage(); 39 | message.setFrom(defaultSourceEmailAddress); 40 | message.setTo(destinationEmail); 41 | message.setSubject(subject); 42 | message.setText(text); 43 | 44 | javaMailSender.send(message); 45 | } catch (MailException e) { 46 | log.error("sendSimpleMessage failed MessagingException {} ", e.getMessage()); 47 | } 48 | } 49 | 50 | public void sendSimpleMessageUsingTemplate(String destinationEmail, 51 | String subject, 52 | String templateText, 53 | String... templateModel) { 54 | SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); 55 | simpleMailMessage.setText(templateText); 56 | 57 | // Sample Example 58 | // templateText = "Hello \n%s\n, \n This is the default fallback test email template for you. \n Send By: %s \n"; 59 | // String[] templateModel = { "TestUser", "Spring-boot-app" }; 60 | 61 | String text = String.format(simpleMailMessage.getText(), templateModel); 62 | sendSimpleMessage(destinationEmail, subject, text); 63 | } 64 | 65 | public void sendMessageWithAttachment(String destinationEmail, 66 | String subject, 67 | String text, 68 | String pathToAttachment, 69 | String attachmentFilename) { 70 | try { 71 | MimeMessage message = javaMailSender.createMimeMessage(); 72 | MimeMessageHelper helper = new MimeMessageHelper(message, true); 73 | helper.setFrom(defaultSourceEmailAddress); 74 | helper.setTo(destinationEmail); 75 | helper.setSubject(subject); 76 | helper.setText(text); 77 | 78 | FileSystemResource file = new FileSystemResource(new File(pathToAttachment)); 79 | helper.addAttachment(attachmentFilename, file); 80 | 81 | javaMailSender.send(message); 82 | } catch (MessagingException e) { 83 | log.error("sendMessageWithAttachment failed MessagingException {} ", e.getMessage()); 84 | } 85 | } 86 | 87 | public void sendMessageUsingFreemarkerTemplate(String destinationEmail, 88 | String subject, 89 | Map templateModel, 90 | MessageTemplateCodeUtil.TemplatesPath templatesPath) { 91 | 92 | log.info("Initiated: sendMessageUsingFreemarkerTemplate - template: {} , toEmailAddress ", templatesPath.getTemplatePath(), destinationEmail); 93 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 94 | 95 | try { 96 | 97 | Template freemarkerTemplate = freemarkerConfigurer.getConfiguration().getTemplate(templatesPath.getTemplatePath()); 98 | String htmlBody = FreeMarkerTemplateUtils.processTemplateIntoString(freemarkerTemplate, templateModel); 99 | 100 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, 101 | MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, 102 | StandardCharsets.UTF_8.name()); 103 | helper.setTo(destinationEmail); 104 | helper.setSubject(subject); 105 | helper.setText(htmlBody, true); 106 | javaMailSender.send(mimeMessage); 107 | } catch (MessagingException e) { 108 | log.error("sendMessageUsingFreemarkerTemplate failed MessagingException {} ", e.getMessage()); 109 | e.printStackTrace(); 110 | } catch (Exception e) { 111 | log.error("sendMessageUsingFreemarkerTemplate failed Exception {} ", e.getMessage()); 112 | e.printStackTrace(); 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/mail/EmailService.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.mail; 2 | 3 | import com.demo.springcustomizedstarterexample.config.AppProperties; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.mail.javamail.JavaMailSender; 6 | import org.springframework.mail.javamail.MimeMessageHelper; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; 9 | import org.springframework.util.MultiValueMap; 10 | import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; 11 | import org.springframework.web.util.UriComponentsBuilder; 12 | 13 | import javax.annotation.PostConstruct; 14 | import javax.mail.MessagingException; 15 | import javax.mail.internet.MimeMessage; 16 | import java.nio.charset.StandardCharsets; 17 | import java.time.Instant; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | @Service 22 | @Slf4j 23 | public class EmailService extends AbstractDefaultEmailService { 24 | 25 | private String defaultSourceEmailAddress; 26 | private String officialCompanyName; 27 | private String officialCompanyDomain; 28 | 29 | private final JavaMailSender javaMailSender; 30 | private final FreeMarkerConfigurer freemarkerConfigurer; 31 | private final AppProperties appProperties; 32 | 33 | public EmailService(JavaMailSender javaMailSender, 34 | FreeMarkerConfigurer freemarkerConfigurer, 35 | AppProperties appProperties) { 36 | super(javaMailSender, freemarkerConfigurer, appProperties.getMail().getDefaultEmailAddress()); 37 | this.javaMailSender = javaMailSender; 38 | this.freemarkerConfigurer = freemarkerConfigurer; 39 | this.appProperties = appProperties; 40 | } 41 | 42 | @PostConstruct 43 | protected void init() { 44 | Instant now = Instant.now(); 45 | defaultSourceEmailAddress = appProperties.getMail().getDefaultEmailAddress(); 46 | officialCompanyName = appProperties.getOfficialCompanyName(); 47 | officialCompanyDomain = appProperties.getOfficialCompanyDomain(); 48 | } 49 | 50 | public void sendVerificationEmail(String destinationEmail, 51 | String firstName, 52 | MultiValueMap appendQueryParamsToPasswordResetLink) { 53 | log.info("Initiated: sendVerificationEmail - to {} ", destinationEmail); 54 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 55 | try { 56 | 57 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, 58 | MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, 59 | StandardCharsets.UTF_8.name()); 60 | 61 | // Populate the template data for Email Verification 62 | Map templateData = new HashMap<>(); 63 | templateData.put(MessageTemplateCodeUtil.TemplateKeys.verificationUserFirstName, firstName); 64 | templateData.putAll(MessageTemplateCodeUtil.templateDefaultValuesMap); 65 | String linkVerifyEmail = UriComponentsBuilder.fromUriString(officialCompanyDomain + "/verify") 66 | .queryParams(appendQueryParamsToPasswordResetLink) 67 | .queryParam("isProcessVerifyEmail", true) 68 | .build().toUriString(); 69 | templateData.put(MessageTemplateCodeUtil.TemplateKeys.linkEmailVerification, linkVerifyEmail); 70 | 71 | // Retrieving (verification-code mail) template file to set populated data 72 | String templatePath = MessageTemplateCodeUtil.TemplatesPath.EMAIL_VERIFICATION_MAIL.getTemplatePath(); 73 | String templateContent = FreeMarkerTemplateUtils.processTemplateIntoString( 74 | freemarkerConfigurer.getConfiguration().getTemplate(templatePath), 75 | templateData); 76 | 77 | // Sending email 78 | helper.setTo(destinationEmail); 79 | helper.setSubject(MessageTemplateCodeUtil.subjectVerifyEmail + " for " + officialCompanyName); 80 | helper.setText(templateContent, true); 81 | javaMailSender.send(mimeMessage); 82 | 83 | log.info("Completed: sendVerificationEmail "); 84 | } catch (MessagingException e) { 85 | log.error("sendWelcomeEmail failed MessagingException {} ", e.getMessage()); 86 | e.printStackTrace(); 87 | } catch (Exception e) { 88 | log.error("sendWelcomeEmail failed Exception {} ", e.getMessage()); 89 | e.printStackTrace(); 90 | } 91 | } 92 | 93 | public void sendPasswordResetEmail(String destinationEmail, 94 | String firstName, 95 | MultiValueMap appendQueryParamsToVerificationLink) { 96 | log.info("Initiated: sendPasswordResetEmail - to {} ", destinationEmail); 97 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 98 | try { 99 | 100 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, 101 | MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, 102 | StandardCharsets.UTF_8.name()); 103 | 104 | // Populate the template data for Password reset 105 | Map templateData = new HashMap<>(); 106 | templateData.put(MessageTemplateCodeUtil.TemplateKeys.verificationUserFirstName, firstName); 107 | templateData.putAll(MessageTemplateCodeUtil.templateDefaultValuesMap); 108 | String linkPasswordReset = UriComponentsBuilder.fromUriString(officialCompanyDomain + "/verify") 109 | .queryParams(appendQueryParamsToVerificationLink) 110 | .queryParam("isProcessPasswordReset", true) 111 | .build().toUriString(); 112 | templateData.put(MessageTemplateCodeUtil.TemplateKeys.linkPasswordReset, linkPasswordReset); 113 | 114 | // Retrieving (password-reset mail) template file to set populated data 115 | String templatePath = MessageTemplateCodeUtil.TemplatesPath.RESET_PASSWORD_MAIL.getTemplatePath(); 116 | String templateContent = FreeMarkerTemplateUtils.processTemplateIntoString( 117 | freemarkerConfigurer.getConfiguration().getTemplate(templatePath), 118 | templateData); 119 | 120 | // Sending email 121 | helper.setTo(destinationEmail); 122 | helper.setSubject(MessageTemplateCodeUtil.subjectResetPasswordEmail + " for " + officialCompanyName); 123 | helper.setText(templateContent, true); 124 | javaMailSender.send(mimeMessage); 125 | 126 | log.info("Completed: sendPasswordResetEmail "); 127 | } catch (MessagingException e) { 128 | log.error("sendPasswordResetEmail failed MessagingException {} ", e.getMessage()); 129 | e.printStackTrace(); 130 | } catch (Exception e) { 131 | log.error("sendPasswordResetEmail failed Exception {} ", e.getMessage()); 132 | e.printStackTrace(); 133 | } 134 | } 135 | 136 | public void sendWelcomeEmail(String destinationEmail, 137 | String fullName) { 138 | log.info("Initiated: sendWelcomeEmail - toEmailAddress {} ", destinationEmail); 139 | String firstName = fullName.contains(" ") ? fullName.split(" ", 2)[0] : fullName; 140 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 141 | try { 142 | 143 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, 144 | MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, 145 | StandardCharsets.UTF_8.name()); 146 | 147 | // Populate the template data 148 | Map templateData = new HashMap<>(); 149 | templateData.put(MessageTemplateCodeUtil.TemplateKeys.welcomedUserFirstName, firstName); 150 | templateData.putAll(MessageTemplateCodeUtil.templateDefaultValuesMap); 151 | templateData.put(MessageTemplateCodeUtil.TemplateKeys.setupItemList, MessageTemplateCodeUtil.welcomeTemplateSetupList); 152 | String visitOfficialSite = UriComponentsBuilder.fromUriString(officialCompanyDomain) 153 | .queryParam("activateGuide", true) 154 | .build().toUriString(); 155 | templateData.put(MessageTemplateCodeUtil.TemplateKeys.visitOfficialSite, visitOfficialSite); 156 | 157 | // Retrieving (welcome mail) template file to set populated data 158 | String templatePath = MessageTemplateCodeUtil.TemplatesPath.WELCOME_MAIL.getTemplatePath(); 159 | String templateContent = FreeMarkerTemplateUtils.processTemplateIntoString( 160 | freemarkerConfigurer.getConfiguration().getTemplate(templatePath), 161 | templateData); 162 | 163 | // Sending email 164 | helper.setTo(destinationEmail); 165 | helper.setSubject(MessageTemplateCodeUtil.subjectWelcomeEmail); 166 | helper.setText(templateContent, true); 167 | javaMailSender.send(mimeMessage); 168 | 169 | log.info("Completed: sendWelcomeEmail "); 170 | } catch (MessagingException e) { 171 | log.error("sendWelcomeEmail failed MessagingException {} ", e.getMessage()); 172 | e.printStackTrace(); 173 | } catch (Exception e) { 174 | log.error("sendWelcomeEmail failed Exception {} ", e.getMessage()); 175 | e.printStackTrace(); 176 | } 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/mail/MessageTemplateCodeUtil.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.mail; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class MessageTemplateCodeUtil { 9 | 10 | public static final String REG_COMPANY_NAME = "XYZ COMPANY"; 11 | 12 | 13 | public enum TemplatesPath { 14 | WELCOME_MAIL("/welcome.ftlh"), 15 | EMAIL_VERIFICATION_MAIL("/verification-code.ftlh"), 16 | RESET_PASSWORD_MAIL("/reset-password.ftlh"); 17 | 18 | private String templatePath; 19 | 20 | TemplatesPath(String templatePath) { 21 | this.templatePath = templatePath; 22 | } 23 | 24 | public String getTemplatePath() { 25 | return templatePath; 26 | } 27 | } 28 | 29 | public static class TemplateKeys { 30 | // Default Registered template values constant Keys 31 | public static final String REGCompanyName = "REGCompanyName"; 32 | public static final String REGCompanyStreet = "REGCompanyStreet"; 33 | public static final String REGCompanyCountry = "REGCompanyCountry"; 34 | public static final String REGCompanyPhone = "REGCompanyPhone"; 35 | 36 | // Welcome user template Keys 37 | public static final String welcomedUserFirstName = "firstName"; 38 | public static final String setupItemList = "setupItemList"; 39 | public static final String visitOfficialSite = "visitOfficialSite"; 40 | 41 | // Verification code template Keys 42 | public static final String verificationUserFirstName = "firstName"; 43 | public static final String linkEmailVerification = "linkEmailVerification"; 44 | 45 | // Password reset template keys 46 | public static final String linkPasswordReset = "linkPasswordReset"; 47 | 48 | } 49 | 50 | 51 | // Template values 52 | public static final Map templateDefaultValuesMap = Collections.unmodifiableMap( 53 | new HashMap<>() {{ 54 | put(TemplateKeys.REGCompanyName, REG_COMPANY_NAME); 55 | put(TemplateKeys.REGCompanyStreet, "Fictional Street"); 56 | put(TemplateKeys.REGCompanyCountry, "Country - Nepal"); 57 | put(TemplateKeys.REGCompanyPhone, "+0 000 000 0000"); 58 | }} 59 | ); 60 | 61 | public static final String subjectWelcomeEmail = "Welcome to the Team"; 62 | public static final List welcomeTemplateSetupList = List.of( 63 | "Laptop and required resources", 64 | "Walk through, project setups, and assistance from the assigned team member", 65 | "Session with the manager and processes walk-through", 66 | "Introduction with the team"); 67 | 68 | public static final String subjectVerifyEmail = "Verify email address"; 69 | 70 | public static final String subjectResetPasswordEmail = "Password reset request"; 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/webapp/user/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.webapp.user; 2 | 3 | import com.demo.springcustomizedstarterexample.entities.UserEntity; 4 | import com.demo.springcustomizedstarterexample.services.common.GenericMapper; 5 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.UserDTO; 6 | import org.mapstruct.Mapper; 7 | 8 | import java.util.List; 9 | 10 | @Mapper(componentModel = "spring") 11 | public interface UserMapper extends GenericMapper { 12 | 13 | @Override 14 | UserEntity toEntity(UserDTO dto); 15 | 16 | @Override 17 | UserDTO toDto(UserEntity entity); 18 | 19 | @Override 20 | List toEntityList(List list); 21 | 22 | @Override 23 | List toDtoList(List list); 24 | 25 | } -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/webapp/user/UserService.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.webapp.user; 2 | 3 | import com.demo.springcustomizedstarterexample.services.common.GenericResponseDTO; 4 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.*; 5 | import org.springframework.data.domain.Pageable; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public interface UserService { 11 | 12 | // CRUD 13 | List getAllUsers(Pageable pageable); 14 | 15 | UserDTO findUserByEmail(String email); 16 | 17 | Optional findOptionalUserByEmail(String email); 18 | 19 | UserDTO getUserById(Long id); 20 | 21 | UserDTO createUser(UserDTO userDTO); 22 | 23 | UserDTO updateUser(UserDTO userDTO); 24 | 25 | // Email Verification 26 | GenericResponseDTO sendVerificationEmail(String email); 27 | 28 | GenericResponseDTO verifyEmailAddress(VerifyEmailRequestDTO verifyEmailRequestDTO); 29 | 30 | // Reset Password 31 | GenericResponseDTO sendResetPasswordEmail(ForgotPasswordRequestDTO forgotPasswordRequestDTO); 32 | 33 | GenericResponseDTO verifyAndProcessPasswordResetRequest(ResetPasswordRequestDTO resetPasswordRequestDTO); 34 | 35 | // Other extras 36 | GenericResponseDTO userEmailExists(String email); 37 | 38 | GenericResponseDTO updatePassword(UpdatePasswordRequestDTO updatePasswordRequest); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/webapp/user/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.webapp.user; 2 | 3 | import com.demo.springcustomizedstarterexample.config.AppProperties; 4 | import com.demo.springcustomizedstarterexample.entities.UserEntity; 5 | import com.demo.springcustomizedstarterexample.repository.UserRepository; 6 | import com.demo.springcustomizedstarterexample.security.AppSecurityUtils; 7 | import com.demo.springcustomizedstarterexample.security.oauth.common.SecurityEnums; 8 | import com.demo.springcustomizedstarterexample.services.common.GenericResponseDTO; 9 | import com.demo.springcustomizedstarterexample.services.mail.EmailService; 10 | import com.demo.springcustomizedstarterexample.services.webapp.user.dto.*; 11 | import com.demo.springcustomizedstarterexample.utils.AppUtils; 12 | import com.demo.springcustomizedstarterexample.utils.exceptions.AppExceptionConstants; 13 | import com.demo.springcustomizedstarterexample.utils.exceptions.BadRequestException; 14 | import com.demo.springcustomizedstarterexample.utils.exceptions.ResourceNotFoundException; 15 | import org.springframework.data.domain.Page; 16 | import org.springframework.data.domain.Pageable; 17 | import org.springframework.security.crypto.password.PasswordEncoder; 18 | import org.springframework.stereotype.Service; 19 | import org.springframework.transaction.annotation.Transactional; 20 | import org.springframework.util.LinkedMultiValueMap; 21 | import org.springframework.util.MultiValueMap; 22 | import org.springframework.util.ObjectUtils; 23 | 24 | import java.time.Instant; 25 | import java.util.List; 26 | import java.util.Optional; 27 | import java.util.Set; 28 | 29 | @Service 30 | @Transactional 31 | public class UserServiceImpl implements UserService { 32 | 33 | private final UserRepository userRepository; 34 | private final PasswordEncoder passwordEncoder; 35 | private final UserMapper userMapper; 36 | private final EmailService emailService; 37 | private final AppProperties appProperties; 38 | 39 | public UserServiceImpl(UserRepository userRepository, 40 | PasswordEncoder passwordEncoder, 41 | UserMapper userMapper, 42 | EmailService emailService, 43 | AppProperties appProperties) { 44 | this.userRepository = userRepository; 45 | this.passwordEncoder = passwordEncoder; 46 | this.userMapper = userMapper; 47 | this.emailService = emailService; 48 | this.appProperties = appProperties; 49 | } 50 | 51 | 52 | @Override 53 | public List getAllUsers(Pageable pageable) { 54 | Page pageUserEntities = userRepository.findAll(pageable); 55 | return userMapper.toDtoList(pageUserEntities.getContent()); 56 | } 57 | 58 | @Override 59 | public UserDTO findUserByEmail(String userEmail) { 60 | UserEntity userEntity = userRepository.findByEmail(userEmail) 61 | .orElseThrow(() -> new ResourceNotFoundException(AppExceptionConstants.USER_RECORD_NOT_FOUND)); 62 | return userMapper.toDto(userEntity); 63 | } 64 | 65 | @Override 66 | public Optional findOptionalUserByEmail(String email) { 67 | return userRepository.findByEmail(email) 68 | .map(userEntity -> userMapper.toDto(userEntity)); 69 | } 70 | 71 | @Override 72 | public UserDTO getUserById(Long id) { 73 | UserEntity userEntity = userRepository.findById(id) 74 | .orElseThrow(() -> new ResourceNotFoundException(AppExceptionConstants.USER_RECORD_NOT_FOUND)); 75 | return userMapper.toDto(userEntity); 76 | } 77 | 78 | @Override 79 | // @Transactional(propagation = Propagation.REQUIRES_NEW) 80 | public UserDTO createUser(UserDTO requestUserDTO) { 81 | if (ObjectUtils.isEmpty(requestUserDTO.getRoles())) { 82 | requestUserDTO.setRoles(Set.of(AppSecurityUtils.ROLE_DEFAULT)); 83 | } 84 | boolean isFromCustomBasicAuth = requestUserDTO.getRegisteredProviderName().equals(requestUserDTO.getRegisteredProviderName()); 85 | if (isFromCustomBasicAuth && requestUserDTO.getPassword() != null) { 86 | requestUserDTO.setPassword(passwordEncoder.encode(requestUserDTO.getPassword())); 87 | } 88 | UserEntity userEntity = userMapper.toEntity(requestUserDTO); 89 | boolean existsByEmail = userRepository.existsByEmail(userEntity.getEmail()); 90 | if (existsByEmail) { 91 | throw new ResourceNotFoundException(AppExceptionConstants.USER_EMAIL_NOT_AVAILABLE); 92 | } 93 | userRepository.save(userEntity); 94 | sendVerificationEmail(userEntity.getEmail()); 95 | return userMapper.toDto(userEntity); 96 | } 97 | 98 | @Override 99 | public UserDTO updateUser(UserDTO reqUserDTO) { 100 | UserEntity userEntity = userRepository.findById(reqUserDTO.getId()) 101 | .orElseThrow(() -> new ResourceNotFoundException(AppExceptionConstants.USER_RECORD_NOT_FOUND)); 102 | userEntity.setFullName(reqUserDTO.getFullName()); 103 | userEntity.setImageUrl(reqUserDTO.getImageUrl()); 104 | userEntity.setPhoneNumber(reqUserDTO.getPhoneNumber()); 105 | userRepository.save(userEntity); 106 | return userMapper.toDto(userEntity); 107 | } 108 | 109 | @Override 110 | // @Transactional(propagation = Propagation.REQUIRES_NEW) 111 | public GenericResponseDTO sendVerificationEmail(String email) { 112 | UserEntity userEntity = userRepository.findByEmail(email) 113 | .orElseThrow(() -> new ResourceNotFoundException(AppExceptionConstants.USER_RECORD_NOT_FOUND)); 114 | String verificationCode = AppUtils.generateRandomAlphaNumericString(20); 115 | long verificationCodeExpirationSeconds = appProperties.getMail().getVerificationCodeExpirationSeconds(); 116 | userEntity.setVerificationCodeExpiresAt(Instant.now().plusSeconds(verificationCodeExpirationSeconds)); 117 | userEntity.setVerificationCode(verificationCode); 118 | MultiValueMap appendQueryParamsToVerificationLink = constructEmailVerificationLinkQueryParams( 119 | userEntity.getEmail(), verificationCode, userEntity.getRegisteredProviderName()); 120 | String fullName = userEntity.getFullName(); 121 | String firstName = fullName.contains(" ") ? fullName.split(" ", 2)[0] : fullName; 122 | userRepository.save(userEntity); 123 | emailService.sendVerificationEmail(userEntity.getEmail(), firstName, appendQueryParamsToVerificationLink); 124 | GenericResponseDTO genericResponseDTO = GenericResponseDTO.builder().response(true).build(); 125 | return genericResponseDTO; 126 | } 127 | 128 | @Override 129 | public GenericResponseDTO sendResetPasswordEmail(ForgotPasswordRequestDTO forgotPasswordRequestDTO) { 130 | UserEntity userEntity = userRepository.findByEmail(forgotPasswordRequestDTO.getEmail()) 131 | .orElseThrow(() -> new ResourceNotFoundException(AppExceptionConstants.USER_EMAIL_NOT_AVAILABLE)); 132 | String forgotPasswordVerCode = AppUtils.generateRandomAlphaNumericString(20); 133 | long verificationCodeExpirationSeconds = appProperties.getMail().getVerificationCodeExpirationSeconds(); 134 | userEntity.setVerificationCodeExpiresAt(Instant.now().plusSeconds(verificationCodeExpirationSeconds)); 135 | userEntity.setVerificationCode(forgotPasswordVerCode); 136 | MultiValueMap appendQueryParamsToPasswordResetLink = constructPasswordResetLinkQueryParams( 137 | userEntity.getEmail(), forgotPasswordVerCode); 138 | String fullName = userEntity.getFullName(); 139 | String firstName = fullName.contains(" ") ? fullName.split(" ", 2)[0] : fullName; 140 | userRepository.save(userEntity); 141 | emailService.sendPasswordResetEmail(userEntity.getEmail(), firstName, appendQueryParamsToPasswordResetLink); 142 | GenericResponseDTO genericResponseDTO = GenericResponseDTO.builder().response(true).build(); 143 | return genericResponseDTO; 144 | } 145 | 146 | @Override 147 | public GenericResponseDTO verifyEmailAddress(VerifyEmailRequestDTO verifyEmailRequestDTO) { 148 | Optional optionalUserEntity = userRepository.verifyAndRetrieveEmailVerificationRequestUser( 149 | verifyEmailRequestDTO.getEmail(), verifyEmailRequestDTO.getAuthProviderId(), verifyEmailRequestDTO.getVerificationCode()); 150 | UserEntity userEntity = optionalUserEntity 151 | .orElseThrow(() -> new ResourceNotFoundException(AppExceptionConstants.MATCHING_VERIFICATION_RECORD_NOT_FOUND)); 152 | userEntity.setEmailVerified(Boolean.TRUE); 153 | userEntity.setVerificationCodeExpiresAt(null); 154 | userEntity.setVerificationCode(null); 155 | userRepository.save(userEntity); 156 | emailService.sendWelcomeEmail(userEntity.getEmail(), userEntity.getFullName()); 157 | GenericResponseDTO emailVerifiedResponseDTO = GenericResponseDTO.builder().response(true).build(); 158 | return emailVerifiedResponseDTO; 159 | } 160 | 161 | @Override 162 | public GenericResponseDTO verifyAndProcessPasswordResetRequest(ResetPasswordRequestDTO resetPasswordRequestDTO) { 163 | Optional optionalUserEntity = userRepository.verifyAndRetrieveForgotPasswordRequestUser( 164 | resetPasswordRequestDTO.getEmail(), SecurityEnums.AuthProviderId.app_custom_authentication, resetPasswordRequestDTO.getForgotPasswordVerCode()); 165 | UserEntity userEntity = optionalUserEntity 166 | .orElseThrow(() -> new ResourceNotFoundException(AppExceptionConstants.INVALID_PASSWORD_RESET_REQUEST)); 167 | userEntity.setVerificationCodeExpiresAt(null); 168 | userEntity.setVerificationCode(null); 169 | userEntity.setEmailVerified(true); 170 | userEntity.setPassword(passwordEncoder.encode(resetPasswordRequestDTO.getNewPassword())); 171 | userRepository.save(userEntity); 172 | GenericResponseDTO emailVerifiedResponseDTO = GenericResponseDTO.builder().response(true).build(); 173 | return emailVerifiedResponseDTO; 174 | } 175 | 176 | @Override 177 | public GenericResponseDTO userEmailExists(String email) { 178 | boolean existsByEmail = userRepository.existsByEmail(email); 179 | return GenericResponseDTO.builder().response(existsByEmail).build(); 180 | } 181 | 182 | @Override 183 | public GenericResponseDTO updatePassword(UpdatePasswordRequestDTO updatePasswordRequest) { 184 | UserEntity userEntity = userRepository.findById(updatePasswordRequest.getUserId()) 185 | .orElseThrow(() -> new ResourceNotFoundException(AppExceptionConstants.USER_RECORD_NOT_FOUND)); 186 | boolean passwordMatches = passwordEncoder.matches(updatePasswordRequest.getOldPassword(), userEntity.getPassword()); 187 | if (!passwordMatches) { 188 | throw new BadRequestException(AppExceptionConstants.OLD_PASSWORD_DOESNT_MATCH); 189 | } 190 | userEntity.setPassword(passwordEncoder.encode(updatePasswordRequest.getNewPassword())); 191 | userRepository.save(userEntity); 192 | return GenericResponseDTO.builder().response(true).build(); 193 | } 194 | 195 | private static MultiValueMap constructEmailVerificationLinkQueryParams(String email, 196 | String verificationCode, 197 | SecurityEnums.AuthProviderId authProvider) { 198 | MultiValueMap appendQueryParams = new LinkedMultiValueMap<>(); 199 | // Generated QueryParams for the verification link, must sync with VerifyEmailRequestDTO 200 | appendQueryParams.add("email", email); 201 | appendQueryParams.add("registeredProviderName", authProvider.toString()); 202 | appendQueryParams.add("verificationCode", verificationCode); 203 | return appendQueryParams; 204 | } 205 | 206 | private static MultiValueMap constructPasswordResetLinkQueryParams(String email, 207 | String forgotPasswordVerCode) { 208 | MultiValueMap appendQueryParams = new LinkedMultiValueMap<>(); 209 | // Generated QueryParams for the password reset link, must sync with ResetPasswordRequestDTO 210 | appendQueryParams.add("email", email); 211 | appendQueryParams.add("forgotPasswordVerCode", forgotPasswordVerCode); 212 | return appendQueryParams; 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/webapp/user/dto/ForgotPasswordRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.webapp.user.dto; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ForgotPasswordRequestDTO { 7 | private String email; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/webapp/user/dto/ResetPasswordRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.webapp.user.dto; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ResetPasswordRequestDTO { 7 | 8 | private String email; 9 | 10 | private String forgotPasswordVerCode; 11 | 12 | private String newPassword; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/webapp/user/dto/UpdatePasswordRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.webapp.user.dto; 2 | 3 | import com.sun.istack.NotNull; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class UpdatePasswordRequestDTO { 8 | 9 | private Long userId; 10 | 11 | @NotNull 12 | private String oldPassword; 13 | 14 | @NotNull 15 | private String newPassword; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/webapp/user/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.webapp.user.dto; 2 | 3 | import com.demo.springcustomizedstarterexample.security.oauth.common.SecurityEnums; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.Set; 12 | 13 | @Data 14 | @Builder 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class UserDTO { 18 | 19 | private Long id; 20 | 21 | private String fullName; 22 | 23 | private String email; 24 | 25 | private boolean emailVerified; 26 | 27 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 28 | private String password; 29 | 30 | private String imageUrl; 31 | 32 | private Set roles; 33 | 34 | private String phoneNumber; 35 | 36 | private SecurityEnums.AuthProviderId registeredProviderName; 37 | 38 | private String registeredProviderId; 39 | 40 | private LocalDateTime createdDate; 41 | 42 | private LocalDateTime lastModifiedDate; 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/services/webapp/user/dto/VerifyEmailRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.services.webapp.user.dto; 2 | 3 | import com.demo.springcustomizedstarterexample.security.oauth.common.SecurityEnums; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class VerifyEmailRequestDTO { 9 | 10 | private String email; 11 | 12 | private String verificationCode; 13 | 14 | @JsonProperty("registeredProviderName") 15 | private SecurityEnums.AuthProviderId authProviderId; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/utils/AppUtils.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.utils; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.util.SerializationUtils; 6 | 7 | import java.io.IOException; 8 | import java.io.Serializable; 9 | import java.util.Base64; 10 | import java.util.UUID; 11 | 12 | public class AppUtils { 13 | 14 | private static ObjectMapper objectMapper; 15 | 16 | public AppUtils(ObjectMapper objectMapper) { 17 | AppUtils.objectMapper = objectMapper; 18 | } 19 | 20 | // Random UUID generator 21 | public static String generateRandomUUID() { 22 | return UUID.randomUUID().toString(); 23 | } 24 | 25 | // Serializes an object 26 | public static String serialize(Serializable obj) { 27 | 28 | return Base64.getUrlEncoder().encodeToString( 29 | SerializationUtils.serialize(obj)); 30 | } 31 | 32 | // Deserializes an object 33 | public static T deserialize(String serializedObj) { 34 | 35 | return (T) SerializationUtils.deserialize( 36 | Base64.getUrlDecoder().decode(serializedObj)); 37 | } 38 | 39 | // Serializes an object to JSON string 40 | public static String toJson(T obj) { 41 | 42 | try { 43 | return objectMapper.writeValueAsString(obj); 44 | } catch (JsonProcessingException e) { 45 | throw new RuntimeException(e); 46 | } 47 | } 48 | 49 | // Deserializes a JSON String 50 | public static T fromJson(String json, 51 | Class clazz) { 52 | try { 53 | return objectMapper.readValue(json, clazz); 54 | } catch (IOException e) { 55 | throw new RuntimeException(e); 56 | } 57 | } 58 | 59 | // Generate random AlphaNumeric string 60 | public static String generateRandomAlphaNumericString(int n) { 61 | 62 | String alphaNumericString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 63 | + "0123456789" 64 | + "abcdefghijklmnopqrstuvxyz"; 65 | 66 | StringBuilder sb = new StringBuilder(n); 67 | for (int i = 0; i < n; i++) { 68 | int index = (int) (alphaNumericString.length() * Math.random()); 69 | sb.append(alphaNumericString.charAt(index)); 70 | } 71 | return sb.toString(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/utils/AppWebUtils.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.utils; 2 | 3 | import org.springframework.util.StringUtils; 4 | 5 | import javax.servlet.http.Cookie; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | import java.util.Optional; 9 | 10 | public class AppWebUtils { 11 | 12 | private static int cookieExpireSeconds; 13 | 14 | public AppWebUtils(int cookieExpireSeconds) { 15 | AppWebUtils.cookieExpireSeconds = cookieExpireSeconds; 16 | } 17 | 18 | // Fetches a cookie from the request 19 | public static Optional getCookie(HttpServletRequest request, 20 | String cookieKey) { 21 | 22 | Cookie[] cookies = request.getCookies(); 23 | 24 | if (cookies != null && cookies.length > 0) { 25 | for (Cookie cookie : cookies) { 26 | if (cookie.getName().equals(cookieKey)) { 27 | return Optional.of(cookie); 28 | } 29 | } 30 | } 31 | return Optional.empty(); 32 | } 33 | 34 | 35 | // Utility for adding cookie 36 | public static void addCookie(HttpServletResponse response, 37 | String cookieKey, 38 | String cookieValue) { 39 | 40 | if (StringUtils.hasText(cookieKey) && StringUtils.hasText(cookieValue)) { 41 | Cookie cookie = new Cookie(cookieKey, cookieValue); 42 | cookie.setPath("/"); 43 | cookie.setHttpOnly(true); 44 | cookie.setMaxAge(cookieExpireSeconds); 45 | response.addCookie(cookie); 46 | } 47 | } 48 | 49 | // Utility for deleting cookie 50 | public static void deleteCookie(HttpServletRequest request, 51 | HttpServletResponse response, 52 | String cookieKey) { 53 | 54 | Cookie[] cookies = request.getCookies(); 55 | if (cookies != null && cookies.length > 0) { 56 | for (Cookie cookie : cookies) { 57 | if (cookie.getName().equals(cookieKey)) { 58 | cookie.setValue(""); 59 | cookie.setPath("/"); 60 | cookie.setMaxAge(0); 61 | response.addCookie(cookie); 62 | } 63 | } 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/utils/StringToEnumConverter.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.utils; 2 | 3 | import com.demo.springcustomizedstarterexample.utils.exceptions.CustomAppException; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.core.convert.converter.ConverterFactory; 6 | 7 | public class StringToEnumConverter implements ConverterFactory { 8 | 9 | @Override 10 | public Converter getConverter(Class targetType) { 11 | return source -> { 12 | try { 13 | return (T) Enum.valueOf(targetType, source.toUpperCase()); 14 | } catch (Exception e) { 15 | throw new CustomAppException(e.getMessage()); 16 | } 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/utils/exceptions/AppExceptionConstants.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.utils.exceptions; 2 | 3 | public final class AppExceptionConstants { 4 | 5 | // Auth Exception 6 | public static final String BAD_LOGIN_CREDENTIALS = "Bad Credentials - Invalid username or password"; 7 | public static final String ACCOUNT_NOT_ACTIVATED = "Account not activated - Please verify your email or reprocess verification using forgot-password."; 8 | 9 | public static final String UNAUTHORIZED_ACCESS = "Insufficient authorization access"; 10 | 11 | // User Exception 12 | public static final String USER_RECORD_NOT_FOUND = "User doesn't exists"; 13 | public static final String USER_EMAIL_NOT_AVAILABLE = "This email isn't available"; 14 | public static final String OLD_PASSWORD_DOESNT_MATCH = "Old and New Password doesn't match"; 15 | public static final String MATCHING_VERIFICATION_RECORD_NOT_FOUND = "Provided verification request doesn't seems correct"; 16 | public static final String INVALID_PASSWORD_RESET_REQUEST = "Provided Password reset request doesn't seems correct"; 17 | 18 | 19 | // Raw-Data Exception 20 | public static final String REQUESTED_RESOURCE_NOT_FOUND = "Couldn't find the requested resource"; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/utils/exceptions/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.utils.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.BAD_REQUEST) 7 | public class BadRequestException extends RuntimeException { 8 | public BadRequestException(String message) { 9 | super(message); 10 | } 11 | 12 | public BadRequestException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/utils/exceptions/CustomAppException.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.utils.exceptions; 2 | 3 | public class CustomAppException extends RuntimeException 4 | { 5 | 6 | public CustomAppException() { } 7 | 8 | public CustomAppException(String message) 9 | { 10 | super(message); 11 | } 12 | 13 | public CustomAppException(String message, Throwable cause) 14 | { 15 | super(message, cause); 16 | } 17 | 18 | public CustomAppException(Throwable cause) 19 | { 20 | super(cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/demo/springcustomizedstarterexample/utils/exceptions/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample.utils.exceptions; 2 | 3 | public class ResourceNotFoundException extends RuntimeException 4 | { 5 | 6 | public ResourceNotFoundException() { } 7 | 8 | public ResourceNotFoundException(String message) 9 | { 10 | super(message); 11 | } 12 | 13 | public ResourceNotFoundException(String message, Throwable cause) 14 | { 15 | super(message, cause); 16 | } 17 | 18 | public ResourceNotFoundException(Throwable cause) 19 | { 20 | super(cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/application-security-example.yml: -------------------------------------------------------------------------------- 1 | # Just an example file 2 | 3 | logging: 4 | level: 5 | root: INFO 6 | org: 7 | springframework: 8 | security: DEBUG 9 | 10 | 11 | security: 12 | oauth2: 13 | client: 14 | registration: 15 | github: 16 | clientId: ${GITHUB_CLIENT_ID} 17 | clientSecret: ${GITHUB_CLIENT_SECRET} 18 | redirectUri: http://localhost:8081/login/oauth2/code/github 19 | scope: read:user 20 | 21 | google: 22 | clientId: ${GOOGLE_CLIENT_ID} 23 | clientSecret: ${GOOGLE_CLIENT_SECRET} 24 | redirectUri: http://localhost:8081/login/oauth2/code/google 25 | scope: email, profile 26 | 27 | facebook: 28 | clientId: ${FACEBOOK_CLIENT_ID} 29 | clientSecret: ${FACEBOOK_CLIENT_SECRET} 30 | redirectUri: http://localhost:8081/login/oauth2/code/facebook 31 | scope: email, public_profile 32 | 33 | okta: 34 | clientId: ${OKTA_CLIENT_ID} 35 | clientSecret: ${OKTA_CLIENT_SECRET} 36 | redirectUri: http://localhost:8081/login/oauth2/code/okta 37 | scope: openid, profile email 38 | 39 | linkedin: 40 | clientId: ${LINKEDIN_CLIENT_ID} 41 | clientSecret: ${LINKEDIN_CLIENT_SECRET} 42 | redirectUri: http://localhost:8081/login/oauth2/code/linkedin 43 | scope: r_liteprofile, r_emailaddress 44 | 45 | discord: 46 | clientId: ${DISCORD_CLIENT_ID} 47 | clientSecret: ${DISCORD_CLIENT_SECRET} 48 | redirectUri: http://localhost:8081/login/oauth2/code/discord 49 | scope: identify, email 50 | 51 | provider: 52 | github: 53 | name: github 54 | authorizationUri: https://github.com/login/oauth/authorize 55 | tokenUri: https://github.com/login/oauth/access_token 56 | userInfoUri: https://api.github.com/user 57 | 58 | google: 59 | name: google 60 | authorizationUri: https://accounts.google.com/o/oauth2/v2/auth 61 | tokenUri: https://oauth2.googleapis.com/token 62 | userInfoUri: https://openidconnect.googleapis.com/v1/userinfo 63 | revokeTokenUri: https://oauth2.googleapis.com/revoke 64 | 65 | facebook: 66 | name: facebook 67 | authorizationUri: https://graph.facebook.com/oauth/authorize 68 | tokenUri: https://graph.facebook.com/oauth/access_token 69 | userInfoUri: https://graph.facebook.com/me?fields=id,name,email 70 | revokePermissionUri: https://graph.facebook.com/{user-id}/permissions 71 | 72 | okta: 73 | name: okta 74 | authorizationUri: https://${OKTA_SUBDOMAIN}.okta.com/oauth2/default/v1/authorize 75 | tokenUri: https://${OKTA_SUBDOMAIN}.okta.com/oauth2/default/v1/token 76 | userInfoUri: https://${OKTA_SUBDOMAIN}.okta.com/oauth2/default/v1/userinfo 77 | revokeTokenUri: https://${OKTA_SUBDOMAIN}.okta.com/oauth2/default/v1/revoke 78 | 79 | linkedin: 80 | name: linkedin 81 | authorizationUri: https://www.linkedin.com/oauth/v2/authorization 82 | tokenUri: https://www.linkedin.com/oauth/v2/accessToken 83 | userInfoUri: https://api.linkedin.com/v2/me 84 | userNameAttribute: localizedFirstName 85 | 86 | discord: 87 | name: discord 88 | authorizationUri: https://discord.com/api/oauth2/authorize 89 | tokenUri: https://discord.com/api/oauth2/token 90 | userInfoUri: https://discord.com/api/users/@me 91 | revokeTokenUri: https://discord.com/api/oauth2/token/revoke 92 | userNameAttribute: username 93 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | # jdbc and jpa config 5 | spring: 6 | datasource: 7 | url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false 8 | username: root 9 | password: root 10 | 11 | jpa: 12 | show-sql: true 13 | hibernate: 14 | ddl-auto: update 15 | naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy 16 | properties: 17 | hibernate: 18 | dialect: org.hibernate.dialect.MySQL5InnoDBDialect 19 | jdbc: 20 | time_zone: UTC 21 | 22 | # Mail Config 23 | mail: 24 | properties: 25 | mail: 26 | smtp: 27 | starttls: 28 | enable: 'true' 29 | timeout: '5000' 30 | auth: 'true' 31 | connectiontimeout: '5000' 32 | writetimeout: '5000' 33 | host: smtp.gmail.com 34 | username: <> 35 | password: <> # Google Account, Search "App Passwords", generate password 36 | port: '587' 37 | 38 | # freemarker config - used for email templates 39 | freemarker: 40 | template-loader-path: classpath:/mail-templates 41 | suffix: .ftl 42 | 43 | # Spring security oauth2 config 44 | security: 45 | oauth2: 46 | client: 47 | registration: 48 | google: 49 | clientId: <> 50 | clientSecret: <> 51 | redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" 52 | scope: email, profile 53 | 54 | facebook: 55 | clientId: <> 56 | clientSecret: <> 57 | redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" 58 | scope: email, public_profile 59 | 60 | github: 61 | clientId: <> 62 | clientSecret: <> 63 | redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" 64 | scope: user, user:email 65 | 66 | 67 | # logging config 68 | logging: 69 | level: 70 | root: INFO 71 | org: 72 | springframework: 73 | web: DEBUG 74 | security: DEBUG 75 | 76 | 77 | # App Custom Properties 78 | myapp: 79 | 80 | appName: spring security OAuth2 and JWT starter example 81 | officialCompanyName: XYZ-Company Inc. 82 | officialCompanyDomain: http://localhost:4200 83 | 84 | mail: 85 | defaultEmailAddress: example@gmail.com 86 | verificationCodeExpirationSeconds: 1800 # 30 minutes 87 | 88 | jwt: 89 | secretKey: secret-jwt-key1234 90 | isSecretKeyBase64Encoded: false 91 | expirationMillis: 3600000 92 | shortLivedMillis: 120000 93 | 94 | cors: 95 | allowedOrigins: http://localhost:8080,http://localhost:4200 96 | 97 | oauth2: 98 | authorizedRedirectOrigins: http://localhost:8080,http://localhost:4200 99 | cookieExpireSeconds: 120 100 | 101 | defaults: 102 | defaultPageStart: 0 103 | defaultPageSize: 50 104 | -------------------------------------------------------------------------------- /src/main/resources/mail-templates/reset-password.ftlh: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome Letter 5 | 6 | 7 |

Hello ${firstName!''}

8 | 9 |

This is a verification for password reset email .

10 | 11 |

Seems like you have requested for password reset in our system

12 | 13 | 14 |

CLick the reset link below, to change your password.

15 | <#-- Example attribute link --> 16 |

Link:
${linkPasswordReset}?urlPath}

17 | 18 |
19 |

If this wasn't you, please simply ignore this email.

20 | 21 | 22 | <#-- ========================= Common Block TODO: fragment ============================ --> 23 |
24 |

${REGCompanyName}

25 |

26 | SB2 ${REGCompanyStreet}
27 | ${REGCompanyCountry}
28 | Phone: ${REGCompanyPhone} 29 |

30 | 31 |

© Copyright ${REGCompanyName} ${.now?time?string('YYYY')}. All Rights Reserved.

32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/resources/mail-templates/verification-code.ftlh: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome Letter 5 | 6 | 7 |

Hello ${firstName!''}

8 | 9 | 10 |

Seems like you have registered to our service with this email

11 | 12 |

This is a verification email .

13 | 14 |

CLick the verification link below, to confirm.

15 | <#-- Example attribute link --> 16 |

Verification Link:
${linkEmailVerification}?urlPath}

17 | 18 |
19 |

If this wasn't you, please simply ignore this email.

20 | 21 | 22 | <#-- ========================= Common Block TODO: fragment ============================ --> 23 |
24 |

${REGCompanyName}

25 |

26 | SB2 ${REGCompanyStreet}
27 | ${REGCompanyCountry}
28 | Phone: ${REGCompanyPhone} 29 |

30 | 31 |

© Copyright ${REGCompanyName} ${.now?time?string('YYYY')}. All Rights Reserved.

32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/resources/mail-templates/welcome.ftlh: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome Letter 5 | 6 | 7 |

Hello ${firstName!''}, Welcome to the team

8 | 9 |

Congratulations on joining us !!! .

10 |

Welcome to the team and cheers on your new position. 11 | Your skills and experience will be a definite asset to us, and we hope to achieve great things together. 12 | We will contact you soon for the follow ups to get you started.
13 | In the meanwhile, you can look through the setups list we will be going through.

14 | 15 | 16 | <#-- Example List of Item List setupItemList; --> 17 | <#list setupItemList> 18 |

An assigned personal or a team member will be available to guide you through each of this steps.

19 |
    20 | <#items as setupItem> 21 |
  1. ${setupItem}
  2. 22 | 23 |
24 | <#else> 25 |

Have a coffee and relax :)

26 | 27 | 28 | <#-- Example attribute link --> 29 |

Visit Site: ${visitOfficialSite}?urlPath}

30 | 31 |

If you have any questions or concerns, please contact ${assignedSupportStaff!'official support'}.

32 | 33 | 34 | 35 | <#-- ========================= Common Block TODO: fragment ============================ --> 36 |
37 |

${REGCompanyName}

38 |

39 | SB2 ${REGCompanyStreet}
40 | ${REGCompanyCountry}
41 | Phone: ${REGCompanyPhone} 42 |

43 | 44 |

© Copyright ${REGCompanyName} ${.now?time?string('YYYY')}. All Rights Reserved.

45 | 46 | 47 | -------------------------------------------------------------------------------- /src/test/java/com/demo/springcustomizedstarterexample/SpringCustomizedStarterExampleApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.demo.springcustomizedstarterexample; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class SpringCustomizedStarterExampleApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------