Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ docs/openapi/*.json-e
.dccache

clients/**/*

# JetBrains IDEs
.idea/
*.iml
36 changes: 24 additions & 12 deletions config/clients/java/template/creds-OAuth2Client.java.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,27 @@ import {{configPackage}}.*;
import {{errorsPackage}}.ApiException;
import {{errorsPackage}}.FgaInvalidParameterException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.http.HttpClient;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;

public class OAuth2Client {
public static final String DEFAULT_TOKEN_ENDPOINT_PATH = "/oauth/token";
private final ApiClient apiClient;
private final Credentials credentials;
private final AccessToken token = new AccessToken();
private final CredentialsFlowRequest authRequest;
private final String apiTokenIssuer;
private final String tokenEndpointUrl;

/**
* 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();

public OAuth2Client(Configuration configuration, ApiClient apiClient) {
Credentials credentials = configuration.getCredentials();
this.apiClient = apiClient;
this.apiTokenIssuer = credentials.getClientCredentials().getApiTokenIssuer();
this.tokenEndpointUrl = buildTokenEndpointUrl(credentials.getClientCredentials().getApiTokenIssuer());
this.authRequest = new CredentialsFlowRequest();
this.authRequest.setClientId(credentials.getClientCredentials().getClientId());
this.authRequest.setClientSecret(credentials.getClientCredentials().getClientSecret());
Expand Down Expand Up @@ -65,10 +62,11 @@ public class OAuth2Client {
try {
byte[] body = apiClient.getObjectMapper().writeValueAsBytes(authRequest);

Configuration config = new Configuration().apiUrl("https://" + apiTokenIssuer);
Configuration config = new Configuration().apiUrl(tokenEndpointUrl);

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 @@ -77,4 +75,18 @@ public class OAuth2Client {
throw new ApiException(e);
}
}

private static String buildTokenEndpointUrl(String issuer) {
var uri = URI.create(issuer);

if (uri.getScheme() == null) {
uri = URI.create("https://" + issuer);
}

if (uri.getPath().isEmpty() || uri.getPath().equals("/")) {
uri = URI.create(uri.getScheme() + "://" + uri.getAuthority() + DEFAULT_TOKEN_ENDPOINT_PATH);
}

return uri.toString();
}
}
67 changes: 40 additions & 27 deletions config/clients/java/template/creds-OAuth2ClientTest.java.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,56 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.pgssoft.httpclient.HttpClientMock;
import {{clientPackage}}.ApiClient;
import {{configPackage}}.*;
import {{errorsPackage}}.FgaInvalidParameterException;
import org.junit.jupiter.api.BeforeEach;
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;

import java.util.stream.Stream;


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> issuersTokenEndpoint() {
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("issuersTokenEndpoint")
public void exchangeToken(String apiTokenIssuer, String tokenEndpointUrl) throws Exception {
// Given
OAuth2Client oAuth2 = configureClient(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);
}

private OAuth2Client configureClient(String apiTokenIssuer) {
System.setProperty("HttpRequestAttempt.debug-logging", "enable");

mockHttpClient = new HttpClientMock();
Expand All @@ -38,31 +68,14 @@ class OAuth2ClientTest {
.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);
}
}