This repository has been archived by the owner on Jan 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
165 additions
and
240 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,24 +27,49 @@ | |
|
||
package dev.vernite.vernite.user.auth; | ||
|
||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
|
||
import java.net.URI; | ||
import java.net.URISyntaxException; | ||
import java.net.URLEncoder; | ||
import java.net.http.HttpClient; | ||
import java.net.http.HttpRequest; | ||
import java.net.http.HttpResponse.BodyHandlers; | ||
import java.security.SecureRandom; | ||
import java.time.Instant; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.Date; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.Random; | ||
import java.util.Set; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.Future; | ||
import java.util.concurrent.ScheduledExecutorService; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import jakarta.validation.constraints.NotNull; | ||
import lombok.Setter; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.dao.DataIntegrityViolationException; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.mail.SimpleMailMessage; | ||
import org.springframework.mail.javamail.JavaMailSender; | ||
import org.springframework.scheduling.annotation.Scheduled; | ||
import org.springframework.web.bind.annotation.CookieValue; | ||
import org.springframework.web.bind.annotation.DeleteMapping; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.ModelAttribute; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
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.RestController; | ||
import org.springframework.web.server.ResponseStatusException; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
||
import dev.vernite.vernite.common.utils.counter.CounterSequence; | ||
import dev.vernite.vernite.event.Event; | ||
import dev.vernite.vernite.event.EventFilter; | ||
|
@@ -64,38 +89,33 @@ | |
import dev.vernite.vernite.utils.ErrorType; | ||
import dev.vernite.vernite.utils.ObjectNotFoundException; | ||
import dev.vernite.vernite.utils.SecureStringUtils; | ||
|
||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.dao.DataIntegrityViolationException; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.mail.SimpleMailMessage; | ||
import org.springframework.mail.javamail.JavaMailSender; | ||
import org.springframework.scheduling.annotation.Scheduled; | ||
import org.springframework.web.bind.annotation.CookieValue; | ||
import org.springframework.web.bind.annotation.DeleteMapping; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.ModelAttribute; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
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.RestController; | ||
import org.springframework.web.server.ResponseStatusException; | ||
|
||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import io.swagger.v3.oas.annotations.media.Content; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import jakarta.validation.constraints.NotNull; | ||
import lombok.Setter; | ||
|
||
@RestController | ||
@RequestMapping("/auth") | ||
public class AuthController { | ||
|
||
public static final String COOKIE_NAME = "session"; | ||
private static final Random RANDOM = new Random(); | ||
private static final URI RECAPTCHA_URI; | ||
static { | ||
try { | ||
RECAPTCHA_URI = new URI("https://www.google.com/recaptcha/api/siteverify"); | ||
} catch (URISyntaxException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
private static final SecureRandom RANDOM = new SecureRandom(); | ||
private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); | ||
private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
|
||
@Autowired | ||
private UserRepository userRepository; | ||
|
@@ -125,6 +145,9 @@ public class AuthController { | |
@Value("${server.servlet.context-path}") | ||
private String cookiePath; | ||
|
||
@Value("${recaptcha.secret}") | ||
private String recaptchaSecret; | ||
|
||
@Operation(summary = "Logged user", description = "This method returns currently logged user.") | ||
@ApiResponse(responseCode = "200", description = "Logged user.") | ||
@ApiResponse(responseCode = "401", description = "User is not logged.", content = @Content()) | ||
|
@@ -235,7 +258,7 @@ public User recoverDeleted(@Parameter(hidden = true) User loggedUser) { | |
@Content(mediaType = "application/json", schema = @Schema(implementation = User.class)) | ||
}) | ||
@ApiResponse(responseCode = "403", description = "User is already logged.", content = @Content()) | ||
@ApiResponse(responseCode = "404", description = "Username or password is incorrect.", content = @Content()) | ||
@ApiResponse(responseCode = "404", description = "Username or password is incorrect or invalid captcha.", content = @Content()) | ||
@PostMapping("/login") | ||
public Future<User> login(@Parameter(hidden = true) User loggedUser, @RequestBody LoginRequest req, | ||
HttpServletRequest request, HttpServletResponse response) { | ||
|
@@ -248,19 +271,27 @@ public Future<User> login(@Parameter(hidden = true) User loggedUser, @RequestBod | |
if (req.getEmail() == null) { | ||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing username"); | ||
} | ||
User u = req.getEmail().indexOf('@') != -1 ? userRepository.findByEmail(req.getEmail()) | ||
: userRepository.findByUsername(req.getEmail()); | ||
CompletableFuture<User> f = new CompletableFuture<>(); | ||
EXECUTOR_SERVICE.schedule(() -> { | ||
if (u == null || !u.checkPassword(req.getPassword())) { | ||
f.completeExceptionally( | ||
new ResponseStatusException(HttpStatus.NOT_FOUND, "username or password incorrect")); | ||
} else { | ||
createSession(request, response, u, req.isRemember()); | ||
f.complete(u); | ||
return verifyCaptcha(req.getCaptcha(), request).thenApply(action -> { | ||
if (!"login".equals(action)) { | ||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "invalid captcha"); | ||
} | ||
}, 500 + RANDOM.nextInt(500), TimeUnit.MILLISECONDS); | ||
return f; | ||
if (req.getEmail().indexOf('@') != -1) { | ||
return userRepository.findByEmail(req.getEmail()); | ||
} | ||
return userRepository.findByUsername(req.getEmail()); | ||
}).thenCompose(u -> { | ||
CompletableFuture<User> f = new CompletableFuture<>(); | ||
EXECUTOR_SERVICE.schedule(() -> { | ||
if (u == null || !u.checkPassword(req.getPassword())) { | ||
f.completeExceptionally( | ||
new ResponseStatusException(HttpStatus.NOT_FOUND, "username or password incorrect")); | ||
} else { | ||
createSession(request, response, u, req.isRemember()); | ||
f.complete(u); | ||
} | ||
}, 500 + RANDOM.nextInt(500), TimeUnit.MILLISECONDS); | ||
return f; | ||
}); | ||
} | ||
|
||
@Operation(summary = "Modify user account", description = "This method edits the account.") | ||
|
@@ -289,9 +320,9 @@ public User edit(@NotNull @Parameter(hidden = true) User loggedUser, @RequestBod | |
@Operation(summary = "Register account", description = "This method registers a new account. On success returns newly created user.") | ||
@ApiResponse(responseCode = "200", description = "Newly created user.") | ||
@ApiResponse(responseCode = "403", description = "User is already logged.", content = @Content()) | ||
@ApiResponse(responseCode = "422", description = "Username or email is already taken.", content = @Content()) | ||
@ApiResponse(responseCode = "422", description = "Username or email is already taken or invalid captcha.", content = @Content()) | ||
@PostMapping("/register") | ||
public User register(@Parameter(hidden = true) User loggedUser, @RequestBody RegisterRequest req, | ||
public Future<User> register(@Parameter(hidden = true) User loggedUser, @RequestBody RegisterRequest req, | ||
HttpServletRequest request, HttpServletResponse response) { | ||
if (loggedUser != null) { | ||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "already logged"); | ||
|
@@ -317,31 +348,36 @@ public User register(@Parameter(hidden = true) User loggedUser, @RequestBody Reg | |
if (req.getEmail().indexOf('@') == -1) { | ||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing at sign in email"); | ||
} | ||
if (userRepository.findByUsername(req.getUsername()) != null) { | ||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "this username is already taken"); | ||
} | ||
if (userRepository.findByEmail(req.getEmail()) != null) { | ||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "this email is already taken"); | ||
} | ||
User u = new User(); | ||
u.setEmail(req.getEmail()); | ||
u.setName(req.getName()); | ||
u.setPassword(req.getPassword()); | ||
u.setSurname(req.getSurname()); | ||
u.setUsername(req.getUsername()); | ||
u.setLanguage(req.getLanguage()); | ||
u.setDateFormat(req.getDateFormat()); | ||
u.setCounterSequence(new CounterSequence()); | ||
u = userRepository.save(u); | ||
createSession(request, response, u, false); | ||
SimpleMailMessage msg = new SimpleMailMessage(); | ||
msg.setTo(req.getEmail()); | ||
msg.setFrom("[email protected]"); | ||
// TODO activation link | ||
msg.setSubject("Dziękujemy za rejestrację"); | ||
msg.setText("Cześć, " + req.getName() + "!\nDziękujemy za zarejestrowanie się w naszym serwisie"); | ||
javaMailSender.send(msg); | ||
return u; | ||
return verifyCaptcha(req.getCaptcha(), request).thenApply(action -> { | ||
if (!"register".equals(action)) { | ||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "invalid captcha"); | ||
} | ||
if (userRepository.findByUsername(req.getUsername()) != null) { | ||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "this username is already taken"); | ||
} | ||
if (userRepository.findByEmail(req.getEmail()) != null) { | ||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "this email is already taken"); | ||
} | ||
User u = new User(); | ||
u.setEmail(req.getEmail()); | ||
u.setName(req.getName()); | ||
u.setPassword(req.getPassword()); | ||
u.setSurname(req.getSurname()); | ||
u.setUsername(req.getUsername()); | ||
u.setLanguage(req.getLanguage()); | ||
u.setDateFormat(req.getDateFormat()); | ||
u.setCounterSequence(new CounterSequence()); | ||
u = userRepository.save(u); | ||
createSession(request, response, u, false); | ||
SimpleMailMessage msg = new SimpleMailMessage(); | ||
msg.setTo(req.getEmail()); | ||
msg.setFrom("[email protected]"); | ||
// TODO activation link | ||
msg.setSubject("Dziękujemy za rejestrację"); | ||
msg.setText("Cześć, " + req.getName() + "!\nDziękujemy za zarejestrowanie się w naszym serwisie"); | ||
javaMailSender.send(msg); | ||
return u; | ||
}); | ||
} | ||
|
||
@Operation(summary = "Log out", description = "This method log outs the user.") | ||
|
@@ -476,6 +512,46 @@ private void createSession(HttpServletRequest req, HttpServletResponse resp, Use | |
resp.addCookie(c); | ||
} | ||
|
||
/** | ||
* Verify captcha response | ||
* | ||
* @param response response from recaptcha | ||
* @param request request | ||
* @return action if success or null if failed | ||
*/ | ||
private CompletableFuture<String> verifyCaptcha(String response, HttpServletRequest request) { | ||
String remoteip = request.getHeader("X-Forwarded-For"); | ||
if (remoteip == null) { | ||
remoteip = request.getRemoteAddr(); | ||
} | ||
|
||
HttpClient client = HttpClient.newHttpClient(); | ||
String data = String.format("secret=%s&response=%s&remoteip=%s", | ||
URLEncoder.encode(recaptchaSecret, UTF_8), | ||
URLEncoder.encode(response, UTF_8), | ||
URLEncoder.encode(remoteip, UTF_8)); | ||
|
||
HttpRequest req = HttpRequest.newBuilder(RECAPTCHA_URI) | ||
.header("Content-Type", "application/x-www-form-urlencoded") | ||
.POST(HttpRequest.BodyPublishers.ofString(data)) | ||
.build(); | ||
return client.sendAsync(req, BodyHandlers.ofString()).thenApply(n -> { | ||
if (n.statusCode() != 200) { | ||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Captcha verification failed"); | ||
} | ||
try { | ||
JsonNode node = MAPPER.readTree(n.body()); | ||
if (node.get("success").asBoolean()) { | ||
return node.get("action").asText(); | ||
} | ||
} catch (JsonProcessingException e) { | ||
e.printStackTrace(); | ||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Captcha verification failed"); | ||
} | ||
return null; | ||
}); | ||
} | ||
|
||
@Scheduled(cron = "0 * * * * *") | ||
public void deleteOldAccount() { | ||
Date d = Date.from(Instant.now().minus(7, ChronoUnit.DAYS)); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.