diff --git a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java index 9b4aa5f..8e0ac92 100644 --- a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java +++ b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java @@ -17,16 +17,18 @@ import dev.openfga.sdk.errors.ApiException; import dev.openfga.sdk.errors.FgaInvalidParameterException; import java.io.IOException; +import java.net.URI; import java.net.http.HttpRequest; import java.time.Instant; import java.util.concurrent.CompletableFuture; public class OAuth2Client { + private static final String DEFAULT_API_TOKEN_ISSUER_PATH = "/oauth/token"; + private final ApiClient apiClient; - private final Credentials credentials; + private final String apiTokenIssuer; private final AccessToken token = new AccessToken(); private final CredentialsFlowRequest authRequest; - private final String apiTokenIssuer; /** * Initializes a new instance of the {@link OAuth2Client} class @@ -34,14 +36,14 @@ public class OAuth2Client { * @param configuration Configuration, including credentials, that can be used to retrieve an access tokens */ public OAuth2Client(Configuration configuration, ApiClient apiClient) throws FgaInvalidParameterException { - this.credentials = configuration.getCredentials(); + var clientCredentials = configuration.getCredentials().getClientCredentials(); this.apiClient = apiClient; - this.apiTokenIssuer = credentials.getClientCredentials().getApiTokenIssuer(); + this.apiTokenIssuer = buildApiTokenIssuer(clientCredentials.getApiTokenIssuer()); this.authRequest = new CredentialsFlowRequest(); - this.authRequest.setClientId(credentials.getClientCredentials().getClientId()); - this.authRequest.setClientSecret(credentials.getClientCredentials().getClientSecret()); - this.authRequest.setAudience(credentials.getClientCredentials().getApiAudience()); + this.authRequest.setClientId(clientCredentials.getClientId()); + this.authRequest.setClientSecret(clientCredentials.getClientSecret()); + this.authRequest.setAudience(clientCredentials.getApiAudience()); this.authRequest.setGrantType("client_credentials"); } @@ -72,10 +74,11 @@ private CompletableFuture exchangeToken() try { byte[] body = apiClient.getObjectMapper().writeValueAsBytes(authRequest); - Configuration config = new Configuration().apiUrl("https://" + apiTokenIssuer); + Configuration config = new Configuration().apiUrl(apiTokenIssuer); + + HttpRequest.Builder requestBuilder = ApiClient.requestBuilder("POST", "", body, config); - HttpRequest request = ApiClient.requestBuilder("POST", "/oauth/token", body, config) - .build(); + HttpRequest request = requestBuilder.build(); return new HttpRequestAttempt<>(request, "exchangeToken", CredentialsFlowResponse.class, apiClient, config) .attemptHttpRequest() @@ -84,4 +87,26 @@ private CompletableFuture exchangeToken() throw new ApiException(e); } } + + private static String buildApiTokenIssuer(String issuer) throws FgaInvalidParameterException { + URI uri; + try { + uri = URI.create(issuer); + } catch (IllegalArgumentException cause) { + throw new FgaInvalidParameterException("apiTokenIssuer", "ClientCredentials", cause); + } + + var scheme = uri.getScheme(); + if (scheme == null) { + uri = URI.create("https://" + issuer); + } else if (!"https".equals(scheme) && !"http".equals(scheme)) { + throw new FgaInvalidParameterException("scheme", "apiTokenIssuer"); + } + + if (uri.getPath().isEmpty() || uri.getPath().equals("/")) { + uri = URI.create(uri.getScheme() + "://" + uri.getAuthority() + DEFAULT_API_TOKEN_ISSUER_PATH); + } + + return uri.toString(); + } } diff --git a/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java index a894001..30c34e6 100644 --- a/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java @@ -1,7 +1,7 @@ package dev.openfga.sdk.api.auth; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -10,25 +10,91 @@ import dev.openfga.sdk.api.client.ApiClient; import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.errors.FgaInvalidParameterException; -import org.junit.jupiter.api.BeforeEach; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class OAuth2ClientTest { private static final String CLIENT_ID = "client"; private static final String CLIENT_SECRET = "secret"; private static final String AUDIENCE = "audience"; private static final String GRANT_TYPE = "client_credentials"; - private static final String API_TOKEN_ISSUER = "test.fga.dev"; - private static final String POST_URL = "https://" + API_TOKEN_ISSUER + "/oauth/token"; private static final String ACCESS_TOKEN = "0123456789"; private final ObjectMapper mapper = new ObjectMapper(); private HttpClientMock mockHttpClient; - private OAuth2Client oAuth2; + private static Stream apiTokenIssuers() { + return Stream.of( + Arguments.of("issuer.fga.example", "https://issuer.fga.example/oauth/token"), + Arguments.of("https://issuer.fga.example", "https://issuer.fga.example/oauth/token"), + Arguments.of("https://issuer.fga.example/", "https://issuer.fga.example/oauth/token"), + Arguments.of("https://issuer.fga.example:8080", "https://issuer.fga.example:8080/oauth/token"), + Arguments.of("https://issuer.fga.example:8080/", "https://issuer.fga.example:8080/oauth/token"), + Arguments.of("issuer.fga.example/some_endpoint", "https://issuer.fga.example/some_endpoint"), + Arguments.of("https://issuer.fga.example/some_endpoint", "https://issuer.fga.example/some_endpoint"), + Arguments.of( + "https://issuer.fga.example:8080/some_endpoint", + "https://issuer.fga.example:8080/some_endpoint")); + } - @BeforeEach - public void setup() throws FgaInvalidParameterException { + @ParameterizedTest + @MethodSource("apiTokenIssuers") + public void exchangeToken(String apiTokenIssuer, String tokenEndpointUrl) throws Exception { + // Given + OAuth2Client oAuth2 = newOAuth2Client(apiTokenIssuer); + String expectedPostBody = String.format( + "{\"client_id\":\"%s\",\"client_secret\":\"%s\",\"audience\":\"%s\",\"grant_type\":\"%s\"}", + CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); + String responseBody = String.format("{\"access_token\":\"%s\"}", ACCESS_TOKEN); + mockHttpClient.onPost(tokenEndpointUrl).withBody(is(expectedPostBody)).doReturn(200, responseBody); + + // When + String result = oAuth2.getAccessToken().get(); + + // Then + mockHttpClient + .verify() + .post(tokenEndpointUrl) + .withBody(is(expectedPostBody)) + .called(); + assertEquals(ACCESS_TOKEN, result); + } + + @Test + public void apiTokenIssuer_invalidScheme() { + // When + var exception = + assertThrows(FgaInvalidParameterException.class, () -> newOAuth2Client("ftp://issuer.fga.example")); + + // Then + assertEquals("Required parameter scheme was invalid when calling apiTokenIssuer.", exception.getMessage()); + } + + private static Stream invalidApiTokenIssuers() { + return Stream.of( + Arguments.of("://issuer.fga.example"), + Arguments.of("http://issuer.fga.example#bad#fragment"), + Arguments.of("http://issuer.fga.example/space in path"), + Arguments.of("http://")); + } + + @ParameterizedTest + @MethodSource("invalidApiTokenIssuers") + public void apiTokenIssuers_invalidURI(String invalidApiTokenIssuer) { + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> newOAuth2Client(invalidApiTokenIssuer)); + + // Then + assertEquals( + "Required parameter apiTokenIssuer was invalid when calling ClientCredentials.", + exception.getMessage()); + assertInstanceOf(IllegalArgumentException.class, exception.getCause()); + } + + private OAuth2Client newOAuth2Client(String apiTokenIssuer) throws FgaInvalidParameterException { System.setProperty("HttpRequestAttempt.debug-logging", "enable"); mockHttpClient = new HttpClientMock(); @@ -38,7 +104,7 @@ public void setup() throws FgaInvalidParameterException { .clientId(CLIENT_ID) .clientSecret(CLIENT_SECRET) .apiAudience(AUDIENCE) - .apiTokenIssuer(API_TOKEN_ISSUER)); + .apiTokenIssuer(apiTokenIssuer)); var configuration = new Configuration().apiUrl("").credentials(credentials); @@ -46,23 +112,6 @@ public void setup() throws FgaInvalidParameterException { when(apiClient.getHttpClient()).thenReturn(mockHttpClient); when(apiClient.getObjectMapper()).thenReturn(mapper); - oAuth2 = new OAuth2Client(configuration, apiClient); - } - - @Test - public void exchangeToken() throws Exception { - // Given - String expectedPostBody = String.format( - "{\"client_id\":\"%s\",\"client_secret\":\"%s\",\"audience\":\"%s\",\"grant_type\":\"%s\"}", - CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); - String responseBody = String.format("{\"access_token\":\"%s\"}", ACCESS_TOKEN); - mockHttpClient.onPost(POST_URL).withBody(is(expectedPostBody)).doReturn(200, responseBody); - - // When - String result = oAuth2.getAccessToken().get(); - - // Then - mockHttpClient.verify().post(POST_URL).withBody(is(expectedPostBody)).called(); - assertEquals(ACCESS_TOKEN, result); + return new OAuth2Client(configuration, apiClient); } }