This is an autogenerated Java SDK for OpenFGA. It provides a wrapper around the OpenFGA API definition.
- About OpenFGA
- Resources
- Installation
- Getting Started
- Contributing
- License
OpenFGA is an open source Fine-Grained Authorization solution inspired by Google's Zanzibar paper. It was created by the FGA team at Auth0 based on Auth0 Fine-Grained Authorization (FGA), available under a permissive license (Apache-2) and welcomes community contributions.
OpenFGA is designed to make it easy for application builders to model their permission layer, and to add and integrate fine-grained authorization into their applications. OpenFGA’s design is optimized for reliability and low latency at a high scale.
- OpenFGA Documentation
- OpenFGA API Documentation
- X
- OpenFGA Community
- Zanzibar Academy
- Google's Zanzibar Paper (2019)
The OpenFGA Java SDK is available on Maven Central.
The OpenFGA Java SDK currently supports Java 17 as the minimum JDK version.
It can be used with the following:
- Gradle (Groovy)
implementation 'dev.openfga:openfga-sdk:0.9.11'- Gradle (Kotlin)
implementation("dev.openfga:openfga-sdk:0.9.11")- Apache Maven
<dependency>
<groupId>dev.openfga</groupId>
<artifactId>openfga-sdk</artifactId>
<version>0.9.11</version>
</dependency>- Ivy
<dependencyorg="dev.openfga"name="openfga-sdk"rev="0.9.11"/>- SBT
libraryDependencies +="dev.openfga"%"openfga-sdk"%"0.9.11"- Leiningen
[dev.openfga/openfga-sdk "0.9.11"]Learn how to initialize your SDK
We strongly recommend you initialize the OpenFgaClient only once and then re-use it throughout your app, otherwise you will incur the cost of having to re-initialize multiple times or at every request, the cost of reduced connection pooling and re-use, and would be particularly costly in the client credentials flow, as that flow will be preformed on every request.
The
Clientwill by default retry API requests up to 3 times. Rate limiting (429) errors are always retried. Server errors (5xx) are retried for all operations, with delay calculation usingRetry-Afterheaders when provided or exponential backoff as fallback.
importcom.fasterxml.jackson.databind.ObjectMapper;
importdev.openfga.sdk.api.client.OpenFgaClient;
importdev.openfga.sdk.api.configuration.ClientConfiguration;
importjava.net.http.HttpClient;
publicclassExample {
publicstaticvoidmain(String[] args) throwsException {
varconfig = newClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_MODEL_ID")); // Optional, can be overridden per requestvarfgaClient = newOpenFgaClient(config);
varresponse = fgaClient.readAuthorizationModels().get();
}
}importcom.fasterxml.jackson.databind.ObjectMapper;
importdev.openfga.sdk.api.client.OpenFgaClient;
importdev.openfga.sdk.api.configuration.ApiToken;
importdev.openfga.sdk.api.configuration.ClientConfiguration;
importdev.openfga.sdk.api.configuration.Credentials;
importjava.net.http.HttpClient;
publicclassExample {
publicstaticvoidmain(String[] args) throwsException {
varconfig = newClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_MODEL_ID")) // Optional, can be overridden per request
.credentials(newCredentials(
newApiToken(System.getenv("FGA_API_TOKEN")) // will be passed as the "Authorization: Bearer ${ApiToken}" request header
));
varfgaClient = newOpenFgaClient(config);
varresponse = fgaClient.readAuthorizationModels().get();
}
}importcom.fasterxml.jackson.databind.ObjectMapper;
importdev.openfga.sdk.api.client.OpenFgaClient;
importdev.openfga.sdk.api.configuration.ClientConfiguration;
importdev.openfga.sdk.api.configuration.ClientCredentials;
importdev.openfga.sdk.api.configuration.Credentials;
importjava.net.http.HttpClient;
publicclassExample {
publicstaticvoidmain(String[] args) throwsException {
varconfig = newClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_MODEL_ID")) // Optional, can be overridden per request
.credentials(newCredentials(
newClientCredentials()
.apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER")) // Full token endpoint URL, e.g. "https://issuer.fga.example/oauth/token"
.apiAudience(System.getenv("FGA_API_AUDIENCE"))
.clientId(System.getenv("FGA_CLIENT_ID"))
.clientSecret(System.getenv("FGA_CLIENT_SECRET"))
));
varfgaClient = newOpenFgaClient(config);
varresponse = fgaClient.readAuthorizationModels().get();
}
}The SDK supports standard OAuth2 client credentials flow for any OAuth2-compliant provider (e.g. Keycloak, Okta). The apiAudience parameter is optional, and an optional scopes parameter can be provided as a space-separated string. The apiTokenIssuer can be set to either a hostname (e.g. issuer.example.com, which defaults to https and appends /oauth/token) or a full token endpoint URL (e.g. https://mykeycloak.fga.example/realms/myrealm/protocol/openid-connect/token).
importcom.fasterxml.jackson.databind.ObjectMapper;
importdev.openfga.sdk.api.client.OpenFgaClient;
importdev.openfga.sdk.api.configuration.ClientConfiguration;
importdev.openfga.sdk.api.configuration.ClientCredentials;
importdev.openfga.sdk.api.configuration.Credentials;
importjava.net.http.HttpClient;
publicclassExample {
publicstaticvoidmain(String[] args) throwsException {
varconfig = newClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_MODEL_ID")) // Optional, can be overridden per request
.credentials(newCredentials(
newClientCredentials()
.apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER")) // Full token endpoint URL, e.g. "https://mykeycloak.fga.example/realms/myrealm/protocol/openid-connect/token"
.scopes(System.getenv("FGA_API_SCOPES")) // Optional, space-separated scopes
.clientId(System.getenv("FGA_CLIENT_ID"))
.clientSecret(System.getenv("FGA_CLIENT_SECRET"))
));
varfgaClient = newOpenFgaClient(config);
varresponse = fgaClient.readAuthorizationModels().get();
}
}You can set default headers to be sent with every request by using the defaultHeaders property of the ClientConfiguration class.
importcom.fasterxml.jackson.databind.ObjectMapper;
importdev.openfga.sdk.api.client.OpenFgaClient;
importdev.openfga.sdk.api.configuration.ClientConfiguration;
importjava.net.http.HttpClient;
importjava.util.Map;
publicclassExample {
publicstaticvoidmain(String[] args) throwsException {
varconfig = newClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL"))
.storeId(System.getenv("FGA_STORE_ID"))
.authorizationModelId(System.getenv("FGA_MODEL_ID"))
.defaultHeaders(Map.of(
"X-Custom-Header", "default-value",
"X-Request-Source", "my-app"
));
varfgaClient = newOpenFgaClient(config);
}
}You can set custom headers to be sent with a specific request by using the additionalHeaders property of the options classes (e.g. ClientReadOptions, ClientWriteOptions, etc.).
importcom.fasterxml.jackson.databind.ObjectMapper;
importdev.openfga.sdk.api.client.OpenFgaClient;
importdev.openfga.sdk.api.configuration.ClientConfiguration;
importjava.net.http.HttpClient;
publicclassExample {
publicstaticvoidmain(String[] args) throwsException {
varconfig = newClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL"))
.storeId(System.getenv("FGA_STORE_ID"))
.authorizationModelId(System.getenv("FGA_MODEL_ID"))
.defaultHeaders(Map.of(
"X-Custom-Header", "default-value",
"X-Request-Source", "my-app"
));
varfgaClient = newOpenFgaClient(config);
varoptions = newClientReadOptions()
.additionalHeaders(Map.of(
"X-Request-Id", "123e4567-e89b-12d3-a456-426614174000",
"X-Custom-Header", "overridden-value"// this will override the default value for this request only
)
);
varresponse = fgaClient.read(request, options).get();
}
}You need your store id to call the OpenFGA API (unless it is to call the CreateStore or ListStores methods).
If your server is configured with authentication enabled, you also need to have your credentials ready.
Get a paginated list of stores.
Passing
ClientListStoresOptionsis optional. All fields ofClientListStoresOptionsare optional.
varoptions = newClientListStoresOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
.pageSize(10)
.continuationToken("...");
varstores = fgaClient.listStores(options);
// stores = [{ "id": "01FQH7V8BEG3GPQW93KTRFR8JB", "name": "FGA Demo Store", "created_at": "2022-01-01T00:00:00.000Z", "updated_at": "2022-01-01T00:00:00.000Z" }]Initialize a store.
Passing
ClientCreateStoreOptionsis optional. All fields ofClientCreateStoreOptionsare optional.
varrequest = newCreateStoreRequest().name("FGA Demo");
varoptions = newClientCreateStoreOptions().additionalHeaders(Map.of("Some-Http-Header", "Some value"));
varstore = fgaClient.createStore(request, options).get();
// store.getId() = "01FQH7V8BEG3GPQW93KTRFR8JB"// store the store.getId() in database// update the storeId of the client instancefgaClient.setStoreId(store.getId());
// continue calling the API normallyGet information about the current store.
Requires a client initialized with a storeId
Passing
ClientGetStoreOptionsis optional. All fields ofClientGetStoreOptionsare optional.
varoptions = newClientGetStoreOptions().additionalHeaders(Map.of("Some-Http-Header", "Some value"));
varstore = fgaClient.getStore(options).get();
// store = { "id": "01FQH7V8BEG3GPQW93KTRFR8JB", "name": "FGA Demo Store", "created_at": "2022-01-01T00:00:00.000Z", "updated_at": "2022-01-01T00:00:00.000Z" }Delete a store.
Requires a client initialized with a storeId
Passing
ClientDeleteStoreOptionsis optional. All fields ofClientDeleteStoreOptionsare optional.
varoptions = newClientDeleteStoreOptions().additionalHeaders(Map.of("Some-Http-Header", "Some value"));
varstore = fgaClient.deleteStore(options).get();Read all authorization models in the store.
Passing
ClientReadAuthorizationModelsOptionsis optional. All fields ofClientReadAuthorizationModelsOptionsare optional.
varoptions = newClientReadAuthorizationModelsOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
.pageSize(10)
.continuationToken("...");
varresponse = fgaClient.readAuthorizationModels(options).get();
// response.getAuthorizationModels() = [// { id: "01GXSA8YR785C4FYS3C0RTG7B1", schemaVersion: "1.1", typeDefinitions: [...] },// { id: "01GXSBM5PVYHCJNRNKXMB4QZTW", schemaVersion: "1.1", typeDefinitions: [...] }];Create a new authorization model.
Note: To learn how to build your authorization model, check the Docs at https://openfga.dev/docs.
Learn more about the OpenFGA configuration language.
You can use the OpenFGA CLI or Syntax Transformer to convert between the OpenFGA DSL and the JSON authorization model.
Passing
ClientWriteAuthorizationModelOptionsis optional. All fields ofClientWriteAuthorizationModelOptionsare optional.
varrequest = newWriteAuthorizationModelRequest()
.schemaVersion("1.1")
.typeDefinitions(List.of(
newTypeDefinition().type("user").relations(Map.of()),
newTypeDefinition()
.type("document")
.relations(Map.of(
"writer", newUserset(),
"viewer", newUserset().union(newUsersets()
.child(List.of(
newUserset(),
newUserset().computedUserset(newObjectRelation().relation("writer"))
))
)
))
.metadata(newMetadata()
.relations(Map.of(
"writer", newRelationMetadata().directlyRelatedUserTypes(
List.of(newRelationReference().type("user"))
),
"viewer", newRelationMetadata().directlyRelatedUserTypes(
List.of(newRelationReference().type("user"))
)
))
)
));
varoptions = newClientWriteAuthorizationModelOptions().additionalHeaders(Map.of("Some-Http-Header", "Some value"));
varresponse = fgaClient.writeAuthorizationModel(request, options).get();
// response.getAuthorizationModelId() = "01GXSA8YR785C4FYS3C0RTG7B1"Read a particular authorization model.
Passing
ClientReadAuthorizationModelOptionsis optional. All fields ofClientReadAuthorizationModelOptionsare optional.
varoptions = newClientReadAuthorizationModelOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1");
varresponse = fgaClient.readAuthorizationModel(options).get();
// response.getAuthorizationModel().getId() = "01GXSA8YR785C4FYS3C0RTG7B1"// response.getAuthorizationModel().getSchemaVersion() = "1.1"// response.getAuthorizationModel().getTypeDefinitions() = [{ "type": "document", "relations": { ... } }, { "type": "user", "relations": { ... }}]Reads the latest authorization model (note: this ignores the model id in configuration).
Passing
ClientReadLatestAuthorizationModelOptionsis optional. All fields ofClientReadLatestAuthorizationModelOptionsare optional.
varoptions = newClientReadLatestAuthorizationModelOptions().additionalHeaders(Map.of("Some-Http-Header", "Some value"));
varresponse = fgaClient.readLatestAuthorizationModel(options).get();
// response.getAuthorizationModel().getId() = "01GXSA8YR785C4FYS3C0RTG7B1"// response.getAuthorizationModel().SchemaVersion() = "1.1"// response.getAuthorizationModel().TypeDefinitions() = [{ "type": "document", "relations": { ... } }, { "type": "user", "relations": { ... }}]Reads the list of historical relationship tuple writes and deletes.
Passing
ClientReadChangesOptionsis optional. All fields ofClientReadChangesOptionsare optional.
varstartTime = OffsetDateTime.parse("2022-01-01T00:00:00+00:00");
varrequest = newClientReadChangesRequest().type("document").startTime(startTime);
varoptions = newClientReadChangesOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
.pageSize(10)
.continuationToken("...");
varresponse = fgaClient.readChanges(request, options).get();
// response.getContinuationToken() = ...// response.getChanges() = [// { tupleKey: { user, relation, object }, operation: TupleOperation.WRITE, timestamp: ... },// { tupleKey: { user, relation, object }, operation: TupleOperation.DELETE, timestamp: ... }// ]Reads the relationship tuples stored in the database. It does not evaluate nor exclude invalid tuples according to the authorization model.
Passing
ClientReadOptionsis optional. All fields ofClientReadOptionsare optional.
// Find if a relationship tuple stating that a certain user is a viewer of a certain documentvarrequest = newClientReadRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a");
// Find all relationship tuples where a certain user has a relationship as any relation to a certain documentvarrequest = newClientReadRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a");
// Find all relationship tuples where a certain user is a viewer of any documentvarrequest = newClientReadRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:");
// Find all relationship tuples where any user has a relationship as any relation with a particular documentvarrequest = newClientReadRequest()
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a");
// Read all stored relationship tuplesvarrequest = newClientReadRequest();
varoptions = newClientReadOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
.pageSize(10)
.continuationToken("...");
varresponse = fgaClient.read(request, options).get();
// In all the above situations, the response will be of the form:// response = { tuples: [{ key: { user, relation, object }, timestamp }, ...]}Create and/or delete relationship tuples to update the system state.
Passing
ClientWriteOptionsis optional. All fields ofClientWriteOptionsare optional.
By default, write runs in a transaction mode where any invalid operation (deleting a non-existing tuple, creating an existing tuple, one of the tuples was invalid) or a server error will fail the entire operation.
varrequest = newClientWriteRequest()
.writes(List.of(
newTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"),
newTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2d-d36e-7cb3-a4a8-5d1d67a300c5")
))
.deletes(List.of(
newTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("writer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
));
varoptions = newClientWriteOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1")
.transactions(true);
varresponse = fgaClient.write(request, options).get();Convenience WriteTuples and DeleteTuples methods are also available.
The SDK will split the writes into separate chunks and send them in parallel. Each chunk is sent as its own transaction.
Passing
ClientWriteOptionswith.transactions(false)is required to use non-transaction mode. All other fields ofClientWriteOptionsare optional.
varrequest = newClientWriteRequest()
.writes(List.of(
newClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"),
newClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2d-d36e-7cb3-a4a8-5d1d67a300c5")
))
.deletes(List.of(
newClientTupleKeyWithoutCondition()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("writer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
));
varoptions = newClientWriteOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1")
.transactions(false)
.transactionChunkSize(5); // Max tuples per chunk; each chunk is sent as its own transactionvarresponse = fgaClient.write(request, options).get();Write conflict handling can be controlled using the onDuplicate option for writes and the onMissing option for deletes.
Note: this requires OpenFGA v1.10.0 or later.
onDuplicate: Controls behavior when attempting to create a tuple that already existsWriteRequestWrites.OnDuplicateEnum.ERROR(default): Return an errorWriteRequestWrites.OnDuplicateEnum.IGNORE: Skip the duplicate tuple and continue
onMissing: Controls behavior when attempting to delete a tuple that doesn't existWriteRequestDeletes.OnMissingEnum.ERROR(default): Return an errorWriteRequestDeletes.OnMissingEnum.IGNORE: Skip the missing tuple and continue
Using conflict options with the write() method:
varrequest = newClientWriteRequest()
.writes(List.of(
newClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
))
.deletes(List.of(
newClientTupleKeyWithoutCondition()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("writer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
));
varoptions = newClientWriteOptions()
.onDuplicate(WriteRequestWrites.OnDuplicateEnum.IGNORE)
.onMissing(WriteRequestDeletes.OnMissingEnum.IGNORE);
varresponse = fgaClient.write(request, options).get();Using conflict options with the writeTuples() convenience method:
vartuples = List.of(
newClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
);
varoptions = newClientWriteTuplesOptions()
.onDuplicate(WriteRequestWrites.OnDuplicateEnum.IGNORE);
varresponse = fgaClient.writeTuples(tuples, options).get();Using conflict options with the deleteTuples() convenience method:
vartuples = List.of(
newClientTupleKeyWithoutCondition()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("writer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
);
varoptions = newClientDeleteTuplesOptions()
.onMissing(WriteRequestDeletes.OnMissingEnum.IGNORE);
varresponse = fgaClient.deleteTuples(tuples, options).get();Check if a user has a particular relation with an object.
Passing
ClientCheckOptionsis optional. All fields ofClientCheckOptionsare optional.
varrequest = newClientCheckRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("writer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a");
varoptions = newClientCheckOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1");
varresponse = fgaClient.check(request, options).get();
// response.getAllowed() = trueSimilar to check, but instead of checking a single user-object relationship, accepts a list of relationships to check. Requires OpenFGA version 1.8.0 or greater.
Note: The order of
batchCheckresults is not guaranteed to match the order of the checks provided. UsecorrelationIdto pair responses with requests.
Passing
ClientBatchCheckOptionsis optional. All fields ofClientBatchCheckOptionsare optional.
varrequest = newClientBatchCheckRequest().checks(
List.of(
newClientBatchCheckItem()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
.correlationId("cor-1") // optional, one will be generated for you if not provided
.contextualTuples(List.of(
newClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("editor")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
)),
newClientCheckRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("admin")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"),
.correlationId("cor-2") // optional, one will be generated for you if not provided
.contextualTuples(List.of(
newClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("editor")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
)),
newClientCheckRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("creator")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
.correlationId("cor-3"), // optional, one will be generated for you if not providednewClientCheckRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("deleter")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
.correlationId("cor-4") // optional, one will be generated for you if not provided
)
);
varoptions = newClientBatchCheckOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1")
.maxParallelRequests(5); // Max number of requests to issue in parallel, defaults to 10
.maxBatchSize(20); // Max number of batches to split the list of checks into, defaults to 50varresponse = fgaClient.batchCheck(request, options).get();
/*response.getResult() = [{ allowed: false, correlationId: "cor-1", request: { user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation: "viewer", _object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", correlationId: "cor-1", contextualTuples: [{ user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation: "editor", _object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }] }, }, { allowed: false, correlationId: "cor-2", request: { user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation: "admin", _object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", correlationId: "cor-2", contextualTuples: [{ user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation: "editor", _object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }] } }, { allowed: false, correlationId: "cor-3", request: { user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation: "creator", _object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", correlationId: "cor-3", }, error: <FgaError ...> }, { allowed: true, correlationId: "cor-4", request: { user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation: "deleter", _object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", correlationId: "cor-4", } },]*/If you are using an OpenFGA version less than 1.8.0, you can use clientBatchCheck,
which calls check in parallel. It will return allowed: false if it encounters an error, and will return the error in the body.
If 429s are encountered, the underlying check will retry up to 3 times. For 5xx errors, all requests will retry with delay calculation using Retry-After headers when provided or exponential backoff as fallback.
var request = List.of(
new ClientBatchCheckItem()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
.contextualTuples(List.of(
new ClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("editor")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
)),
new ClientCheckRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("admin")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"),
.contextualTuples(List.of(
new ClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("editor")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
)),
new ClientCheckRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("creator")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"),
new ClientCheckRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("deleter")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
);
var options = new ClientBatchCheckClientOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1")
.maxParallelRequests(5); // Max number of requests to issue in parallel, defaults to 10
var response = fgaClient.batchCheck(request, options).get();
/*
response.getResponses() = [{
allowed: false,
request: {
user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
relation: "viewer",
_object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
contextualTuples: [{
user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
relation: "editor",
_object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"
}]
}
}, {
allowed: false,
request: {
user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
relation: "admin",
_object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
contextualTuples: [{
user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
relation: "editor",
_object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"
}]
}
}, {
allowed: false,
request: {
user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
relation: "creator",
_object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
},
error: <FgaError ...>
}, {
allowed: true,
request: {
user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b",
relation: "deleter",
_object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
}},
]
*/
Expands the relationships in userset tree format.
Passing
ClientExpandOptionsis optional. All fields ofClientExpandOptionsare optional.
varrequest = newClientExpandRequest()
.relation("viewer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a");
varoptions = newClientExpandOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1");
varresponse = fgaClient.expand(request, options).get();
// response.getTree().getRoot() = {"name":"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#viewer","leaf":{"users":{"users":["user:81684243-9356-4421-8fbf-a4f8d36aa31b","user:f52a4f7a-054d-47ff-bb6e-3ac81269988f"]}}}List the objects of a particular type a user has access to.
Passing
ClientListObjectsOptionsis optional. All fields ofClientListObjectsOptionsare optional.
varrequest = newClientListObjectsRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
.type("document")
.contextualTuples(List.of(
newClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("writer")
._object("document:0192ab2d-d36e-7cb3-a4a8-5d1d67a300c5")
));
varoptions = newClientListObjectsOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1");
varresponse = fgaClient.listObjects(request, options).get();
// response.getObjects() = ["document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"]List the relations a user has on an object.
Passing
ClientListRelationsOptionsis optional. All fields ofClientListRelationsOptionsare optional.
varrequest = newClientListRelationsRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
.relations(List.of("can_view", "can_edit", "can_delete", "can_rename"))
.contextualTuples(List.of(
newClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("editor")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
)
);
varoptions = newClientListRelationsOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// When unspecified, defaults to 10
.maxParallelRequests()
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId(DEFAULT_AUTH_MODEL_ID);
varresponse = fgaClient.listRelations(request, options).get();
// response.getRelations() = ["can_view", "can_edit"]List the users who have a certain relation to a particular type.
// Only a single filter is allowed for the time beingvaruserFilters = newArrayList<UserTypeFilter>() {
{
add(newUserTypeFilter().type("user"));
// user filters can also be of the form// add(new UserTypeFilter().type("team").relation("member"));
}
};
varrequest = newClientListUsersRequest()
._object(newFgaObject().type("document").id("roadmap"))
.relation("can_read")
.userFilters(userFilters)
.context(Map.of("view_count", 100))
.contextualTupleKeys(List.of(
newClientTupleKey()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("editor")
._object("folder:product"),
newClientTupleKey()
.user("folder:product")
.relation("parent")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
));
varoptions = newClientListUsersOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1");
varresponse = fgaClient.listUsers(request, options).get();
// response.getUsers() = [{object: {type: "user", id: "81684243-9356-4421-8fbf-a4f8d36aa31b"}}, {userset: { type: "user" }}, ...]Read assertions for a particular authorization model.
Passing
ClientReadAssertionsOptionsis optional. All fields ofClientReadAssertionsOptionsare optional.
varoptions = newClientReadAssertionsOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1");
varresponse = fgaClient.readAssertions(options).get();Update the assertions for a particular authorization model.
Passing
ClientWriteAssertionsOptionsis optional. All fields ofClientWriteAssertionsOptionsare optional.
varoptions = newClientWriteAssertionsOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1");
varassertions = List.of(
newClientAssertion()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("viewer")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
.expectation(true)
);
fgaClient.writeAssertions(assertions, options).get();The API Executor provides direct HTTP access to OpenFGA endpoints not yet wrapped by the SDK. It maintains the SDK's client configuration including authentication, telemetry, retries, and error handling.
Use cases:
- Calling endpoints not yet supported by the SDK
- Using an SDK version that lacks support for a particular endpoint
- Accessing custom endpoints that extend the OpenFGA API
Initialize the SDK normally and access the API Executor via the fgaClient instance:
// Initialize the client, same as aboveClientConfigurationconfig = newClientConfiguration()
.apiUrl("http://localhost:8080")
.storeId("01YCP46JKYM8FJCQ37NMBYHE5X");
OpenFgaClientfgaClient = newOpenFgaClient(config);
// Custom new endpoint that doesn't exist in the SDK yetMap<String, Object> requestBody = Map.of(
"user", "user:bob",
"action", "custom_action",
"resource", "resource:123"
);
// Build the requestApiExecutorRequestBuilderrequest = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/custom-endpoint")
.pathParam("store_id", storeId)
.queryParam("page_size", "20")
.queryParam("continuation_token", "eyJwayI6...")
.body(requestBody)
.header("X-Experimental-Feature", "enabled")
.build();// Get raw response without automatic decodingApiResponse<String> rawResponse = fgaClient.apiExecutor().send(request).get();
StringrawJson = rawResponse.getData();
System.out.println("Response: " + rawJson);
// You can access fields like headers, status code, etc. from rawResponse:System.out.println("Status Code: " + rawResponse.getStatusCode());
System.out.println("Headers: " + rawResponse.getHeaders());// Define a class to hold the responseclassCustomEndpointResponse {
privatebooleanallowed;
privateStringreason;
publicbooleanisAllowed() { returnallowed; }
publicvoidsetAllowed(booleanallowed) { this.allowed = allowed; }
publicStringgetReason() { returnreason; }
publicvoidsetReason(Stringreason) { this.reason = reason; }
}
// Get response decoded into CustomEndpointResponse classApiResponse<CustomEndpointResponse> response = fgaClient.apiExecutor()
.send(request, CustomEndpointResponse.class)
.get();
CustomEndpointResponsecustomEndpointResponse = response.getData();
System.out.println("Allowed: " + customEndpointResponse.isAllowed());
System.out.println("Reason: " + customEndpointResponse.getReason());
// You can access fields like headers, status code, etc. from response:System.out.println("Status Code: " + response.getStatusCode());
System.out.println("Headers: " + response.getHeaders());For streaming endpoints, use streamingApiExecutor instead. Pass the response class directly — the SDK handles the rest. It delivers each response object to a consumer callback as it arrives, and returns a CompletableFuture<Void> that completes when the stream is exhausted.
ApiExecutorRequestBuilderrequest = ApiExecutorRequestBuilder.builder(HttpMethod.POST, "/stores/{store_id}/streamed-list-objects")
.body(newListObjectsRequest().user("user:anne").relation("viewer").type("document"))
.build();
fgaClient.streamingApiExecutor(StreamedListObjectsResponse.class)
.stream(
request,
response -> System.out.println("Object: " + response.getObject()), // called per objecterror -> System.err.println("Stream error: " + error.getMessage()) // optional
)
.thenRun(() -> System.out.println("Streaming complete"))
.exceptionally(err -> {
System.err.println("Fatal error: " + err.getMessage());
returnnull;
});For a complete working example, see examples/api-executor.
See docs/ApiExecutor.md for complete API reference and examples for both ApiExecutor and StreamingApiExecutor.
The SDK implements RFC 9110 compliant retry behavior with support for the Retry-After header. By default, the SDK will automatically retry failed requests up to 3 times with delay calculation (maximum allowable: 15 retries).
Rate Limiting (429 errors): Always retried regardless of HTTP method.
Server Errors (5xx): All requests are retried on 5xx errors (except 501 Not Implemented) regardless of HTTP method:
- All operations (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS): Always retried on 5xx errors with delay calculation
- Retry-After header present: Uses the server-specified delay (supports both integer seconds and HTTP-date formats)
- No Retry-After header: Uses exponential backoff with jitter (base delay: 2^retryCount * 100ms, capped at 120 seconds)
- Minimum delay: Respects the configured
minimumRetryDelayas a floor value
Customize retry behavior using the ClientConfiguration builder. The SDK enforces a maximum of 15 retries to prevent accidental server overload:
- Configuration validation now prevents setting
maxRetriesabove 15 FgaErrornow exposes theRetry-Afterheader value viagetRetryAfterHeader()
importcom.fasterxml.jackson.databind.ObjectMapper;
importdev.openfga.sdk.api.client.OpenFgaClient;
importdev.openfga.sdk.api.configuration.ClientConfiguration;
importjava.net.http.HttpClient;
publicclassExample {
publicstaticvoidmain(String[] args) throwsException {
varconfig = newClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_MODEL_ID")) // Optional, can be overridden per request
.maxRetries(3) // retry up to 3 times on API requests (default: 3, maximum: 15)
.minimumRetryDelay(Duration.ofMillis(100)); // minimum wait time between retries in milliseconds (default: 100ms)varfgaClient = newOpenFgaClient(config);
varresponse = fgaClient.readAuthorizationModels().get();
}
}When handling errors, you can access the Retry-After header value for debugging or custom retry logic:
try {
varresponse = fgaClient.check(request).get();
} catch (ExecutionExceptione) {
if (e.getCause() instanceofFgaError) {
FgaErrorerror = (FgaError) e.getCause();
// Access Retry-After header if presentStringretryAfter = error.getRetryAfterHeader();
if (retryAfter != null) {
System.out.println("Server requested retry after: " + retryAfter + " seconds");
}
System.out.println("Error: " + error.getMessage());
}
}| Method | HTTP request | Description |
|---|---|---|
| batchCheck | POST /stores/{store_id}/batch-check | Send a list of `check` operations in a single request |
| check | POST /stores/{store_id}/check | Check whether a user is authorized to access an object |
| createStore | POST /stores | Create a store |
| deleteStore | DELETE /stores/{store_id} | Delete a store |
| expand | POST /stores/{store_id}/expand | Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship |
| getStore | GET /stores/{store_id} | Get a store |
| listObjects | POST /stores/{store_id}/list-objects | List all objects of the given type that the user has a relation with |
| listStores | GET /stores | List all stores |
| listUsers | POST /stores/{store_id}/list-users | List the users matching the provided filter who have a certain relation to a particular type. |
| read | POST /stores/{store_id}/read | Get tuples from the store that matches a query, without following userset rewrite rules |
| readAssertions | GET /stores/{store_id}/assertions/{authorization_model_id} | Read assertions for an authorization model ID |
| readAuthorizationModel | GET /stores/{store_id}/authorization-models/{id} | Return a particular version of an authorization model |
| readAuthorizationModels | GET /stores/{store_id}/authorization-models | Return all the authorization models for a particular store |
| readChanges | GET /stores/{store_id}/changes | Return a list of all the tuple changes |
| streamedListObjects | POST /stores/{store_id}/streamed-list-objects | Stream all objects of the given type that the user has a relation with |
| write | POST /stores/{store_id}/write | Add or delete tuples from the store |
| writeAssertions | PUT /stores/{store_id}/assertions/{authorization_model_id} | Upsert assertions for an authorization model ID |
| writeAuthorizationModel | POST /stores/{store_id}/authorization-models | Create a new authorization model |
This SDK supports producing metrics that can be consumed as part of an OpenTelemetry setup. For more information, please see the documentation
See CONTRIBUTING for details.
This project is licensed under the Apache-2.0 license. See the LICENSE file for more info.
The code in this repo was auto generated by OpenAPI Generator from a template based on the Java template, licensed under the Apache License 2.0.