diff --git a/README.md b/README.md index 264d5376..0bd65ba5 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ public class Example { .authorizationModelId(System.getenv("FGA_MODEL_ID")) // Optional, can be overridden per request .credentials(new Credentials( new ClientCredentials() - .apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER")) + .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")) @@ -220,7 +220,9 @@ public class Example { } ``` -#### Oauth2 Credentials +#### OAuth2 Client Credentials + +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`). ```java import com.fasterxml.jackson.databind.ObjectMapper; @@ -238,8 +240,8 @@ public class Example { .authorizationModelId(System.getenv("FGA_MODEL_ID")) // Optional, can be overridden per request .credentials(new Credentials( new ClientCredentials() - .apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER")) - .scopes(System.getenv("FGA_API_SCOPES")) // optional space separated scopes + .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")) )); diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java index 8e7d76ab..dfcac13a 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java @@ -3,6 +3,7 @@ import static dev.openfga.sdk.util.Validation.assertParamExists; import dev.openfga.sdk.errors.FgaInvalidParameterException; +import dev.openfga.sdk.util.StringUtil; public class ClientCredentials { private String clientId; @@ -22,7 +23,6 @@ public void assertValid() throws FgaInvalidParameterException { assertParamExists(clientId, "clientId", "ClientCredentials"); assertParamExists(clientSecret, "clientSecret", "ClientCredentials"); assertParamExists(apiTokenIssuer, "apiTokenIssuer", "ClientCredentials"); - assertParamExists(apiAudience, "apiAudience", "ClientCredentials"); } public String getClientId() { @@ -48,7 +48,7 @@ public String getApiTokenIssuer() { } public ClientCredentials apiAudience(String apiAudience) { - this.apiAudience = apiAudience; + this.apiAudience = StringUtil.isNullOrWhitespace(apiAudience) ? null : apiAudience; return this; } @@ -57,7 +57,7 @@ public String getApiAudience() { } public ClientCredentials scopes(String scopes) { - this.scopes = scopes; + this.scopes = StringUtil.isNullOrWhitespace(scopes) ? null : scopes; return this; } diff --git a/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java b/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java index a03554f7..58ece583 100644 --- a/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java +++ b/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java @@ -80,4 +80,57 @@ public void assertValid_invalidApiTokenIssuer() { "Required parameter apiTokenIssuer was invalid when calling ClientCredentials.", exception.getMessage())); } + + @Test + public void assertValid_withoutApiAudience() throws FgaInvalidParameterException { + // audience is optional for standard OAuth2 servers + ClientCredentials creds = new ClientCredentials() + .clientId(VALID_CLIENT_ID) + .clientSecret(VALID_CLIENT_SECRET) + .apiTokenIssuer(VALID_API_TOKEN_ISSUER); + + // Should not throw + creds.assertValid(); + assertNull(creds.getApiAudience()); + } + + @Test + public void assertValid_withScopes() throws FgaInvalidParameterException { + ClientCredentials creds = new ClientCredentials() + .clientId(VALID_CLIENT_ID) + .clientSecret(VALID_CLIENT_SECRET) + .apiTokenIssuer(VALID_API_TOKEN_ISSUER) + .scopes("read write"); + + creds.assertValid(); + assertEquals("read write", creds.getScopes()); + } + + @Test + public void assertValid_blankApiAudienceNormalizedToNull() throws FgaInvalidParameterException { + for (String blank : Arrays.asList("", " ", "\t\r\n")) { + ClientCredentials creds = new ClientCredentials() + .clientId(VALID_CLIENT_ID) + .clientSecret(VALID_CLIENT_SECRET) + .apiTokenIssuer(VALID_API_TOKEN_ISSUER) + .apiAudience(blank); + + creds.assertValid(); + assertNull(creds.getApiAudience(), "Blank apiAudience should be normalized to null"); + } + } + + @Test + public void assertValid_blankScopesNormalizedToNull() throws FgaInvalidParameterException { + for (String blank : Arrays.asList("", " ", "\t\r\n")) { + ClientCredentials creds = new ClientCredentials() + .clientId(VALID_CLIENT_ID) + .clientSecret(VALID_CLIENT_SECRET) + .apiTokenIssuer(VALID_API_TOKEN_ISSUER) + .scopes(blank); + + creds.assertValid(); + assertNull(creds.getScopes(), "Blank scopes should be normalized to null"); + } + } }