Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,6 @@
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
Expand Down Expand Up @@ -204,10 +199,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
Expand Down Expand Up @@ -255,14 +246,6 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/openpodcastapi/opa/OpenPodcastAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/// Main application
@SpringBootApplication
@EnableScheduling
public class OpenPodcastAPI {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,72 +1,69 @@
package org.openpodcastapi.opa.advice;

import jakarta.persistence.EntityNotFoundException;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.jspecify.annotations.NonNull;
import org.openpodcastapi.opa.exceptions.ValidationErrorResponse;
import org.slf4j.Logger;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.Instant;
import java.util.List;

import static org.slf4j.LoggerFactory.getLogger;

/// A global handler for common exceptions thrown by the application.
///
/// Where possible, controllers should throw their own exceptions.
/// However, for common exceptions such as invalid parameters and
/// not found entities, a global exception handler can be added.
@RestControllerAdvice
@RequiredArgsConstructor
@Log4j2
public class GlobalExceptionHandler {

private static final Logger log = getLogger(GlobalExceptionHandler.class);

/// Returns a 404 if a database entity is not found
///
/// @param exception the thrown [EntityNotFoundException]
/// @return a [ResponseEntity] containing the error message
/// @param exception the thrown exception
/// @return a response containing the error message
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<@NonNull String> handleEntityNotFoundException(EntityNotFoundException exception) {
log.debug("{}", exception.getMessage());
log.info("{}", exception.getMessage());
return ResponseEntity.notFound().build();
}

/// Returns a 400 error when conflicting data is entered
///
/// @param exception the thrown [DataIntegrityViolationException]
/// @return a [ResponseEntity] containing the error message
/// @param exception the thrown exception
/// @return a response containing the error message
@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<@NonNull String> handleDataIntegrityViolationException(DataIntegrityViolationException exception) {
return ResponseEntity.badRequest().body(exception.getMessage());
}

/// Returns a 400 error when illegal arguments are passed
///
/// @param exception the thrown [IllegalArgumentException]
/// @return a [ResponseEntity] containing the error message
/// @param exception the thrown exception
/// @return a response containing the error message
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<@NonNull String> handleIllegalArgumentException(IllegalArgumentException exception) {
return ResponseEntity.badRequest().body(exception.getMessage());
}

/// Returns a 400 error when invalid arguments are passed to an endpoint
///
/// @param exception the thrown [MethodArgumentNotValidException]
/// @return a [ResponseEntity] containing the error message
/// @param exception the thrown exception
/// @return a response containing the error message
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<@NonNull ValidationErrorResponse> handleValidationException(MethodArgumentNotValidException exception) {
List<ValidationErrorResponse.FieldError> errors = exception.getBindingResult().getFieldErrors().stream()
final var errors = exception.getBindingResult().getFieldErrors().stream()
.map(fe -> new ValidationErrorResponse.FieldError(fe.getField(), fe.getDefaultMessage()))
.toList();

var body = new ValidationErrorResponse(
final var body = new ValidationErrorResponse(
Instant.now(),
HttpStatus.BAD_REQUEST.value(),
errors
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.openpodcastapi.opa.advice;

import lombok.extern.log4j.Log4j2;
import org.slf4j.Logger;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.ui.Model;
Expand All @@ -9,34 +9,40 @@

import java.security.Principal;

import static org.slf4j.LoggerFactory.getLogger;

/// A helper class for adding user information to requests.
///
/// This class is used to populate user details in templates
/// and to ensure that a user is authenticated when viewing
/// web pages.
@Log4j2
@ControllerAdvice
public class GlobalModelAttributeAdvice {

private static final Logger log = getLogger(GlobalModelAttributeAdvice.class);

/// Adds a boolean `isAuthenticated` property to the request model based on
/// whether the user is logged-in.
///
/// @param model the [Model] attached to the request
/// @param model the variables attached to the request
@ModelAttribute
public void addAuthenticationFlag(Model model) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
var isAuthenticated = authentication != null && authentication.isAuthenticated()
final var isAuthenticated = authentication != null && authentication.isAuthenticated()
&& !"anonymousUser".equals(authentication.getPrincipal());
assert authentication != null;
log.debug("Authentication flag for {} added", authentication.getPrincipal());
model.addAttribute("isAuthenticated", isAuthenticated);
}

/// Adds user details to the request model.
///
/// @param principal the [Principal] representing the user
/// @param model the [Model] attached to the request
/// @param principal the principal representing the user
/// @param model the variables attached to the request
@ModelAttribute
public void addUserDetails(Principal principal, Model model) {
var username = principal != null ? principal.getName() : "Guest";
final var username = principal != null ? principal.getName() : "Guest";
log.debug("User details for {} added to model", username);
model.addAttribute("username", username);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package org.openpodcastapi.opa.auth;

import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.stereotype.Component;

import static org.slf4j.LoggerFactory.getLogger;

/// A converter that handles JWT-based auth for API requests.
///
/// This converter targets only the API endpoints at `/api`.
/// Auth for the frontend is handled by Spring's form login.
@Component
public class ApiBearerTokenAuthenticationConverter implements AuthenticationConverter {

private static final Logger log = getLogger(ApiBearerTokenAuthenticationConverter.class);

private final BearerTokenAuthenticationConverter delegate =
new BearerTokenAuthenticationConverter();

Expand All @@ -23,15 +28,18 @@ public Authentication convert(HttpServletRequest request) {

// Don't authenticate the auth endpoints
if (path.startsWith("/api/auth/")) {
log.debug("Bypassing token check for auth endpoint");
return null;
}

// If the request has no Bearer token, return null
final var header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
log.debug("Request with no auth header sent to {}", request.getRequestURI());
return null;
}

log.debug("Converting request");
// Task Spring Boot with handling the request
return delegate.convert(request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.NonNull;
import org.jspecify.annotations.NonNull;
import org.openpodcastapi.opa.service.CustomUserDetails;
import org.openpodcastapi.opa.user.UserRepository;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -28,7 +28,7 @@ public class JwtAuthenticationProvider implements AuthenticationProvider {
/// Constructor with secret value provided in `.env` file
/// or environment variables.
///
/// @param repository the [UserRepository] interface for user entities
/// @param repository the repository interface for user entities
/// @param secret the secret value used to generate JWT values
public JwtAuthenticationProvider(
UserRepository repository,
Expand Down
36 changes: 17 additions & 19 deletions src/main/java/org/openpodcastapi/opa/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.openpodcastapi.opa.config;

import lombok.RequiredArgsConstructor;
import org.openpodcastapi.opa.auth.ApiBearerTokenAuthenticationConverter;
import org.openpodcastapi.opa.auth.JwtAuthenticationProvider;
import org.springframework.context.annotation.Bean;
Expand All @@ -25,7 +24,6 @@
/// Security configuration for the Spring application
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfig {

Expand All @@ -44,12 +42,12 @@ public class SecurityConfig {

/// API-related security configuration
///
/// @param http the [HttpSecurity] object to be configured
/// @param jwtAuthenticationProvider the [JwtAuthenticationProvider] used to handle JWT auth
/// @param http the security object to be configured
/// @param jwtAuthenticationProvider the JWT provider used to handle JWT auth
/// @param entryPoint the entrypoint that commences the JWT auth
/// @param deniedHandler the [AccessDeniedHandler] that handles auth failures
/// @param converter the [ApiBearerTokenAuthenticationConverter] that manages JWT validation
/// @return the configured [HttpSecurity] object
/// @param deniedHandler the handler that handles auth failures
/// @param converter the bearer token converter that manages JWT validation
/// @return the configured security object
@Bean
@Order(1)
public SecurityFilterChain apiSecurity(
Expand All @@ -60,9 +58,9 @@ public SecurityFilterChain apiSecurity(
ApiBearerTokenAuthenticationConverter converter
) {

AuthenticationManager jwtManager = new ProviderManager(jwtAuthenticationProvider);
final var jwtManager = new ProviderManager(jwtAuthenticationProvider);

BearerTokenAuthenticationFilter bearerFilter =
final var bearerFilter =
new BearerTokenAuthenticationFilter(jwtManager, converter);

bearerFilter.setAuthenticationFailureHandler(
Expand Down Expand Up @@ -90,8 +88,8 @@ public SecurityFilterChain apiSecurity(

/// Web-related security configuration
///
/// @param http the [HttpSecurity] object to be configured
/// @return the configured [HttpSecurity] object
/// @param http the security object to be configured
/// @return the configured security object
@Bean
@Order(2)
public SecurityFilterChain webSecurity(HttpSecurity http) {
Expand Down Expand Up @@ -119,29 +117,29 @@ public SecurityFilterChain webSecurity(HttpSecurity http) {

/// The default password encoder used for hashing and encoding user passwords and JWTs
///
/// @return a configured [BCryptPasswordEncoder]
/// @return a configured password encoder
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

/// An authentication provider for password-based authentication
///
/// @param userDetailsService the [UserDetailsService] for loading user data
/// @param userDetailsService the service for loading user data
/// @param passwordEncoder the default password encoder
/// @return the configured [DaoAuthenticationProvider]
/// @return the configured authentication provider
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService,
BCryptPasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
final var provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}

/// An authentication provider for JWT-based authentication
///
/// @param provider a configured [JwtAuthenticationProvider]
/// @return a configured [ProviderManager] that uses the JWT auth provider
/// @param provider a configured provider
/// @return a configured manager that uses the JWT auth provider
/// @see JwtAuthenticationProvider for provider details
@Bean(name = "jwtAuthManager")
public AuthenticationManager jwtAuthenticationManager(JwtAuthenticationProvider provider) {
Expand All @@ -150,8 +148,8 @@ public AuthenticationManager jwtAuthenticationManager(JwtAuthenticationProvider

/// An authentication provider for API POST login
///
/// @param daoProvider a configured [DaoAuthenticationProvider]
/// @return a configured [ProviderManager] that uses basic username/password auth
/// @param daoProvider a configured auth provider
/// @return a configured manager that uses basic username/password auth
@Bean(name = "apiLoginManager", defaultCandidate = false)
public AuthenticationManager apiLoginAuthenticationManager(
DaoAuthenticationProvider daoProvider) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/openpodcastapi/opa/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
///
/// See [Thymeleaf Layout Dialect](https://ultraq.github.io/thymeleaf-layout-dialect/) for more information
///
/// @return the configured [LayoutDialect]
/// @return the configured layout dialect
@Bean
public LayoutDialect layoutDialect() {
return new LayoutDialect();
Expand Down
Loading