diff options
author | Orangerot <purple@orangerot.dev> | 2024-06-19 00:14:49 +0200 |
---|---|---|
committer | Orangerot <purple@orangerot.dev> | 2024-06-27 12:11:14 +0200 |
commit | 5b8851b6c268d0e93c158908fbfae9f8473db5ff (patch) | |
tree | 7010eb85d86fa2da06ea4ffbcdb01a685d502ae8 /pse-server/src/main/java/org/psesquared/server/authentication/api/service |
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/authentication/api/service')
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; |