summaryrefslogtreecommitdiff
path: root/pse-server/src/main/java/org/psesquared/server/authentication/api
diff options
context:
space:
mode:
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/authentication/api')
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/controller/AuthenticationController.java251
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ChangePasswordRequest.java15
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/controller/DeviceWrapper.java46
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ForgotPasswordRequest.java13
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/controller/PasswordRequest.java13
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/controller/UserInfoRequest.java16
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/controller/package-info.java13
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/data/access/AuthenticationDao.java62
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/data/access/package-info.java11
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/service/AuthenticationService.java389
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/service/EmailServiceImpl.java222
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/service/EncryptionService.java85
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/service/InputCheckService.java170
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/service/ResourceReader.java33
-rw-r--r--pse-server/src/main/java/org/psesquared/server/authentication/api/service/package-info.java13
15 files changed, 1352 insertions, 0 deletions
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/AuthenticationController.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/AuthenticationController.java
new file mode 100644
index 0000000..f580969
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/AuthenticationController.java
@@ -0,0 +1,251 @@
+package org.psesquared.server.authentication.api.controller;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.List;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import org.psesquared.server.authentication.api.service.AuthenticationService;
+import org.psesquared.server.config.EmailConfigProperties;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+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.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.RestController;
+
+/**
+ * This is a controller class for the Authentication API that handles the
+ * requests from the client concerning login/logout and user account management.
+ */
+@RequestMapping("/api/2")
+@RestController
+@RequiredArgsConstructor
+public class AuthenticationController {
+
+ /**
+ * The name of the HTTP location header.
+ */
+ private static final String LOCATION_HEADER = "Location";
+
+ /**
+ * The service class that this controller calls to further process requests.
+ */
+ private final AuthenticationService authenticationService;
+
+ /**
+ * The properties class that is used to return some externally stored URLs.
+ */
+ private final EmailConfigProperties emailConfigProperties;
+
+ /**
+ * The API-endpoint for registering a new
+ * {@link org.psesquared.server.model.User} with a username, email address and
+ * password. In order for the account to be used, the registration process
+ * must be concluded with the verification of the email address. For this an
+ * email with a link for verification is sent to {@code userInfo.email()}.
+ *
+ * @param userInfo The request-wrapper containing username, email and
+ * password.
+ * @return {@link HttpStatus#OK} on success, <br>
+ * {@link HttpStatus#BAD_REQUEST} for invalid user information
+ * @see AuthenticationService#registerUser(UserInfoRequest)
+ */
+ @PostMapping("/auth/register.json")
+ public ResponseEntity<String> registerUser(
+ @RequestBody final UserInfoRequest userInfo) {
+ return new ResponseEntity<>(authenticationService.registerUser(userInfo));
+ }
+
+ /**
+ * The API-endpoint for verifying a newly created
+ * {@link org.psesquared.server.model.User}. This method is invoked via the
+ * link in the verification email that is sent in
+ * {@link #registerUser(UserInfoRequest)}.
+ * On success, it transfers the user to the dashboard and on failure it sets
+ * the following status codes:
+ * {@link HttpStatus#OK} on success, <br>
+ * {@link HttpStatus#BAD_REQUEST} user exists and is already verified,
+ * <br>
+ * {@link HttpStatus#UNAUTHORIZED} invalid token, <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ *
+ * @param username The username of the user that needs to be verified
+ * @param token The JWT that indicates the authority of the request
+ * @param response The {@link HttpServletResponse} for setting up a
+ * redirection to the frontend
+ * @see AuthenticationService#verifyRegistration(String, String)
+ */
+ @GetMapping("/auth/{username}/verify.json")
+ public void verifyRegistration(
+ @PathVariable final String username,
+ @RequestParam("token") final String token,
+ @NonNull final HttpServletResponse response) {
+ HttpStatus status
+ = authenticationService.verifyRegistration(username, token);
+ if (status.equals(HttpStatus.OK)) {
+ response.setHeader(LOCATION_HEADER,
+ emailConfigProperties.dashboardBaseUrl());
+ response.setStatus(HttpStatus.FOUND.value());
+ } else {
+ response.setStatus(status.value());
+ }
+ }
+
+ /**
+ * The API-endpoint for setting a JWT access token with a lifespan of one hour
+ * as the "sessionid" cookie for authorization with further requests. <br>
+ * (This is a secured endpoint requiring authorization via HTTP basic or JWT.)
+ *
+ * @param username The username of the user who wants to log in
+ * @param response The {@link HttpServletResponse} for setting the "sessionid"
+ * cookie
+ * @return {@link HttpStatus#OK} on success, <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ * @see AuthenticationService#login(String, HttpServletResponse)
+ */
+ @PostMapping("/auth/{username}/login.json")
+ public ResponseEntity<String> login(
+ @PathVariable final String username,
+ @NonNull final HttpServletResponse response) {
+ return new ResponseEntity<>(
+ authenticationService.login(username, response));
+ }
+
+ /**
+ * The API-endpoint for invalidating the "sessionid" cookie containing a JWT
+ * access token. Following authorized requests require HTTP basic
+ * authentication or a new login. <br>
+ * (This is a secured endpoint requiring authorization via HTTP basic or JWT.)
+ *
+ * @param username The username of the user who wants to log out
+ * @param response The {@link HttpServletResponse} for invalidating the
+ * "sessionid" cookie
+ * @return {@link HttpStatus#OK} on success, <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ * @see AuthenticationService#logout(String, HttpServletResponse)
+ */
+ @PostMapping("/auth/{username}/logout.json")
+ public ResponseEntity<String> logout(
+ @PathVariable final String username,
+ @NonNull final HttpServletResponse response) {
+ return new ResponseEntity<>(
+ authenticationService.logout(username, response));
+ }
+
+ /**
+ * The API-endpoint for sending an email to the given address
+ * ({@link ForgotPasswordRequest#email()} with an url to reset the password of
+ * the user with that email address.
+ *
+ * @param email The email address of the user who wants to reset their
+ * password
+ * @return {@link HttpStatus#OK} on success, <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ * @see AuthenticationService#forgotPassword(String)
+ */
+ @PostMapping("/auth/{email}/forgot.json")
+ public ResponseEntity<String> forgotPassword(
+ @PathVariable final String email) {
+ return new ResponseEntity<>(authenticationService.forgotPassword(email));
+ }
+
+ /**
+ * The API-endpoint for resetting the password of a
+ * {@link org.psesquared.server.model.User}. This method is invoked via the
+ * link in the verification email that is sent in
+ * {@link #forgotPassword(String)}.
+ *
+ * @param username The username of the user who wants to reset their
+ * password
+ * @param token The JWT that indicates the authority of the request
+ * @param requestBody The request-wrapper containing the new password
+ * @return {@link HttpStatus#OK} on success, <br>
+ * {@link HttpStatus#BAD_REQUEST} password doesn't meet requirements,
+ * <br>
+ * {@link HttpStatus#UNAUTHORIZED} invalid token, <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ * @see AuthenticationService#resetPassword(String, String, PasswordRequest)
+ */
+ @PutMapping("/auth/{username}/resetpassword.json")
+ public ResponseEntity<String> resetPassword(
+ @PathVariable final String username,
+ @RequestParam("token") final String token,
+ @RequestBody final PasswordRequest requestBody) {
+ return new ResponseEntity<>(
+ authenticationService.resetPassword(username, token, requestBody));
+ }
+
+ /**
+ * The API-endpoint for changing the password of a
+ * {@link org.psesquared.server.model.User}, who is logged-in in the
+ * dashboard.
+ * (This is a secured endpoint requiring authorization via HTTP basic or JWT.)
+ *
+ * @param username The username of the user who wants to change their
+ * password
+ * @param requestBody The request-wrapper containing old and new password
+ * @return {@link HttpStatus#OK} on success, <br>
+ * {@link HttpStatus#BAD_REQUEST} old password is wrong, <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ * @see AuthenticationService#changePassword(String, ChangePasswordRequest)
+ */
+ @PutMapping("/auth/{username}/changepassword.json")
+ public ResponseEntity<String> changePassword(
+ @PathVariable final String username,
+ @RequestBody final ChangePasswordRequest requestBody) {
+ return new ResponseEntity<>(
+ authenticationService.changePassword(username, requestBody));
+ }
+
+ /**
+ * The API-endpoint for deleting a {@link org.psesquared.server.model.User}.
+ * This action is performed by a logged-in user from the dashboard.
+ * The user must enter their password ({@link PasswordRequest#password()})
+ * and if correct, the user along with all associated data is deleted.
+ * (This is a secured endpoint requiring authorization via HTTP basic or JWT.)
+ *
+ * @param username The username of the user who wants to delete their
+ * account
+ * @param requestBody The request-wrapper containing the user's password
+ * @return {@link HttpStatus#OK} on success, <br>
+ * {@link HttpStatus#BAD_REQUEST} wrong password, <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ * @see AuthenticationService#deleteUser(String, PasswordRequest)
+ */
+ @DeleteMapping("/auth/{username}/delete.json")
+ public ResponseEntity<String> deleteUser(
+ @PathVariable final String username,
+ @RequestBody final PasswordRequest requestBody) {
+ return new ResponseEntity<>(
+ authenticationService.deleteUser(username, requestBody));
+ }
+
+ /**
+ * This API-endpoint exists for compatibility with podcatchers, especially
+ * AntennaPod and Kasts, which initially call this endpoint instead of
+ * {@link #login(String, HttpServletResponse)}.
+ * Accordingly, a call to this endpoint is internally treated as a login.
+ * In particular, devices remain unsupported.
+ *
+ * @param username The username of the user to be synchronized
+ * @param response The {@link HttpServletResponse} for setting the "sessionid"
+ * cookie
+ * @return A dummy response with a single dummy device for the given user
+ * @see AuthenticationService#login(String, HttpServletResponse)
+ */
+ @GetMapping("/devices/{username}.json")
+ public ResponseEntity<List<DeviceWrapper>> getDeviceList(
+ @PathVariable final String username,
+ @NonNull final HttpServletResponse response) {
+ DeviceWrapper dummyDevice = new DeviceWrapper();
+ return new ResponseEntity<>(
+ List.of(dummyDevice),
+ authenticationService.login(username, response));
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ChangePasswordRequest.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ChangePasswordRequest.java
new file mode 100644
index 0000000..d8b2357
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ChangePasswordRequest.java
@@ -0,0 +1,15 @@
+package org.psesquared.server.authentication.api.controller;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A request for changing the password containing the old, i.e. current, and new
+ * password.
+ *
+ * @param oldPassword The user's current password
+ * @param newPassword The new password
+ */
+public record ChangePasswordRequest(
+ @JsonProperty(value = "password", required = true) String oldPassword,
+ @JsonProperty(value = "new_password", required = true) String newPassword) {
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/DeviceWrapper.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/DeviceWrapper.java
new file mode 100644
index 0000000..35dae3d
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/DeviceWrapper.java
@@ -0,0 +1,46 @@
+package org.psesquared.server.authentication.api.controller;
+
+/**
+ * This record wraps a dummy device that is required to be returned by <br>
+ * {@link AuthenticationController#getDeviceList(String,
+ * jakarta.servlet.http.HttpServletResponse)}.
+ *
+ * @param id The device id
+ * @param caption The caption, i.e. name, of the device
+ * @param type The device type
+ * @param subscriptions The number of subscriptions of the device
+ */
+public record DeviceWrapper(
+ String id,
+ String caption,
+ String type,
+ int subscriptions) {
+
+ /**
+ * The id of the dummy device.
+ */
+ private static final String DUMMY_ID = "dummy";
+
+ /**
+ * The name of the dummy device.
+ */
+ private static final String DUMMY_DEVICE = "device";
+
+ /**
+ * The type of the dummy device.
+ */
+ private static final String DUMMY_TYPE = "other";
+
+ /**
+ * The number of subscriptions of the dummy device.
+ */
+ private static final int DUMMY_SUBSCRIPTIONS = 0;
+
+ /**
+ * The no-args-constructor for a device-wrapper containing a dummy device.
+ */
+ public DeviceWrapper() {
+ this(DUMMY_ID, DUMMY_DEVICE, DUMMY_TYPE, DUMMY_SUBSCRIPTIONS);
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ForgotPasswordRequest.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ForgotPasswordRequest.java
new file mode 100644
index 0000000..700fb08
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/ForgotPasswordRequest.java
@@ -0,0 +1,13 @@
+package org.psesquared.server.authentication.api.controller;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A request for sending an email with a link for resetting a user's password to
+ * the user's {@link #email} address.
+ *
+ * @param email The email address
+ */
+public record ForgotPasswordRequest(
+ @JsonProperty(required = true) String email) {
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/PasswordRequest.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/PasswordRequest.java
new file mode 100644
index 0000000..f772c7b
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/PasswordRequest.java
@@ -0,0 +1,13 @@
+package org.psesquared.server.authentication.api.controller;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A request that contains a {@link #password}, which is either set as a new
+ * password or used for confirming the deletion of an account.
+ *
+ * @param password The password
+ */
+public record PasswordRequest(
+ @JsonProperty(required = true) String password) {
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/UserInfoRequest.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/UserInfoRequest.java
new file mode 100644
index 0000000..9c112b1
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/UserInfoRequest.java
@@ -0,0 +1,16 @@
+package org.psesquared.server.authentication.api.controller;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A request that contains the {@link #username}, {@link #email} address and
+ * {@link #password} for registering a new user.
+ *
+ * @param username The username
+ * @param email The email
+ * @param password The password
+ */
+public record UserInfoRequest(@JsonProperty(required = true) String username,
+ @JsonProperty(required = true) String email,
+ @JsonProperty(required = true) String password) {
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/package-info.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/package-info.java
new file mode 100644
index 0000000..79ae33d
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/controller/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * This package represents the highest logical layer of the authentication API
+ * ({@link org.psesquared.server.authentication.api}) - the controller layer.
+ * <br>
+ * It contains the
+ * {@link
+ * org.psesquared.server.authentication.api.controller.AuthenticationController}
+ * along with a series of wrapper classes for JSON request and response bodies.
+ *
+ * @author PSE-Squared Team
+ * @version 1.0
+ */
+package org.psesquared.server.authentication.api.controller;
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/data/access/AuthenticationDao.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/data/access/AuthenticationDao.java
new file mode 100644
index 0000000..2073633
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/data/access/AuthenticationDao.java
@@ -0,0 +1,62 @@
+package org.psesquared.server.authentication.api.data.access;
+
+import java.util.Optional;
+import org.psesquared.server.model.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * This JPA repository manages all database transactions by automatically
+ * implementing the logic behind custom queries via method naming convention.
+ */
+@Repository
+public interface AuthenticationDao extends JpaRepository<User, Long> {
+
+ /**
+ * Checks if a user exists via their username.
+ *
+ * @param username The username
+ * @return {@code true} if the user with the given username exists, <br>
+ * {@code false} otherwise
+ */
+ boolean existsByUsername(String username);
+
+ /**
+ * Finds the {@link User} with the given username if present.
+ *
+ * @param username The username of the user that is being searched for
+ * @return An {@link Optional} containing the user with the given
+ * username if present
+ */
+ Optional<User> findByUsername(String username);
+
+ /**
+ * Finds the {@link User} with the given email address if present.
+ *
+ * @param email The email address of the user that is being searched for
+ * @return An {@link Optional} containing the user with the given email
+ * address if present
+ */
+ Optional<User> findByEmail(String email);
+
+ /**
+ * Finds a {@link User} with the given username if present or with the
+ * given email address otherwise.
+ *
+ * @param username The username of the user that is being searched for
+ * @param email The email address of the user that is being searched for
+ * @return An {@link Optional} containing the user with the given username
+ * or email address if present
+ */
+ Optional<User> findByUsernameOrEmail(String username, String email);
+
+ /**
+ * Deletes all users that haven't been verified yet and have registered
+ * before the time specified by the given timestamp.
+ *
+ * @param timestamp The timestamp representing the number of seconds from
+ * the epoch of 1970-01-01T00:00:00Z.
+ */
+ void deleteAllByEnabledFalseAndCreatedAtLessThan(long timestamp);
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/data/access/package-info.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/data/access/package-info.java
new file mode 100644
index 0000000..1b20cab
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/data/access/package-info.java
@@ -0,0 +1,11 @@
+/**
+ * This package represents the lowest logical layer of the authentication API
+ * ({@link org.psesquared.server.authentication.api}) - the data-access layer.
+ * <br>
+ * It features the interface {@link
+ * org.psesquared.server.authentication.api.data.access.AuthenticationDao}.
+ *
+ * @author PSE-Squared Team
+ * @version 1.0
+ */
+package org.psesquared.server.authentication.api.data.access;
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/service/AuthenticationService.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/AuthenticationService.java
new file mode 100644
index 0000000..e21c3fc
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/AuthenticationService.java
@@ -0,0 +1,389 @@
+package org.psesquared.server.authentication.api.service;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletResponse;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.NoSuchElementException;
+import lombok.RequiredArgsConstructor;
+import org.psesquared.server.authentication.api.controller.ChangePasswordRequest;
+import org.psesquared.server.authentication.api.controller.PasswordRequest;
+import org.psesquared.server.authentication.api.controller.UserInfoRequest;
+import org.psesquared.server.authentication.api.data.access.AuthenticationDao;
+import org.psesquared.server.config.JwtService;
+import org.psesquared.server.model.Role;
+import org.psesquared.server.model.User;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * This service class manages all business logic associated with the
+ * authentication API.
+ * <br>
+ * It is called from the
+ * {@link
+ * org.psesquared.server.authentication.api.controller.AuthenticationController}
+ * and passes on requests concerning data access to the
+ * {@link AuthenticationDao}.
+ */
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class AuthenticationService {
+
+ /**
+ * A {@link User} is not enabled until verification.
+ */
+ private static final boolean ENABLED_DEFAULT = false;
+
+ /**
+ * A {@link User} becomes enabled after verification.
+ */
+ private static final boolean VERIFIED = true;
+
+ /**
+ * The age of an expired cookie.
+ */
+ private static final int EXPIRED_AGE = 0;
+
+ /**
+ * The name of the cookie used by podcatchers for authentication.
+ * If a {@link User} has logged in, this cookie holds the JWT.
+ */
+ private static final String COOKIE_NAME = "sessionid";
+
+ /**
+ * Specifies that the cookie should be sent to all URLs.
+ */
+ private static final String COOKIE_PATH_GLOBAL = "/";
+
+ /**
+ * The default role of a {@link User} is {@link Role#USER}.
+ */
+ private static final Role DEFAULT_USER = Role.USER;
+
+ /**
+ * The JPA repository that handles all user related database requests.
+ */
+ private final AuthenticationDao authenticationDao;
+
+ /**
+ * The class used for the encryption of passwords.
+ */
+ private final PasswordEncoder passwordEncoder;
+
+ /**
+ * The service class used for managing JWTs.
+ */
+ private final JwtService jwtService;
+
+ /**
+ * The service class used for sending emails.
+ */
+ private final EmailServiceImpl emailService;
+
+ /**
+ * The service class used for encrypting email addresses.
+ */
+ private final EncryptionService encryptionService;
+
+ /**
+ * The service class used for checking if the given user information meets
+ * the specified requirements.
+ */
+ private final InputCheckService inputCheckService;
+
+ /**
+ * This method is invoked by the register method of the authentication
+ * controller.
+ * <br>
+ * 1. Checks if the given user information meets the requirements.
+ * <br>
+ * 2. Checks if no user with the given username already exists (if so,
+ * and email as well as password match, the verification email is sent again).
+ * <br>
+ * 3. Creates a user with the given information and sends verification email.
+ *
+ * @param userInfo The wrapper object containing username, email and password
+ * @return @return {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#BAD_REQUEST} for invalid user information
+ */
+ public HttpStatus registerUser(final UserInfoRequest userInfo) {
+ if (inputCheckService.checkUsernameInvalid(userInfo.username())
+ || inputCheckService.checkEmailInvalid(userInfo.email())
+ || inputCheckService.checkPasswordInvalid(userInfo.password())) {
+ return HttpStatus.BAD_REQUEST;
+ }
+
+ final String encryptedEmailFromRequest
+ = encryptionService.saltAndHashEmail(userInfo.email());
+ User user;
+
+ try {
+ user = authenticationDao
+ .findByUsernameOrEmail(userInfo.username(), encryptedEmailFromRequest)
+ .orElseThrow();
+
+ if (user.isEnabled()
+ || !user.getEmail().equals(encryptedEmailFromRequest)
+ || !user.getUsername().equals(userInfo.username())
+ || !passwordEncoder
+ .matches(userInfo.password(), user.getPassword())) {
+ return HttpStatus.BAD_REQUEST;
+ }
+
+ user.setCreatedAt(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC));
+
+ } catch (NoSuchElementException e) {
+ user = User.builder()
+ .username(userInfo.username())
+ .email(encryptionService.saltAndHashEmail(userInfo.email()))
+ .password(passwordEncoder.encode(userInfo.password()))
+ .enabled(ENABLED_DEFAULT)
+ .createdAt(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC))
+ .role(DEFAULT_USER)
+ .build();
+ authenticationDao.save(user);
+ }
+ emailService.sendVerification(userInfo.email(), user);
+ return HttpStatus.OK;
+ }
+
+ /**
+ * This method is invoked by the verifyRegistration method of the
+ * authentication controller.
+ * <br>
+ * If a not yet verified {@link User} with the given username exists,
+ * this user is verified via {@link User#setEnabled(boolean)}.
+ *
+ * @param username The username of the to be verified user
+ * @param token The JWT for authentication
+ * @return {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#BAD_REQUEST} user exists and is already verified,
+ * <br>
+ * {@link HttpStatus#UNAUTHORIZED} invalid token,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ */
+ public HttpStatus verifyRegistration(final String username,
+ final String token) {
+ try {
+ var user = authenticationDao.findByUsername(username)
+ .orElseThrow();
+
+ if (user.isEnabled()) {
+ return HttpStatus.BAD_REQUEST;
+ }
+
+ if (!jwtService.isUrlTokenValid(token, user)) {
+ return HttpStatus.UNAUTHORIZED;
+ }
+
+ user.setEnabled(VERIFIED);
+ return HttpStatus.OK;
+ } catch (NoSuchElementException e) {
+ return HttpStatus.NOT_FOUND;
+ }
+ }
+
+ /**
+ * This method is invoked by the login method of the authentication
+ * controller.
+ * <br>
+ * Sets the "sessionid" cookie with a valid JWT for further authentication.
+ *
+ * @param username The username of the user who wants to log in
+ * @param response The {@link HttpServletResponse} for setting the "sessionid"
+ * cookie
+ * @return {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ */
+ public HttpStatus login(final String username,
+ final HttpServletResponse response) {
+ try {
+ var user = authenticationDao.findByUsername(username)
+ .orElseThrow();
+ var token = jwtService.generateAccessTokenString(user);
+ Cookie cookie = new Cookie(COOKIE_NAME, token);
+ cookie.setPath(COOKIE_PATH_GLOBAL);
+ response.addCookie(cookie);
+ return HttpStatus.OK;
+ } catch (NoSuchElementException e) {
+ return HttpStatus.NOT_FOUND;
+ }
+ }
+
+ /**
+ * This method is invoked by the logout method of the authentication
+ * controller.
+ * <br>
+ * Invalidates the "sessionid" cookie.
+ * Thus, for further authentication until the next login only HTTP basic
+ * authentication (and no JWT authentication) is possible.
+ *
+ * @param username The username of the user who wants to log out
+ * @param response The {@link HttpServletResponse} for invalidating the
+ * "sessionid" cookie
+ * @return {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ */
+ public HttpStatus logout(final String username,
+ final HttpServletResponse response) {
+ if (authenticationDao.existsByUsername(username)) {
+ Cookie cookie = new Cookie(COOKIE_NAME, null);
+ cookie.setMaxAge(EXPIRED_AGE);
+ cookie.setPath(COOKIE_PATH_GLOBAL);
+ response.addCookie(cookie);
+ return HttpStatus.OK;
+ }
+ return HttpStatus.NOT_FOUND;
+ }
+
+ /**
+ * This method is invoked by the forgotPassword method of the authentication
+ * controller.
+ * <br>
+ * Sends an email with a link to reset the password to the given email
+ * address, if a user with this email address exists.
+ *
+ * @param email The email address of the user who wants to reset their
+ * password
+ * @return {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ */
+ public HttpStatus forgotPassword(final String email) {
+ try {
+ var user = authenticationDao
+ .findByEmail(encryptionService.saltAndHashEmail(email))
+ .orElseThrow();
+ emailService.sendPasswordReset(email, user);
+ return HttpStatus.OK;
+ } catch (NoSuchElementException e) {
+ return HttpStatus.NOT_FOUND;
+ }
+ }
+
+ /**
+ * This method is invoked by the resetPassword method of the authentication
+ * controller.
+ * <br>
+ * Sets a new password for the given user who has forgotten their
+ * password if the JWT is valid.
+ *
+ * @param username The username of the user who wants to reset their
+ * password
+ * @param token The JWT for authentication
+ * @param requestBody The request-wrapper containing the new password
+ * @return {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#BAD_REQUEST} password doesn't meet requirements,
+ * <br>
+ * {@link HttpStatus#UNAUTHORIZED} invalid token,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ */
+ public HttpStatus resetPassword(final String username,
+ final String token,
+ final PasswordRequest requestBody) {
+ if (inputCheckService.checkPasswordInvalid(requestBody.password())) {
+ return HttpStatus.BAD_REQUEST;
+ }
+ try {
+ var user = authenticationDao.findByUsername(username)
+ .orElseThrow();
+ if (jwtService.isUrlTokenValid(token, user)) {
+ user.setPassword(passwordEncoder.encode(requestBody.password()));
+ return HttpStatus.OK;
+ }
+ return HttpStatus.UNAUTHORIZED;
+ } catch (NoSuchElementException e) {
+ return HttpStatus.NOT_FOUND;
+ }
+ }
+
+ /**
+ * This method is invoked by the changePassword method of the authentication
+ * controller.
+ * <br>
+ * Changes the password of a logged-in user.
+ *
+ * @param username The username of the user who wants to change their
+ * password
+ * @param requestBody The request-wrapper containing old and new password
+ * @return {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#BAD_REQUEST} old password is wrong, or new
+ * password doesn't meet requirements,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ */
+ public HttpStatus changePassword(final String username,
+ final ChangePasswordRequest requestBody) {
+ if (inputCheckService.checkPasswordInvalid(requestBody.newPassword())) {
+ return HttpStatus.BAD_REQUEST;
+ }
+ try {
+ var user = authenticationDao.findByUsername(username)
+ .orElseThrow();
+ if (passwordEncoder
+ .matches(requestBody.oldPassword(), user.getPassword())) {
+ user.setPassword(passwordEncoder.encode(requestBody.newPassword()));
+ return HttpStatus.OK;
+ }
+ return HttpStatus.BAD_REQUEST;
+ } catch (NoSuchElementException e) {
+ return HttpStatus.NOT_FOUND;
+ }
+ }
+
+ /**
+ * This method is invoked by the deleteUser method of the authentication
+ * controller.
+ * <br>
+ * Deletes the user with the given username if existent and if the given
+ * password for confirmation is correct.
+ *
+ * @param username The username of the user who wants to delete their account
+ * @param requestBody The request-wrapper containing the password for
+ * confirmation
+ * @return {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#BAD_REQUEST} wrong password,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ */
+ public HttpStatus deleteUser(final String username,
+ final PasswordRequest requestBody) {
+ try {
+ var user = authenticationDao.findByUsername(username)
+ .orElseThrow();
+ if (passwordEncoder.matches(requestBody.password(), user.getPassword())) {
+ authenticationDao.delete(user);
+ return HttpStatus.OK;
+ }
+ return HttpStatus.BAD_REQUEST;
+ } catch (NoSuchElementException e) {
+ return HttpStatus.NOT_FOUND;
+ }
+ }
+
+ /**
+ * This method is invoked by {@link org.psesquared.server.util.Scheduler}
+ * for cleaning the database from expired {@link User}s.
+ *
+ * @param timestamp The timestamp representing the number of seconds from
+ * the epoch of 1970-01-01T00:00:00Z.
+ * @see AuthenticationDao#deleteAllByEnabledFalseAndCreatedAtLessThan(long)
+ */
+ public void deleteInvalidUsersOlderThan(final long timestamp) {
+ authenticationDao.deleteAllByEnabledFalseAndCreatedAtLessThan(timestamp);
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/service/EmailServiceImpl.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/EmailServiceImpl.java
new file mode 100644
index 0000000..c752286
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/EmailServiceImpl.java
@@ -0,0 +1,222 @@
+package org.psesquared.server.authentication.api.service;
+
+import lombok.RequiredArgsConstructor;
+import org.psesquared.server.config.EmailConfigProperties;
+import org.psesquared.server.config.JwtService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+/**
+ * This service class is responsible for sending emails to
+ * {@link org.springframework.security.core.userdetails.User}s.
+ *
+ * @see JavaMailSender
+ */
+@Service
+@RequiredArgsConstructor
+public class EmailServiceImpl {
+
+ /**
+ * The {@link JavaMailSender} used for the sending of emails.
+ */
+ private final JavaMailSender emailSender;
+
+ /**
+ * The properties class that is used to return some externally stored URLs.
+ */
+ private final EmailConfigProperties emailConfigProperties;
+
+ /**
+ * The service class for managing the JWTs that are sent via email.
+ */
+ private final JwtService jwtService;
+
+ /**
+ * The email address from which the emails are sent.
+ */
+ @Value("${spring.mail.username}")
+ private String sender;
+
+ /**
+ * The subject of the email that is sent for account verification.
+ */
+ private static final String VERIFICATION_MAIL_SUBJECT
+ = "Bestätige deine E-Mail-Adresse für unseren"
+ + " Podcast-Synchronisations-Server | Validate your Mail";
+
+ /**
+ * The subject of the email that is sent for resetting the password of a user.
+ */
+ private static final String PASSWORD_RESET_MAIL_SUBJECT
+ = "Setze dein Passwort für unseren Podcast-Synchronisation-Server"
+ + " zurück! | Reset Password";
+
+ /**
+ * The placeholder for the username.
+ */
+ private static final String USERNAME_MAIL_PLACEHOLDER = "username";
+
+ /**
+ * The placeholder for the verification URL.
+ */
+ private static final String VERIFICATION_MAIL_PLACEHOLDER
+ = "verificationURL";
+
+ /**
+ * The placeholder for the URL for resetting the password of a user.
+ */
+ private static final String PASSWORD_RESET_MAIL_PLACEHOLDER
+ = "passwordResetURL";
+
+ /**
+ * The question mark symbol announcing a URL query parameter.
+ */
+ private static final String URL_QUERY_PARAM = "?";
+
+ /**
+ * The format of the username URL query parameter.
+ */
+ private static final String USERNAME_PARAM = "username=";
+
+ /**
+ * The separator for URL query parameters.
+ */
+ private static final String PARAM_SEPARATOR = "&";
+
+ /**
+ * The format of the token URL query parameter.
+ */
+ private static final String TOKEN_PARAM = "token=";
+
+ /**
+ * The contents of the verification URL with placeholders read from an
+ * external file.
+ */
+ @Value("#{T(org.psesquared.server.authentication.api.service"
+ + ".ResourceReader).readFileToString('VerificationMail.txt')}")
+ private String verificationMailText;
+
+ /**
+ * The contents of the URL for resetting the password of a user with
+ * placeholders read from an external file.
+ */
+ @Value("#{T(org.psesquared.server.authentication.api.service"
+ + ".ResourceReader).readFileToString('PasswordResetMail.txt')}")
+ private String passwordResetMailText;
+
+ /**
+ * Sends a generic email to a user enabling him/her to perform a certain
+ * action when clicking on the contained url.
+ * This method uses a template which lies at resources and contains a
+ * "verificationURL"-placeholder, which is replaced by the url.
+ *
+ * @param to Recipients email address
+ * @param mailSubject Subject of the email
+ * @param body Body of the email
+ */
+ private void sendMail(final String to,
+ final String mailSubject,
+ final String body) {
+ // send simple mail message with credential from application.properties
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom(sender);
+ message.setTo(to);
+ message.setSubject(mailSubject);
+ message.setText(body);
+ emailSender.send(message);
+ }
+
+ /**
+ * Substitutes username and URL placeholders in email template.
+ *
+ * @param template The email template with placeholders
+ * @param user The name of the user
+ * @param url The URL with the JWT for request authentication
+ * @return The email text with the actual username and URL
+ */
+ private String substitutePlaceholders(final String template,
+ final UserDetails user,
+ final String url) {
+ return template
+ .replace(USERNAME_MAIL_PLACEHOLDER, user.getUsername())
+ .replace(VERIFICATION_MAIL_PLACEHOLDER, url)
+ .replace(PASSWORD_RESET_MAIL_PLACEHOLDER, url);
+ }
+
+ /**
+ * Generates the URL for verifying the account of a
+ * {@link org.springframework.security.core.userdetails.User} containing
+ * a JWT for authentication.
+ *
+ * @param userDetails The user details of the user who wants to verify their
+ * account
+ * @return The URL for verifying the user's account
+ */
+ private String generateVerificationUrlString(final UserDetails userDetails) {
+ String token = jwtService.generateUrlTokenString(userDetails);
+ String verificationUrl
+ = String.format(emailConfigProperties.verificationUrl(),
+ userDetails.getUsername());
+
+ return verificationUrl + URL_QUERY_PARAM + TOKEN_PARAM + token;
+ }
+
+ /**
+ * Generates the URL for resetting the password of a
+ * {@link org.springframework.security.core.userdetails.User} containing
+ * a JWT for authentication.
+ *
+ * @param userDetails The user details of the user who wants to reset their
+ * password
+ * @return The URL for resetting the user's password
+ */
+ private String generatePasswordResetUrlString(final UserDetails userDetails) {
+ final String token = jwtService.generateUrlTokenString(userDetails);
+ return emailConfigProperties.dashboardBaseUrl()
+ + emailConfigProperties.resetUrlPath()
+ + URL_QUERY_PARAM
+ + USERNAME_PARAM
+ + userDetails.getUsername()
+ + PARAM_SEPARATOR
+ + TOKEN_PARAM
+ + token;
+ }
+
+ /**
+ * Sends a validation E-Mail to validate a user account by clicking on the
+ * given URL.
+ * It uses a template which lies at resources/ValidationMail.txt and contains
+ * a "validationURL"-placeholder.
+ *
+ * @param to The email address of the user who wants to verify their account
+ * @param userDetails The user details of that user
+ */
+ public void sendVerification(final String to, final UserDetails userDetails) {
+ final String url = generateVerificationUrlString(userDetails);
+ String mailText
+ = substitutePlaceholders(verificationMailText, userDetails, url);
+
+ sendMail(to, VERIFICATION_MAIL_SUBJECT, mailText);
+ }
+
+ /**
+ * Sends a password-reset E-Mail to a user with a URL which lets the user
+ * change his/her password.
+ * It uses a template which lies at resources/PasswordResetMail.txt and
+ * contains a "passwordResetURL"-placeholder.
+ *
+ * @param to The email address of the user who wants to reset their password
+ * @param userDetails The user details of that user
+ */
+ public void sendPasswordReset(final String to,
+ final UserDetails userDetails) {
+ final String url = generatePasswordResetUrlString(userDetails);
+ String mailText
+ = substitutePlaceholders(passwordResetMailText, userDetails, url);
+
+ sendMail(to, PASSWORD_RESET_MAIL_SUBJECT, mailText);
+ }
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/service/EncryptionService.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/EncryptionService.java
new file mode 100644
index 0000000..f9cb68c
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/EncryptionService.java
@@ -0,0 +1,85 @@
+package org.psesquared.server.authentication.api.service;
+
+import io.jsonwebtoken.io.Decoders;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import lombok.RequiredArgsConstructor;
+import org.psesquared.server.config.SecurityConfigProperties;
+import org.springframework.stereotype.Service;
+
+/**
+ * The service class responsible for encrypting the email addresses of
+ * {@link org.psesquared.server.model.User}s.
+ */
+@Service
+@RequiredArgsConstructor
+public class EncryptionService {
+
+ /**
+ * The mask for a byte.
+ */
+ private static final int BYTE_MASK = 0xff;
+
+ /**
+ * The value added to the byte before conversion to String.
+ */
+ private static final int ADDITION = 0x100;
+
+ /**
+ * The hexadecimal radix.
+ */
+ private static final int RADIX = 16;
+
+ /**
+ * The index specifying the starting index for the substring method.
+ */
+ private static final int BEGIN_INDEX = 1;
+
+ /**
+ * The name of the hashing algorithm.
+ */
+ private static final String SHA_512_ALGORITHM_NAME = "SHA-512";
+
+ /**
+ * The properties class that is used to return externally stored secret salt.
+ */
+ private final SecurityConfigProperties securityConfigProperties;
+
+ /**
+ * Encrypts a given email address by salting it with a fixed salt and hashing
+ * it afterwards.
+ *
+ * @param email The email address that needs to be salted and hashed
+ * @return The salted and hashed email address
+ */
+ public String saltAndHashEmail(final String email) {
+ String generatedEmail = null;
+ try {
+ MessageDigest md = MessageDigest.getInstance(SHA_512_ALGORITHM_NAME);
+ md.update(getSalt());
+ byte[] bytes = md.digest(email.getBytes(StandardCharsets.UTF_8));
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(Integer
+ .toString((b & BYTE_MASK) + ADDITION, RADIX)
+ .substring(BEGIN_INDEX));
+ }
+ generatedEmail = sb.toString();
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ return generatedEmail;
+ }
+
+ /**
+ * Returns the salt for encrypting email addresses in the form of
+ * base64-decoded bytes of a locally stored secret signing key.
+ *
+ * @return {@code byte[]} containing the salt
+ */
+ private byte[] getSalt() {
+ return Decoders.BASE64.decode(securityConfigProperties.emailSigningKey());
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/service/InputCheckService.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/InputCheckService.java
new file mode 100644
index 0000000..be74a88
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/InputCheckService.java
@@ -0,0 +1,170 @@
+package org.psesquared.server.authentication.api.service;
+
+import jakarta.mail.internet.AddressException;
+import jakarta.mail.internet.InternetAddress;
+import java.util.regex.Pattern;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+/**
+ * The service class responsible for checking if user information (i.e.
+ * username, email address and password) meets the specified requirements.
+ */
+@Service
+@RequiredArgsConstructor
+public class InputCheckService {
+
+ /**
+ * The strict boolean for
+ * {@link InternetAddress#InternetAddress(String, boolean)}.
+ */
+ private static final boolean STRICT = true;
+
+ /**
+ * The return value for valid user information.
+ */
+ private static final boolean VALID = false;
+
+ /**
+ * The return value for invalid user information.
+ */
+ private static final boolean INVALID = true;
+
+ /**
+ * Asserts position at start of a line.
+ */
+ private static final String REGEX_START = "^";
+
+ /**
+ * Matches any word character (equivalent to [a-zA-Z0-9_]) character '-'
+ * between 1 and 255 times.
+ */
+ private static final String USERNAME_REGEX_GROUP = "[\\w\\u002d]{1,255}";
+
+ /**
+ * Asserts that the password contains at least one digit.
+ */
+ private static final String PW_REGEX_GROUP1 = "(?=.*\\d)";
+
+ /**
+ * Asserts that the password contains at least one lower case character.
+ */
+ private static final String PW_REGEX_GROUP2 = "(?=.*[a-z])";
+
+ /**
+ * Asserts that the password contains at least one upper case character.
+ */
+ private static final String PW_REGEX_GROUP3 = "(?=.*[A-Z])";
+
+ /**
+ * Asserts that the password contains at least one special character from
+ * the list [€°§´] or the Punct script extension.
+ */
+ private static final String PW_REGEX_GROUP4 = "(?=.*[\\p{Punct}€°§´])";
+
+ /**
+ * Asserts that the password contains only word characters
+ * (equivalent to [a-zA-Z0-9_]) and the special characters specified in
+ * {@link InputCheckService#PW_REGEX_GROUP4}.
+ */
+ private static final String PW_REGEX_GROUP5 = "[\\w\\p{Punct}€°§´]{8,255}";
+
+ /**
+ * Asserts position at the end of a line.
+ */
+ private static final String REGEX_END = "$";
+
+
+ /**
+ * The complete regex for a valid username consisting of the following regex
+ * groups:
+ * <br>
+ * {@link #REGEX_START}, {@link #USERNAME_REGEX_GROUP}, {@link #REGEX_END}.
+ */
+ private static final String USERNAME_REGEX = REGEX_START
+ + USERNAME_REGEX_GROUP
+ + REGEX_END;
+
+ /**
+ * The complete regex for a valid password consisting of the following regex
+ * groups:
+ * <br>
+ * {@link #REGEX_START}, {@link #PW_REGEX_GROUP1}, {@link #PW_REGEX_GROUP2},
+ * {@link #PW_REGEX_GROUP3}, {@link #PW_REGEX_GROUP4},
+ * {@link #PW_REGEX_GROUP5}, {@link #REGEX_END}.
+ */
+ private static final String PW_REGEX = REGEX_START
+ + PW_REGEX_GROUP1
+ + PW_REGEX_GROUP2
+ + PW_REGEX_GROUP3
+ + PW_REGEX_GROUP4
+ + PW_REGEX_GROUP5
+ + REGEX_END;
+
+ /**
+ * Checks if the given {@code username} meets the following requirements:
+ * <br>
+ * - contains only word characters (equivalent to [a-zA-Z0-9_])
+ * and the character '-'.
+ * <br>
+ * - is between 1 and 255 characters long.
+ *
+ * @param username The username that needs to be validated
+ * @return {@code false} if the username meets the requirements,
+ * <br>
+ * {@code true} otherwise
+ */
+ public boolean checkUsernameInvalid(final String username) {
+ return !Pattern
+ .compile(USERNAME_REGEX)
+ .matcher(username)
+ .matches();
+ }
+
+ /**
+ * Checks if the given email address conforms to the RFC822 standard using
+ * {@link InternetAddress#validate()}.
+ *
+ * @param email The email address that needs to be validated
+ * @return {@code false} if the username meets the requirements,
+ * <br>
+ * {@code true} otherwise
+ */
+ public boolean checkEmailInvalid(final String email) {
+ try {
+ InternetAddress internetAddress = new InternetAddress(email, STRICT);
+ internetAddress.validate();
+ return VALID;
+ } catch (AddressException e) {
+ return INVALID;
+ }
+ }
+
+ /**
+ * Checks if the given {@code password} meets the following requirements:
+ * <br>
+ * - contains at least one digit.
+ * <br>
+ * - contains at least one lower case character.
+ * <br>
+ * - contains at least one upper case character.
+ * <br>
+ * - contains at least one special character from the list [€°§´] or the
+ * Punct script extension.
+ * <br>
+ * - contains only word characters (equivalent to [a-zA-Z0-9_]) and special
+ * characters specified above.
+ *
+ * @param password The username that needs to be validated
+ * @return {@code false} if the username meets the requirements,
+ * <br>
+ * {@code true} otherwise
+ */
+ public boolean checkPasswordInvalid(final String password) {
+ return !Pattern
+ .compile(PW_REGEX)
+ .matcher(password)
+ .matches();
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/service/ResourceReader.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/ResourceReader.java
new file mode 100644
index 0000000..3501f9e
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/ResourceReader.java
@@ -0,0 +1,33 @@
+package org.psesquared.server.authentication.api.service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import org.apache.commons.io.IOUtils;
+
+/**
+ * This class allows reading text from files.
+ */
+public final class ResourceReader {
+
+ /**
+ * Private constructor - cannot be called.
+ */
+ private ResourceReader() { }
+
+ /**
+ * This method reads text from a file specified by the given path.
+ *
+ * @param path The path to the file
+ * @return The contents of the file
+ * @throws java.io.IOException If an I/O error occurs
+ */
+ public static String readFileToString(final String path)
+ throws java.io.IOException {
+ return IOUtils.toString(Objects.requireNonNull(
+ ResourceReader.class.getClassLoader().getResourceAsStream(path)),
+ StandardCharsets.UTF_8);
+
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/authentication/api/service/package-info.java b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/package-info.java
new file mode 100644
index 0000000..9f16a0d
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/authentication/api/service/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * This package represents the logical middle layer of the authentication API
+ * ({@link org.psesquared.server.authentication.api}) - the service layer.
+ * <br>
+ * All business logic is handled here with the
+ * {@link
+ * org.psesquared.server.authentication.api.service.AuthenticationService}
+ * class, which in turn relies on some other service classes.
+ *
+ * @author PSE-Squared Team
+ * @version 1.0
+ */
+package org.psesquared.server.authentication.api.service;