summaryrefslogtreecommitdiff
path: root/pse-server/src/main/java/org/psesquared/server/authentication/api/service
diff options
context:
space:
mode:
authorOrangerot <purple@orangerot.dev>2024-06-19 00:14:49 +0200
committerOrangerot <purple@orangerot.dev>2024-06-27 12:11:14 +0200
commit5b8851b6c268d0e93c158908fbfae9f8473db5ff (patch)
tree7010eb85d86fa2da06ea4ffbcdb01a685d502ae8 /pse-server/src/main/java/org/psesquared/server/authentication/api/service
Initial commitHEADmain
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/authentication/api/service')
-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
6 files changed, 912 insertions, 0 deletions
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;