Skip to content

streamedListObjects omits Authorization header — 401 against FGA Cloud with ClientCredentials #330

@cportcvent

Description

@cportcvent

Checklist

  • I have looked into the README and have not found a suitable solution or answer.
  • I have looked into the documentation and have not found a suitable solution or answer.
  • I have searched the issues and have not found a suitable solution or answer.
  • I have upgraded to the latest version of OpenFGA and the issue still persists.
  • I have searched the Slack community and have not found a suitable solution or answer.
  • I agree to the terms within the OpenFGA Code of Conduct.

Description

OpenFgaClient#streamedListObjects sends HTTPS requests without the Authorization: Bearer <token> header, so every call against an FGA deployment that requires auth (e.g. FGA Cloud via api.us1.fga.dev) fails with:

dev.openfga.sdk.errors.ApiException: API error: 401

Every other method on the same OpenFgaClient instance — check, batchCheck, listObjects, read, etc. — succeeds with the identical ClientCredentials. A plain curl POST to /stores/{storeId}/streamed-list-objects with a manually-obtained bearer token succeeds. The issue is reproducible on 100% of calls.

Root cause (confirmed by reading sources of 0.9.7 and main):

Non-streaming calls route through OpenFgaApi.buildHttpRequestWithPublisher, which attaches the bearer token:

// src/main/java/dev/openfga/sdk/api/OpenFgaApi.java (0.9.7), around L1297–L1300
if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) {
    String accessToken = getAccessToken(configuration);
    httpRequest.header("Authorization", "Bearer " + accessToken);
}

OpenFgaApi is the only class that constructs and owns an OAuth2Client.

Streaming calls take a different path:

  • OpenFgaClient.streamedListObjects (src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java, around L1317) instantiates StreamedListObjectsApi directly with (configuration, apiClient) — it does not route through OpenFgaApi, and has no reference to an OAuth2Client.
  • StreamedListObjectsApi.streamedListObjects calls BaseStreamingApi.buildHttpRequest.
  • BaseStreamingApi.buildHttpRequest calls ApiClient.requestBuilder(method, path, bodyBytes, configuration), which sets only accept, content-type, URI, method, and timeout. It never reads credentials, never consults an OAuth2Client, and never adds an Authorization header. It then applies apiClient.getRequestInterceptor() if one is set, which is the only user-facing escape hatch — but by default no such interceptor exists.

The same omission exists in the newer StreamingApiExecutor / ApiExecutorRequestBuilder.buildHttpRequest (merged via PR #296), which likewise calls ApiClient.requestBuilder(...) without attaching auth.

Version of SDK

0.9.7

Version of OpenFGA (if known)

Auth0 FGA (> v1.5.x)

OpenFGA Flags/Custom Configuration Applicable

N/A

Reproduction

Consistent on every call. Full standalone Maven project below — single class, no dependency on anything else.

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>local.repro</groupId>
  <artifactId>openfga-streaming-repro</artifactId>
  <version>0.1.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>dev.openfga</groupId>
      <artifactId>openfga-sdk</artifactId>
      <version>0.9.7</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.6.3</version>
        <configuration>
          <mainClass>Repro</mainClass>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

src/main/java/Repro.java:

// Repro.java — run against any FGA deployment requiring auth (e.g. FGA Cloud).
// Requires dev.openfga:openfga-sdk:0.9.7 on the classpath.

import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.model.ClientCheckRequest;
import dev.openfga.sdk.api.client.model.ClientListObjectsRequest;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
import dev.openfga.sdk.api.configuration.ClientCredentials;
import dev.openfga.sdk.api.configuration.Credentials;

public final class Repro {
    public static void main(String[] args) throws Exception {
        ClientConfiguration cfg = new ClientConfiguration()
                .apiUrl(getenvOrDefault("FGA_API_URL", "https://api.us1.fga.dev"))
                .storeId(System.getenv("FGA_STORE_ID"))
                .credentials(new Credentials(new ClientCredentials()
                        .apiTokenIssuer(getenvOrDefault("FGA_API_TOKEN_ISSUER", "auth.fga.dev"))
                        .apiAudience(getenvOrDefault("FGA_API_AUDIENCE", "https://api.us1.fga.dev/"))
                        .clientId(System.getenv("FGA_CLIENT_ID"))
                        .clientSecret(System.getenv("FGA_CLIENT_SECRET"))));

        OpenFgaClient client = new OpenFgaClient(cfg);

        // A. Non-streaming check — succeeds, bearer token attached.
        var check = client.check(new ClientCheckRequest()
                .user("user:baz")
                .relation("owner")
                ._object("foo:0016a5ab-38ac-49c4-86c7-0ece9c212ea6")).get();
        System.out.println("check OK, allowed=" + check.getAllowed());

        // B. Non-streaming listObjects — succeeds, bearer token attached.
        var list = client.listObjects(new ClientListObjectsRequest()
                .user("user:baz")
                .relation("owner")
                .type("foo")).get();
        System.out.println("listObjects OK, count=" + list.getObjects().size());

        // C. Streaming — fails with API error: 401. Request goes out with no Authorization header.
        var req = new ClientListObjectsRequest()
                .user("user:baz")
                .relation("owner")
                .type("foo");
        client.streamedListObjects(req, resp -> System.out.println("got " + resp.getObject()))
                .get();  // throws ExecutionException -> ApiException: API error: 401
    }

    private static String getenvOrDefault(String name, String defaultValue) {
        String value = System.getenv(name);
        return value != null ? value : defaultValue;
    }
}

Run:

export FGA_API_URL='https://api.us1.fga.dev'
export FGA_STORE_ID='<store id>'
export FGA_API_TOKEN_ISSUER='auth.fga.dev'
export FGA_API_AUDIENCE='https://api.us1.fga.dev/'
export FGA_CLIENT_ID='<client id>'
export FGA_CLIENT_SECRET='<client secret>'
mvn -q compile exec:java

Steps:

  1. Given an FGA store with any valid authorization model and at least one tuple.
  2. When streamedListObjects is invoked with ClientCredentials auth.
  3. Then the outgoing HTTPS POST to /stores/{store_id}/streamed-list-objects contains no Authorization header and FGA returns 401.

The same configuration works for check (step A) and listObjects (step B) on the very same OpenFgaClient instance, proving the credentials themselves are valid.

OpenFGA SDK version

dev.openfga:openfga-sdk:0.9.7 — also confirmed on main as of 2026-04-21.

OpenFGA version

FGA Cloud (api.us1.fga.dev). Also reproducible against any self-hosted OpenFGA that requires a bearer token.

SDK Configuration

new ClientConfiguration()
    .apiUrl("https://api.us1.fga.dev")
    .storeId("<redacted>")
    .credentials(new Credentials(new ClientCredentials()
        .apiTokenIssuer("auth.fga.dev")
        .apiAudience("https://api.us1.fga.dev/")
        .clientId("<redacted>")
        .clientSecret("<redacted>")));

Backtrace

Actual output of the repro above against FGA Cloud:

check OK, allowed=true

listObjects OK, count=1234

Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException: dev.openfga.sdk.errors.ApiException: API error: 401
        at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396)
        at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2073)
        at Repro.main(Repro.java:46)
Caused by: java.lang.RuntimeException: dev.openfga.sdk.errors.ApiException: API error: 401
        at dev.openfga.sdk.api.BaseStreamingApi.lambda$processStreamingResponse$2(BaseStreamingApi.java:113)
        at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934)
        at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911)
        at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
        at java.base/java.util.concurrent.CompletableFuture.postFire(CompletableFuture.java:614)
        at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:844)
        at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483)
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373)
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182)
        at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655)
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622)
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
Caused by: dev.openfga.sdk.errors.ApiException: API error: 401
        at dev.openfga.sdk.api.BaseStreamingApi.lambda$processStreamingResponse$1(BaseStreamingApi.java:81)
        at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1150)
        ... 9 more

BaseStreamingApi.java:81 is the status-code check in processStreamingResponse that raises the ApiException for the 401 response.

Capturing the outgoing request via an ApiClient#setRequestInterceptor confirms the streaming call sends only accept: application/json and content-type: application/json — no Authorization header.

Expectation

OpenFgaClient#streamedListObjects should attach Authorization: Bearer <token> to the outgoing HTTP request whenever configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE, mirroring the behavior of OpenFgaApi.buildHttpRequestWithPublisher. With the same ClientCredentials that already work for listObjects, streamedListObjects should stream results without any 401.

References

Suggested fix direction (maintainers may prefer otherwise)

Centralize auth once so that every request-builder path benefits. Concretely:

  1. Consolidate OpenFgaApi#getAccessToken in a reusable fashion, eg. on OAuth2Client itself.
  2. Store the OAuth2Client instance on ApiClient (setOAuth2Client / getOAuth2Client) so the cached token is shared across every request path that uses the same ApiClient. OpenFgaApi's constructor registers its OAuth2Client there.
  3. Call equivalent #buildHttpRequestWithPublisher to assess auth method and credentials, apply headers, from the three request-builder paths — OpenFgaApi.buildHttpRequestWithPublisher, BaseStreamingApi.buildHttpRequest, and ApiExecutorRequestBuilder.buildHttpRequest.

Happy to open a PR against the current main if that would be useful and encouraged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status

    Intake

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions