diff --git a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java index 6a88c8a..a1a4b3d 100644 --- a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java +++ b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java @@ -1248,6 +1248,29 @@ private String pathWithParams(String basePath, Object... params) { return path.toString(); } + /** + * Get an access token asynchronously. This method is only available when using CLIENT_CREDENTIALS + * authentication. The token can be used for making API calls to other services that accept the same token. + * + * @return A CompletableFuture containing the access token + * @throws IllegalStateException when the credentials method is not CLIENT_CREDENTIALS + * @throws FgaInvalidParameterException when the configuration is invalid + * @throws ApiException when token retrieval fails + */ + public CompletableFuture getAccessToken() throws IllegalStateException, FgaInvalidParameterException, ApiException { + CredentialsMethod credentialsMethod = this.configuration.getCredentials().getCredentialsMethod(); + + if (credentialsMethod != CredentialsMethod.CLIENT_CREDENTIALS) { + throw new IllegalStateException("getAccessToken() is only available when using CLIENT_CREDENTIALS authentication method"); + } + + if (oAuth2Client == null) { + throw new IllegalStateException("OAuth2Client is not initialized"); + } + + return oAuth2Client.getAccessToken(); + } + /** * Get an access token. Expects that configuration is valid (meaning it can * pass {@link Configuration#assertValid()}) and expects that if the diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index f3d28b4..5f171b8 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -75,6 +75,20 @@ public void setConfiguration(ClientConfiguration configuration) throws FgaInvali this.api = new OpenFgaApi(configuration, apiClient); } + /** + * Get an access token asynchronously. This method is only available when using CLIENT_CREDENTIALS + * authentication. The token can be used for making API calls to other services that accept the same token. + * + * @return A CompletableFuture containing the access token + * @throws IllegalStateException when the credentials method is not CLIENT_CREDENTIALS + * @throws FgaInvalidParameterException when the configuration is invalid + * @throws ApiException when token retrieval fails + */ + public CompletableFuture getAccessToken() throws IllegalStateException, FgaInvalidParameterException, ApiException { + configuration.assertValid(); + return api.getAccessToken(); + } + /* ******** * Stores * **********/ diff --git a/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java b/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java index e0ff20b..0104a55 100644 --- a/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java +++ b/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java @@ -1928,4 +1928,68 @@ DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID, new WriteAssertionsRequest()) assertEquals( "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseData()); } + + @Test + public void getAccessToken_withClientCredentials() throws Exception { + // Given + String accessToken = "test-access-token-12345"; + String tokenResponse = String.format("{\"access_token\":\"%s\",\"token_type\":\"Bearer\",\"expires_in\":3600}", accessToken); + + // Mock the token endpoint + mockHttpClient.onPost("https://auth.fga.example/oauth/token").doReturn(200, tokenResponse); + + ClientCredentials clientCredentials = new ClientCredentials() + .clientId("test-client-id") + .clientSecret("test-client-secret") + .apiTokenIssuer("https://auth.fga.example") + .apiAudience("https://api.fga.example"); + + Configuration configuration = new Configuration() + .apiUrl("https://api.fga.example") + .credentials(new Credentials(clientCredentials)); + + OpenFgaApi api = new OpenFgaApi(configuration, mockApiClient); + + // When + String result = api.getAccessToken().get(); + + // Then + assertEquals(accessToken, result); + mockHttpClient.verify().post("https://auth.fga.example/oauth/token").called(); + } + + @Test + public void getAccessToken_withApiToken() throws Exception { + // Given - API token configuration + ApiToken apiToken = new ApiToken("static-api-token"); + Configuration configuration = new Configuration() + .apiUrl("https://api.fga.example") + .credentials(new Credentials(apiToken)); + + OpenFgaApi api = new OpenFgaApi(configuration, mockApiClient); + + // When & Then + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { + api.getAccessToken().get(); + }); + + assertEquals("getAccessToken() is only available when using CLIENT_CREDENTIALS authentication method", exception.getMessage()); + } + + @Test + public void getAccessToken_withNoCredentials() throws Exception { + // Given - No credentials configuration + Configuration configuration = new Configuration() + .apiUrl("https://api.fga.example") + .credentials(new Credentials()); + + OpenFgaApi api = new OpenFgaApi(configuration, mockApiClient); + + // When & Then + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { + api.getAccessToken().get(); + }); + + assertEquals("getAccessToken() is only available when using CLIENT_CREDENTIALS authentication method", exception.getMessage()); + } } diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java index fff5bf2..f30bad8 100644 --- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java @@ -2993,6 +2993,65 @@ public void setAuthorizationModelId() throws Exception { + "If this behavior ever changes, it could be a subtle breaking change."); } + @Test + public void getAccessToken_withClientCredentials() throws Exception { + // Given + String accessToken = "client-test-token-67890"; + String tokenResponse = String.format("{\"access_token\":\"%s\",\"token_type\":\"Bearer\",\"expires_in\":3600}", accessToken); + + // Mock the token endpoint + mockHttpClient.onPost("https://auth.fga.example/oauth/token").doReturn(200, tokenResponse); + + ClientCredentials clientCredentials = new ClientCredentials() + .clientId("test-client-id") + .clientSecret("test-client-secret") + .apiTokenIssuer("https://auth.fga.example") + .apiAudience("https://api.fga.example"); + + ClientConfiguration clientConfiguration = new ClientConfiguration() + .apiUrl("https://api.fga.example") + .storeId(DEFAULT_STORE_ID) + .credentials(new Credentials(clientCredentials)); + + var mockApiClient = mock(ApiClient.class); + when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); + when(mockApiClient.getObjectMapper()).thenReturn(new ObjectMapper()); + when(mockApiClient.getHttpClientBuilder()).thenReturn(mock(HttpClient.Builder.class)); + + OpenFgaClient client = new OpenFgaClient(clientConfiguration, mockApiClient); + + // When + String result = client.getAccessToken().get(); + + // Then + assertEquals(accessToken, result); + mockHttpClient.verify().post("https://auth.fga.example/oauth/token").called(); + } + + @Test + public void getAccessToken_withApiToken() throws Exception { + // Given - API token configuration + ApiToken apiToken = new ApiToken("static-api-token-client"); + ClientConfiguration clientConfiguration = new ClientConfiguration() + .apiUrl("https://api.fga.example") + .storeId(DEFAULT_STORE_ID) + .credentials(new Credentials(apiToken)); + + var mockApiClient = mock(ApiClient.class); + when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); + when(mockApiClient.getObjectMapper()).thenReturn(new ObjectMapper()); + when(mockApiClient.getHttpClientBuilder()).thenReturn(mock(HttpClient.Builder.class)); + + OpenFgaClient client = new OpenFgaClient(clientConfiguration, mockApiClient); + + // When & Then - The exception is thrown directly, not wrapped in ExecutionException + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { + client.getAccessToken(); + }); + + assertEquals("getAccessToken() is only available when using CLIENT_CREDENTIALS authentication method", exception.getMessage()); + } + private Matcher anyValidUUID() { return new UUIDMatcher(); }