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 | - ${setupItem}
22 | #items>
23 |
24 | <#else>
25 | Have a coffee and relax :)
26 | #list>
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 |
--------------------------------------------------------------------------------