spring-projects/spring-security · error · AccessDeniedException

Access is denied

Error message

Access is denied

What it means

ExpressionBasedPostInvocationAdvice evaluates @PostAuthorize expressions after a secured method returns. When the SpEL expression evaluates to false, it throws AccessDeniedException('Access is denied'). The method's result is discarded and the caller receives an authorization failure.

Source

Thrown at access/src/main/java/org/springframework/security/access/expression/method/ExpressionBasedPostInvocationAdvice.java:72

	public Object after(Authentication authentication, MethodInvocation mi, PostInvocationAttribute postAttr,
			Object returnedObject) throws AccessDeniedException {
		PostInvocationExpressionAttribute pia = (PostInvocationExpressionAttribute) postAttr;
		EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi);
		Expression postFilter = pia.getFilterExpression();
		Expression postAuthorize = pia.getAuthorizeExpression();
		if (postFilter != null) {
			this.logger.debug(LogMessage.format("Applying PostFilter expression %s", postFilter));
			if (returnedObject != null) {
				returnedObject = this.expressionHandler.filter(returnedObject, postFilter, ctx);
			}
			else {
				this.logger.debug("Return object is null, filtering will be skipped");
			}
		}
		this.expressionHandler.setReturnObject(returnedObject, ctx);
		if (postAuthorize != null && !ExpressionUtils.evaluateAsBoolean(postAuthorize, ctx)) {
			this.logger.debug("PostAuthorize expression rejected access");
			throw new AccessDeniedException("Access is denied");
		}
		return returnedObject;
	}

}

View on GitHub (pinned to 96852e8860)

Solutions

  1. Inspect the @PostAuthorize SpEL expression and the returned object's properties to see why it evaluates false
  2. Ensure returnObject is non-null or guard the expression, e.g. @PostAuthorize("returnObject == null or returnObject.owner == authentication.name")
  3. Verify the expression handler has access to needed beans/properties (useCustomPermissionEvaluator, RoleHierarchy)
  4. Temporarily log the returned object and authentication to debug the expression
  5. Catch AccessDeniedException at the caller/web layer and map to HTTP 403

Example fix

// before
@PostAuthorize("returnObject.owner == authentication.name")
public Account getAccount(Long id) { ... }

// after
@PostAuthorize("returnObject == null || returnObject.owner == authentication.name")
public Account getAccount(Long id) { ... }
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling the secured method
Object returned = null; // result only known post-invocation; validate post-hoc
if (returned != null && !Objects.equals(getOwner(returned), currentUsername)) {
    throw new AccessDeniedException("Predicted post-authorize failure");
}

Type guard

boolean canView(Object returnObject, Authentication auth) {
    return returnObject == null
        || (returnObject instanceof Account a && auth.getName().equals(a.getOwner()));
}

Try / catch

try {
    Object result = securedService.method();
} catch (AccessDeniedException e) {
    log.warn("PostAuthorize rejected return object", e);
    throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
}

Prevention

When it happens

Trigger: A @PostAuthorize("returnObject.owner == authentication.name") (or similar) expression evaluates to false on the returned object; thrown from afterInvocation after the method body already executed.

Common situations: Returning an entity owned by another user; forgetting that returnObject may be null (filtering skipped, null returned if expression not evaluated) or accessing properties of the wrong return type; SpEL typo so expression compares unequal unexpectedly.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/d74b6f4ce73a19ce. Report an issue: GitHub.