diff --git a/README.md b/README.md index 3ab8d10f..0b98289d 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,19 @@ Check out the [docker section](./docker/README.) Individual test result reports can be found in wisemapping-open-source/wise-webapp/target/failsafe-reports/index.html Test coverage report of unit and integration test can be found in wisemapping-open-source/wise-webapp/target/site/jacoco and wisemapping-open-source/wise-webapp/target/site/jacoco-it folders. Coverage report is generated in the verify phase of [lifecicle](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#introduction-to-the-build-lifecyclea) using [jacoco](https://www.jacoco.org/jacoco/trunk/doc/maven.html) + +## Google authorization + +You must configure the following properties (app.properties) in order to get google authorization working + * `google.oauth2.callbackUrl`: url where google will redirect after user authentication, tipically {frontendBaseUrl}/c/registration-google. Also, this url must be defined in google app configuration + * `google.oauth2.clientId`: client id from google app + * `google.oauth2.clientSecret`: client secret from google app + +Be sure your google app has the following scopes allowed: + * `https://www.googleapis.com/auth/userinfo.profile` + * `https://www.googleapis.com/auth/userinfo.email` + + ## Members ### Founders diff --git a/config/database/hsql/create-schemas.sql b/config/database/hsql/create-schemas.sql index 3e98ae79..50fe1d76 100644 --- a/config/database/hsql/create-schemas.sql +++ b/config/database/hsql/create-schemas.sql @@ -15,6 +15,9 @@ CREATE TABLE USER ( activation_date DATE, allow_send_email CHAR(1) NOT NULL, locale VARCHAR(5), + google_sync BOOLEAN, + sync_code VARCHAR(255), + google_token VARCHAR(255), FOREIGN KEY (colaborator_id) REFERENCES COLLABORATOR (id) ); diff --git a/config/database/mysql/create-schemas.sql b/config/database/mysql/create-schemas.sql index 974a14a4..e8d67e90 100644 --- a/config/database/mysql/create-schemas.sql +++ b/config/database/mysql/create-schemas.sql @@ -25,6 +25,9 @@ CREATE TABLE USER ( activation_date DATE, allow_send_email CHAR(1) CHARACTER SET utf8 NOT NULL DEFAULT 0, locale VARCHAR(5), + google_sync BOOL, + sync_code VARCHAR(255), + google_token VARCHAR(255), FOREIGN KEY (colaborator_id) REFERENCES COLLABORATOR (id) ON DELETE CASCADE ON UPDATE NO ACTION diff --git a/config/database/postgres/create-schemas.sql b/config/database/postgres/create-schemas.sql index d9190c7c..c9ec9b20 100644 --- a/config/database/postgres/create-schemas.sql +++ b/config/database/postgres/create-schemas.sql @@ -15,6 +15,9 @@ CREATE TABLE "user" ( activation_date DATE, allow_send_email TEXT NOT NULL DEFAULT 0, locale VARCHAR(5), + google_sync BOOLEAN, + sync_code VARCHAR(255), + google_token VARCHAR(255), FOREIGN KEY (colaborator_id) REFERENCES COLLABORATOR (id) ON DELETE CASCADE ON UPDATE NO ACTION ); diff --git a/distribution/mysql-init/0002create-schemas.sql b/distribution/mysql-init/0002create-schemas.sql index 51d391ba..1fee2371 100644 --- a/distribution/mysql-init/0002create-schemas.sql +++ b/distribution/mysql-init/0002create-schemas.sql @@ -25,6 +25,9 @@ CREATE TABLE USER ( activation_date DATE, allow_send_email CHAR(1) CHARACTER SET utf8 NOT NULL DEFAULT 0, locale VARCHAR(5), + google_sync BOOL, + sync_code VARCHAR(255), + google_token VARCHAR(255), FOREIGN KEY (colaborator_id) REFERENCES COLLABORATOR (id) ON DELETE CASCADE ON UPDATE NO ACTION diff --git a/distribution/registration-google/update-db-mysql.sql b/distribution/registration-google/update-db-mysql.sql new file mode 100644 index 00000000..45dc059e --- /dev/null +++ b/distribution/registration-google/update-db-mysql.sql @@ -0,0 +1,4 @@ +ALTER TABLE User +ADD COLUMN `google_sync` TINYINT(1) NULL, +ADD COLUMN `sync_code` VARCHAR(255) NULL, +ADD COLUMN `google_token` VARCHAR(255) NULL; diff --git a/distribution/registration-google/update-db-postgres.sql b/distribution/registration-google/update-db-postgres.sql new file mode 100644 index 00000000..38bc2ea1 --- /dev/null +++ b/distribution/registration-google/update-db-postgres.sql @@ -0,0 +1,4 @@ +ALTER TABLE User +ADD COLUMN `google_sync` BOOLEAN NULL, +ADD COLUMN `sync_code` VARCHAR(255) NULL, +ADD COLUMN `google_token` VARCHAR(255) NULL; diff --git a/wise-webapp/src/main/java/com/wisemapping/dao/UserManagerImpl.java b/wise-webapp/src/main/java/com/wisemapping/dao/UserManagerImpl.java index d331d2e6..364cc611 100644 --- a/wise-webapp/src/main/java/com/wisemapping/dao/UserManagerImpl.java +++ b/wise-webapp/src/main/java/com/wisemapping/dao/UserManagerImpl.java @@ -19,6 +19,7 @@ package com.wisemapping.dao; import com.wisemapping.model.AccessAuditory; +import com.wisemapping.model.AuthenticationType; import com.wisemapping.model.Collaboration; import com.wisemapping.model.Collaborator; import com.wisemapping.model.User; @@ -31,7 +32,6 @@ import org.springframework.orm.hibernate5.HibernateTemplate; import org.springframework.orm.hibernate5.support.HibernateDaoSupport; import org.springframework.security.crypto.password.PasswordEncoder; -import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -101,7 +101,10 @@ public class UserManagerImpl @Override public void createUser(User user) { assert user != null : "Trying to store a null user"; - user.setPassword(passwordEncoder.encode(user.getPassword())); + if (!AuthenticationType.GOOGLE_OAUTH2.equals(user.getAuthenticationType())) + user.setPassword(passwordEncoder.encode(user.getPassword())); + else + user.setPassword(""); getHibernateTemplate().saveOrUpdate(user); } diff --git a/wise-webapp/src/main/java/com/wisemapping/filter/CorsFilter.java b/wise-webapp/src/main/java/com/wisemapping/filter/CorsFilter.java new file mode 100644 index 00000000..a2bb50e2 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/filter/CorsFilter.java @@ -0,0 +1,52 @@ +/* +* Copyright [2022] [wisemapping] +* +* Licensed under WiseMapping Public License, Version 1.0 (the "License"). +* It is basically the Apache License, Version 2.0 (the "License") plus the +* "powered by wisemapping" text requirement on every single page; +* you may not use this file except in compliance with the License. +* You may obtain a copy of the license at +* +* http://www.wisemapping.org/license +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.wisemapping.filter; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +public class CorsFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) + throws IOException, ServletException { + if (servletResponse != null) { + // Authorize (allow) all domains to consume the content + ((HttpServletResponse) servletResponse).addHeader("Access-Control-Allow-Origin", "*"); + ((HttpServletResponse) servletResponse).addHeader("Access-Control-Allow-Methods","GET, OPTIONS, HEAD, PUT, POST"); + } + + chain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() { + } +} diff --git a/wise-webapp/src/main/java/com/wisemapping/filter/RequestPropertiesInterceptor.java b/wise-webapp/src/main/java/com/wisemapping/filter/RequestPropertiesInterceptor.java index 4317910a..6e670a4e 100644 --- a/wise-webapp/src/main/java/com/wisemapping/filter/RequestPropertiesInterceptor.java +++ b/wise-webapp/src/main/java/com/wisemapping/filter/RequestPropertiesInterceptor.java @@ -54,6 +54,9 @@ public class RequestPropertiesInterceptor implements HandlerInterceptor { @Value("${security.type}") private String securityType; + @Value("${google.oauth2.url}") + private String googleOauth2Url; + @Override public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, Object object) throws Exception { @@ -64,6 +67,8 @@ public class RequestPropertiesInterceptor implements HandlerInterceptor { request.setAttribute("google.recaptcha2.enabled", recaptcha2Enabled); request.setAttribute("google.recaptcha2.siteKey", recaptcha2SiteKey); + request.setAttribute("google.oauth2.url", googleOauth2Url); + request.setAttribute("site.homepage", siteHomepage); request.setAttribute("site.static.js.url", siteStaticUrl); diff --git a/wise-webapp/src/main/java/com/wisemapping/model/AuthenticationType.java b/wise-webapp/src/main/java/com/wisemapping/model/AuthenticationType.java index 92b7725e..f1fd4b71 100644 --- a/wise-webapp/src/main/java/com/wisemapping/model/AuthenticationType.java +++ b/wise-webapp/src/main/java/com/wisemapping/model/AuthenticationType.java @@ -23,7 +23,8 @@ import org.jetbrains.annotations.NotNull; public enum AuthenticationType { DATABASE('D'), LDAP('L'), - OPENID('O'); + GOOGLE_OAUTH2('G'); + private final char schemaCode; AuthenticationType(char schemaCode) { diff --git a/wise-webapp/src/main/java/com/wisemapping/model/User.java b/wise-webapp/src/main/java/com/wisemapping/model/User.java index 261471a8..4040e4f2 100644 --- a/wise-webapp/src/main/java/com/wisemapping/model/User.java +++ b/wise-webapp/src/main/java/com/wisemapping/model/User.java @@ -18,15 +18,12 @@ package com.wisemapping.model; -import org.hibernate.annotations.CacheConcurrencyStrategy; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.persistence.*; import java.io.Serializable; import java.util.Calendar; -import java.util.HashSet; -import java.util.Set; @Entity @Table(name = "USER") @@ -39,21 +36,30 @@ public class User private String lastname; private String password; private String locale; - + @Column(name = "activation_code") private long activationCode; - + @Column(name = "activation_date") private Calendar activationDate; - + @Column(name = "allow_send_email") private boolean allowSendEmail = false; - + @Column(name = "authentication_type") private Character authenticationTypeCode = AuthenticationType.DATABASE.getCode(); - + @Column(name = "authenticator_uri") private String authenticatorUri; + + @Column(name = "google_sync") + private Boolean googleSync; + + @Column(name = "sync_code") + private String syncCode; + + @Column(name = "google_token") + private String googleToken; public User() { } @@ -151,7 +157,35 @@ public class User this.authenticatorUri = authenticatorUri; } - @Override + public void setAuthenticationTypeCode(Character authenticationTypeCode) { + this.authenticationTypeCode = authenticationTypeCode; + } + + public Boolean getGoogleSync() { + return googleSync; + } + + public void setGoogleSync(Boolean googleSync) { + this.googleSync = googleSync; + } + + public String getSyncCode() { + return syncCode; + } + + public void setSyncCode(String syncCode) { + this.syncCode = syncCode; + } + + public String getGoogleToken() { + return googleToken; + } + + public void setGoogleToken(String googleToken) { + this.googleToken = googleToken; + } + + @Override public String toString() { return "User{" + "firstname='" + firstname + '\'' + diff --git a/wise-webapp/src/main/java/com/wisemapping/rest/UserController.java b/wise-webapp/src/main/java/com/wisemapping/rest/UserController.java index 0b7a1ec1..542989cc 100644 --- a/wise-webapp/src/main/java/com/wisemapping/rest/UserController.java +++ b/wise-webapp/src/main/java/com/wisemapping/rest/UserController.java @@ -22,6 +22,7 @@ import com.wisemapping.exceptions.EmailNotExistsException; import com.wisemapping.exceptions.WiseMappingException; import com.wisemapping.model.AuthenticationType; import com.wisemapping.model.User; +import com.wisemapping.rest.model.RestOath2CallbackResponse; import com.wisemapping.rest.model.RestUserRegistration; import com.wisemapping.service.*; import com.wisemapping.validator.Messages; @@ -33,100 +34,143 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.stereotype.Controller; import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + import java.util.Arrays; import java.util.List; @Controller @CrossOrigin public class UserController extends BaseController { - @Qualifier("userService") - @Autowired - private UserService userService; + @Qualifier("userService") + @Autowired + private UserService userService; - @Autowired - private RecaptchaService captchaService; + @Autowired + private RecaptchaService captchaService; - @Value("${google.recaptcha2.enabled}") - private Boolean recatchaEnabled; + @Qualifier("authenticationManager") + @Autowired + private AuthenticationManager authManager; - @Value("${accounts.exclusion.domain:''}") - private String domainBanExclusion; + @Value("${google.recaptcha2.enabled}") + private Boolean recatchaEnabled; - private static final Logger logger = LogManager.getLogger(); - private static final String REAL_IP_ADDRESS_HEADER = "X-Real-IP"; + @Value("${accounts.exclusion.domain:''}") + private String domainBanExclusion; - @RequestMapping(method = RequestMethod.POST, value = "/users", produces = {"application/json"}) - @ResponseStatus(value = HttpStatus.CREATED) - public void registerUser(@RequestBody RestUserRegistration registration, @NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws WiseMappingException, BindException { - logger.debug("Register new user:" + registration.getEmail()); + private static final Logger logger = LogManager.getLogger(); + private static final String REAL_IP_ADDRESS_HEADER = "X-Real-IP"; - // If tomcat is behind a reverse proxy, ip needs to be found in other header. - String remoteIp = request.getHeader(REAL_IP_ADDRESS_HEADER); - if (remoteIp == null || remoteIp.isEmpty()) { - remoteIp = request.getRemoteAddr(); - } - logger.debug("Remote address" + remoteIp); + @RequestMapping(method = RequestMethod.POST, value = "/users", produces = { "application/json" }) + @ResponseStatus(value = HttpStatus.CREATED) + public void registerUser(@RequestBody RestUserRegistration registration, @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response) throws WiseMappingException, BindException { + logger.debug("Register new user:" + registration.getEmail()); - verify(registration, remoteIp); + // If tomcat is behind a reverse proxy, ip needs to be found in other header. + String remoteIp = request.getHeader(REAL_IP_ADDRESS_HEADER); + if (remoteIp == null || remoteIp.isEmpty()) { + remoteIp = request.getRemoteAddr(); + } + logger.debug("Remote address" + remoteIp); - final User user = new User(); - user.setEmail(registration.getEmail().trim()); - user.setFirstname(registration.getFirstname()); - user.setLastname(registration.getLastname()); - user.setPassword(registration.getPassword()); + verify(registration, remoteIp); - user.setAuthenticationType(AuthenticationType.DATABASE); - userService.createUser(user, false, true); - response.setHeader("Location", "/service/users/" + user.getId()); - } + final User user = new User(); + user.setEmail(registration.getEmail().trim()); + user.setFirstname(registration.getFirstname()); + user.setLastname(registration.getLastname()); + user.setPassword(registration.getPassword()); - @RequestMapping(method = RequestMethod.PUT, value = "/users/resetPassword", produces = {"application/json"}) - @ResponseStatus(value = HttpStatus.OK) - public void resetPassword(@RequestParam String email) throws InvalidAuthSchemaException, EmailNotExistsException { - try { - userService.resetPassword(email); - } catch (InvalidUserEmailException e) { - throw new EmailNotExistsException(e); - } - } + user.setAuthenticationType(AuthenticationType.DATABASE); + userService.createUser(user, false, true); + response.setHeader("Location", "/service/users/" + user.getId()); + } - private void verify(@NotNull final RestUserRegistration registration, @NotNull String remoteAddress) throws BindException { + private void doLogin(HttpServletRequest request, String email) { + PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(email,null); + Authentication auth = authManager.authenticate(token); + SecurityContextHolder.getContext().setAuthentication(auth); + // update spring mvc session + HttpSession session = request.getSession(true); + session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext()); + } - final BindException errors = new RegistrationException(registration, "registration"); - final UserValidator validator = new UserValidator(); - validator.setUserService(userService); - validator.validate(registration, errors); + @RequestMapping(method = RequestMethod.POST, value = "/users/googleCallback", produces = { "application/json" }) + @ResponseStatus(value = HttpStatus.OK) + public RestOath2CallbackResponse processGoogleCallback(@NotNull @RequestParam String code, @NotNull HttpServletRequest request) throws WiseMappingException { + User user = userService.createUserFromGoogle(code); + if (user.getGoogleSync() != null && user.getGoogleSync().booleanValue()) + doLogin(request, user.getEmail()); + RestOath2CallbackResponse response = new RestOath2CallbackResponse(); + response.setEmail(user.getEmail()); + response.setGoogleSync(user.getGoogleSync()); + response.setSyncCode(user.getSyncCode()); + return response; + } - // If captcha is enabled, generate it ... - if (recatchaEnabled) { - final String recaptcha = registration.getRecaptcha(); - if (recaptcha != null) { - final String reCaptchaResponse = captchaService.verifyRecaptcha(remoteAddress, recaptcha); - if (reCaptchaResponse != null && !reCaptchaResponse.isEmpty()) { - errors.rejectValue("recaptcha", reCaptchaResponse); - } - } else { - errors.rejectValue("recaptcha", Messages.CAPTCHA_LOADING_ERROR); - } - } else { - logger.warn("captchaEnabled is enabled.Recommend to enable it for production environments."); - } + @RequestMapping(method = RequestMethod.PUT, value = "/users/confirmAccountSync", produces = { "application/json" }) + @ResponseStatus(value = HttpStatus.OK) + public void confirmAccountSync(@NotNull @RequestParam String email, @NotNull @RequestParam String code, @NotNull HttpServletRequest request) throws WiseMappingException { + userService.confirmAccountSync(email, code); + doLogin(request, email); + } - if (errors.hasErrors()) { - throw errors; - } + @RequestMapping(method = RequestMethod.PUT, value = "/users/resetPassword", produces = { "application/json" }) + @ResponseStatus(value = HttpStatus.OK) + public void resetPassword(@RequestParam String email) throws InvalidAuthSchemaException, EmailNotExistsException { + try { + userService.resetPassword(email); + } catch (InvalidUserEmailException e) { + throw new EmailNotExistsException(e); + } + } - // Is excluded ?. - final List excludedDomains = Arrays.asList(domainBanExclusion.split(",")); - final String emailDomain = registration.getEmail().split("@")[1]; - if (excludedDomains.contains(emailDomain)) { - throw new IllegalArgumentException("Email is part of ban exclusion list due to abuse. Please, contact site admin if you think this is an error." + emailDomain); - } - } + private void verify(@NotNull final RestUserRegistration registration, @NotNull String remoteAddress) + throws BindException { + + final BindException errors = new RegistrationException(registration, "registration"); + final UserValidator validator = new UserValidator(); + validator.setUserService(userService); + validator.validate(registration, errors); + + // If captcha is enabled, generate it ... + if (recatchaEnabled) { + final String recaptcha = registration.getRecaptcha(); + if (recaptcha != null) { + final String reCaptchaResponse = captchaService.verifyRecaptcha(remoteAddress, recaptcha); + if (reCaptchaResponse != null && !reCaptchaResponse.isEmpty()) { + errors.rejectValue("recaptcha", reCaptchaResponse); + } + } else { + errors.rejectValue("recaptcha", Messages.CAPTCHA_LOADING_ERROR); + } + } else { + logger.warn("captchaEnabled is enabled.Recommend to enable it for production environments."); + } + + if (errors.hasErrors()) { + throw errors; + } + + // Is excluded ?. + final List excludedDomains = Arrays.asList(domainBanExclusion.split(",")); + final String emailDomain = registration.getEmail().split("@")[1]; + if (excludedDomains.contains(emailDomain)) { + throw new IllegalArgumentException( + "Email is part of ban exclusion list due to abuse. Please, contact site admin if you think this is an error." + + emailDomain); + } + } } diff --git a/wise-webapp/src/main/java/com/wisemapping/rest/model/RestOath2CallbackResponse.java b/wise-webapp/src/main/java/com/wisemapping/rest/model/RestOath2CallbackResponse.java new file mode 100644 index 00000000..f8b040f3 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/rest/model/RestOath2CallbackResponse.java @@ -0,0 +1,33 @@ +package com.wisemapping.rest.model; + +public class RestOath2CallbackResponse { + + private String email; + private Boolean googleSync; + private String syncCode; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Boolean getGoogleSync() { + return googleSync; + } + + public void setGoogleSync(Boolean googleSync) { + this.googleSync = googleSync; + } + + public String getSyncCode() { + return syncCode; + } + + public void setSyncCode(String syncCode) { + this.syncCode = syncCode; + } + +} diff --git a/wise-webapp/src/main/java/com/wisemapping/security/GoogleAuthenticationProvider.java b/wise-webapp/src/main/java/com/wisemapping/security/GoogleAuthenticationProvider.java new file mode 100644 index 00000000..89e94077 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/security/GoogleAuthenticationProvider.java @@ -0,0 +1,62 @@ +package com.wisemapping.security; + +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import com.wisemapping.model.User; + +public class GoogleAuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider { + + private UserDetailsService userDetailsService; + + public UserDetailsService getUserDetailsService() { + return userDetailsService; + } + + public void setUserDetailsService(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + /** + * Authenticate the given PreAuthenticatedAuthenticationToken. + * + * If the principal contained in the authentication object is null, the request will + * be ignored to allow other providers to authenticate it. + */ + @Override + public Authentication authenticate(Authentication inputToken) throws AuthenticationException { + if (!supports(inputToken.getClass())) { + return null; + } + if (inputToken.getPrincipal() == null) { + throw new BadCredentialsException("No pre-authenticated principal found in request."); + } + UserDetails userDetails = userDetailsService.loadUserByUsername(inputToken.getName()); + final User user = userDetails.getUser(); + + if (!user.isActive()) { + throw new BadCredentialsException("User has been disabled for login " + inputToken.getName()); + } + + PreAuthenticatedAuthenticationToken resultToken = new PreAuthenticatedAuthenticationToken(userDetails, + inputToken.getCredentials(), userDetails.getAuthorities()); + resultToken.setDetails(userDetails); + + userDetailsService.getUserService().auditLogin(user); + + return resultToken; + } + + /** + * Indicate that this provider only supports PreAuthenticatedAuthenticationToken + * (sub)classes. + */ + @Override + public final boolean supports(Class authentication) { + return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication); + } + + +} diff --git a/wise-webapp/src/main/java/com/wisemapping/security/UserDetailsService.java b/wise-webapp/src/main/java/com/wisemapping/security/UserDetailsService.java index 4a32bf35..0d86c946 100644 --- a/wise-webapp/src/main/java/com/wisemapping/security/UserDetailsService.java +++ b/wise-webapp/src/main/java/com/wisemapping/security/UserDetailsService.java @@ -23,7 +23,6 @@ import com.wisemapping.model.User; import com.wisemapping.service.UserService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataAccessException; import org.springframework.security.core.userdetails.UsernameNotFoundException; diff --git a/wise-webapp/src/main/java/com/wisemapping/service/UserService.java b/wise-webapp/src/main/java/com/wisemapping/service/UserService.java index ef0cf222..0908a777 100755 --- a/wise-webapp/src/main/java/com/wisemapping/service/UserService.java +++ b/wise-webapp/src/main/java/com/wisemapping/service/UserService.java @@ -28,6 +28,10 @@ public interface UserService { User createUser(@NotNull User user, boolean emailConfirmEnabled, boolean welcomeEmail) throws WiseMappingException; + User createUserFromGoogle(@NotNull String callbackCode) throws WiseMappingException; + + User confirmAccountSync(@NotNull String email, @NotNull String code) throws WiseMappingException; + void changePassword(@NotNull User user); User getUserBy(String email); diff --git a/wise-webapp/src/main/java/com/wisemapping/service/UserServiceImpl.java b/wise-webapp/src/main/java/com/wisemapping/service/UserServiceImpl.java index 07866c1e..bc1252db 100755 --- a/wise-webapp/src/main/java/com/wisemapping/service/UserServiceImpl.java +++ b/wise-webapp/src/main/java/com/wisemapping/service/UserServiceImpl.java @@ -23,6 +23,8 @@ import com.wisemapping.exceptions.InvalidMindmapException; import com.wisemapping.exceptions.WiseMappingException; import com.wisemapping.mail.NotificationService; import com.wisemapping.model.*; +import com.wisemapping.service.google.GoogleAccountBasicData; +import com.wisemapping.service.google.GoogleService; import com.wisemapping.util.VelocityEngineUtils; import com.wisemapping.util.VelocityEngineWrapper; import org.jetbrains.annotations.NotNull; @@ -39,7 +41,7 @@ public class UserServiceImpl private NotificationService notificationService; private MessageSource messageSource; private VelocityEngineWrapper velocityEngineWrapper; - + private GoogleService googleService; @Override public void activateAccount(long code) @@ -147,6 +149,55 @@ public class UserServiceImpl return user; } + @NotNull + public User createUserFromGoogle(@NotNull String callbackCode) throws WiseMappingException { + try { + GoogleAccountBasicData data = googleService.processCallback(callbackCode); + User existingUser = userManager.getUserBy(data.getEmail()); + if (existingUser == null) { + User newUser = new User(); + // new registrations from google starts synched + newUser.setGoogleSync(true); + newUser.setEmail(data.getEmail()); + newUser.setFirstname(data.getName()); + newUser.setLastname(data.getLastName()); + newUser.setAuthenticationType(AuthenticationType.GOOGLE_OAUTH2); + newUser.setGoogleToken(data.getAccessToken()); + existingUser = this.createUser(newUser, false, true); + } else { + // user exists and doesnt have confirmed account linking, I must wait for confirmation + if (existingUser.getGoogleSync() == null) { + existingUser.setGoogleSync(false); + existingUser.setSyncCode(callbackCode); + existingUser.setGoogleToken(data.getAccessToken()); + userManager.updateUser(existingUser); + } + + } + return existingUser; + } catch (Exception e) { + throw new WiseMappingException("Cant create user", e); + } + } + + public User confirmAccountSync(@NotNull String email, @NotNull String code) throws WiseMappingException { + User existingUser = userManager.getUserBy(email); + // additional security check + if (existingUser == null || !existingUser.getSyncCode().equals(code)) { + throw new WiseMappingException("User not found / incorrect code"); + } + existingUser.setGoogleSync(true); + existingUser.setSyncCode(null); + // user will not be able to login again with usr/pwd schema + existingUser.setAuthenticationType(AuthenticationType.GOOGLE_OAUTH2); + existingUser.setPassword(""); + userManager.updateUser(existingUser); + + return existingUser; + } + + + public Mindmap buildTutorialMindmap(@NotNull String firstName) throws InvalidMindmapException { //To change body of created methods use File | Settings | File Templates. final Locale locale = LocaleContextHolder.getLocale(); @@ -209,7 +260,11 @@ public class UserServiceImpl this.velocityEngineWrapper = velocityEngineWrapper; } - @Override + public void setGoogleService(GoogleService googleService) { + this.googleService = googleService; + } + + @Override public User getCasUserBy(String uid) { // TODO Auto-generated method stub return null; diff --git a/wise-webapp/src/main/java/com/wisemapping/service/google/GoogleAccountBasicData.java b/wise-webapp/src/main/java/com/wisemapping/service/google/GoogleAccountBasicData.java new file mode 100644 index 00000000..f8f43f76 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/service/google/GoogleAccountBasicData.java @@ -0,0 +1,66 @@ +package com.wisemapping.service.google; + +public class GoogleAccountBasicData { + + private String email; + private String accountId; + private String name; + private String lastName; + private String accessToken; + private String refreshToken; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + @Override + public String toString() { + return "GoogleAccountBasicData [email=" + email + ", accountId=" + accountId + ", name=" + name + ", lastName=" + + lastName + ", accessToken=" + accessToken + ", refreshToken=" + refreshToken + "]"; + } + +} diff --git a/wise-webapp/src/main/java/com/wisemapping/service/google/GoogleService.java b/wise-webapp/src/main/java/com/wisemapping/service/google/GoogleService.java new file mode 100644 index 00000000..2a4e8f6d --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/service/google/GoogleService.java @@ -0,0 +1,106 @@ +package com.wisemapping.service.google; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.wisemapping.service.http.HttpInvoker; +import com.wisemapping.service.http.HttpInvokerContentType; +import com.wisemapping.service.http.HttpInvokerException; +import com.wisemapping.service.http.HttpMethod; + +@Service +public class GoogleService { + private HttpInvoker httpInvoker; + private String optinConfirmUrl; + private String accountBasicDataUrl; + private String clientId; + private String clientSecret; + private String callbackUrl; + + public void setHttpInvoker(HttpInvoker httpInvoker) { + this.httpInvoker = httpInvoker; + } + + public void setOptinConfirmUrl(String optinConfirmUrl) { + this.optinConfirmUrl = optinConfirmUrl; + } + + public void setAccountBasicDataUrl(String accountBasicDataUrl) { + this.accountBasicDataUrl = accountBasicDataUrl; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public void setCallbackUrl(String callbackUrl) { + this.callbackUrl = callbackUrl; + } + + private String getNodeAsString(JsonNode node, String fieldName) { + return getNodeAsString(node, fieldName, null); + } + + private String getNodeAsString(JsonNode node, String fieldName, String defaultValue) { + JsonNode subNode = node.get(fieldName); + return subNode != null ? subNode.asText() : defaultValue; + } + + private Map getHeaders(String token) { + Map headers = new HashMap(); + headers.put("Content-type", "application/json"); + headers.put("Authorization", "Bearer " + token); + return headers; + } + + private GoogleAccountBasicData getAccountBasicData(String token) throws HttpInvokerException { + JsonNode response = httpInvoker.invoke(accountBasicDataUrl, null, HttpMethod.GET, this.getHeaders(token), null, + null); + GoogleAccountBasicData data = new GoogleAccountBasicData(); + data.setEmail(getNodeAsString(response, "email")); + data.setAccountId(getNodeAsString(response, "id")); + data.setName(getNodeAsString(response, "given_name", data.getEmail())); + data.setLastName(getNodeAsString(response, "family_name")); + return data; + } + + private Map getOptinConfirmBody(String code) { + Map result = new HashMap(); + result.put("client_id", clientId); + result.put("client_secret", clientSecret); + result.put("code", code); + result.put("redirect_uri", callbackUrl); + result.put("grant_type", "authorization_code"); + return result; + } + + public GoogleAccountBasicData processCallback(String code) + throws HttpInvokerException, JsonMappingException, JsonProcessingException { + Map body = this.getOptinConfirmBody(code); + JsonNode optinConfirmResponse = httpInvoker.invoke( + optinConfirmUrl, + HttpInvokerContentType.FORM_ENCODED, + HttpMethod.POST, + null, + null, + body); + + String accessToken = getNodeAsString(optinConfirmResponse, "access_token"); + String refreshToken = getNodeAsString(optinConfirmResponse, "refresh_token"); + + GoogleAccountBasicData data = this.getAccountBasicData(accessToken); + data.setAccessToken(accessToken); + data.setRefreshToken(refreshToken); + return data; + } + +} \ No newline at end of file diff --git a/wise-webapp/src/main/java/com/wisemapping/service/http/HttpInvoker.java b/wise-webapp/src/main/java/com/wisemapping/service/http/HttpInvoker.java new file mode 100644 index 00000000..be05d8a4 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/service/http/HttpInvoker.java @@ -0,0 +1,147 @@ +package com.wisemapping.service.http; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Service +public class HttpInvoker { + + protected static Logger logger = LogManager.getLogger(HttpInvoker.class); + + private ObjectMapper mapper = new ObjectMapper(); + + public HttpInvoker() { + super(); + } + + public JsonNode invoke( + String url, + HttpInvokerContentType requestContentType, + HttpMethod method, + Map headers, + String jsonPayload, + Map formData) + throws HttpInvokerException { + String responseBody = null; + try { + if (logger.isDebugEnabled()) { + logger.debug("finalUrl: " + url); + logger.debug("method: " + method); + logger.debug("payload: " + jsonPayload); + logger.debug("header: " + headers); + } + + CloseableHttpClient httpClient = HttpClients.createDefault(); + HttpRequestBase httpRequst = null; + + // build request + if (method.equals(HttpMethod.POST)) + httpRequst = new HttpPost(url); + else if (method.equals(HttpMethod.PUT)) + httpRequst = new HttpPut(url); + else if (method.equals(HttpMethod.GET)) + httpRequst = new HttpGet(url); + else if (method.equals(HttpMethod.DELETE)) + httpRequst = new HttpDelete(url); + else + throw new HttpInvokerException("Method " + method + " not suppoprted by http connector"); + + if (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT)) { + HttpEntity entity = null; + if (requestContentType.equals(HttpInvokerContentType.JSON)) { + if (jsonPayload == null) + throw new HttpInvokerException("Json content is required"); + entity = new StringEntity(jsonPayload, Charset.forName("UTF-8")); + ((HttpEntityEnclosingRequestBase) httpRequst).setEntity(entity); + } + if (requestContentType.equals(HttpInvokerContentType.FORM_ENCODED)) { + List nameValuePairs = new ArrayList(); + Set keys = formData.keySet(); + for (String key : keys) { + nameValuePairs.add(new BasicNameValuePair(key, formData.get(key).toString())); + } + entity = new UrlEncodedFormEntity(nameValuePairs); + ((HttpEntityEnclosingRequestBase) httpRequst).setEntity(entity); + } + if (entity == null) + throw new HttpInvokerException("Cant build entity to send"); + } + + if (headers != null) { + Set keys = headers.keySet(); + for (String key : keys) { + httpRequst.setHeader(key, headers.get(key)); + } + } + + if (requestContentType != null) + httpRequst.setHeader("Content-Type", requestContentType.getHttpContentType()); + + // invoke + CloseableHttpResponse response = httpClient.execute(httpRequst); + // response process + JsonNode root = null; + responseBody = response.getEntity() != null && response.getEntity().getContent() != null + ? IOUtils.toString(response.getEntity().getContent(), (String) null) + : null; + if (responseBody != null) { + if (logger.isDebugEnabled()) { + logger.debug("response plain: " + responseBody); + } + try { + root = mapper.readTree(responseBody); + } catch (Exception e) { + int returnCode = response.getStatusLine().getStatusCode(); + throw new HttpInvokerException("cant transform response to JSON. RQ: " + jsonPayload + ", RS: " + + responseBody + ", status: " + returnCode, e); + } + } + + if (response.getStatusLine().getStatusCode() >= 400) { + logger.error("error response: " + responseBody); + throw new HttpInvokerException("error invoking " + url + ", response: " + responseBody + ", status: " + + response.getStatusLine().getStatusCode()); + } + + httpRequst.releaseConnection(); + response.close(); + httpClient.close(); + + return root; + } catch (HttpInvokerException e) { + throw e; + } catch (Exception e) { + logger.error("cant invoke service " + url); + logger.error("response: " + responseBody, e); + throw new HttpInvokerException("cant invoke service " + url, e); + } + } + + + +} diff --git a/wise-webapp/src/main/java/com/wisemapping/service/http/HttpInvokerContentType.java b/wise-webapp/src/main/java/com/wisemapping/service/http/HttpInvokerContentType.java new file mode 100644 index 00000000..e80da507 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/service/http/HttpInvokerContentType.java @@ -0,0 +1,18 @@ +package com.wisemapping.service.http; + +public enum HttpInvokerContentType { + + JSON("application/json"), + FORM_ENCODED("application/x-www-form-urlencoded"); + + private String httpContentType; + + private HttpInvokerContentType(String type) { + this.httpContentType = type; + } + + public String getHttpContentType() { + return httpContentType; + } + +} \ No newline at end of file diff --git a/wise-webapp/src/main/java/com/wisemapping/service/http/HttpInvokerException.java b/wise-webapp/src/main/java/com/wisemapping/service/http/HttpInvokerException.java new file mode 100644 index 00000000..8ee37835 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/service/http/HttpInvokerException.java @@ -0,0 +1,13 @@ +package com.wisemapping.service.http; + +public class HttpInvokerException extends Exception { + + public HttpInvokerException(String message) { + super(message); + } + + public HttpInvokerException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/wise-webapp/src/main/java/com/wisemapping/service/http/HttpMethod.java b/wise-webapp/src/main/java/com/wisemapping/service/http/HttpMethod.java new file mode 100644 index 00000000..42553bfa --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/service/http/HttpMethod.java @@ -0,0 +1,5 @@ +package com.wisemapping.service.http; + +public enum HttpMethod { + POST, GET, DELETE, PUT +} diff --git a/wise-webapp/src/main/java/com/wisemapping/webmvc/UsersController.java b/wise-webapp/src/main/java/com/wisemapping/webmvc/UsersController.java index 2db75cd5..e760c017 100644 --- a/wise-webapp/src/main/java/com/wisemapping/webmvc/UsersController.java +++ b/wise-webapp/src/main/java/com/wisemapping/webmvc/UsersController.java @@ -39,6 +39,11 @@ public class UsersController { return new ModelAndView("forgot-password"); } + @RequestMapping(value = "registration-google", method = RequestMethod.GET) + public ModelAndView processGoogleCallback() { + return new ModelAndView("registration-google"); + } + @RequestMapping(value = "registration", method = RequestMethod.GET) public ModelAndView showRegistrationPage() { return new ModelAndView("registration"); diff --git a/wise-webapp/src/main/webapp/WEB-INF/app.properties b/wise-webapp/src/main/webapp/WEB-INF/app.properties index 3cbab059..d1fbc169 100755 --- a/wise-webapp/src/main/webapp/WEB-INF/app.properties +++ b/wise-webapp/src/main/webapp/WEB-INF/app.properties @@ -139,6 +139,17 @@ security.ldap.firstName.attribute=givenName # Coma separated list of domains and emails ban #accounts.exclusion.domain= - - +# google will redirect to this url, this url must be configured in the google app +# {baseurl}/c/registration-google +google.oauth2.callbackUrl=https://wisemapping.com/c/registration-google +# google app client id +google.oauth2.clientId= +# google app client secret +google.oauth2.clientSecret= +# google service for finish registration process, ie. exchange temporal code for user token +google.oauth2.confirmUrl=https://oauth2.googleapis.com/token +# google service for get user data (name, email, etc) +google.oauth2.userinfoUrl=https://www.googleapis.com/oauth2/v3/userinfo +# url for starting auth process with google +google.oauth2.url=https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=${google.oauth2.callbackUrl}&prompt=consent&response_type=code&client_id=${google.oauth2.clientId}&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&state=wisemapping&include_granted_scopes=true diff --git a/wise-webapp/src/main/webapp/WEB-INF/defs/definitions.xml b/wise-webapp/src/main/webapp/WEB-INF/defs/definitions.xml index 805880c2..0277589b 100644 --- a/wise-webapp/src/main/webapp/WEB-INF/defs/definitions.xml +++ b/wise-webapp/src/main/webapp/WEB-INF/defs/definitions.xml @@ -8,6 +8,7 @@ + diff --git a/wise-webapp/src/main/webapp/WEB-INF/wisemapping-security-db.xml b/wise-webapp/src/main/webapp/WEB-INF/wisemapping-security-db.xml index 4d63d585..695f7b62 100644 --- a/wise-webapp/src/main/webapp/WEB-INF/wisemapping-security-db.xml +++ b/wise-webapp/src/main/webapp/WEB-INF/wisemapping-security-db.xml @@ -11,7 +11,8 @@ - + + @@ -19,4 +20,7 @@ + + + \ No newline at end of file diff --git a/wise-webapp/src/main/webapp/WEB-INF/wisemapping-security.xml b/wise-webapp/src/main/webapp/WEB-INF/wisemapping-security.xml index 5f23acc0..12e726f1 100644 --- a/wise-webapp/src/main/webapp/WEB-INF/wisemapping-security.xml +++ b/wise-webapp/src/main/webapp/WEB-INF/wisemapping-security.xml @@ -3,12 +3,12 @@ - + @@ -34,6 +34,8 @@ + + @@ -47,6 +49,7 @@ + diff --git a/wise-webapp/src/main/webapp/WEB-INF/wisemapping-service.xml b/wise-webapp/src/main/webapp/WEB-INF/wisemapping-service.xml index bedf0bdd..6d962e81 100755 --- a/wise-webapp/src/main/webapp/WEB-INF/wisemapping-service.xml +++ b/wise-webapp/src/main/webapp/WEB-INF/wisemapping-service.xml @@ -18,12 +18,25 @@ + + + + + + + + + + + + + diff --git a/wise-webapp/src/main/webapp/jsp/reactInclude.jsp b/wise-webapp/src/main/webapp/jsp/reactInclude.jsp index 93484e12..5efb2798 100644 --- a/wise-webapp/src/main/webapp/jsp/reactInclude.jsp +++ b/wise-webapp/src/main/webapp/jsp/reactInclude.jsp @@ -20,7 +20,8 @@ analyticsAccount: '${requestScope['google.analytics.account']}', clientType: 'rest', recaptcha2Enabled: ${requestScope['google.recaptcha2.enabled']}, - recaptcha2SiteKey: '${requestScope['google.recaptcha2.siteKey']}' + recaptcha2SiteKey: '${requestScope['google.recaptcha2.siteKey']}', + googleOauth2Url: '${requestScope['google.oauth2.url']}' };