Eventual Consistency when Performing Checks, Reads and Expand
note
Please note that at this point in time, it is not considered production-ready and does not come with any SLAs; availability and uptime are not guaranteed. Limitations of Auth0 FGA during the Developer Community Preview can be found here.
Background
The Auth0 FGA has been optimized for lower latency and high availability on all read requests rather than strong consistency. That means that results from the check, expand and read 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:
Eventually consistent, multi-region active-active database
The Auth0 FGA service uses an eventually consistent database in its implementation, that means that if a read request is performed on a region different than the original write request directly after the write request was made, the data returned by the read might not reflect the data written by the write.
For example, assuming the following authorization model
- DSL
- JSON
type document
relations
define viewer as self
define can_view as viewer
{
"type_definitions": [
{
"type": "document",
"relations": {
"viewer": {
"this": {}
},
"can_view": {
"computedUserset": {
"object": "",
"relation": "viewer"
}
}
}
}
]
}
Immediately after writing a tuple of Bob is a viewer of document:meeting_notes.doc
, calling check request on whether Bob can view document:meeting_notes.doc
may return false.
- Node.js
- Go
- .NET
- curl
- Pseudocode
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');
// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});
await fgaClient.write({
writes: {
tuple_keys: [
{ user: 'bob', relation: 'viewer', object: 'document:meeting_notes.doc'}
]
}
});
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
import (
fgaSdk "github.com/auth0-lab/fga-go-sdk"
"os"
)
func Main() {
configuration, err := fgaSdk.NewConfiguration(fgaSdk.UserConfiguration{
Environment: os.Getenv("FGA_ENVIRONMENT"),
StoreId: os.Getenv("FGA_STORE_ID"),
ClientId: os.Getenv("FGA_CLIENT_ID"),
ClientSecret: os.Getenv("FGA_CLIENT_SECRET"),
})
if err != nil {
// .. Handle error
}
fgaClient := fgaSdk.NewAPIClient(configuration)
}
body := fgaSdk.WriteRequest{
Writes: &fgaSdk.TupleKeys{
TupleKeys: []fgaSdk.TupleKey {
{
User: fgaSdk.PtrString("bob"),
Relation: fgaSdk.PtrString("viewer"),
Object: fgaSdk.PtrString("document:meeting_notes.doc"),
},
},
},
}
_, response, err := fgaClient.Auth0FgaApi.Write(context.Background()).Body(body).Execute()
if err != nil {
// .. Handle error
}
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
using Auth0.Fga.Api;
using Auth0.Fga.Configuration;
using Environment = System.Environment;
namespace ExampleApp;
class MyProgram {
static async Task Main() {
var storeId = Environment.GetEnvironmentVariable("FGA_STORE_ID");
var environment = Environment.GetEnvironmentVariable("FGA_ENVIRONMENT")
var configuration = new Configuration(storeId, environment) {
ClientId = Environment.GetEnvironmentVariable("FGA_CLIENT_ID"),
ClientSecret = Environment.GetEnvironmentVariable("FGA_CLIENT_SECRET"),
};
var fgaClient = new Auth0FgaApi(configuration);
}
}
await fgaClient.Write(new WriteRequest{
Writes = new TupleKeys(new List<TupleKey>() {
new() { User = "bob", Relation = "viewer", Object = "document:meeting_notes.doc" }
})
});
Get the Bearer Token and set up the FGA_API_URL environment variable
# Not needed when calling the Playground API
curl -X POST \
https://fga.us.auth0.com/oauth/token \
-H 'content-type: application/json' \
-d '{"client_id":"'$FGA_CLIENT_ID'","client_secret":"'$FGA_CLIENT_SECRET'","audience":"https://api.us1.fga.dev/","grant_type":"client_credentials"}'
# The response will be returned in the form
# {
# "access_token": "eyJ...Ggg",
# "expires_in": 86400,
# "scope": "read:tuples write:tuples check:tuples ... write:authorization-models",
# "token_type": "Bearer"
# }
# Store this `access_token` value in environment variable `FGA_BEARER_TOKEN`
# For non-playground environment
FGA_API_URL='https://api.us1.fga.dev'
# For playground environment
# FGA_API_URL='https://api.playground.fga.dev'
curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/write \
-H "Authorization: Bearer $FGA_BEARER_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{"writes": { "tuple_keys" : [{"user":"bob","relation":"viewer","object":"document:meeting_notes.doc"}] }}'
write([
{
"user":"bob",
"relation":"viewer",
"object":"document:meeting_notes.doc"
}
])
- Node.js
- Go
- .NET
- curl
- Pseudocode
- Playground
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');
// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});
// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'bob',
relation: 'can_view',
object: 'document:meeting_notes.doc',
},});
// allowed = false
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
import (
fgaSdk "github.com/auth0-lab/fga-go-sdk"
"os"
)
func Main() {
configuration, err := fgaSdk.NewConfiguration(fgaSdk.UserConfiguration{
Environment: os.Getenv("FGA_ENVIRONMENT"),
StoreId: os.Getenv("FGA_STORE_ID"),
ClientId: os.Getenv("FGA_CLIENT_ID"),
ClientSecret: os.Getenv("FGA_CLIENT_SECRET"),
})
if err != nil {
// .. Handle error
}
fgaClient := fgaSdk.NewAPIClient(configuration)
}
body := fgaSdk.CheckRequest{
TupleKey: &fgaSdk.TupleKey{
User: fgaSdk.PtrString("bob"),
Relation: fgaSdk.PtrString("can_view"),
Object: fgaSdk.PtrString("document:meeting_notes.doc"),
},
data, response, err := fgaClient.Auth0FgaApi.Check(context.Background()).Body(body).Execute()
// data = { allowed: false }
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
using Auth0.Fga.Api;
using Auth0.Fga.Configuration;
using Environment = System.Environment;
namespace ExampleApp;
class MyProgram {
static async Task Main() {
var storeId = Environment.GetEnvironmentVariable("FGA_STORE_ID");
var environment = Environment.GetEnvironmentVariable("FGA_ENVIRONMENT")
var configuration = new Configuration(storeId, environment) {
ClientId = Environment.GetEnvironmentVariable("FGA_CLIENT_ID"),
ClientSecret = Environment.GetEnvironmentVariable("FGA_CLIENT_SECRET"),
};
var fgaClient = new Auth0FgaApi(configuration);
}
}
// Run a check
var response = await fgaClient.Check(new CheckRequest(new TupleKey {
User = "bob",
Relation = "can_view",
Object = "document:meeting_notes.doc"
});
// response.Allowed = false
Get the Bearer Token and set up the FGA_API_URL environment variable
# Not needed when calling the Playground API
curl -X POST \
https://fga.us.auth0.com/oauth/token \
-H 'content-type: application/json' \
-d '{"client_id":"'$FGA_CLIENT_ID'","client_secret":"'$FGA_CLIENT_SECRET'","audience":"https://api.us1.fga.dev/","grant_type":"client_credentials"}'
# The response will be returned in the form
# {
# "access_token": "eyJ...Ggg",
# "expires_in": 86400,
# "scope": "read:tuples write:tuples check:tuples ... write:authorization-models",
# "token_type": "Bearer"
# }
# Store this `access_token` value in environment variable `FGA_BEARER_TOKEN`
# For non-playground environment
FGA_API_URL='https://api.us1.fga.dev'
# For playground environment
# FGA_API_URL='https://api.playground.fga.dev'
curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/check \
-H "Authorization: Bearer $FGA_BEARER_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{"tuple_key":{"user":"bob","relation":"can_view","object":"document:meeting_notes.doc"}}'
# Response: {"allowed":false}
check(
"bob", // check if the user `bob`
"can_view", // has an `can_view` relation
"document:meeting_notes.doc", // with the object `document:meeting_notes.doc`
);
Reply: false
is 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.
Caching DB requests & evaluations
To further reduce latency, Auth0 FGA is configured to cache certain evaluations for a brief period of time.
That means if a certain tuple is read (externally or internally) from the DB, is then modified by a write and then subsequently read - the result of the first read will be returned for up to 10 seconds after the first call.
So calling the following check request
- Node.js
- Go
- .NET
- curl
- Pseudocode
- Playground
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');
// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});
// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'bob',
relation: 'can_view',
object: 'document:meeting_notes.doc',
},});
// allowed = false
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
import (
fgaSdk "github.com/auth0-lab/fga-go-sdk"
"os"
)
func Main() {
configuration, err := fgaSdk.NewConfiguration(fgaSdk.UserConfiguration{
Environment: os.Getenv("FGA_ENVIRONMENT"),
StoreId: os.Getenv("FGA_STORE_ID"),
ClientId: os.Getenv("FGA_CLIENT_ID"),
ClientSecret: os.Getenv("FGA_CLIENT_SECRET"),
})
if err != nil {
// .. Handle error
}
fgaClient := fgaSdk.NewAPIClient(configuration)
}
body := fgaSdk.CheckRequest{
TupleKey: &fgaSdk.TupleKey{
User: fgaSdk.PtrString("bob"),
Relation: fgaSdk.PtrString("can_view"),
Object: fgaSdk.PtrString("document:meeting_notes.doc"),
},
data, response, err := fgaClient.Auth0FgaApi.Check(context.Background()).Body(body).Execute()
// data = { allowed: false }
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
using Auth0.Fga.Api;
using Auth0.Fga.Configuration;
using Environment = System.Environment;
namespace ExampleApp;
class MyProgram {
static async Task Main() {
var storeId = Environment.GetEnvironmentVariable("FGA_STORE_ID");
var environment = Environment.GetEnvironmentVariable("FGA_ENVIRONMENT")
var configuration = new Configuration(storeId, environment) {
ClientId = Environment.GetEnvironmentVariable("FGA_CLIENT_ID"),
ClientSecret = Environment.GetEnvironmentVariable("FGA_CLIENT_SECRET"),
};
var fgaClient = new Auth0FgaApi(configuration);
}
}
// Run a check
var response = await fgaClient.Check(new CheckRequest(new TupleKey {
User = "bob",
Relation = "can_view",
Object = "document:meeting_notes.doc"
});
// response.Allowed = false
Get the Bearer Token and set up the FGA_API_URL environment variable
# Not needed when calling the Playground API
curl -X POST \
https://fga.us.auth0.com/oauth/token \
-H 'content-type: application/json' \
-d '{"client_id":"'$FGA_CLIENT_ID'","client_secret":"'$FGA_CLIENT_SECRET'","audience":"https://api.us1.fga.dev/","grant_type":"client_credentials"}'
# The response will be returned in the form
# {
# "access_token": "eyJ...Ggg",
# "expires_in": 86400,
# "scope": "read:tuples write:tuples check:tuples ... write:authorization-models",
# "token_type": "Bearer"
# }
# Store this `access_token` value in environment variable `FGA_BEARER_TOKEN`
# For non-playground environment
FGA_API_URL='https://api.us1.fga.dev'
# For playground environment
# FGA_API_URL='https://api.playground.fga.dev'
curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/check \
-H "Authorization: Bearer $FGA_BEARER_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{"tuple_key":{"user":"bob","relation":"can_view","object":"document:meeting_notes.doc"}}'
# Response: {"allowed":false}
check(
"bob", // check if the user `bob`
"can_view", // has an `can_view` relation
"document:meeting_notes.doc", // with the object `document:meeting_notes.doc`
);
Reply: false
is bob related to document:meeting_notes.doc as can_view?
# Response: A red object indicating that the response from the API is `{"allowed":false}`
Will result in Auth0 FGA returning false to that same subsequent check for up to 10 seconds, even if the tuple (bob, reader, document:meeting_notes.doc)
was written and replicated during that time. Additionally any further read requests will not return the (bob, reader, document:meeting_notes.doc)
tuple till 10 seconds have passed from the original check request.
Implication
We have talked to a few customers and others in the industry regarding the need for strong consistency and concepts in the Zanzibar paper alluding to it (e.g. Zookies), and found that while strong consistency is important to some customers, availability and low latency are the essential requirements that users need.
You must take into consideration the eventually consistent design of Auth0 FGA when designing your services. Do not expect to write and then read that same data shortly after.
In most cases, when users share a resource, by the time they have sent the link and the recipient has clicked on it, the stale cache would have cleared.
Also watch out that the same applies when revoking access. What we found is that for many customers, they are fine with a user having access for a few seconds after access was revoked, but that may not be true for your use-case.
In case this is an issue for you, please reach out to us and we can discuss alternative solutions.