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/config |
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/config')
8 files changed, 804 insertions, 0 deletions
diff --git a/pse-server/src/main/java/org/psesquared/server/config/ApplicationConfig.java b/pse-server/src/main/java/org/psesquared/server/config/ApplicationConfig.java new file mode 100644 index 0000000..a67e53d --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/config/ApplicationConfig.java @@ -0,0 +1,125 @@ +package org.psesquared.server.config; + +import lombok.RequiredArgsConstructor; +import org.psesquared.server.authentication.api.data.access.AuthenticationDao; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * The application configuration class declaring several beans. + */ +@Configuration +@EnableScheduling +@EnableTransactionManagement +@EnableAsync +@RequiredArgsConstructor +public class ApplicationConfig implements WebMvcConfigurer { + + /** + * The message passed on to {@link UsernameNotFoundException}. + */ + private static final String USERNAME_NOT_FOUND + = "No user with the given username was found."; + + /** + * The JPA repository that handles user related database requests. + */ + private final AuthenticationDao authenticationDao; + + /** + * Returns a {@link UserDetailsService} bean for retrieving users via username + * from the database. + * + * @return {@link UserDetailsService} + */ + @Bean + public UserDetailsService userDetailsService() { + return username -> authenticationDao.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException(USERNAME_NOT_FOUND)); + } + + /** + * Returns an {@link AuthenticationProvider} bean for authenticating + * {@link org.springframework.security.core.userdetails.User}s with username + * and password using {@link #userDetailsService()} and + * {@link #passwordEncoder()}. + * + * @return {@link AuthenticationProvider} + */ + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService()); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + /** + * Returns a {@link BCryptPasswordEncoder} bean for password encryption. + * + * @return {@link PasswordEncoder} + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * Returns an {@link AuthenticationManager} bean for processing authentication + * requests from the given {@link AuthenticationConfiguration}. + * + * @param config The application's authentication configuration + * @return {@link AuthenticationManager} + * @throws Exception When the authentication manager couldn't be retrieved + * from the given configuration + */ + @Bean + public AuthenticationManager authenticationManager( + final AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + /** + * Returns a {@link WebMvcConfigurer} bean with CORS enabled globally. + * + * @return {@link WebMvcConfigurer} + */ + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(@NonNull final CorsRegistry registry) { + registry + .addMapping("/**") + .allowedOrigins("*") + .allowedMethods("*"); + } + }; + } + + /** + * Registers an {@link AuthenticationValidatorInterceptor}. + * + * @param registry The {@link InterceptorRegistry} + */ + @Override + public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(new AuthenticationValidatorInterceptor()); + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/config/AuthenticationValidatorInterceptor.java b/pse-server/src/main/java/org/psesquared/server/config/AuthenticationValidatorInterceptor.java new file mode 100644 index 0000000..3482164 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/config/AuthenticationValidatorInterceptor.java @@ -0,0 +1,95 @@ +package org.psesquared.server.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; + +/** + * This interceptor class intercepts requests between the DispatcherServlet and + * the Controller (i.e. when already mapped to Controller method). + * It checks if the currently authenticated + * {@link org.psesquared.server.model.User} is the same user for whom the + * request is sent. + */ +public class AuthenticationValidatorInterceptor implements HandlerInterceptor { + + /** + * The return value for aborting the execution of the Controller method. + */ + private static final boolean ABORT_EXECUTION = false; + + /** + * The return value for resuming the execution of the Controller method. + */ + private static final boolean RESUME_EXECUTION = true; + + /** + * The name of the username URL path variable. + */ + private static final String PATH_VARIABLE_USERNAME = "username"; + + /** + * The default name associated with authentication. + */ + private static final String USERNAME_NO_AUTH = "anonymousUser"; + + /** + * Checks if the currently authenticated + * {@link org.psesquared.server.model.User} is the same user specified in the + * URL path variable of the request. + * + * @param request The {@link HttpServletRequest} + * @param response The {@link HttpServletResponse} + * @param handler The chosen handler + * @return {@code true} if the users match, + * <br> + * {@code false} otherwise + */ + @Override + public boolean preHandle(@NonNull final HttpServletRequest request, + @NonNull final HttpServletResponse response, + @NonNull final Object handler) { + + final String usernamePathVariable = extractUsernamePathVariable(request); + final AbstractAuthenticationToken auth + = (AbstractAuthenticationToken) SecurityContextHolder.getContext() + .getAuthentication(); + final String usernameAuthenticated; + + if (usernamePathVariable == null || auth == null) { + return RESUME_EXECUTION; + } + + usernameAuthenticated = auth.getName(); + if (usernameAuthenticated == null + || usernameAuthenticated.equals(USERNAME_NO_AUTH) + || usernameAuthenticated.equals(usernamePathVariable)) { + return RESUME_EXECUTION; + } + + return ABORT_EXECUTION; + } + + /** + * Extracts the username path variable from the {@link HttpServletRequest}. + * + * @param request The {@link HttpServletRequest} + * @return The value of the username path variable + */ + private String extractUsernamePathVariable(final HttpServletRequest request) { + // returns HttpServletRequest attribute that contains the URI templates map, + // mapping variable names to values + final Map<String, String> pathVariables = (Map<String, String>) request + .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + // this attribute is of type Map<String, String> per definition, + // so no type checks are needed + return (pathVariables != null) + ? pathVariables.get(PATH_VARIABLE_USERNAME) : null; + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/config/EmailConfigProperties.java b/pse-server/src/main/java/org/psesquared/server/config/EmailConfigProperties.java new file mode 100644 index 0000000..4b2c79e --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/config/EmailConfigProperties.java @@ -0,0 +1,16 @@ +package org.psesquared.server.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * The properties class that is used to return some externally stored URLs. + * + * @param dashboardBaseUrl The base URL of the PSE-Dashboard + * @param verificationUrl The URL for account verification + * @param resetUrlPath The URL for resetting the password of a user + */ +@ConfigurationProperties("email") +public record EmailConfigProperties(String dashboardBaseUrl, + String verificationUrl, + String resetUrlPath) { +} diff --git a/pse-server/src/main/java/org/psesquared/server/config/JwtAuthenticationFilter.java b/pse-server/src/main/java/org/psesquared/server/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..bf00ecb --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/config/JwtAuthenticationFilter.java @@ -0,0 +1,143 @@ +package org.psesquared.server.config; + +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.WebUtils; + +/** + * This filter class handles authentication via JWT. + * <br> + * Its method + * {@link + * #doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain)} + * is invoked before the mapping of the request to the Controller happens. + */ +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + /** + * The URL for the unsecured register-API-endpoint. + */ + private static final String REGISTER_URL + = "/api/2/auth/register.json"; + + /** + * The URL for the unsecured forgotPassword-API-endpoint. + */ + private static final String FORGOT_URL + = "/api/2/auth/{email}/forgot.json"; + + /** + * The URL for the unsecured verify-API-endpoint. + */ + private static final String VERIFY_URL + = "/api/2/auth/{username}/verify.json"; + + /** + * The URL for the unsecured resetPassword-API-endpoint. + */ + private static final String RESET_PASSWORD_URL + = "/api/2/auth/{username}/resetpassword.json"; + + /** + * The name of the cookie used for JWT authentication. + */ + private static final String COOKIE_NAME = "sessionid"; + + /** + * The service class used for managing JWTs. + */ + private final JwtService jwtService; + + /** + * The service class used for retrieving users from the database. + */ + private final UserDetailsService userDetailsService; + + /** + * The filter method does nothing for the specified unsecured URLs + * and otherwise calls + * {@link #authenticateIfValid(Cookie, HttpServletRequest)}. + * + * @param request The {@link HttpServletRequest} + * @param response The {@link HttpServletResponse} + * @param filterChain The {@link FilterChain} + * @throws ServletException If error occurs when processing request + * @throws IOException If I/O error occurs + */ + @Override + protected void doFilterInternal(@NonNull final HttpServletRequest request, + @NonNull final HttpServletResponse response, + @NonNull final FilterChain filterChain) + throws ServletException, IOException { + + final Cookie cookie = WebUtils.getCookie(request, COOKIE_NAME); + final String url = request.getRequestURI(); + + if (url.equals(REGISTER_URL) || url.equals(FORGOT_URL) + || url.equals(VERIFY_URL) || url.equals(RESET_PASSWORD_URL) + || cookie == null) { + filterChain.doFilter(request, response); + return; + } + + authenticateIfValid(cookie, request); + filterChain.doFilter(request, response); + } + + /** + * Authenticates the {@link org.psesquared.server.model.User} associated with + * the JWT from the cookie if it is valid. + * + * @param cookie The cookie containing the JWT + * @param request The {@link HttpServletRequest} for creating a new + * authentication details instance + */ + private void authenticateIfValid(final Cookie cookie, + final HttpServletRequest request) { + final String jwt = cookie.getValue(); + final String usernameFromToken; + + try { + usernameFromToken = jwtService.extractAuthUsername(jwt); + } catch (ExpiredJwtException e) { + return; + } + + if (usernameFromToken != null + && SecurityContextHolder + .getContext() + .getAuthentication() == null) { + UserDetails userDetails + = userDetailsService.loadUserByUsername(usernameFromToken); + if (jwtService.isAuthTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken + = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/config/JwtService.java b/pse-server/src/main/java/org/psesquared/server/config/JwtService.java new file mode 100644 index 0000000..157055a --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/config/JwtService.java @@ -0,0 +1,283 @@ +package org.psesquared.server.config; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +/** + * The service class responsible for creating, managing and validating JWTs. + */ +@Service +@RequiredArgsConstructor +public class JwtService { + + /** + * The boolean value for expressing that a JWT is not valid. + */ + private static final boolean INVALID = false; + + /** + * The 1h lifespan of an access token. + */ + private static final long ACCESS_TOKEN_LIFESPAN_MILLIS + = 1000 * 60 * (long) 60; + + /** + * The 24h lifespan of a URL token (verification/resetting password). + */ + private static final long URL_TOKEN_LIFESPAN_MILLIS + = ACCESS_TOKEN_LIFESPAN_MILLIS * 24; + + /** + * The properties class that is used to return externally stored signing key. + */ + private final SecurityConfigProperties securityConfigProperties; + + /** + * Extracts the username from a JWT for authentication. + * + * @param token The JWT + * @return The extracted username + */ + public String extractAuthUsername(final String token) { + return extractClaim(token, getAuthSigningKey(), Claims::getSubject); + } + + /** + * Extracts a generic claim from the JWT. + * + * @param <T> The type of the claim + * @param token The JWT + * @param signingKey The JWT signing key + * @param claimsResolver The function to resolve the claim + * @return The extracted generic claim + * @throws ExpiredJwtException If the JWT has expired + * @throws UnsupportedJwtException If the JWT is not supported + * @throws MalformedJwtException If the JWT is malformed + * @throws SignatureException If the signature doesn't match + * @throws IllegalArgumentException If the token has an inappropriate format + */ + public <T> T extractClaim(final String token, + final Key signingKey, + final Function<Claims, T> claimsResolver) + throws ExpiredJwtException, + UnsupportedJwtException, + MalformedJwtException, + SignatureException, + IllegalArgumentException { + + final Claims claims = extractAllClaims(token, signingKey); + return claimsResolver.apply(claims); + } + + /** + * Generates the JWT with additional claims and a lifespan for the + * {@link org.psesquared.server.model.User} with the given details. + * + * @param additionalClaims The {@link Map} with additional claims + * @param userDetails The user details + * @param tokenLifespan The lifespan of the token + * @param signingKey The JWT signing key + * @return The generated JWT + */ + public String generateTokenString(final Map<String, Object> additionalClaims, + final UserDetails userDetails, + final long tokenLifespan, + final Key signingKey) { + return Jwts.builder() + .setClaims(additionalClaims) + .setSubject(userDetails.getUsername()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + tokenLifespan)) + .signWith(signingKey, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * Generates a JWT access token for the + * {@link org.psesquared.server.model.User} with the given details. + * (no additional claims). + * + * @param userDetails The user details + * @return The generated JWT access token + */ + public String generateAccessTokenString(final UserDetails userDetails) { + //no additional claims supported but open for extension with roles e.g. + return generateTokenString(new HashMap<>(), + userDetails, + ACCESS_TOKEN_LIFESPAN_MILLIS, + getAuthSigningKey()); + } + + /** + * Generates a JWT URL token required for authentication/resetting password + * for the {@link org.psesquared.server.model.User} with the given details + * (no additional claims). + * + * @param userDetails The user details + * @return The generated JWT access token + */ + public String generateUrlTokenString(final UserDetails userDetails) { + return generateTokenString(new HashMap<>(), + userDetails, + URL_TOKEN_LIFESPAN_MILLIS, + getUrlSigningKey()); + } + + /** + * Validates the given JWT for authentication against the given + * {@link UserDetails} and checks if it has not expired. + * + * @param token The to be validated JWT + * @param userDetails The user details + * @return {@code true} if the JWT is valid, + * <br> + * {@code false} otherwise + */ + public boolean isAuthTokenValid(final String token, + final UserDetails userDetails) { + return isTokenValid(token, userDetails, getAuthSigningKey()); + } + + /** + * Validates the given JWT for URLs against the given {@link UserDetails} + * and checks if it has not expired. + * + * @param token The to be validated JWT + * @param userDetails The user details + * @return {@code true} if the JWT is valid, + * <br> + * {@code false} otherwise + */ + public boolean isUrlTokenValid(final String token, + final UserDetails userDetails) { + return isTokenValid(token, userDetails, getUrlSigningKey()); + } + + /** + * Validates the given JWT against the given {@link UserDetails} + * with the given signing key and checks if it has not expired. + * + * @param token The to be validated JWT + * @param userDetails The user details + * @param signingKey The JWT signing key + * @return {@code true} if the JWT is valid, + * <br> + * {@code false} otherwise + */ + private boolean isTokenValid(final String token, + final UserDetails userDetails, + final Key signingKey) { + try { + final String username = extractUsername(token, signingKey); + return username.equals(userDetails.getUsername()) + && !isTokenExpired(token, signingKey); + } catch (ExpiredJwtException + | UnsupportedJwtException + | MalformedJwtException + | SignatureException + | IllegalArgumentException e) { + return INVALID; + } + } + + /** + * Checks if the given JWT is expired. + * + * @param token The JWT + * @param signingKey The JWT signing key + * @return {@code true} if the JWT is expired, + * <br> + * {@code false} otherwise + */ + private boolean isTokenExpired(final String token, final Key signingKey) { + return extractExpiration(token, signingKey).before(new Date()); + } + + /** + * Extracts the username from a JWT. + * + * @param token The JWT + * @param signingKey The JWT signing key + * @return The extracted username + */ + private String extractUsername(final String token, final Key signingKey) { + return extractClaim(token, signingKey, Claims::getSubject); + } + + /** + * Extracts the expiration {@link Date} of the JWT. + * + * @param token The JWT + * @param signingKey The JWT signing key + * @return The expiration date + */ + private Date extractExpiration(final String token, final Key signingKey) { + return extractClaim(token, signingKey, Claims::getExpiration); + } + + /** + * Extracts all claims from the JWT in order for + * {@link #extractClaim(String, Key, Function)} to be able + * to filter out one claim. + * + * @param token The JWT + * @param signingKey The JWT signing key + * @return All claims of the JWT + * @throws ExpiredJwtException If the JWT has expired + * @throws UnsupportedJwtException If the JWT is not supported + * @throws MalformedJwtException If the JWT is malformed + * @throws SignatureException If the signature doesn't match + * @throws IllegalArgumentException If the token has an inappropriate format + */ + private Claims extractAllClaims(final String token, final Key signingKey) + throws ExpiredJwtException, + UnsupportedJwtException, + MalformedJwtException, + SignatureException, + IllegalArgumentException { + + return Jwts.parserBuilder() + .setSigningKey(signingKey) + .build() + .parseClaimsJws(token) + .getBody(); + } + + /** + * Returns the signing {@link Key} for signing the JWT. + * + * @return The signing key + */ + private Key getAuthSigningKey() { + byte[] keyBytes + = Decoders.BASE64.decode(securityConfigProperties.jwtAuthSigningKey()); + return Keys.hmacShaKeyFor(keyBytes); + } + + /** + * Returns the signing {@link Key} for signing the JWT. + * + * @return The signing key + */ + private Key getUrlSigningKey() { + byte[] keyBytes + = Decoders.BASE64.decode(securityConfigProperties.jwtUrlSigningKey()); + return Keys.hmacShaKeyFor(keyBytes); + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/config/SecurityConfig.java b/pse-server/src/main/java/org/psesquared/server/config/SecurityConfig.java new file mode 100644 index 0000000..8005160 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/config/SecurityConfig.java @@ -0,0 +1,117 @@ +package org.psesquared.server.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +/** + * This class is responsible for configuring the {@link SecurityFilterChain} + * which determines the way authentication is handled with the server. + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + /** + * The URL of the unsecured register-API-endpoint. + */ + private static final String REGISTER_URL + = "/api/2/auth/register.json"; + + /** + * The URL of the unsecured forgotPassword-API-endpoint. + */ + private static final String FORGOT_URL + = "/api/2/auth/{email}/forgot.json"; + + /** + * The URL of the unsecured verify-API-endpoint. + */ + private static final String VERIFY_URL + = "/api/2/auth/{username}/verify.json"; + + /** + * The URL of the unsecured resetPassword-API-endpoint. + */ + private static final String RESET_PASSWORD_URL + = "/api/2/auth/{username}/resetpassword.json"; + + /** + * The authentication filter for JWT authentication. + */ + private final JwtAuthenticationFilter jwtAuthFilter; + + /** + * The authentication provider specified in {@link ApplicationConfig}. + */ + private final AuthenticationProvider authenticationProvider; + + /** + * Configures the {@link SecurityFilterChain} with {@link HttpSecurity} + * in the following way: + * <br> + * 1. JWT authentication ("sessionid" cookie) + * <br> + * 2. HTTP basic authentication ("Authorization" header) + * + * @param http The HTTP security class + * @return The security filter chain + * @throws Exception If an error occurs + */ + @Bean + public SecurityFilterChain securityFilterChain(final HttpSecurity http) + throws Exception { + http + .cors() + .and() + .csrf() + .disable() + .authorizeHttpRequests() + .requestMatchers( + REGISTER_URL, + FORGOT_URL, + VERIFY_URL, + RESET_PASSWORD_URL) + .permitAll() + .anyRequest() + .authenticated() + .and() + .authenticationProvider(authenticationProvider) + .addFilterBefore(jwtAuthFilter, + UsernamePasswordAuthenticationFilter.class) + .httpBasic() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + return http.build(); + } + + /** + * Ensures CORS is processed before Spring Security. + * + * @return The specified CORS configuration source + */ + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowCredentials(true); + configuration.addAllowedOriginPattern("*"); + configuration.addAllowedHeader("*"); + configuration.addAllowedMethod("*"); + UrlBasedCorsConfigurationSource source + = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/config/SecurityConfigProperties.java b/pse-server/src/main/java/org/psesquared/server/config/SecurityConfigProperties.java new file mode 100644 index 0000000..74303fe --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/config/SecurityConfigProperties.java @@ -0,0 +1,17 @@ +package org.psesquared.server.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * The properties class that is used to return externally stored signing key. + * + * @param jwtAuthSigningKey The base64-encoded JWT signing key for + * authentication + * @param jwtUrlSigningKey The base64-encoded JWT signing key for URLs + * @param emailSigningKey The base64-encoded salt for email encryption + */ +@ConfigurationProperties("security") +public record SecurityConfigProperties(String jwtAuthSigningKey, + String jwtUrlSigningKey, + String emailSigningKey) { +} diff --git a/pse-server/src/main/java/org/psesquared/server/config/package-info.java b/pse-server/src/main/java/org/psesquared/server/config/package-info.java new file mode 100644 index 0000000..814be40 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/config/package-info.java @@ -0,0 +1,8 @@ +/** + * This package features all relevant classes for the application + * configuration and security. + * + * @author PSE-Squared Team + * @version 1.0 + */ +package org.psesquared.server.config; |