Commit bfe72191 authored by Artem Hrybeniuk's avatar Artem Hrybeniuk Committed by Matija Obreza
Browse files

Password reset API

parent 39d5a9c5
......@@ -15,23 +15,35 @@
*/
package org.gringlobal.api.v1.impl;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.EmailValidator;
import org.genesys.blocks.oauth.model.OAuthClient;
import org.genesys.blocks.oauth.persistence.OAuthClientRepository;
import org.genesys.blocks.oauth.service.OAuthClientDetailsService;
import org.genesys.blocks.security.NoUserFoundException;
import org.genesys.blocks.security.UserException;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.v1.ApiBaseController;
import org.gringlobal.model.SysUser;
import org.gringlobal.service.EMailVerificationService;
import org.gringlobal.service.TemplatingService;
import org.gringlobal.service.TokenVerificationService;
import org.gringlobal.service.UserService;
import org.gringlobal.spring.CaptchaChecker;
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.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
......@@ -40,6 +52,10 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Optional;
@RestController("meApi1")
@RequestMapping(MeController.API_URL)
@PreAuthorize("isAuthenticated() && (hasRole('USER') || hasRole('ADMINISTRATOR'))") // Don't allow OAuth clients here
......@@ -49,6 +65,15 @@ public class MeController extends ApiBaseController {
/** The Constant API_URL. */
public static final String API_URL = ApiBaseController.APIv1_BASE + "/me";
@Value("${captcha.enabled}")
private Boolean captchaEnabled;
@Value("${default.oauthclient.clientId}")
private String defaultOAuthClientId;
@Autowired
private OAuthClientRepository oauthClientRepository;
@Autowired
private UserService userService;
......@@ -59,6 +84,14 @@ public class MeController extends ApiBaseController {
@Qualifier("soapPasswordEncoder")
private PasswordEncoder soapPasswordEncoder;
@Autowired
private CaptchaChecker captchaChecker;
@Autowired
private EMailVerificationService eMailVerificationService;
private final EmailValidator emailValidator = EmailValidator.getInstance();
/**
* Gets the profile.
*
......@@ -122,4 +155,80 @@ public class MeController extends ApiBaseController {
throw new InvalidApiUsageException("The current password is not valid.");
}
}
@PreAuthorize("isAuthenticated()")
@PostMapping(value = "/password/reset")
public boolean resetPassword(HttpServletRequest request, @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
@RequestParam("email") String email) throws UserException {
// Validate the reCAPTCHA
if (captchaEnabled) {
captchaChecker.assureValidResponseForClient(captchaResponse, request.getRemoteAddr());
}
if (!emailValidator.isValid(email)) {
LOG.warn("Invalid email provided: {}", email);
throw new InvalidApiUsageException("Invalid email provided: " + email);
}
String referer = request.getHeader("Referer");
OAuthClient client = oauthClientRepository.findByClientId(defaultOAuthClientId);
Optional<String> origin = client.getAllowedOrigins().stream().filter(referer::startsWith).findFirst();
if (origin.isEmpty()) {
LOG.warn("Invalid origin provided for: {}", referer);
throw new InvalidApiUsageException("Provided origin is not allowed: " + referer);
}
try {
final SysUser user = userService.loadSysUserByEmail(email);
// if (!user.isAccountNonLocked()) {
// LOG.warn("Password for locked user accounts can't be reset!");
// throw new UserException("Password for locked user accounts can't be reset!");
// }
if (!user.isEnabled()) {
LOG.warn("Password for disabled user accounts can't be reset!");
throw new UserException("Password for disabled user accounts can't be reset!");
}
eMailVerificationService.sendPasswordResetEmail(email, user.getUsername(), TemplatingService.PASSWORD_USER_RESET, origin.get());
return true;
} catch (UsernameNotFoundException | NoUserFoundException e) {
throw new UserException("No such user!");
}
}
@PreAuthorize("isAuthenticated()")
@PostMapping(value = "/password/reset/{tokenUuid:.+}")
public boolean updatePassword(@PathVariable("tokenUuid") String tokenUuid, HttpServletRequest request, @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
@RequestParam(value = "key", required = true) String key, @RequestParam("password") String password) throws IOException, UserException {
// Validate the reCAPTCHA
if (captchaEnabled) {
captchaChecker.assureValidResponseForClient(captchaResponse, request.getRemoteAddr());
}
try {
eMailVerificationService.changeSysUserPassword(tokenUuid, key, password);
return true;
} catch (final TokenVerificationService.NoSuchVerificationTokenException e) {
throw new UserException("No such verification token!");
} catch (TokenVerificationService.TokenExpiredException e) {
throw new UserException("Your token expired!");
}
}
@PostMapping(value = "/password/reset/{tokenUuid:.+}/cancel")
public boolean cancelPasswordReset(@PathVariable("tokenUuid") String tokenUuid, @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
HttpServletRequest request) throws Exception {
// Validate the reCAPTCHA
if (captchaEnabled) {
captchaChecker.assureValidResponseForClient(captchaResponse, request.getRemoteAddr());
}
eMailVerificationService.cancelPasswordReset(tokenUuid);
return true;
}
}
\ No newline at end of file
......@@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
......@@ -199,4 +200,17 @@ public class OAuthManagementController extends ApiBaseController {
return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, clientDetailsService.listClientDetails(page.toPageRequest(100, Sort.Direction.ASC, "clientId")));
}
@PostMapping(value = "/{clientId}/set-recaptcha-keys")
public @ResponseBody boolean setRecaptchaKeys(@PathVariable("clientId") String clientId, @RequestParam("privateKey") String privateKey,
@RequestParam("publicKey") String publicKey) {
OAuthClient oauthClient = clientDetailsService.getClient(clientId);
if (oauthClient == null) {
throw new NotFoundElement("No such client");
}
oauthClient.setPrivateRecaptchaKey(privateKey);
oauthClient.setPublicRecaptchaKey(publicKey);
clientDetailsService.updateClient(oauthClient.getId(), oauthClient.getVersion(), oauthClient);
return true;
}
}
......@@ -15,6 +15,8 @@
*/
package org.gringlobal.api.v1.impl;
import org.genesys.blocks.oauth.persistence.OAuthClientRepository;
import org.genesys.blocks.security.SecurityContextUtil;
import org.gringlobal.api.v1.ApiBaseController;
import org.gringlobal.persistence.AccessionRepository;
import org.gringlobal.persistence.GeographyRepository;
......@@ -39,8 +41,11 @@ public class SystemStatusController {
/** The Constant API_URL. */
public static final String API_URL = ApiBaseController.APIv1_BASE + "/status";
@Value("${captcha.siteKey}")
private String captchaSiteKey;
@Value("${captcha.enabled}")
private Boolean captchaEnabled;
@Autowired
private OAuthClientRepository oauthClientRepository;
@Autowired
private SysUserRepository sysUserRepository;
......@@ -65,7 +70,12 @@ public class SystemStatusController {
status.accessionCount = accessionRepository.count();
status.geographyCount = geographyRepository.count();
status.inventoryCount = inventoryRepository.count();
status.captchaSiteKey = captchaSiteKey;
if (captchaEnabled) {
var oAuthClient = oauthClientRepository.findByClientId(SecurityContextUtil.getOAuthClientId());
if (oAuthClient != null) {
status.captchaSiteKey = oAuthClient.getPublicRecaptchaKey();
}
}
return status;
}
......
......@@ -16,10 +16,16 @@
package org.gringlobal.api.v1.impl;
import java.io.IOException;
import java.util.Optional;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.validator.routines.EmailValidator;
import org.genesys.blocks.oauth.model.OAuthClient;
import org.genesys.blocks.oauth.persistence.OAuthClientRepository;
import org.genesys.blocks.security.UserException;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.PageableAsQueryParam;
import org.gringlobal.api.exception.NotFoundElement;
import org.gringlobal.api.v1.ApiBaseController;
import org.gringlobal.api.v1.FilteredPage;
import org.gringlobal.api.v1.Pagination;
......@@ -27,18 +33,24 @@ import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.QWebUser;
import org.gringlobal.model.WebCooperator;
import org.gringlobal.model.WebUser;
import org.gringlobal.service.EMailVerificationService;
import org.gringlobal.service.LanguageService;
import org.gringlobal.service.ShortFilterService;
import org.gringlobal.service.TemplatingService;
import org.gringlobal.service.TokenVerificationService;
import org.gringlobal.service.WebCooperatorService;
import org.gringlobal.service.WebUserService;
import org.gringlobal.service.filter.WebUserFilter;
import org.gringlobal.spring.CaptchaChecker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
......@@ -56,6 +68,8 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.servlet.http.HttpServletRequest;
@RestController("webUserApi1")
@RequestMapping(WebUserController.API_URL)
@PreAuthorize("isAuthenticated()")
......@@ -65,6 +79,15 @@ public class WebUserController extends ApiBaseController {
/** The Constant API_URL. */
public static final String API_URL = ApiBaseController.APIv1_BASE + "/webuser";
@Value("${captcha.enabled}")
private Boolean captchaEnabled;
@Value("${default.oauthclient.clientId}")
private String defaultOAuthClientId;
@Autowired
private OAuthClientRepository oauthClientRepository;
@Autowired
private LanguageService languageService;
......@@ -77,6 +100,14 @@ public class WebUserController extends ApiBaseController {
@Autowired
private WebCooperatorService webCooperatorService;
@Autowired
private EMailVerificationService eMailVerificationService;
@Autowired
private CaptchaChecker captchaChecker;
private final EmailValidator emailValidator = EmailValidator.getInstance();
protected Class<WebUserFilter> filterType() {
return WebUserFilter.class;
}
......@@ -229,4 +260,85 @@ public class WebUserController extends ApiBaseController {
throw new InvalidApiUsageException("Not using user authentication");
}
@PreAuthorize("isAuthenticated()")
@PostMapping(value = "/password/reset")
public boolean resetPassword(HttpServletRequest request, @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
@RequestParam("email") String email) throws UserException {
// Validate the reCAPTCHA
if (captchaEnabled) {
captchaChecker.assureValidResponseForClient(captchaResponse, request.getRemoteAddr());
}
if (!emailValidator.isValid(email)) {
LOG.warn("Invalid email provided: {}", email);
throw new InvalidApiUsageException("Invalid email provided: " + email);
}
String referer = request.getHeader("Referer");
OAuthClient client = oauthClientRepository.findByClientId(defaultOAuthClientId);
Optional<String> origin = client.getAllowedOrigins().stream().filter(referer::startsWith).findFirst();
if (origin.isEmpty()) {
LOG.warn("Invalid origin provided for: {}", referer);
throw new InvalidApiUsageException("Provided origin is not allowed: " + referer);
}
try {
final WebUser user = (WebUser) crudService.loadUserByUsername(email);
if (user == null) {
LOG.warn("User with email {} doesn't exist", email);
throw new NotFoundElement("User not found");
}
// if (!user.isAccountNonLocked()) {
// LOG.warn("Password for locked user accounts can't be reset!");
// throw new UserException("Password for locked user accounts can't be reset!");
// }
if (!user.isEnabled()) {
LOG.warn("Password for disabled user accounts can't be reset!");
throw new UserException("Password for disabled user accounts can't be reset!");
}
eMailVerificationService.sendPasswordResetEmail(email, user.getUsername(), TemplatingService.PASSWORD_WEBUSER_RESET, origin.get());
return true;
} catch (UsernameNotFoundException e) {
throw new UserException("No such user!");
}
}
@PreAuthorize("isAuthenticated()")
@PostMapping(value = "/password/reset/{tokenUuid:.+}")
public boolean updatePassword(@PathVariable("tokenUuid") String tokenUuid, HttpServletRequest request, @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
@RequestParam(value = "key", required = true) String key, @RequestParam("password") String password) throws IOException, UserException {
// Validate the reCAPTCHA
if (captchaEnabled) {
captchaChecker.assureValidResponseForClient(captchaResponse, request.getRemoteAddr());
}
try {
eMailVerificationService.changeWebUserPassword(tokenUuid, key, password);
return true;
} catch (final TokenVerificationService.NoSuchVerificationTokenException e) {
throw new UserException("No such verification token!");
} catch (TokenVerificationService.TokenExpiredException e) {
throw new UserException("Your token expired!");
}
}
@PostMapping(value = "/password/reset/{tokenUuid:.+}/cancel")
public boolean cancelPasswordReset(@PathVariable("tokenUuid") String tokenUuid, @RequestParam(value = "g-recaptcha-response", required = false) String captchaResponse,
HttpServletRequest request) throws Exception {
// Validate the reCAPTCHA
if (captchaEnabled) {
captchaChecker.assureValidResponseForClient(captchaResponse, request.getRemoteAddr());
}
eMailVerificationService.cancelPasswordReset(tokenUuid);
return true;
}
}
......@@ -29,6 +29,7 @@ import org.gringlobal.custom.security.WebUserTokenGranter;
import org.gringlobal.service.WebUserService;
import org.gringlobal.spring.AccessTokenInCookieFilter;
import org.gringlobal.spring.CachedInMemoryAuthorizationCodeServices;
import org.gringlobal.spring.CaptchaChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -111,6 +112,16 @@ public class OAuth2ServerConfig {
return new JwtTokenStore(accessTokenConverter());
}
/**
* Captcha Checker.
*
* @return the captcha checker
*/
@Bean
public CaptchaChecker captchaChecker() {
return new CaptchaChecker();
}
/**
* Access token converter.
*
......
......@@ -16,6 +16,8 @@
package org.gringlobal.service;
import org.genesys.blocks.security.NoUserFoundException;
import org.genesys.blocks.security.UserException;
import org.gringlobal.model.WebUser;
/**
......@@ -25,6 +27,16 @@ public interface EMailVerificationService {
void sendVerificationEmail(WebUser webUser);
void sendPasswordResetEmail(String email, String username, String userResetTemplate, String origin);
void changeWebUserPassword(String tokenUuid, String key, String password)
throws TokenVerificationService.NoSuchVerificationTokenException, TokenVerificationService.TokenExpiredException;
void changeSysUserPassword(String tokenUuid, String key, String password)
throws TokenVerificationService.NoSuchVerificationTokenException, TokenVerificationService.TokenExpiredException, NoUserFoundException, UserException;
void cancelPasswordReset(String tokenUuid) throws Exception;
void validateEMail(String tokenUuid, String key) throws Exception;
void cancelValidation(String tokenUuid) throws Exception;
......
......@@ -25,6 +25,8 @@ import com.github.mustachejava.Mustache;
public interface TemplatingService {
String EMAIL_WEBUSER_CONFIRMATION = "email.webuser.confirmation";
String PASSWORD_WEBUSER_RESET = "password.webuser.reset";
String PASSWORD_USER_RESET = "password.user.reset";
String fillTemplate(String template, Map<String, Object> params);
......
......@@ -38,6 +38,14 @@ public interface UserService extends UserDetailsService {
*/
SysUser loadSysUser(Long id) throws NoUserFoundException;
/**
* Load sys user by cooperator email
*
* @param email the user email
* @return the loaded user
*/
SysUser loadSysUserByEmail(String email) throws NoUserFoundException;
/**
* Disable or enable user account
*
......
......@@ -23,8 +23,11 @@ import java.util.Map;
import java.util.concurrent.Callable;
import com.google.common.collect.Lists;
import org.genesys.blocks.security.NoUserFoundException;
import org.genesys.blocks.security.UserException;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.model.AppResource;
import org.gringlobal.model.SysUser;
import org.gringlobal.model.VerificationToken;
import org.gringlobal.model.WebUser;
import org.gringlobal.service.AppResourceService;
......@@ -44,6 +47,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
......@@ -138,6 +142,74 @@ public class EMailVerificationServiceImpl implements EMailVerificationService {
}
}
@Override
public void sendPasswordResetEmail(String email, String username, String userResetTemplate, String origin) {
// Generate new token
final VerificationToken verificationToken = tokenVerificationService.generateToken("password-reset", email);
final Map<String, Object> scopes = new HashMap<>();
scopes.put("url", origin);
scopes.put("tokenUUID", verificationToken.getUuid());
scopes.put("tokenKey", verificationToken.getKey());
scopes.put("username", username);
final AppResource resource = appResourceService.getResource(AppResourceService.APP_NAME_GG_CE, userResetTemplate, Locale.ENGLISH);
if (resource != null) {
final String mailBody = templatingService.fillTemplate(resource.getDescription(), scopes);
emailService.sendMail(resource.getDisplayMember(), mailBody, email);
} else {
LOG.warn("{} app resource not found. Not sending verification email", userResetTemplate);
}
}
@Override
@Transactional(rollbackFor = Throwable.class)
public void changeWebUserPassword(final String tokenUuid, final String key, final String password) throws NoSuchVerificationTokenException, TokenExpiredException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("password-reset", tokenUuid, key);
final WebUser user = (WebUser) webUserService.loadUserByUsername(consumedToken.getData());
Authentication prevAuth = SecurityContextHolder.getContext().getAuthentication();
try {
LOG.warn("Setting temporary authorization for password reset for {}", user.getUsername());
final UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
webUserService.setPassword(user.getId(), password);
} finally {
LOG.warn("Restoring authorization away from {}", user.getUsername());
SecurityContextHolder.getContext().setAuthentication(prevAuth);
}
}
@Override
@Transactional(rollbackFor = Throwable.class)
public void changeSysUserPassword(final String tokenUuid, final String key, final String password) throws NoSuchVerificationTokenException, TokenExpiredException, UserException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("password-reset", tokenUuid, key);
final SysUser user = userService.loadSysUserByEmail(consumedToken.getData());
Authentication prevAuth = SecurityContextHolder.getContext().getAuthentication();
try {
LOG.warn("Setting temporary authorization for password reset for {}", user.getUsername());
final UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
userService.setPassword(user, password);
} finally {
LOG.warn("Restoring authorization away from {}", user.getUsername());
SecurityContextHolder.getContext().setAuthentication(prevAuth);
}
}
@Override
@Transactional
public void cancelPasswordReset(String tokenUuid) throws Exception {
try {
tokenVerificationService.cancel(tokenUuid);
} catch (final NoSuchVerificationTokenException e) {
LOG.warn("No such token. Error message {}", e.getMessage());
throw new InvalidApiUsageException("No such verification token");
}
}
private <T> T asAdmin(Callable<T> callable) throws Exception {
UserDetails administrator = userService.loadUserByUsername("administrator");
List<GrantedAuthority> authorities = Lists.newArrayList(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"));
......
......@@ -18,6 +18,7 @@ package org.gringlobal.service.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
......@@ -144,6 +145,25 @@ public class UserServiceImpl implements UserService, InitializingBean {
return user;
}
@Override
public SysUser loadSysUserByEmail(final String email) throws UsernameNotFoundException {
Optional<SysUser> userOptional = userRepository.findOne(QSysUser.sysUser.cooperator.email.eq(email));
if (userOptional.isEmpty()) {
throw new UsernameNotFoundException("User with email=" + email + " not found");
}
SysUser user = userOptional.get();
LOG.debug("Found user {} for email: {}", user, email);
lazyLoad(user);
user.setRuntimeAuthorities(getRuntimeAuthorities(user));
LOG.debug("User {}#{} has authorities: {}", user.getUsername(), user.getId(), user.getAuthorities().stream().map(auth -> auth