Skip to content

Commit

Permalink
fix(java-sdk): send the credentials flow request as form-urlencoded (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
rhamzeh authored Jan 26, 2024
2 parents 887f015 + 345ddf4 commit e021223
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package {{clientPackage}};

import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

Expand Down Expand Up @@ -128,14 +129,15 @@ public class OpenFgaClientTest {
.apiAudience(apiAudience)));
fga.setConfiguration(clientConfiguration);
String expectedOAuth2Body = String.format(
"{\"client_id\":\"%s\",\"client_secret\":\"%s\",\"audience\":\"%s\",\"grant_type\":\"client_credentials\"}",
clientId, clientSecret, apiAudience);
String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME);
String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME);
mockHttpClient
.onPost(String.format("https://%s/oauth/token", apiTokenIssuer))
.withBody(is(expectedOAuth2Body))
.withBody(allOf(
containsString(String.format("client_id=%s", clientId)),
containsString(String.format("client_secret=%s", clientSecret)),
containsString(String.format("audience=%s", apiAudience)),
containsString(String.format("grant_type=%s", "client_credentials"))))
.doReturn(200, String.format("{\"access_token\":\"%s\"}", apiToken));
mockHttpClient
.onPost("https://localhost/stores")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,91 +1,45 @@
{{>licenseInfo}}
package {{authPackage}};

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

/**
* A credentials flow request. It contains a Client ID and Secret that can be exchanged for an access token.
* <p>
* {@see "https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow"}
*/
@JsonPropertyOrder({
CredentialsFlowRequest.JSON_PROPERTY_CLIENT_ID,
CredentialsFlowRequest.JSON_PROPERTY_CLIENT_SECRET,
CredentialsFlowRequest.JSON_PROPERTY_AUDIENCE,
CredentialsFlowRequest.JSON_PROPERTY_SCOPE,
CredentialsFlowRequest.JSON_PROPERTY_GRANT_TYPE
})
class CredentialsFlowRequest {
public static final String JSON_PROPERTY_CLIENT_ID = "client_id";
private String clientId;
public static final String CLIENT_ID_PARAM_NAME = "client_id";
public static final String CLIENT_SECRET_PARAM_NAME = "client_secret";
public static final String AUDIENCE_PARAM_NAME = "audience";
public static final String SCOPE_PARAM_NAME = "scope";
public static final String GRANT_TYPE_PARAM_NAME = "grant_type";
public static final String JSON_PROPERTY_CLIENT_SECRET = "client_secret";
private String clientSecret;
private final Map<String, String> parameters = new HashMap<>();
public static final String JSON_PROPERTY_AUDIENCE = "audience";
private String audience;
public static final String JSON_PROPERTY_SCOPE = "scope";
private String scope;
public static final String JSON_PROPERTY_GRANT_TYPE = "grant_type";
private String grantType;
@JsonCreator
public CredentialsFlowRequest() {}

@JsonProperty(JSON_PROPERTY_CLIENT_ID)
public String getClientId() {
return clientId;
}

@JsonProperty(JSON_PROPERTY_CLIENT_ID)
public void setClientId(String clientId) {
this.clientId = clientId;
}

@JsonProperty(JSON_PROPERTY_CLIENT_SECRET)
public String getClientSecret() {
return clientSecret;
}

@JsonProperty(JSON_PROPERTY_CLIENT_SECRET)
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}

@JsonProperty(JSON_PROPERTY_AUDIENCE)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public String getAudience() {
return audience;
public CredentialsFlowRequest(String clientId, String clientSecret) {
this.parameters.put(CLIENT_ID_PARAM_NAME, clientId);
this.parameters.put(CLIENT_SECRET_PARAM_NAME, clientSecret);
this.parameters.put(GRANT_TYPE_PARAM_NAME, "client_credentials");
}

@JsonProperty(JSON_PROPERTY_AUDIENCE)
public void setAudience(String audience) {
this.audience = audience;
}

@JsonProperty(JSON_PROPERTY_SCOPE)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public String getScope() {
return scope;
}

@JsonProperty(JSON_PROPERTY_SCOPE)
public void setScope(String scope) {
this.scope = scope;
this.parameters.put(SCOPE_PARAM_NAME, scope);
}

@JsonProperty(JSON_PROPERTY_GRANT_TYPE)
public String getGrantType() {
return grantType;
public void setAudience(String audience) {
this.parameters.put(AUDIENCE_PARAM_NAME, audience);
}

@JsonProperty(JSON_PROPERTY_GRANT_TYPE)
public void setGrantType(String grantType) {
this.grantType = grantType;
public String buildFormRequestBody() {
return parameters.entrySet()
.stream()
.filter(e -> e.getValue() != null)
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}
}
17 changes: 3 additions & 14 deletions config/clients/java/template/creds-OAuth2Client.java.mustache
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
{{>licenseInfo}}
package {{authPackage}};

import com.fasterxml.jackson.databind.ObjectMapper;
import {{invokerPackage}}.*;
import {{configPackage}}.*;
import {{errorsPackage}}.ApiException;
import {{errorsPackage}}.FgaInvalidParameterException;
import java.io.IOException;

import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Instant;
Expand All @@ -30,12 +29,9 @@ public class OAuth2Client {
this.apiClient = apiClient;
this.apiTokenIssuer = buildApiTokenIssuer(clientCredentials.getApiTokenIssuer());
this.authRequest = new CredentialsFlowRequest();
this.authRequest.setClientId(clientCredentials.getClientId());
this.authRequest.setClientSecret(clientCredentials.getClientSecret());
this.authRequest = new CredentialsFlowRequest(clientCredentials.getClientId(), clientCredentials.getClientSecret());
this.authRequest.setAudience(clientCredentials.getApiAudience());
this.authRequest.setScope(clientCredentials.getScopes());
this.authRequest.setGrantType("client_credentials");
}

/**
Expand All @@ -62,22 +58,15 @@ public class OAuth2Client {
*/
private CompletableFuture<CredentialsFlowResponse> exchangeToken()
throws ApiException, FgaInvalidParameterException {
try {
byte[] body = apiClient.getObjectMapper().writeValueAsBytes(authRequest);
Configuration config = new Configuration().apiUrl(apiTokenIssuer);
HttpRequest.Builder requestBuilder = ApiClient.requestBuilder("POST", "", body, config)
.header("Content-Type", "application/x-www-form-urlencoded");
HttpRequest.Builder requestBuilder = ApiClient.formRequestBuilder("POST", "", this.authRequest.buildFormRequestBody(), config);
HttpRequest request = requestBuilder.build();
return new HttpRequestAttempt<>(request, "exchangeToken", CredentialsFlowResponse.class, apiClient, config)
.attemptHttpRequest()
.thenApply(ApiResponse::getData);
} catch (IOException e) {
throw new ApiException(e);
}
}

private static String buildApiTokenIssuer(String issuer) throws FgaInvalidParameterException {
Expand Down
107 changes: 75 additions & 32 deletions config/clients/java/template/creds-OAuth2ClientTest.java.mustache
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
{{>licenseInfo}}
package {{authPackage}};

import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.pgssoft.httpclient.HttpClientMock;
import {{clientPackage}}.ApiClient;
import {{configPackage}}.*;
import {{errorsPackage}}.FgaInvalidParameterException;
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;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class OAuth2ClientTest {
private static final String CLIENT_ID = "client";
private static final String CLIENT_SECRET = "secret";
Expand All @@ -42,16 +46,27 @@ class OAuth2ClientTest {
"https://issuer.fga.example:8080/some_endpoint"));
}

private String urlEncode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}

@ParameterizedTest
@MethodSource("apiTokenIssuers")
public void exchangeAuth0Token(String apiTokenIssuer, String tokenEndpointUrl) throws Exception {
// Given
OAuth2Client auth0 = newAuth0Client(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);
mockHttpClient
.onPost(tokenEndpointUrl)
.withBody(
allOf(
containsString(String.format("client_id=%s", CLIENT_ID)),
containsString(String.format("client_secret=%s", CLIENT_SECRET)),
containsString(String.format("audience=%s", AUDIENCE)),
containsString(String.format("grant_type=%s", GRANT_TYPE)))
)
.doReturn(200, responseBody);
// When
String result = auth0.getAccessToken().get();
Expand All @@ -60,7 +75,14 @@ class OAuth2ClientTest {
mockHttpClient
.verify()
.post(tokenEndpointUrl)
.withBody(is(expectedPostBody))
.withBody(
allOf(
containsString(String.format("client_id=%s", CLIENT_ID)),
containsString(String.format("client_secret=%s", CLIENT_SECRET)),
containsString(String.format("audience=%s", AUDIENCE)),
containsString(String.format("grant_type=%s", GRANT_TYPE)))
)
.withHeader("Content-Type", "application/x-www-form-urlencoded")
.called();
assertEquals(ACCESS_TOKEN, result);
}
Expand All @@ -69,21 +91,34 @@ class OAuth2ClientTest {
@MethodSource("apiTokenIssuers")
public void exchangeOAuth2Token(String apiTokenIssuer, String tokenEndpointUrl) throws Exception {
// Given
OAuth2Client oAuth2 = newOAuth2Client(apiTokenIssuer);
String expectedPostBody = String.format(
"{\"client_id\":\"%s\",\"client_secret\":\"%s\",\"scope\":\"%s\",\"grant_type\":\"%s\"}",
CLIENT_ID, CLIENT_SECRET, SCOPES, GRANT_TYPE);
OAuth2Client auth0 = newOAuth2Client(apiTokenIssuer);
String responseBody = String.format("{\"access_token\":\"%s\"}", ACCESS_TOKEN);
mockHttpClient.onPost(tokenEndpointUrl).withBody(is(expectedPostBody)).doReturn(200, responseBody);
mockHttpClient
.onPost(tokenEndpointUrl)
.withBody(
allOf(
containsString(String.format("client_id=%s", CLIENT_ID)),
containsString(String.format("client_secret=%s", CLIENT_SECRET)),
containsString(String.format("scope=%s", urlEncode(SCOPES))),
containsString(String.format("grant_type=%s", GRANT_TYPE)))
)
.doReturn(200, responseBody);
// When
String result = oAuth2.getAccessToken().get();
String result = auth0.getAccessToken().get();
// Then
mockHttpClient
.verify()
.post(tokenEndpointUrl)
.withBody(is(expectedPostBody))
.withBody(
allOf(
containsString(String.format("client_id=%s", CLIENT_ID)),
containsString(String.format("client_secret=%s", CLIENT_SECRET)),
containsString(String.format("scope=%s", urlEncode(SCOPES))),
containsString(String.format("grant_type=%s", GRANT_TYPE)))
)
.withHeader("Content-Type", "application/x-www-form-urlencoded")
.called();
assertEquals(ACCESS_TOKEN, result);
Expand All @@ -92,7 +127,8 @@ class OAuth2ClientTest {
@Test
public void apiTokenIssuer_invalidScheme() {
// When
var exception = assertThrows(FgaInvalidParameterException.class, () -> newAuth0Client("ftp://issuer.fga.example"));
var exception =
assertThrows(FgaInvalidParameterException.class, () -> newAuth0Client("ftp://issuer.fga.example"));
// Then
assertEquals("Required parameter scheme was invalid when calling apiTokenIssuer.", exception.getMessage());
Expand All @@ -113,27 +149,34 @@ class OAuth2ClientTest {
var exception = assertThrows(FgaInvalidParameterException.class, () -> newAuth0Client(invalidApiTokenIssuer));
// Then
assertEquals("Required parameter apiTokenIssuer was invalid when calling ClientCredentials.", exception.getMessage());
assertEquals(
"Required parameter apiTokenIssuer was invalid when calling ClientCredentials.",
exception.getMessage());
assertInstanceOf(IllegalArgumentException.class, exception.getCause());
}

private OAuth2Client newAuth0Client(String apiTokenIssuer) throws FgaInvalidParameterException {
return newClientCredentialsClient(apiTokenIssuer, new Credentials(new ClientCredentials()
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.apiAudience(AUDIENCE)
.apiTokenIssuer(apiTokenIssuer)));
return newClientCredentialsClient(
apiTokenIssuer,
new Credentials(new ClientCredentials()
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.apiAudience(AUDIENCE)
.apiTokenIssuer(apiTokenIssuer)));
}

private OAuth2Client newOAuth2Client(String apiTokenIssuer) throws FgaInvalidParameterException {
return newClientCredentialsClient(apiTokenIssuer, new Credentials(new ClientCredentials()
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.scopes(SCOPES)
.apiTokenIssuer(apiTokenIssuer)));
return newClientCredentialsClient(
apiTokenIssuer,
new Credentials(new ClientCredentials()
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.scopes(SCOPES)
.apiTokenIssuer(apiTokenIssuer)));
}

private OAuth2Client newClientCredentialsClient(String apiTokenIssuer, Credentials credentials) throws FgaInvalidParameterException {
private OAuth2Client newClientCredentialsClient(String apiTokenIssuer, Credentials credentials)
throws FgaInvalidParameterException {
System.setProperty("HttpRequestAttempt.debug-logging", "enable");
mockHttpClient = new HttpClientMock();
Expand Down
15 changes: 15 additions & 0 deletions config/clients/java/template/libraries/native/ApiClient.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ public class ApiClient {
return builder;
}

/**
* Creates a {@link HttpRequest.Builder} for a {@code x-www-form-urlencoded} request.
* @param method the HTTP method to be make.
* @param path the URL path.
* @param body the request body. It must be URL-encoded.
* @param configuration the client configuration.
* @return a configured builder.
* @throws FgaInvalidParameterException
*/
public static HttpRequest.Builder formRequestBuilder(String method, String path, String body, Configuration configuration) throws FgaInvalidParameterException {
HttpRequest.Builder builder = requestBuilder(method, path, HttpRequest.BodyPublishers.ofString(body), configuration);
builder.header("content-type", "application/x-www-form-urlencoded");
return builder;
}

private static HttpRequest.Builder requestBuilder(
String method, String path, HttpRequest.BodyPublisher bodyPublisher, Configuration configuration)
throws FgaInvalidParameterException {
Expand Down

0 comments on commit e021223

Please sign in to comment.