I'm building a Spring Boot backend API with JWT authentication. The domain model is:
Project
owned by a User
Project
has many Exams
Exam
has many Questions
Authentication is working, but authorization/ownership checks are becoming repetitive and error-prone when I go down into nested endpoints. Examples:
GET /projects
filters projects by the authenticated user.GET /projects/{projectId}/exams
checks that the project belongs to the user before returning exams.DELETE /projects/{projectId}/exams/{examId}
could be abused if someone supplies a random examId
unless I verify ownership again.Right now I end up manually passing the User
everywhere and doing checks like findAllByOwnerAndId(user, projectId)
for example. This repetition is spreading across controllers and services. Is there a cleaner, centralized pattern to handle ownership/authorization for nested resources so I don't have to litter every handler with the same logic?
ProjectController:
@RestController
@RequestMapping("/projects")
public class ProjectController {
private final ProjectService projectService;
public ProjectController(ProjectService projectService) {
this.projectService = projectService;
}
@GetMapping
public ResponseEntity<Iterable<ProjectDTO>> getAll(@AuthenticationPrincipal UserPrincipal principal) {
return ResponseEntity.ok(projectService.getAllByUser(principal.getUser()));
}
@DeleteMapping("/{projectId}")
public ResponseEntity<?> delete(@PathVariable Integer projectId, @AuthenticationPrincipal UserPrincipal principal) {
projectService.delete(projectId, principal.getUser());
return ResponseEntity.ok().build();
}
}
ExamController:
@RestController
@RequestMapping("/projects/{projectId}/exams")
public class ExamController {
private final ExamService examService;
public ExamController(ExamService examService) {
this.examService = examService;
}
@GetMapping
public ResponseEntity<Iterable<Exam>> getAll(
@PathVariable Integer projectId,
@AuthenticationPrincipal UserPrincipal principal
) {
return ResponseEntity.ok(examService.getAllByProject(projectId, principal.getUser()));
}
@DeleteMapping("/{examId}")
public ResponseEntity<?> delete(@PathVariable Integer examId) {
examService.delete(examId);
return ResponseEntity.ok().build();
}
}
ExamService (ownership check inside): This ownership check feels very awkward.
public Iterable<Exam> getAllByProject(Integer projectId, User user) {
Optional<Project> project = projectRepository.findAllByOwnerAndId(user, projectId);
if (project.isEmpty()) {
throw new ProjectNotFoundException(projectId);
}
return project.get().getExams();
}
Is there a better architectural or Spring idiomatic way to centralize or eliminate these repetitive ownership checks for nested resources (project → exam → question)?
Create @CheckOwnership
annotation to mark methods needing verification. try AOP to intercept calls and validate resource ownership automatically.
Centralize checks in an AuthorizationService
and inject this service wherever needed
validateProjectOwnership(projectId, user)
validateExamOwnership(examId, user)
Also prevalidate ownership using @ModelAttribute
methods in a @ControllerAdvice
so controllers can safely use the prevalidated resources:
@RestController
@RequestMapping("/projects/{projectId}/exams")
public class ExamController {
private final ExamService examService;
@GetMapping
public ResponseEntity<Iterable<Exam>> getAll(@ModelAttribute("project") Project project) {
return ResponseEntity.ok(examService.getAllByProject(project));
}
@DeleteMapping("/{examId}")
public ResponseEntity<?> delete(@ModelAttribute("exam") Exam exam) {
examService.delete(exam);
return ResponseEntity.ok().build();
}
}