Access Control Plugin
Overview
The Access Control Plugin is a plugin that enforces security rules during JSON:API request processing without altering the core execution flow. It evaluates access requirements at well-defined stages of the request lifecycle and conditionally allows, anonymizes, or short-circuits parts of the request or response based on the resolved principal context.
In order to enable JsonApi4j Access Control Plugin (AC) - add the next dependency:
<dependency>
<groupId>pro.api4</groupId>
<artifactId>jsonapi4j-ac-plugin</artifactId>
<version>${jsonapi4j.version}</version>
</dependency>
If you’re using JsonApi4j in the scope of Spring Boot or Quarkus App - everything will be autoconfigured using default values.
Access control is applied in two phases:
- Inbound evaluation – before any data is fetched. Rules are evaluated against the incoming
JsonApiRequest. If access is denied, downstream execution is skipped and the response is safely anonymized. - Outbound evaluation – after data has been fetched and the JSON:API document has been composed. Rules are evaluated per resource and relationship element, allowing fine-grained control over visibility of attributes, meta, links, and relationship identifiers.
The plugin derives its rules from @AccessControl annotations placed on operations, resources, relationships, attributes, or individual fields.
During execution, it traverses JSON:API structures using explicit visitor points and applies access decisions consistently across resource objects and resource identifier objects.
This design enables declarative, centralized security policies while keeping domain logic and request handling clean, predictable, and specification-compliant.
Evaluation stages
As it was mentioned above access control evaluation is performed twice during the request lifecycle - during the inbound and outbound stages.

Inbound Evaluation Stage
During the inbound stage, the JsonApi4j application has received a request but has not yet fetched any data from downstream sources.
Access control rules are evaluated against the JsonApiRequest since no other data is available at this point.
If access control requirements are not met, data fetching is skipped, and the data field in the response will be fully anonymized.
Inbound access control requirements can be defined on an operations level by placing @AccessControl annotation on top of the class declaration. When implementing ResourceOperations<RESOURCE_DTO>, ToOneRelationshipOperations<RESOURCE_DTO, RELATIONSHIP_DTO> or ToManyRelationshipOperations<RESOURCE_DTO, RELATIONSHIP_DTO> interfaces @AccessControl annotation must be placed above the corresponding method.
Outbound Evaluation Stage
The outbound stage occurs after data has been fetched from the data source, the response document has been composed, and right before it is sent to the client. At this point, access control rules are evaluated for each JSON:API Resource Object or Resource Identifier Object within the generated JSON:API document.
Resource Documents
Resource documents typically contain full JSON:API Resource Objects.
Access control requirements can be defined for:
- Entire Resource Object - if requirements are not met, the whole resource is anonymized.
@AccessControlannotation must be placed on top of the class that implementsResource<RESOURCE_DTO>interface. - Specific members (e.g.,
attributes,links,meta) - if requirements are not met, only those members are anonymized.@AccessControlannotation must be placed above theresolveAttributes(...),resolveResourceLinks(...)or other methods accordingly. - Individual
attributefields - if requirements are not met, only the affected fields are anonymized.@AccessControlannotation must be placed for the needed field.
Relationship Documents
Relationship documents contain only Resource Identifier Objects. Access control rules can be defined for:
- Entire Resource Identifier Object - if requirements are not met, the entire resource identifier will be anonymized.
@AccessControlannotation must be placed on top of the class that implementsToOneRelationship<RELATIONSHIP_DTO>orToManyRelationship<RELATIONSHIP_DTO>interface. - Specific members (e.g.,
meta) - if requirements are not met, only those members will be anonymized.@AccessControlannotation must be placed above theresolveResourceIdentifierMeta(...)method.
Access Control Requirements
By default, JsonApi4j does not enforce any access control (i.e., all requests are allowed). However, you can configure and enforce access control rules for either or both stages - inbound and outbound - depending on your security and data exposure requirements.
There are four types of access control requirements, which can be combined in any way as needed:
- Authentication requirement - verifies whether the request is made on behalf of an authenticated client or user. This can be used to restrict anonymous access.
- Access tier requirement - verifies whether the client or user belongs to a specific access tier or group. The recommended default set of tiers includes: Root Admin, Admin, Partner, Internal, and Public. This structure helps organize access policies by predefined privilege levels. You don’t need to use all tiers - just rely on the ones that fit your needs. It’s also possible to define a custom set of access tiers. See more details below.
- OAuth2 scope(s) requirement - verifies whether the request was authorized to access user data protected by certain OAuth2 scopes. This information is typically embedded within the JWT access token.
- Ownership requirement - ensures that the requested resource belongs to the client or user making the request. This is typically used for APIs where users are only allowed to view their own data, but not others’.
If any of the specified requirements are not met, the corresponding section - or the entire object - will be anonymized.
Setting Principal Context
By default, the plugin uses the DefaultPrincipalResolver, which relies on the following HTTP headers to resolve the current authentication context:
X-Authenticated-User-Id- identifies whether the request is sent on behalf of an authenticated client or user. Considered authenticated if the value is not null or blank. Also used for ownership checks.X-Authenticated-Client-Access-Tier- defines the principal’s access tier. By default, the framework supports the following values: NO_ACCESS, PUBLIC, PARTNER, ADMIN, and ROOT_ADMIN. Custom tiers can be registered by implementing theAccessTierRegistryinterface.X-Authenticated-User-Granted-Scopes- specifies the OAuth2 scopes granted to the client by the user. This should be a space-separated string.
You can also implement a custom PrincipalResolver to define how the framework retrieves principal-related information from incoming HTTP requests.
The resolved principal context is then used by the framework during both inbound and outbound access control evaluations.
Setting Access Requirements
How and where should you declare your access control requirements?
There is one annotation that defines all access control requirement in one place - @AccessControl.
It encapsulates rules for all currently supported dimensions: authenticated, scopes, tier, and ownership. Just populate you requirements there.
Please review the list of examples down below to getter a better grasp how and where to declare your access requirements.
Examples
Example 1: Inbound Access Control
Let’s allow new user creation only for authenticated clients with the ADMIN access tier.
In this case, we’ll use the @AccessControl annotation to enforce the access rule at the operation level.
public class UserOperations implements ResourceOperations<UserDbEntity> {
@AccessControl(
authenticated = Authenticated.AUTHENTICATED,
tier = @AccessControlAccessTier(ADMIN_ACCESS_TIER)
)
@Override
public UserDbEntity create(JsonApiRequest request) {
// ...
}
}
Example 2: Outbound Access Control for Attributes Object
First, let’s limit access to a personal data for all non-authorized users.
Secondly, let’s hide the user’s credit card number from everyone except the owner. To achieve this, we need place the @AccessControl annotation on top of the class declaration and on the creditCardNumber field.
Notes:
authenticated = Authenticated.AUTHENTICATED- requires the framework to check whether the client that initiated this request is authenticated.@AccessControlScopes(requiredScopes = {"users.sensitive.read"})- forces the framework to check if client initiated this request has got permissions from the resource owner to access their sensitive data.@AccessControlOwnership(ownerIdFieldPath = "id")- tells the framework that the owner id is located in theidfield of the JSON:API Resource Object. That is true because we deal with users and user id represents who own this data.
@AccessControl(authenticated = Authenticated.AUTHENTICATED)
public class UserAttributes {
private final String firstName;
private final String lastName;
private final String email;
@AccessControl(
authenticated = Authenticated.AUTHENTICATED,
scopes = @AccessControlScopes(requiredScopes = {"users.sensitive.read"}),
tier = @AccessControlAccessTier(TierAdmin.ADMIN_ACCESS_TIER),
ownership = @AccessControlOwnership(ownerIdFieldPath = "id")
)
private final String creditCardNumber;
// ...
}
Example 3: Outbound Access Control for Resource Object
Now, let’s showcase how to hide some sections on the Resource Object level. Since we don’t have a dedicated class for it, we need to use our Resource declaration class for it.
Here is the list of available places where you can place @AccessControl annotation:
- On top of the Resource declaration - in order to control access to the entire JSON:API Resource Object
- For
Resource#resolveAttributes(...)method to control access just for resourceattributessection. As it was already shown above an alternative option is also to place@AccessControlon top of the attributes custom class. - For
Resource#resolveResourceLinks(...)method to control access just for resourcelinkssection. - For
Resource#resolveResourceMeta(...)method to control access just for resourcemetasection.
In the example below we’ve configured our entire UserResource in a way it’s visible only for authenticated users while its meta section is only visible for clients with ADMIN access tier:
@AccessControl(authenticated = Authenticated.AUTHENTICATED)
public class UserResource implements Resource<UserDbEntity> {
@AccessControl(tier = @AccessControlAccessTier(TierAdmin.ADMIN_ACCESS_TIER))
@Override
public Object resolveResourceMeta(JsonApiRequest request, UserDbEntity dataSourceDto) {
// ...
}
}
Example 4: Outbound Access Control for Resource Identifier Object
The last example will show how to hide some sections on the Resource Identifier Object level. This object is used for all relationship operations in a response document instead of well known Resource Object. Since we don’t have a dedicated class for it, we need to use our Relationship declaration class for it.
Here is the list of available places where you can place @AccessControl annotation:
- On top of the
Relationshipdeclaration - in order to control access to the entire JSON:API Resource Identifier Object - For
Relationship#resolveResourceIdentifierMeta(...)method to control access just for resource identifiermetasection.
In the example below we’ve configured our entire UserCitizenshipsRelationship in a way this relationship is visible only for authenticated users that have been granted ‘users.citizenships.read’ scope for a client. Moreover, ownership setting requires a user to be an owner; thus, this information is only visible for a user it belongs to. And finally, lets expose its meta section for clients with ADMIN access tier only:
@AccessControl(
authenticated = Authenticated.AUTHENTICATED,
scopes = @AccessControlScopes(requiredScopes = {"users.citizenships.read"}),
ownership = @AccessControlOwnership(ownerIdExtractor = ResourceIdFromUrlPathExtractor.class)
)
public class UserCitizenshipsRelationship implements ToManyRelationship<DownstreamCountry> {
@AccessControl(tier = @AccessControlAccessTier(TierAdmin.ADMIN_ACCESS_TIER))
@Override
public Object resolveResourceIdentifierMeta(JsonApiRequest relationshipRequest,
DownstreamCountry downstreamCountry) {
// ...
}
}
Notes
- If you’re using
@AccessControlannotation please note thatownershipsetting is different for inbound and outbound stages. If you want to configure these rules for the inbound stage - please useAccessControlOwnership#ownerIdExtractorproperty that allows you to tell the framework how to extract the owner id from the incoming request. For the outbound stage - useAccessControlOwnership#ownerIdFieldPathto point the framework to the field in the response that holds the owner id value. - If you’re working with
jsonapi4j-coremodule you can place@AccessControlannotation on either a customResourceObject, or anAttributesobject and their fields for the outbound evaluations. For the inbound evaluations the annotation can be also placed on the class-level of theRequestclass.
Available Properties
| Property name | Default value | Description |
|---|---|---|
jsonapi4j.ac.enabled |
true |
Enables/Disables Access Control plugin |