Authorization Beyond Roles in Spring Boot

Two editors share the same role, but only one owns the document. Follow a Spring Boot authorization flaw, fix it with a lookup scoped to the authenticated owner.

Alexandru DancauOctober 25, 2026 · 7 min read
Alice and Bob both have the editor role, but only Alice's green path reaches her private document; Bob's coral path is stopped by an ownership check.

Alice and Bob are both editors. They can both sign in, open the dashboard, and work on documents. They both have the EDITOR role.

Alice creates a private draft. Bob obtains its URL and opens it. Should the application return the document?

For this example, the rule is that editors can read only their own documents. Checking Bob's role gets us part of the way there. The application must also check his relationship to the document he requested.

In Understanding CSRF and CORS policies, we looked at browser requests and the boundaries around them. Here, we will follow a request that passes authentication and the role check, but still asks for data the caller should not receive.

Build Failed currently has one administrator. The example below is a separate document application with multiple editors, using Java 21, Spring Boot 3.5, Spring Security 6.5, and Spring Data JPA.

Define access to a specific document

Our document has an ID, an owner, a title, and a body. For Alice's document, the policy is:

Caller Has the EDITOR role Owns the document Result
Alice Yes Yes Return the document
Bob Yes No Do not return the document
Alice without the editor role No Yes Deny access
Someone who has not signed in No No Require authentication

Both conditions matter: the caller must have the editor role and own the requested document. This example has no administrator override or document sharing.

Roles describe a broad capability. Ownership adds a relationship to one particular resource. More complex applications may also need project membership, organization boundaries, or document state in their decisions. OWASP on authorization

The vulnerable lookup

Suppose the service checks the role and then loads a document by its ID:

@PreAuthorize("hasRole('EDITOR')")
public DocumentView read(UUID id) {
    Document document = documents.findById(id)
            .orElseThrow(() -> new ResponseStatusException(
                    HttpStatus.NOT_FOUND, "Document not found"));
    return DocumentView.from(document);
}

DocumentView is a response record containing the document's ID, title, and body. The controller passes the ID from the URL to this service:

@RestController
@RequestMapping("/api/documents")
public class DocumentController {
    private final DocumentService documents;

    public DocumentController(DocumentService documents) {
        this.documents = documents;
    }

    @GetMapping("/{id}")
    public DocumentView read(@PathVariable UUID id) {
        return documents.read(id);
    }
}

Bob signs in and requests Alice's document:

GET /api/documents/5f55b830-a150-4edf-88ab-c16ab6ddcc54

He has the required role. The document exists. Nothing in the service checks its owner, so the application returns 200 OK with Alice's private text.

This is an insecure direct object reference, or IDOR: an identifier leads to an object without the required access check. In API security, this also falls under broken object level authorization, often shortened to BOLA. A UUID makes an identifier harder to guess, but a user who obtains it must still be denied unauthorized access. OWASP on IDOR, OWASP on BOLA

Hiding Alice's document from Bob's dashboard does not fix the lookup. He can make the HTTP request directly.

Keep the role checks enabled

For this small API, the security configuration allows document reads for editors and denies other application requests:

@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
public class SecurityConfiguration {
    @Bean
    SecurityFilterChain applicationSecurity(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(authorize -> authorize
                        .dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()
                        .requestMatchers(HttpMethod.GET, "/api/documents/*")
                        .hasRole("EDITOR")
                        .anyRequest().denyAll())
                .httpBasic(Customizer.withDefaults())
                .build();
    }
}

hasRole("EDITOR") checks for the ROLE_EDITOR authority. The ERROR rule permits an internal error dispatch, so error handling can preserve the intended status. It does not make a direct request to /error public. Spring Security request authorization

@EnableMethodSecurity activates checks such as @PreAuthorize; adding the security starter alone does not enable them. The service check also applies when another Spring component calls the service through its proxy. Ordinary calls from one method to another within the same instance do not pass through that proxy. Spring Security method security

The example uses HTTP Basic so an unauthenticated API request receives a 401 response. A browser application can retain its existing session login and CSRF protection. Changing authentication mechanisms does not supply the missing ownership check.

Obtain identity from the security context

We need the authenticated caller's identity before looking up the document. For the example, a small component reads the username from Spring Security:

@Component
public class CurrentAccount {
    public String username() {
        return SecurityContextHolder.getContext()
                .getAuthentication().getName();
    }
}

The secured service calls this helper after its role check succeeds. The caller does not supply an owner argument to the service.

In this demonstration, usernames are fixed account keys: alice and bob. In an application that allows username changes, I would store an immutable account ID on the document and obtain the matching ID from the trusted authenticated principal.

Accepting ownerUsername=alice from a query parameter or JSON body would let Bob choose whose identity the lookup uses. The request may select a document; it must not select the authenticated account.

Restrict the repository lookup

The repository needs a query that includes both the document ID and the owner:

public interface DocumentRepository extends JpaRepository<Document, UUID> {
    Optional<Document> findByIdAndOwnerUsername(UUID id, String ownerUsername);
}

Spring Data JPA derives the query from the method name. Both conditions must match. Spring Data JPA query methods

Now the service becomes:

@Service
@Transactional(readOnly = true)
public class DocumentService {
    private final DocumentRepository documents;
    private final CurrentAccount currentAccount;

    public DocumentService(DocumentRepository documents, CurrentAccount currentAccount) {
        this.documents = documents;
        this.currentAccount = currentAccount;
    }

    @PreAuthorize("hasRole('EDITOR')")
    public DocumentView read(UUID id) {
        Document document = documents
                .findByIdAndOwnerUsername(id, currentAccount.username())
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.NOT_FOUND, "Document not found"));
        return DocumentView.from(document);
    }
}

The controller stays the same. For Alice, the query finds her document. For Bob, the query finds no document in his permitted set.

We return 404 Not Found for both a missing document and one owned by someone else. This avoids explicitly confirming its existence through a different status code. Returning 403 Forbidden can be appropriate when the caller is allowed to know the resource exists; it is a policy choice. OWASP on scoped lookups

The role check still runs. Ownership alone does not let Alice read a document after she loses the editor role.

Apply the rule to every operation

This fix protects the read path shown here. Lists, exports, updates, and deletes need their own enforcement of the access policy. A secure detail endpoint does not compensate for an export that reads every document.

For a list, scope the database query to the permitted documents before pagination. Loading a page of everybody's documents and filtering it afterward can produce incomplete pages and expose counts that do not belong to the caller.

For an update or delete, verify access before making the change. If ownership or permissions can change concurrently, choose transaction and locking rules that keep the authorization decision valid through the write. A check performed after a side effect is too late. OWASP on enforcing authorization

For our document application, I want the rule visible where the data is selected: an editor can read a document only when that document belongs to the authenticated account. The role gets the request through the first gate; the ownership condition decides which document can come back.