OpenFGA
Since Camel 4.23
Only producer is supported
The OpenFGA component turns a Camel route into a Policy Enforcement Point (PEP) in front of OpenFGA, the CNCF authorization engine that implements Google’s Zanzibar model. Where a policy engine asks what do the rules say, OpenFGA answers a different question: what is this subject’s relationship to this resource.
That distinction is the reason the component exists. Expressing "anne may read document:budget because she owns the folder it lives in" as policy-as-code means shipping the relationship graph into the policy input on every message, which does not scale past a handful of relationships. OpenFGA keeps the graph — a set of (user, relation, object) tuples — in the decision point, and the route asks it a question instead of handing it data.
The component is a companion to camel-opa and camel-spiffe, and deliberately shares camel-opa’s shape and its security posture, so that an operator who knows one knows the other:
| establishes who the caller is (workload identity, X.509-SVID / JWT-SVID) |
| evaluates what the rules say (policy-as-code, Rego) |
camel-openfga | answers what the caller’s relationship to the resource is (ReBAC, relationship tuples) |
Maven users will need to add the following dependency to their pom.xml.
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-openfga</artifactId>
<version>x.x.x</version>
<!-- use the same version as your Camel core version -->
</dependency> URI Format
openfga:operation[?options]
Where operation is one of check, batchCheck, listObjects, listRelations, listUsers, writeTuples or deleteTuples.
Configuring Options
Camel components are configured on two separate levels:
-
component level
-
endpoint level
Configuring Component Options
At the component level, you set general and shared configurations that are, then, inherited by the endpoints. It is the highest configuration level.
For example, a component may have security settings, credentials for authentication, urls for network connection and so forth.
Some components only have a few options, and others may have many. Because components typically have pre-configured defaults that are commonly used, then you may often only need to configure a few options on a component; or none at all.
You can configure components using:
-
the Component DSL.
-
in a configuration file (
application.properties,*.yamlfiles, etc). -
directly in the Java code.
Configuring Endpoint Options
You usually spend more time setting up endpoints because they have many options. These options help you customize what you want the endpoint to do. The options are also categorized into whether the endpoint is used as a consumer (from), as a producer (to), or both.
Configuring endpoints is most often done directly in the endpoint URI as path and query parameters. You can also use the Endpoint DSL and DataFormat DSL as a type safe way of configuring endpoints and data formats in Java.
A good practice when configuring options is to use Property Placeholders.
Property placeholders provide a few benefits:
-
They help prevent using hardcoded urls, port numbers, sensitive information, and other settings.
-
They allow externalizing the configuration from the code.
-
They help the code to become more flexible and reusable.
The following two sections list all the options, firstly for the component followed by the endpoint.
Component Options
The OpenFGA component supports the following options which are listed below.
| Name | Description | Default | Type |
|---|---|---|---|
The base URL of the OpenFGA HTTP API, without a trailing path. The default assumes OpenFGA running as a sidecar on its standard HTTP port. | String | ||
The identifier of the authorization model revision to evaluate against. Leave it empty to use whichever model the store considers latest. Pin it in production. A store keeps every model it was ever given and latest moves the moment somebody writes a new one, so an unpinned endpoint can start answering a different question than the one it was reviewed with - without any change to the route. Pinning also makes a model rollout a deliberate, reviewable configuration change. | String | ||
The component configuration. | OpenFgaConfiguration | ||
The consistency the query is answered with. OpenFGA’s default, MINIMIZE_LATENCY, may answer from a replica that has not caught up yet, which right after a revoke means a tuple that was deleted can still grant access for a moment. Set HIGHER_CONSISTENCY on the paths where that window matters, at the cost of latency. Left unset, OpenFGA’s own default applies. Enum values:
| String | ||
Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel’s routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing. | false | boolean | |
The object being accessed, as an OpenFGA object identifier such as \{code document:budget}. Evaluated as a Simple expression against each exchange, so document:$\{header.documentId} names the resource the message is about. Unlike the subject, taking the object from a header is normal and safe: the caller is entitled to say which resource it wants, and the check is what decides whether it may have it. | String | ||
The relation to demand, such as reader or owner. Evaluated as a Simple expression against each exchange, though a literal is what you usually want. The relation is the permission being demanded, so resolving it from an inbound header lets the caller pick the weakest one the model defines. Keep it literal, or derive it from something the route controls such as $\{header.CamelHttpMethod}. | String | ||
Comma-separated list of relations the listRelations operation asks about, for example reader,writer,owner. Only the ones the subject actually holds come back. | String | ||
Required The identifier of the OpenFGA store holding the relationship tuples and the authorization model, as returned by \{code fga store create}. The store is the relationship graph that judges the exchange, so it comes from the endpoint only and is never taken from a message header. | String | ||
The object type to enumerate for the listObjects operation, for example document. This is a type name from the authorization model, so it is taken literally rather than evaluated. | String | ||
The subject to authorize, as an OpenFGA user identifier such as \{code user:anne}. Evaluated as a Simple expression against each exchange, so a literal is used as-is and user:$\{exchangeProperty.CamelKeycloakTokenSubject} resolves whatever an earlier step established. Read it from an exchange property rather than a header wherever you can. An exchange property is set by the route itself - by the step that verified the caller - and nothing outside the route can set one. A header, by contrast, is often whatever the caller sent, and an endpoint configured as user:$\{header.userId} lets the caller choose who to be. \{code camel-keycloak}'s KeycloakSecurityPolicy already follows that reasoning: it reads the subject from the CamelKeycloakTokenSubject exchange property in preference to the header of the same name, its preferPropertyOverHeader option defaulting to true. Nothing in Camel sets the property for you, so the step that validates the token has to record it - but recording it under that name lets one identity serve both. An expression that resolves to blank, or to a bare \{code user:} prefix, denies the exchange: an exchange carrying no identity is not authorized, and failOpen does not apply to it. | String | ||
Comma-separated list of user filters for the listUsers operation, naming which kinds of subject to return. An entry is either a type, user, or a type and a relation, team#member, to return the usersets holding the relation rather than the individual subjects. Defaults to user. | String | ||
Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc. | true | boolean | |
How long to wait for the connection to OpenFGA to be established. The component applies this itself rather than through the SDK’s own connectTimeout setting, which as of openfga-sdk 0.10.1 is accepted and then never read, leaving the connect phase bounded only by the operating system. | 10000 | long | |
How many of a batchCheck’s checks may be in flight at once. The batch is issued as one request per object, so this bounds the load one exchange puts on the server. | 10 | int | |
How many times the SDK retries a request that failed in a way worth retrying, such as a rate limit or a 5xx. Set it to 0 to disable retries; the overall wait a routing thread can spend on one exchange grows with it. | 3 | int | |
Autowired An existing OpenFgaClient to use. When set, every option describing how to reach the server - apiUrl, storeId, the credentials, the timeouts and sslContextParameters - is ignored, because they are baked into the client that was handed over. | OpenFgaClient | ||
How long to wait for one request to OpenFGA to complete once connected. A request that times out is a failure to obtain a verdict rather than a deny, so it fails closed - or proceeds when failOpen is set. | 10000 | long | |
Used for enabling or disabling all consumer based health checks from this component. | true | boolean | |
Used for enabling or disabling all producer based health checks from this component. Notice: Camel has by default disabled all producer based health-checks. You can turn on producer checks globally by setting camel.health.producersEnabled=true. | true | boolean | |
The audience to request the access token for in the client-credentials flow. | String | ||
Pre-shared token sent to OpenFGA in the Authorization header, for a server started with \{code --authn-method preshared}. | String | ||
The token endpoint the client-credentials flow exchanges its credentials at. | String | ||
Client identifier for the OAuth 2.0 client-credentials flow, for a server that authenticates through an OIDC provider. Setting it selects that flow, so clientSecret, apiTokenIssuer and apiAudience are then required too. | String | ||
Client secret for the OAuth 2.0 client-credentials flow. | String | ||
Whether to let the exchange proceed when OpenFGA could not be asked at all, for example because the server is unreachable. Disabled by default so that an unavailable decision point denies rather than grants access. Do not enable this in production. It applies to the check operation and to OpenFgaSecurityPolicy, the two places where proceed has a meaning, and it covers only a failure to obtain a verdict. An exchange that was denied, and an exchange that carried no usable subject or object, are decisions rather than failures and are never turned into an allow by this flag. The other operations ignore it. A batchCheck or listObjects that failed has no safe way to proceed - returning the objects it never managed to filter would be the leak the filtering was there to prevent - so a failure there is reported as an error for the route’s own error handling to deal with. | false | boolean | |
Space-separated scopes to request in the client-credentials flow. | String | ||
TLS configuration for the connection to OpenFGA. Needed to trust a server whose certificate comes from a private CA, and to present a client certificate to a server that requires mutual TLS - a SPIFFE X.509-SVID obtained with \{code camel-spiffe}, for instance, so the workload authenticates to the decision point as itself. | SSLContextParameters | ||
Enable usage of global SSL context parameters. | false | boolean |
Endpoint Options
The OpenFGA endpoint is configured using URI syntax:
openfga:operation
With the following path and query parameters:
Path Parameters
| Name | Description | Default | Type |
|---|---|---|---|
Required The operation to perform. The operation is taken from the endpoint only: it is deliberately not overridable by a message header, so that an inbound message cannot turn a check into a tuple write, nor a check for one relation into a check for a weaker one. Enum values:
| OpenFgaOperation |
Query Parameters
| Name | Description | Default | Type |
|---|---|---|---|
The base URL of the OpenFGA HTTP API, without a trailing path. The default assumes OpenFGA running as a sidecar on its standard HTTP port. | String | ||
The identifier of the authorization model revision to evaluate against. Leave it empty to use whichever model the store considers latest. Pin it in production. A store keeps every model it was ever given and latest moves the moment somebody writes a new one, so an unpinned endpoint can start answering a different question than the one it was reviewed with - without any change to the route. Pinning also makes a model rollout a deliberate, reviewable configuration change. | String | ||
The consistency the query is answered with. OpenFGA’s default, MINIMIZE_LATENCY, may answer from a replica that has not caught up yet, which right after a revoke means a tuple that was deleted can still grant access for a moment. Set HIGHER_CONSISTENCY on the paths where that window matters, at the cost of latency. Left unset, OpenFGA’s own default applies. Enum values:
| String | ||
The object being accessed, as an OpenFGA object identifier such as \{code document:budget}. Evaluated as a Simple expression against each exchange, so document:$\{header.documentId} names the resource the message is about. Unlike the subject, taking the object from a header is normal and safe: the caller is entitled to say which resource it wants, and the check is what decides whether it may have it. | String | ||
The relation to demand, such as reader or owner. Evaluated as a Simple expression against each exchange, though a literal is what you usually want. The relation is the permission being demanded, so resolving it from an inbound header lets the caller pick the weakest one the model defines. Keep it literal, or derive it from something the route controls such as $\{header.CamelHttpMethod}. | String | ||
Comma-separated list of relations the listRelations operation asks about, for example reader,writer,owner. Only the ones the subject actually holds come back. | String | ||
Required The identifier of the OpenFGA store holding the relationship tuples and the authorization model, as returned by \{code fga store create}. The store is the relationship graph that judges the exchange, so it comes from the endpoint only and is never taken from a message header. | String | ||
The object type to enumerate for the listObjects operation, for example document. This is a type name from the authorization model, so it is taken literally rather than evaluated. | String | ||
The subject to authorize, as an OpenFGA user identifier such as \{code user:anne}. Evaluated as a Simple expression against each exchange, so a literal is used as-is and user:$\{exchangeProperty.CamelKeycloakTokenSubject} resolves whatever an earlier step established. Read it from an exchange property rather than a header wherever you can. An exchange property is set by the route itself - by the step that verified the caller - and nothing outside the route can set one. A header, by contrast, is often whatever the caller sent, and an endpoint configured as user:$\{header.userId} lets the caller choose who to be. \{code camel-keycloak}'s KeycloakSecurityPolicy already follows that reasoning: it reads the subject from the CamelKeycloakTokenSubject exchange property in preference to the header of the same name, its preferPropertyOverHeader option defaulting to true. Nothing in Camel sets the property for you, so the step that validates the token has to record it - but recording it under that name lets one identity serve both. An expression that resolves to blank, or to a bare \{code user:} prefix, denies the exchange: an exchange carrying no identity is not authorized, and failOpen does not apply to it. | String | ||
Comma-separated list of user filters for the listUsers operation, naming which kinds of subject to return. An entry is either a type, user, or a type and a relation, team#member, to return the usersets holding the relation rather than the individual subjects. Defaults to user. | String | ||
Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel’s routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing. | false | boolean | |
How long to wait for the connection to OpenFGA to be established. The component applies this itself rather than through the SDK’s own connectTimeout setting, which as of openfga-sdk 0.10.1 is accepted and then never read, leaving the connect phase bounded only by the operating system. | 10000 | long | |
How many of a batchCheck’s checks may be in flight at once. The batch is issued as one request per object, so this bounds the load one exchange puts on the server. | 10 | int | |
How many times the SDK retries a request that failed in a way worth retrying, such as a rate limit or a 5xx. Set it to 0 to disable retries; the overall wait a routing thread can spend on one exchange grows with it. | 3 | int | |
Autowired An existing OpenFgaClient to use. When set, every option describing how to reach the server - apiUrl, storeId, the credentials, the timeouts and sslContextParameters - is ignored, because they are baked into the client that was handed over. | OpenFgaClient | ||
How long to wait for one request to OpenFGA to complete once connected. A request that times out is a failure to obtain a verdict rather than a deny, so it fails closed - or proceeds when failOpen is set. | 10000 | long | |
The audience to request the access token for in the client-credentials flow. | String | ||
Pre-shared token sent to OpenFGA in the Authorization header, for a server started with \{code --authn-method preshared}. | String | ||
The token endpoint the client-credentials flow exchanges its credentials at. | String | ||
Client identifier for the OAuth 2.0 client-credentials flow, for a server that authenticates through an OIDC provider. Setting it selects that flow, so clientSecret, apiTokenIssuer and apiAudience are then required too. | String | ||
Client secret for the OAuth 2.0 client-credentials flow. | String | ||
Whether to let the exchange proceed when OpenFGA could not be asked at all, for example because the server is unreachable. Disabled by default so that an unavailable decision point denies rather than grants access. Do not enable this in production. It applies to the check operation and to OpenFgaSecurityPolicy, the two places where proceed has a meaning, and it covers only a failure to obtain a verdict. An exchange that was denied, and an exchange that carried no usable subject or object, are decisions rather than failures and are never turned into an allow by this flag. The other operations ignore it. A batchCheck or listObjects that failed has no safe way to proceed - returning the objects it never managed to filter would be the leak the filtering was there to prevent - so a failure there is reported as an error for the route’s own error handling to deal with. | false | boolean | |
Space-separated scopes to request in the client-credentials flow. | String | ||
TLS configuration for the connection to OpenFGA. Needed to trust a server whose certificate comes from a private CA, and to present a client certificate to a server that requires mutual TLS - a SPIFFE X.509-SVID obtained with \{code camel-spiffe}, for instance, so the workload authenticates to the decision point as itself. | SSLContextParameters |
Message Headers
The OpenFGA component supports the following message header(s), which is/are listed below:
| Name | Description | Default | Type |
|---|---|---|---|
CamelOpenFgaAllowed (producer) Constant: | The allow/deny verdict of the authorization check. Always overwritten by the component, so a value set by an inbound message never survives into the route. | Boolean | |
CamelOpenFgaDenyReason (producer) Constant: | Why the exchange was denied, set only on a deny. denied when OpenFGA evaluated the relationship and answered no; missing-user, missing-object, missing-relation, wildcard-subject or invalid-identifier when the exchange never reached OpenFGA because what it carried could not be used as a subject or an object. | String | |
CamelOpenFgaFailedOpen (producer) Constant: | Set to true only when the exchange proceeded because failOpen is enabled and OpenFGA could not be asked - nothing authorized it. Absent on every verdict OpenFGA actually gave, so a route or an audit trail can tell the two apart rather than seeing the same CamelOpenFgaAllowed=true for both. | Boolean | |
| Constant: | The subject the check was made for, as resolved from the endpoint’s user expression. Set for observability; it is not read as an input. | String | |
| Constant: | The object the check was made against, as resolved from the endpoint’s object expression. Set for observability; it is not read as an input. | String | |
CamelOpenFgaRelation (producer) Constant: | The relation that was checked. Set for observability; it is not read as an input and cannot be used to demand a weaker permission than the endpoint configured. | String | |
CamelOpenFgaStoreId (producer) Constant: | The identifier of the OpenFGA store that was consulted, so an audit trail records which relationship graph produced the verdict. | String | |
CamelOpenFgaWrittenTuples (producer) Constant: | How many relationship tuples the writeTuples operation wrote. | Integer | |
CamelOpenFgaDeletedTuples (producer) Constant: | How many relationship tuples the deleteTuples operation deleted. | Integer |
Resolving the subject and the object
user, object and relation are evaluated as Simple expressions against each Exchange, so a literal is used as written and an expression resolves per message:
to("openfga:check"
+ "?storeId=01HQMVAJ..."
+ "&relation=reader"
+ "&user=user:${exchangeProperty.CamelKeycloakTokenSubject}"
+ "&object=document:${header.documentId}"); The component evaluates these itself, per Exchange, so a plain to() is enough — there is no need for toD() and the endpoint cache it churns.
| Read the subject from an exchange property, not a header An endpoint configured as Put the subject where the caller cannot reach it. An exchange property set by the step in your route that verified the caller cannot be set from outside the route, and a header usually can. Name that property here, and let authentication decide who the caller is. There is a convention worth following rather than inventing one: Taking the object from a header is a different matter and is entirely normal: the caller is entitled to say which resource it wants, and the check is what decides whether it may have it. |
Usage
As a producer, deciding with a filter
The check operation records the verdict in the CamelOpenFgaAllowed header and leaves the message body untouched, so the route decides what to do with it:
from("platform-http:/documents")
.to("openfga:check?storeId={{fga.store}}&authorizationModelId={{fga.model}}"
+ "&relation=reader&user=user:${exchangeProperty.CamelKeycloakTokenSubject}"
+ "&object=document:${header.documentId}")
.choice()
.when(header("CamelOpenFgaAllowed").isEqualTo(true))
.to("direct:serveDocument")
.otherwise()
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(403)); As a security policy, stopping the route
OpenFgaSecurityPolicy is an AuthorizationPolicy, so the check guards a section of the route and a denial throws CamelAuthorizationException for the usual onException machinery to handle:
OpenFgaSecurityPolicy policy = new OpenFgaSecurityPolicy();
policy.setApiUrl("http://openfga:8080");
policy.setStoreId("01HQMVAJ...");
policy.setAuthorizationModelId("01HQMVAK...");
policy.setRelation("writer");
policy.setUser("user:${exchangeProperty.CamelKeycloakTokenSubject}");
policy.setObject("document:${header.documentId}");
from("platform-http:/documents")
.policy(policy)
.to("direct:updateDocument"); Filtering a collection
batchCheck takes the object identifiers from the body and replaces it with the ones the check allowed, in the order the body asked in:
from("direct:listDocuments")
.to("sql:SELECT id FROM documents")
// sql: answers with a List<Map>, one entry per row, so turn it into the object identifiers first - a Map
// would stringify to {ID=budget}, which is not an identifier and would simply be skipped
.process(exchange -> {
List<Map<String, Object>> rows = exchange.getMessage().getBody(List.class);
exchange.getMessage().setBody(rows.stream().map(row -> "document:" + row.get("id")).toList());
})
.to("openfga:batchCheck?storeId={{fga.store}}&relation=reader"
+ "&user=user:${exchangeProperty.CamelKeycloakTokenSubject}")
.to("direct:render"); The body comes back holding only the identifiers the check allowed, in the order it was given them.
listObjects answers the same question from the other end — ask OpenFGA which objects the subject can reach, rather than filtering a list you already have:
to("openfga:listObjects?storeId={{fga.store}}&relation=reader"
+ "&user=user:${exchangeProperty.CamelKeycloakTokenSubject}&type=document");
// body becomes [document:budget, document:roadmap] listRelations and listUsers round this out: what a subject may do with one object, and who may do something with it. Both replace the body with a List<String>.
Granting and revoking access
A route that creates a resource usually has to grant access to it too. writeTuples takes its tuples from the body, and falls back to the endpoint’s user/relation/object when the body carries none — which is the shape that reads best right after a resource was created:
from("direct:createDocument")
.to("sql:INSERT INTO documents ...")
.to("openfga:writeTuples?storeId={{fga.store}}"
+ "&user=user:${exchangeProperty.CamelKeycloakTokenSubject}&relation=owner"
+ "&object=document:${header.documentId}"); For several tuples at once, leave user/relation/object unset and put them in the body instead — a collection of Map`s with `user, relation and object entries, or of ClientTupleKey objects. deleteTuples accepts the same shapes and revokes instead.
| The endpoint wins over the body If the endpoint configures any of The same rule that governs a check governs a write: the configuration decides and the message does not. Were the body preferred, a route that unmarshals an untrusted payload before writing would hand the caller the choice of which relationship to grant — and A partly configured triple is treated as a mistake rather than an invitation to fill the rest in from the message: the producer fails and names the part that is missing. |
A typed wildcard — user:*, OpenFGA’s "everyone" — is accepted here, because writing that tuple is how a resource is shared publicly and is a deliberate act by the route. It is refused as the subject of a check; see below.
Security
It fails closed
An OpenFGA server that cannot be reached, or that answers with an error, denies. The failOpen option reverses that and is off by default; it is marked insecure:dev and should not be enabled in production.
failOpen covers only a failure to obtain an answer, and only for check and for OpenFgaSecurityPolicy — the two places where "proceed" means something. It never turns a denial into an allow, and it never applies to an Exchange that carried no usable subject (see below).
When failOpen does let an exchange through, it carries CamelOpenFgaFailedOpen=true. The verdict header alone cannot distinguish "OpenFGA allowed this" from "OpenFGA was never reached and we were told to proceed", and an audit asking which exchanges went through unauthorized needs something it can filter on. The marker is set only on that deliberate path, and it is cleared on entry like the other decision headers so a sender cannot preload it.
It also does not apply when OpenFGA answers with an HTTP 4xx. A 400 means the question was malformed, a 401 or 403 that this client may not ask it, a 404 that what was asked about does not exist — none of which is a decision point that has gone away. This matters concretely: an object identifier can get past the component’s own guards and still be rejected by the server (document:a:b and document:x#y are both HTTP 400 on OpenFGA 1.21.0), and if failOpen covered that, any caller able to influence the identifier could turn a check into an allow. Those fail closed, and the component logs why it did not fail open. A 429 is the one 4xx that does count as unavailable — it means "not right now" — as do transport errors, timeouts and 5xx.
The rule is an allowlist rather than a list of exclusions: failOpen applies to a rate limit, a 5xx, a transport error and a timeout, and to nothing else. A failure the component does not recognise — a request the SDK refused to build, input it could not serialise, an interrupt during shutdown — is not evidence that OpenFGA was unreachable, so it denies. Anything unfamiliar therefore fails closed rather than being waved through for being unfamiliar.
The query operations ignore failOpen entirely: a batchCheck or listObjects that failed has no safe way to proceed, since handing back the objects it never managed to filter would be exactly the leak the filtering was there to prevent.
What is asked is not up to the message
storeId, authorizationModelId, relation and the operation all come from the endpoint. An inbound message cannot point the check at a different store, pin an older model revision, downgrade the relation demanded from owner to reader, or turn a check into a tuple write.
The CamelOpenFga* headers are outputs only. They are cleared on entry — before the expressions are evaluated — written on every evaluation, and never read back as inputs, so a verdict a message arrived with never survives into the route, including down the paths that throw.
A missing identity is a denial, not a failure
If the user or object expression resolves to blank — or to a bare user: prefix, which is what a configured prefix plus an unresolved expression leaves behind — the Exchange is denied with CamelOpenFgaDenyReason set to missing-user or missing-object, and OpenFGA is never asked.
This split matters. OpenFGA would reject a blank subject with an HTTP 400, which is a failure to obtain a verdict — and with failOpen set a failure becomes an allow. An Exchange that carried no identity has not been authorized by anything, so it is denied, and failOpen does not reach it.
A wildcard subject is refused
user:* is a legitimate thing to write a tuple for. It is not a legitimate thing to check: asking "may everyone read this" is not asking whether the caller may, and OpenFGA answers true for it wherever a public-access tuple exists. A user expression that resolved to a typed wildcard would therefore hand out every publicly shared object, so it is denied with CamelOpenFgaDenyReason set to wildcard-subject.
Pin the authorization model
authorizationModelId is optional, and leaving it out means "whichever model the store considers latest". A store keeps every model it was ever given and "latest" moves the moment somebody writes a new one, so an unpinned endpoint can start answering a different question than the one it was reviewed with — with no change to the route. The component logs a warning at startup when it is unset. Pin it in production, so that a model rollout is a deliberate, reviewable configuration change.
Consistency
OpenFGA’s default read consistency may answer from a replica that has not caught up, which right after a revoke means a deleted tuple can still grant access for a moment. Set consistency=HIGHER_CONSISTENCY on the paths where that window matters, at the cost of latency.
The value is checked when the endpoint starts, and an unrecognised one — higher_consistency, say — is refused rather than accepted and quietly sent as unknown_default_open_api on every request.
Authentication and TLS
For a server started with --authn-method preshared, set apiToken. For one behind an OIDC provider, set clientId, clientSecret, apiTokenIssuer and apiAudience for the OAuth 2.0 client-credentials flow. Both apiToken and clientSecret are marked secret and are masked wherever Camel prints an endpoint URI.
sslContextParameters configures TLS, including presenting a client certificate to a server that requires mutual TLS. A SPIFFE X.509-SVID obtained through camel-spiffe works here, which closes the loop: the workload authenticates to the authorization decision point as itself.
useGlobalSslContextParameters on the component has no effect on OpenFgaSecurityPolicy. The policy is a standalone bean and is not bound to a component instance, so set sslContextParameters on it explicitly. |
Timeouts
connectTimeout and readTimeout bound the call, and maxRetries bounds how often a retryable failure is re-attempted. The component supplies the SDK’s HTTP client itself so that connectTimeout actually takes effect; the SDK’s own connectTimeout setting is accepted and then never read, and the client it builds by default sets none, leaving the connect phase bounded only by the operating system.
Health checks
Both the producer and OpenFgaSecurityPolicy register a readiness check that probes OpenFGA’s /healthz endpoint. Because the component fails closed, a route that is up but cannot reach OpenFGA fails every message, so it is not ready — and this makes that visible before traffic arrives rather than only in the error logs afterwards.
The check requires the server to report SERVING, not merely to answer with HTTP 200: the HTTP gateway can answer 200 while the health service behind it reports NOT_SERVING.
No check is registered for an injected openFgaClient, which can point anywhere the component has no way to ask about. Set healthCheckProducerEnabled=false on the component, or healthCheckEnabled=false on the policy, to turn them off.
What this component does not do yet
-
Contextual tuples and condition context on a check. A contextual tuple derived from a message is a self-authorization primitive —
(user:me, owner, document:secret)— so rather than ship a gate for it that has not been reviewed, the first release leaves it out entirely. -
The
expand,readTuplesandreadChangesoperations, and store or authorization-model management.
Examples
Guarding an HTTP endpoint, with the identity coming from an authentication step earlier in the route:
from("platform-http:/orders/{orderId}")
// whatever verifies the caller, ending in a processor that records the verified subject as a property
.to("direct:authenticate")
.to("openfga:check?storeId={{fga.store}}&authorizationModelId={{fga.model}}"
+ "&relation=can_view&user=user:${exchangeProperty.CamelKeycloakTokenSubject}"
+ "&object=order:${header.orderId}")
.filter(header("CamelOpenFgaAllowed").isEqualTo(true))
.to("direct:serveOrder");