getResource() {
+ return resource;
+ }
+
+ /**
+ * Set the RFC 8707 resource indicator URIs for this token request.
+ * Each value must be an absolute URI per RFC 8707 §2. Query components
+ * SHOULD NOT be included; fragments MUST NOT be included.
+ * Sent as repeated {@code resource=} form parameters in the
+ * {@code application/x-www-form-urlencoded} POST body — not as URL query parameters.
+ *
+ * No client-side URI validation is performed. Validation is the authorization
+ * server's responsibility per RFC 8707 §2; client-side checks would duplicate
+ * server logic and risk divergence.
+ *
+ * @param resource list of absolute resource URIs
+ * @return this
+ * @see RFC 8707
+ */
+ public AccessTokenRequest setResource(List resource) {
+ this.resource = resource;
+ return this;
+ }
+
public Map> toFormParams() {
Map> formParams = new HashMap>();
addFormParam(formParams, GRANT_TYPE_FORM, getGrantType());
addFormParam(formParams, EXPIRES_IN_FORM, getExpiresIn());
addFormParam(formParams, SCOPE_FORM, getScope());
+ addFormParams(formParams, RESOURCE_FORM, getResource());
return formParams;
}
@@ -233,4 +267,19 @@ protected final static void addFormParam(Map> formParams, S
formParams.put(name, Collections.singletonList(value.toString()));
}
}
+
+ /**
+ * Adds a multi-value form parameter entry to the given map.
+ * A defensive copy of {@code values} is stored.
+ * No-op if any of {@code formParams}, {@code name}, or {@code values} is null or empty.
+ *
+ * @param formParams the map to populate; must not be null
+ * @param name the parameter name; must not be null
+ * @param values the parameter values; must not be null or empty
+ */
+ protected final static void addFormParams(Map> formParams, String name, List values) {
+ if (null != formParams && null != name && null != values && !values.isEmpty()) {
+ formParams.put(name, new ArrayList<>(values));
+ }
+ }
}
diff --git a/here-oauth-client/src/main/java/com/here/account/oauth2/ClientAssertionCredentialsGrantRequest.java b/here-oauth-client/src/main/java/com/here/account/oauth2/ClientAssertionCredentialsGrantRequest.java
index 5d98943a..5d97fa53 100644
--- a/here-oauth-client/src/main/java/com/here/account/oauth2/ClientAssertionCredentialsGrantRequest.java
+++ b/here-oauth-client/src/main/java/com/here/account/oauth2/ClientAssertionCredentialsGrantRequest.java
@@ -82,4 +82,13 @@ public Map> toFormParams() {
addFormParam(formParams, CLIENT_ASSERTION_FORM, clientAssertion);
return formParams;
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ClientAssertionCredentialsGrantRequest setResource(List resource) {
+ super.setResource(resource);
+ return this;
+ }
}
diff --git a/here-oauth-client/src/main/java/com/here/account/oauth2/ClientCredentialsGrantRequest.java b/here-oauth-client/src/main/java/com/here/account/oauth2/ClientCredentialsGrantRequest.java
index 6a3c904b..551b980b 100644
--- a/here-oauth-client/src/main/java/com/here/account/oauth2/ClientCredentialsGrantRequest.java
+++ b/here-oauth-client/src/main/java/com/here/account/oauth2/ClientCredentialsGrantRequest.java
@@ -15,6 +15,8 @@
*/
package com.here.account.oauth2;
+import java.util.List;
+
/**
* An {@link AccessTokenRequest} for grant_type=client_credentials.
*
@@ -37,5 +39,14 @@ public ClientCredentialsGrantRequest setExpiresIn(Long expiresIn) {
super.setExpiresIn(expiresIn);
return this;
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ClientCredentialsGrantRequest setResource(List resource) {
+ super.setResource(resource);
+ return this;
+ }
}
diff --git a/here-oauth-client/src/main/java/com/here/account/oauth2/HereAccount.java b/here-oauth-client/src/main/java/com/here/account/oauth2/HereAccount.java
index 63ff8b88..0f4f92f3 100644
--- a/here-oauth-client/src/main/java/com/here/account/oauth2/HereAccount.java
+++ b/here-oauth-client/src/main/java/com/here/account/oauth2/HereAccount.java
@@ -570,7 +570,9 @@ public Fresh requestAutoRefreshingToken(AccessTokenRequest
return requestAutoRefreshingToken(() -> {
return new ClientCredentialsGrantRequest()
.setExpiresIn(request.getExpiresIn())
- .setScope(request.getScope());
+ .setScope(request.getScope())
+ // null if not set; addFormParams() no-op for null
+ .setResource(request.getResource());
});
}
diff --git a/here-oauth-client/src/test/java/com/here/account/http/apache/ApacheHttpClientProviderTest.java b/here-oauth-client/src/test/java/com/here/account/http/apache/ApacheHttpClientProviderTest.java
index dd8291bc..958f9ec9 100644
--- a/here-oauth-client/src/test/java/com/here/account/http/apache/ApacheHttpClientProviderTest.java
+++ b/here-oauth-client/src/test/java/com/here/account/http/apache/ApacheHttpClientProviderTest.java
@@ -256,6 +256,28 @@ public void test_formParamsPut_null() throws NoSuchFieldException, SecurityExcep
assertTrue("httpEntity was expected null, actual "+httpEntity, null == httpEntity);
}
+ @Test
+ public void test_formParams_multiValue_repeatedParams() throws Exception {
+ formParams = new HashMap>();
+ formParams.put("grant_type", Collections.singletonList("client_credentials"));
+ formParams.put("resource", Arrays.asList("https://api.example.com", "https://data.example.com"));
+ httpRequest = httpProvider.getRequest(httpRequestAuthorizer, "POST", url, formParams);
+ HttpRequestBase httpRequestBase = getHttpRequestBase();
+ HttpPost httpPost = (HttpPost) httpRequestBase;
+ HttpEntity httpEntity = httpPost.getEntity();
+ assertNotNull("httpEntity was null", httpEntity);
+ String body = org.apache.http.util.EntityUtils.toString(httpEntity);
+ long resourceCount = 0;
+ for (String part : body.split("&")) {
+ if (part.startsWith("resource=")) resourceCount++;
+ }
+ assertEquals("expected 2 resource= params in: " + body, 2, resourceCount);
+ assertTrue("body should contain resource=https%3A%2F%2Fapi.example.com: " + body,
+ body.contains("resource=https%3A%2F%2Fapi.example.com"));
+ assertTrue("body should contain resource=https%3A%2F%2Fdata.example.com: " + body,
+ body.contains("resource=https%3A%2F%2Fdata.example.com"));
+ }
+
@Test
public void test_methodDoesntSupportJson() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
diff --git a/here-oauth-client/src/test/java/com/here/account/http/java/JavaHttpProviderTest.java b/here-oauth-client/src/test/java/com/here/account/http/java/JavaHttpProviderTest.java
index a2370643..3f139689 100644
--- a/here-oauth-client/src/test/java/com/here/account/http/java/JavaHttpProviderTest.java
+++ b/here-oauth-client/src/test/java/com/here/account/http/java/JavaHttpProviderTest.java
@@ -29,6 +29,7 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -254,6 +255,24 @@ public void test_getFormBody_second2() throws UnsupportedEncodingException {
);
}
+ @Test
+ public void test_getFormBody_repeatedResourceParams() throws UnsupportedEncodingException {
+ Map> formParams = new HashMap>();
+ formParams.put("grant_type", Collections.singletonList("client_credentials"));
+ formParams.put("resource", Arrays.asList("https://api.example.com", "https://data.example.com"));
+ byte[] formBody = JavaHttpProvider.getFormBody(formParams);
+ String body = new String(formBody, "UTF-8");
+ long resourceCount = 0;
+ for (String part : body.split("&")) {
+ if (part.startsWith("resource=")) resourceCount++;
+ }
+ assertEquals("expected 2 resource= params in: " + body, 2, resourceCount);
+ assertTrue("body should contain resource=https%3A%2F%2Fapi.example.com: " + body,
+ body.contains("resource=https%3A%2F%2Fapi.example.com"));
+ assertTrue("body should contain resource=https%3A%2F%2Fdata.example.com: " + body,
+ body.contains("resource=https%3A%2F%2Fdata.example.com"));
+ }
+
@Test
public void test_404() throws HttpException, IOException {
diff --git a/here-oauth-client/src/test/java/com/here/account/oauth2/ClientAssertionCredentialsGrantRequestTest.java b/here-oauth-client/src/test/java/com/here/account/oauth2/ClientAssertionCredentialsGrantRequestTest.java
index 5a1d102e..8a494cb4 100644
--- a/here-oauth-client/src/test/java/com/here/account/oauth2/ClientAssertionCredentialsGrantRequestTest.java
+++ b/here-oauth-client/src/test/java/com/here/account/oauth2/ClientAssertionCredentialsGrantRequestTest.java
@@ -17,6 +17,8 @@
import org.junit.Test;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -89,4 +91,27 @@ public void testConstructor_rejectsNull() {
public void testConstructor_rejectsEmpty() {
new ClientAssertionCredentialsGrantRequest("");
}
+
+ @Test
+ public void testFormParams_singleResource() {
+ ClientAssertionCredentialsGrantRequest request = new ClientAssertionCredentialsGrantRequest(FAKE_JWT)
+ .setResource(Collections.singletonList("https://example.com/api"));
+ Map> formParams = request.toFormParams();
+ assertEquals(Collections.singletonList("https://example.com/api"), formParams.get("resource"));
+ }
+
+ @Test
+ public void testFormParams_multipleResources() {
+ List resources = Arrays.asList("https://api.example.com", "https://data.example.com");
+ ClientAssertionCredentialsGrantRequest request = new ClientAssertionCredentialsGrantRequest(FAKE_JWT)
+ .setResource(resources);
+ Map> formParams = request.toFormParams();
+ assertEquals(resources, formParams.get("resource"));
+ }
+
+ @Test
+ public void testFormParams_noResource() {
+ ClientAssertionCredentialsGrantRequest request = new ClientAssertionCredentialsGrantRequest(FAKE_JWT);
+ assertNull(request.toFormParams().get("resource"));
+ }
}
diff --git a/here-oauth-client/src/test/java/com/here/account/oauth2/ClientCredentialsGrantRequestTest.java b/here-oauth-client/src/test/java/com/here/account/oauth2/ClientCredentialsGrantRequestTest.java
index 1df9c10c..3a5ed694 100644
--- a/here-oauth-client/src/test/java/com/here/account/oauth2/ClientCredentialsGrantRequestTest.java
+++ b/here-oauth-client/src/test/java/com/here/account/oauth2/ClientCredentialsGrantRequestTest.java
@@ -15,10 +15,13 @@
*/
package com.here.account.oauth2;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -81,6 +84,75 @@ public void test_ClientCredentialsGrantRequest_form_expiresIn() throws IOExcepti
}
+ @Test
+ public void test_ClientCredentialsGrantRequest_form_singleResource() {
+ ClientCredentialsGrantRequest request = new ClientCredentialsGrantRequest()
+ .setResource(Collections.singletonList("https://example.com/api"));
+ Map> form = request.toFormParams();
+ assertEquals(Collections.singletonList("https://example.com/api"), form.get("resource"));
+ }
+
+ @Test
+ public void test_ClientCredentialsGrantRequest_form_multipleResources() {
+ List resources = Arrays.asList("https://api.example.com", "https://data.example.com");
+ ClientCredentialsGrantRequest request = new ClientCredentialsGrantRequest()
+ .setResource(resources);
+ Map> form = request.toFormParams();
+ assertEquals(resources, form.get("resource"));
+ }
+
+ @Test
+ public void test_ClientCredentialsGrantRequest_form_noResource() {
+ ClientCredentialsGrantRequest request = new ClientCredentialsGrantRequest();
+ Map> form = request.toFormParams();
+ assertNull(form.get("resource"));
+ }
+
+ @Test
+ public void test_ClientCredentialsGrantRequest_getResource() {
+ List resources = Collections.singletonList("https://example.com");
+ ClientCredentialsGrantRequest request = new ClientCredentialsGrantRequest()
+ .setResource(resources);
+ assertEquals(resources, request.getResource());
+ }
+
+ @Test
+ public void test_addFormParams_nullFormParams_noOp() {
+ // must not throw
+ AccessTokenRequest.addFormParams(null, "resource", Collections.singletonList("https://example.com"));
+ }
+
+ @Test
+ public void test_addFormParams_nullName_noOp() {
+ Map> formParams = new HashMap>();
+ AccessTokenRequest.addFormParams(formParams, null, Collections.singletonList("https://example.com"));
+ assertTrue(formParams.isEmpty());
+ }
+
+ @Test
+ public void test_addFormParams_nullValues_noOp() {
+ Map> formParams = new HashMap>();
+ AccessTokenRequest.addFormParams(formParams, "resource", null);
+ assertTrue(formParams.isEmpty());
+ }
+
+ @Test
+ public void test_addFormParams_emptyValues_noOp() {
+ Map> formParams = new HashMap>();
+ AccessTokenRequest.addFormParams(formParams, "resource", Collections.emptyList());
+ assertTrue(formParams.isEmpty());
+ }
+
+ @Test
+ public void test_addFormParams_defensiveCopy() {
+ Map> formParams = new HashMap>();
+ List values = new java.util.ArrayList(Collections.singletonList("https://example.com"));
+ AccessTokenRequest.addFormParams(formParams, "resource", values);
+ values.add("https://other.com");
+ assertEquals(1, formParams.get("resource").size());
+ }
+
+
private Map toMap(String json) throws IOException {
byte[] bytes = json.getBytes(OAuthConstants.UTF_8_CHARSET);
ByteArrayInputStream jsonInputStream = null;
diff --git a/here-oauth-client/src/test/java/com/here/account/oauth2/HereAccountTest.java b/here-oauth-client/src/test/java/com/here/account/oauth2/HereAccountTest.java
index f30935bf..6d580777 100644
--- a/here-oauth-client/src/test/java/com/here/account/oauth2/HereAccountTest.java
+++ b/here-oauth-client/src/test/java/com/here/account/oauth2/HereAccountTest.java
@@ -1076,4 +1076,47 @@ public void test_requestResponse_with_correlationId() {
assertEquals(expectedCorrelationId, token.getCorrelationId());
}
+
+ @Test
+ public void testRequestAutoRefreshingToken_propagatesResource() throws Exception {
+ HttpProvider mockHttpProvider = Mockito.mock(HttpProvider.class);
+ final String body = "{\"access_token\":\"my-token\",\"expires_in\":300}";
+ final HttpProvider.HttpResponse mockHttpResponse = new HttpProvider.HttpResponse() {
+ @Override public int getStatusCode() { return 200; }
+ @Override public long getContentLength() { return body.getBytes(StandardCharsets.UTF_8).length; }
+ @Override public Map> getHeaders() { return new HashMap>(); }
+ @Override public InputStream getResponseBody() throws IOException {
+ return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
+ }
+ };
+ Mockito.when(mockHttpProvider.execute(Mockito.any())).thenReturn(mockHttpResponse);
+
+ final List