Auth0 FGA Consistency Modes
Background
The Auth0 FGA service has been optimized for lower latency and high availability rather than strong consistency on all read requests and evaluation queries. That means that results from the Read and Check, ListObjects, ListUsers and Expand requests may not reflect the latest state and what was just written in the database, and might take a few seconds to converge.
Some of the choices that Auth0 FGA has made include:
Query Consistency Modes
To reduce latency, Auth0 FGA implements a multi-layer cache. It uses a database-level cache, which caches database reads, and a sub-problem cache, which caches partial query evaluations that can be reused across requests. Each cache has a TTL of 10 seconds. That means if a certain tuple is read from the DB, is then deleted and then subsequently read - the result of the first read might be returned for up to 20 seconds after the first call.
When querying Auth0 FGA using Read or any of the query APIs like Check, Expand, ListObjects and ListUsers, you can specify a query consistency parameter that can have one of the following values:
Name | Description |
---|---|
MINIMIZE_LATENCY (default) | Auth0 FGA will serve queries from the cache when possible |
HIGHER_CONSISTENCY | Auth0 FGA will skip the cache and query the database directly |
If you write a tuple and you immediately make a Check on a relation affected by that tuple using MINIMIZE_LATENCY
, the tuple change might not be taken in consideration if Auth0 FGA serves the result from the cache.
When to use higher consistency
When specifying HIGHER_CONSISTENCY
you are trading off consistency for latency and system performance. Always specifying HIGHER_CONSISTENCY
will have a significant impact in performance.
If you have a use case where higher consistency is needed, it's recommended that whenever possible, you decide in runtime the consistency level you need. If you are storing a timestamp indicating when a resource was last modified in your database, you can use that to decide the kind of request you do.
For example, if you share document:readme
with a user:anne
and you update a modified_date
field in the document
table when that happens, you can write code like the below when calling check("user:anne", "can_view", "document:readme")
to avoid paying the price of additional latency when calling the API.
if (date_modified + cache_time_to_live_period > Date.now()) {
const { allowed } = await fgaClient.check(
{ user: "user:anne", relation: "can_view", object: "document:roadmap"}
);
} else {
const { allowed } = await fgaClient.check(
{ user: "user:anne", relation: "can_view", object: "document:roadmap"},
{ consistency: ConsistencyPreference.HigherConsistency }
);
}
Eventually consistent, multi-region active-active database
The Auth0 FGA service uses an eventually consistent database in its implementation. This means that if a read request is performed on a region different than the region where the original write request was made, the data returned by the read might not reflect the data written by the write.
When you specify the HIGHER_CONSISTENCY
parameter in queries, Auth0 FGA will not use the cache, but it might still be affected by replication lag.
If your application services are hosted in a single cloud region, all the API requests will be always routed to the same Auth0 FGA region, and the replication lag will be negligible. However, if you have servers in multiple regions, Auth0 FGA might route requests to different regions, and the replication lag can still impact how consistent the API response is.
For example, assuming the following authorization model
- DSL
- JSON
model
schema 1.1
type user
type document
relations
define viewer: [user]
define can_view: viewer
{
"schema_version": "1.1",
"type_definitions": [
{
"type": "user",
"relations": {}
},
{
"type": "document",
"relations": {
"viewer": {
"this": {}
},
"can_view": {
"computedUserset": {
"object": "",
"relation": "viewer"
}
}
},
"metadata": {
"relations": {
"viewer": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"can_view": {
"directly_related_user_types": []
}
}
}
}
]
}
Immediately after writing a tuple saying that Bob is a viewer of document:meeting_notes.doc
, calling Check to see whether Bob can view document:meeting_notes.doc
may return false
.
- Node.js
- Go
- .NET
- Python
- Java
- curl
- CLI
- Pseudocode
Initialize the SDK
await fgaClient.write({
writes: [
{"user":"user:bob","relation":"viewer","object":"document:meeting_notes.doc"}
],
}, {
authorizationModelId: "01HVMMBCMGZNT3SED4Z17ECXCA"
});
Initialize the SDK
options := ClientWriteOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientWriteRequest{
Writes: []ClientTupleKey{
{
User: "user:bob",
Relation: "viewer",
Object: "document:meeting_notes.doc",
},
},
}
data, err := fgaClient.Write(context.Background()).
Body(body).
Options(options).
Execute()
if err != nil {
// .. Handle error
}
_ = data // use the response
Initialize the SDK
var options = new ClientWriteOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
new() {
User = "user:bob",
Relation = "viewer",
Object = "document:meeting_notes.doc"
}
},
};
var response = await fgaClient.Write(body, options);
Initialize the SDK
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientWriteRequest(
writes=[
ClientTuple(
user="user:bob",
relation="viewer",
object="document:meeting_notes.doc",
),
],
)
response = await fga_client.write(body, options)
Initialize the SDK
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()
.user("user:bob")
.relation("viewer")
._object("document:meeting_notes.doc")
));
var response = fgaClient.write(body, options).get();
Set the environment variables according to the "How to get your API keys"
curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/write \
-H "Authorization: Bearer $FGA_API_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{"writes": { "tuple_keys" : [{"user":"user:bob","relation":"viewer","object":"document:meeting_notes.doc"}] }, "authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"}'
Set the environment variables according to the "How to get your API keys"
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:bob viewer document:meeting_notes.doc
write([
{
"user":"user:bob",
"relation":"viewer",
"object":"document:meeting_notes.doc"
}
], authorization_model_id="01HVMMBCMGZNT3SED4Z17ECXCA")
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
- Pseudocode
- Playground
Initialize the SDK
// Run a check
const { allowed } = await fgaClient.check({
user: 'user:bob',
relation: 'can_view',
object: 'document:meeting_notes.doc',
}, {
authorizationModelId: '01HVMMBCMGZNT3SED4Z17ECXCA',
});
// allowed = false
Initialize the SDK
options := ClientCheckOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientCheckRequest{
User: "user:bob",
Relation: "can_view",
Object: "document:meeting_notes.doc",
}
data, err := fgaClient.Check(context.Background()).
Body(body).
Options(options).
Execute()
// data = { allowed: false }
Initialize the SDK
var options = new ClientCheckOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientCheckRequest {
User = "user:bob",
Relation = "can_view",
Object = "document:meeting_notes.doc",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = false
Initialize the SDK
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientCheckRequest(
user="user:bob",
relation="can_view",
object="document:meeting_notes.doc",
)
response = await fga_client.check(body, options)
# response.allowed = false
Initialize the SDK
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("user:bob")
.relation("can_view")
._object("document:meeting_notes.doc");
var response = fgaClient.check(body, options).get();
// response.getAllowed() = false
Set the environment variables according to the "How to get your API keys"
fga query check --store-id=$FGA_STORE_ID --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:bob can_view document:meeting_notes.doc
# Response: {"allowed":false}
Set the environment variables according to the "How to get your API keys"
curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/check \
-H "Authorization: Bearer $FGA_API_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA", "tuple_key":{"user":"user:bob","relation":"can_view","object":"document:meeting_notes.doc"}}'
# Response: {"allowed":false}
check(
user = "user:bob", // check if the user `user:bob`
relation = "can_view", // has an `can_view` relation
object = "document:meeting_notes.doc", // with the object `document:meeting_notes.doc`
authorization_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
);
Reply: false
is user:bob related to document:meeting_notes.doc as can_view?
# Response: A red object indicating that the response from the API is `{"allowed":false}`
Call flow for check and write. Notice that before cross-region replication happens, check will return false even though the tuple has already been written.