Skip to content

Commit

Permalink
feat: add configurable apiTokenIssuer
Browse files Browse the repository at this point in the history
Co-authored-by: Yann D'Isanto <[email protected]>
  • Loading branch information
booniepepper and le-yams committed Dec 13, 2023
1 parent 9fcdbf2 commit cc3df4b
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 36 deletions.
45 changes: 35 additions & 10 deletions src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,33 @@
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
*
* @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");
}

Expand Down Expand Up @@ -72,10 +74,11 @@ private CompletableFuture<CredentialsFlowResponse> 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()
Expand All @@ -84,4 +87,26 @@ private CompletableFuture<CredentialsFlowResponse> 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();
}
}
101 changes: 75 additions & 26 deletions src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<Arguments> 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<Arguments> 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();
Expand All @@ -38,31 +104,14 @@ 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);

var apiClient = mock(ApiClient.class);
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);
}
}

0 comments on commit cc3df4b

Please sign in to comment.