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; | 
