This is an automated email from the ASF dual-hosted git repository.
nscendoni pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-auth-oauth-client.git
The following commit(s) were added to refs/heads/master by this push:
new cb8d827 SLING-13047 Add RFC 8707 Resource Indicators support to Sling
OIDC Au… (#40)
cb8d827 is described below
commit cb8d827af3fbf2bcbc6d9b086fc31ffe160f29b3
Author: Nicola Scendoni <[email protected]>
AuthorDate: Thu Jan 8 11:59:03 2026 +0100
SLING-13047 Add RFC 8707 Resource Indicators support to Sling OIDC Au… (#40)
* SLING-13047 Add RFC 8707 Resource Indicators support to Sling OIDC
Authentication Handler
---
.../oauth_client/impl/OAuthEntryPointServlet.java | 2 +-
.../impl/OidcAuthenticationHandler.java | 13 +-
.../auth/oauth_client/impl/RedirectHelper.java | 14 +-
.../impl/OidcAuthenticationHandlerTest.java | 205 +++++++++++++++++++++
.../auth/oauth_client/impl/RedirectHelperTest.java | 117 ++++++++++++
5 files changed, 348 insertions(+), 3 deletions(-)
diff --git
a/src/main/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServlet.java
b/src/main/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServlet.java
index 0f02b56..fcda20b 100644
---
a/src/main/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServlet.java
+++
b/src/main/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServlet.java
@@ -116,6 +116,6 @@ public class OAuthEntryPointServlet extends
SlingAllMethodsServlet {
OAuthCookieValue oAuthCookieValue = new
OAuthCookieValue(perRequestKey, connection.name(), redirect);
return RedirectHelper.buildRedirectTarget(
- new String[] {PATH}, callbackUri, conn, oAuthCookieValue,
cryptoService);
+ new String[] {PATH}, callbackUri, conn, oAuthCookieValue,
cryptoService, null);
}
}
diff --git
a/src/main/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandler.java
b/src/main/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandler.java
index 1bdf648..9eb31c7 100644
---
a/src/main/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandler.java
+++
b/src/main/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandler.java
@@ -109,6 +109,8 @@ public class OidcAuthenticationHandler extends
DefaultAuthenticationFeedbackHand
private final String[] path;
+ private final String[] resource;
+
private final CryptoService cryptoService;
@ObjectClassDefinition(
@@ -138,6 +140,14 @@ public class OidcAuthenticationHandler extends
DefaultAuthenticationFeedbackHand
@AttributeDefinition(name = "UserInfo Enabled", description =
"UserInfo Enabled")
boolean userInfoEnabled() default true;
+
+ @AttributeDefinition(
+ name = "Resource",
+ description = "Resource values to include in the
authentication request. "
+ + "This is used to request access tokens for specific
resource servers or APIs. "
+ + "Multiple values can be specified.",
+ cardinality = Integer.MAX_VALUE)
+ String[] resource() default {};
}
@Activate
@@ -159,6 +169,7 @@ public class OidcAuthenticationHandler extends
DefaultAuthenticationFeedbackHand
this.userInfoEnabled = config.userInfoEnabled();
this.pkceEnabled = config.pkceEnabled();
this.path = config.path();
+ this.resource = config.resource();
this.cryptoService = cryptoService;
logger.debug("activate: registering ExternalIdentityProvider");
@@ -537,7 +548,7 @@ public class OidcAuthenticationHandler extends
DefaultAuthenticationFeedbackHand
OAuthCookieValue oAuthCookieValue =
new OAuthCookieValue(perRequestKey, connection.name(),
redirect, nonce, codeVerifier);
- return RedirectHelper.buildRedirectTarget(path, callbackUri, conn,
oAuthCookieValue, cryptoService);
+ return RedirectHelper.buildRedirectTarget(path, callbackUri, conn,
oAuthCookieValue, cryptoService, resource);
}
@Override
diff --git
a/src/main/java/org/apache/sling/auth/oauth_client/impl/RedirectHelper.java
b/src/main/java/org/apache/sling/auth/oauth_client/impl/RedirectHelper.java
index 296ecf4..7a7f2af 100644
--- a/src/main/java/org/apache/sling/auth/oauth_client/impl/RedirectHelper.java
+++ b/src/main/java/org/apache/sling/auth/oauth_client/impl/RedirectHelper.java
@@ -56,7 +56,8 @@ class RedirectHelper {
@NotNull URI callbackUri,
@NotNull ResolvedConnection conn,
@NotNull OAuthCookieValue oAuthCookieValue,
- @NotNull CryptoService cryptoService) {
+ @NotNull CryptoService cryptoService,
+ @Nullable String[] resource) {
String path = findLongestPathMatching(paths, callbackUri.getPath());
@@ -87,6 +88,17 @@ class RedirectHelper {
authRequestBuilder.codeChallenge(codeVerifier,
CodeChallengeMethod.S256);
}
+ // Add resource parameter(s) to the authentication request (RFC 8707)
+ if (resource != null) {
+ List<URI> resourceUris = java.util.Arrays.stream(resource)
+ .filter(r -> r != null && !r.trim().isEmpty())
+ .map(URI::create)
+ .collect(Collectors.toList());
+ if (!resourceUris.isEmpty()) {
+ authRequestBuilder.resources(resourceUris.toArray(new URI[0]));
+ }
+ }
+
List<String[]> parameters =
conn.additionalAuthorizationParameters().stream()
.map(s -> s.split("="))
.filter(p -> p.length == 2)
diff --git
a/src/test/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandlerTest.java
b/src/test/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandlerTest.java
index 67df3cb..f257fc8 100644
---
a/src/test/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandlerTest.java
+++
b/src/test/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandlerTest.java
@@ -1289,4 +1289,209 @@ class OidcAuthenticationHandlerTest {
return false;
}));
}
+
+ @Test
+ void requestCredentialsWithResourceAttribute() {
+ // This is the class used by Sling to configure the Authentication
Handler
+ OidcProviderMetadataRegistry oidcProviderMetadataRegistry =
mock(OidcProviderMetadataRegistry.class);
+ String mockIdPUrl = "http://localhost:8080";
+
when(oidcProviderMetadataRegistry.getJWKSetURI(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/jwks.json"));
+
when(oidcProviderMetadataRegistry.getIssuer(mockIdPUrl)).thenReturn(ISSUER);
+ when(oidcProviderMetadataRegistry.getAuthorizationEndpoint(mockIdPUrl))
+ .thenReturn(URI.create(mockIdPUrl + "/authorize"));
+
when(oidcProviderMetadataRegistry.getTokenEndpoint(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/token"));
+
+ connections.add(new MockOidcConnection(
+ new String[] {"openid"},
+ MOCK_OIDC_PARAM,
+ "client-id",
+ "client-secret",
+ "http://localhost:8080",
+ new String[] {"access_type=offline"},
+ oidcProviderMetadataRegistry));
+
+ when(config.defaultConnectionName()).thenReturn(MOCK_OIDC_PARAM);
+ when(config.callbackUri()).thenReturn("http://redirect");
+ when(config.pkceEnabled()).thenReturn(false);
+ when(config.path()).thenReturn(new String[] {"/"});
+ when(config.resource()).thenReturn(new String[]
{"https://api.example.com"});
+
+ when(request.getRequestURI()).thenReturn("/");
+ MockSlingHttpServletResponse mockResponse = new
MockSlingHttpServletResponse();
+
+ createOidcAuthenticationHandler();
+ assertTrue(oidcAuthenticationHandler.requestCredentials(request,
mockResponse));
+
+ // Verify that the resource parameter is present in the redirect URL
+ assertEquals(302, mockResponse.getStatus());
+ String location = mockResponse.getHeader("location");
+ assertTrue(
+ location.contains("resource=https%3A%2F%2Fapi.example.com"),
+ "Expected resource parameter in redirect URL but got: " +
location);
+ }
+
+ @Test
+ void requestCredentialsWithMultipleResourceAttributes() {
+ // This is the class used by Sling to configure the Authentication
Handler
+ OidcProviderMetadataRegistry oidcProviderMetadataRegistry =
mock(OidcProviderMetadataRegistry.class);
+ String mockIdPUrl = "http://localhost:8080";
+
when(oidcProviderMetadataRegistry.getJWKSetURI(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/jwks.json"));
+
when(oidcProviderMetadataRegistry.getIssuer(mockIdPUrl)).thenReturn(ISSUER);
+ when(oidcProviderMetadataRegistry.getAuthorizationEndpoint(mockIdPUrl))
+ .thenReturn(URI.create(mockIdPUrl + "/authorize"));
+
when(oidcProviderMetadataRegistry.getTokenEndpoint(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/token"));
+
+ connections.add(new MockOidcConnection(
+ new String[] {"openid"},
+ MOCK_OIDC_PARAM,
+ "client-id",
+ "client-secret",
+ "http://localhost:8080",
+ new String[] {"access_type=offline"},
+ oidcProviderMetadataRegistry));
+
+ when(config.defaultConnectionName()).thenReturn(MOCK_OIDC_PARAM);
+ when(config.callbackUri()).thenReturn("http://redirect");
+ when(config.pkceEnabled()).thenReturn(false);
+ when(config.path()).thenReturn(new String[] {"/"});
+ when(config.resource()).thenReturn(new String[]
{"https://api1.example.com", "https://api2.example.com"});
+
+ when(request.getRequestURI()).thenReturn("/");
+ MockSlingHttpServletResponse mockResponse = new
MockSlingHttpServletResponse();
+
+ createOidcAuthenticationHandler();
+ assertTrue(oidcAuthenticationHandler.requestCredentials(request,
mockResponse));
+
+ // Verify that both resource parameters are present in the redirect URL
+ assertEquals(302, mockResponse.getStatus());
+ String location = mockResponse.getHeader("location");
+ assertTrue(
+ location.contains("resource=https%3A%2F%2Fapi1.example.com"),
+ "Expected first resource parameter in redirect URL but got: "
+ location);
+ assertTrue(
+ location.contains("resource=https%3A%2F%2Fapi2.example.com"),
+ "Expected second resource parameter in redirect URL but got: "
+ location);
+ }
+
+ @Test
+ void requestCredentialsWithEmptyResourceAttribute() {
+ // This is the class used by Sling to configure the Authentication
Handler
+ OidcProviderMetadataRegistry oidcProviderMetadataRegistry =
mock(OidcProviderMetadataRegistry.class);
+ String mockIdPUrl = "http://localhost:8080";
+
when(oidcProviderMetadataRegistry.getJWKSetURI(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/jwks.json"));
+
when(oidcProviderMetadataRegistry.getIssuer(mockIdPUrl)).thenReturn(ISSUER);
+ when(oidcProviderMetadataRegistry.getAuthorizationEndpoint(mockIdPUrl))
+ .thenReturn(URI.create(mockIdPUrl + "/authorize"));
+
when(oidcProviderMetadataRegistry.getTokenEndpoint(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/token"));
+
+ connections.add(new MockOidcConnection(
+ new String[] {"openid"},
+ MOCK_OIDC_PARAM,
+ "client-id",
+ "client-secret",
+ "http://localhost:8080",
+ new String[] {"access_type=offline"},
+ oidcProviderMetadataRegistry));
+
+ when(config.defaultConnectionName()).thenReturn(MOCK_OIDC_PARAM);
+ when(config.callbackUri()).thenReturn("http://redirect");
+ when(config.pkceEnabled()).thenReturn(false);
+ when(config.path()).thenReturn(new String[] {"/"});
+ when(config.resource()).thenReturn(new String[] {});
+
+ when(request.getRequestURI()).thenReturn("/");
+ MockSlingHttpServletResponse mockResponse = new
MockSlingHttpServletResponse();
+
+ createOidcAuthenticationHandler();
+ assertTrue(oidcAuthenticationHandler.requestCredentials(request,
mockResponse));
+
+ // Verify that no resource parameter is present in the redirect URL
+ assertEquals(302, mockResponse.getStatus());
+ String location = mockResponse.getHeader("location");
+ assertFalse(
+ location.contains("resource="), "Expected no resource
parameter in redirect URL but got: " + location);
+ }
+
+ @Test
+ void requestCredentialsWithNullResourceAttribute() {
+ // This is the class used by Sling to configure the Authentication
Handler
+ OidcProviderMetadataRegistry oidcProviderMetadataRegistry =
mock(OidcProviderMetadataRegistry.class);
+ String mockIdPUrl = "http://localhost:8080";
+
when(oidcProviderMetadataRegistry.getJWKSetURI(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/jwks.json"));
+
when(oidcProviderMetadataRegistry.getIssuer(mockIdPUrl)).thenReturn(ISSUER);
+ when(oidcProviderMetadataRegistry.getAuthorizationEndpoint(mockIdPUrl))
+ .thenReturn(URI.create(mockIdPUrl + "/authorize"));
+
when(oidcProviderMetadataRegistry.getTokenEndpoint(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/token"));
+
+ connections.add(new MockOidcConnection(
+ new String[] {"openid"},
+ MOCK_OIDC_PARAM,
+ "client-id",
+ "client-secret",
+ "http://localhost:8080",
+ new String[] {"access_type=offline"},
+ oidcProviderMetadataRegistry));
+
+ when(config.defaultConnectionName()).thenReturn(MOCK_OIDC_PARAM);
+ when(config.callbackUri()).thenReturn("http://redirect");
+ when(config.pkceEnabled()).thenReturn(false);
+ when(config.path()).thenReturn(new String[] {"/"});
+ when(config.resource()).thenReturn(null);
+
+ when(request.getRequestURI()).thenReturn("/");
+ MockSlingHttpServletResponse mockResponse = new
MockSlingHttpServletResponse();
+
+ createOidcAuthenticationHandler();
+ assertTrue(oidcAuthenticationHandler.requestCredentials(request,
mockResponse));
+
+ // Verify that no resource parameter is present in the redirect URL
+ assertEquals(302, mockResponse.getStatus());
+ String location = mockResponse.getHeader("location");
+ assertFalse(
+ location.contains("resource="), "Expected no resource
parameter in redirect URL but got: " + location);
+ }
+
+ @Test
+ void requestCredentialsWithResourceAttributeContainingEmptyStrings() {
+ // This is the class used by Sling to configure the Authentication
Handler
+ OidcProviderMetadataRegistry oidcProviderMetadataRegistry =
mock(OidcProviderMetadataRegistry.class);
+ String mockIdPUrl = "http://localhost:8080";
+
when(oidcProviderMetadataRegistry.getJWKSetURI(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/jwks.json"));
+
when(oidcProviderMetadataRegistry.getIssuer(mockIdPUrl)).thenReturn(ISSUER);
+ when(oidcProviderMetadataRegistry.getAuthorizationEndpoint(mockIdPUrl))
+ .thenReturn(URI.create(mockIdPUrl + "/authorize"));
+
when(oidcProviderMetadataRegistry.getTokenEndpoint(mockIdPUrl)).thenReturn(URI.create(mockIdPUrl
+ "/token"));
+
+ connections.add(new MockOidcConnection(
+ new String[] {"openid"},
+ MOCK_OIDC_PARAM,
+ "client-id",
+ "client-secret",
+ "http://localhost:8080",
+ new String[] {"access_type=offline"},
+ oidcProviderMetadataRegistry));
+
+ when(config.defaultConnectionName()).thenReturn(MOCK_OIDC_PARAM);
+ when(config.callbackUri()).thenReturn("http://redirect");
+ when(config.pkceEnabled()).thenReturn(false);
+ when(config.path()).thenReturn(new String[] {"/"});
+ // Array with empty strings and whitespace should be filtered out
+ when(config.resource()).thenReturn(new String[] {"", " ",
"https://api.example.com"});
+
+ when(request.getRequestURI()).thenReturn("/");
+ MockSlingHttpServletResponse mockResponse = new
MockSlingHttpServletResponse();
+
+ createOidcAuthenticationHandler();
+ assertTrue(oidcAuthenticationHandler.requestCredentials(request,
mockResponse));
+
+ // Verify that only the valid resource parameter is present in the
redirect URL
+ assertEquals(302, mockResponse.getStatus());
+ String location = mockResponse.getHeader("location");
+ assertTrue(
+ location.contains("resource=https%3A%2F%2Fapi.example.com"),
+ "Expected valid resource parameter in redirect URL but got: "
+ location);
+ // Count occurrences of "resource=" - should be exactly 1
+ int count = location.split("resource=", -1).length - 1;
+ assertEquals(1, count, "Expected exactly one resource parameter but
found " + count);
+ }
}
diff --git
a/src/test/java/org/apache/sling/auth/oauth_client/impl/RedirectHelperTest.java
b/src/test/java/org/apache/sling/auth/oauth_client/impl/RedirectHelperTest.java
index 4c21870..ef4266b 100644
---
a/src/test/java/org/apache/sling/auth/oauth_client/impl/RedirectHelperTest.java
+++
b/src/test/java/org/apache/sling/auth/oauth_client/impl/RedirectHelperTest.java
@@ -18,11 +18,18 @@
*/
package org.apache.sling.auth.oauth_client.impl;
+import java.net.URI;
+import java.util.List;
+
+import com.nimbusds.openid.connect.sdk.Nonce;
+import org.apache.sling.commons.crypto.CryptoService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
class RedirectHelperTest {
@@ -134,4 +141,114 @@ class RedirectHelperTest {
assertTrue(exception.getMessage().contains("Invalid redirect URL"));
assertTrue(exception.getCause() instanceof IllegalArgumentException);
}
+
+ @Test
+ void testBuildRedirectTargetWithSingleAudience() {
+ ResolvedConnection conn = createMockResolvedConnection();
+ CryptoService cryptoService = new StubCryptoService();
+ OAuthCookieValue oAuthCookieValue =
+ new OAuthCookieValue("perRequestKey", "connectionName",
"/redirect", new Nonce("nonce"), null);
+ String[] audience = new String[] {"https://api.example.com"};
+
+ RedirectTarget result = RedirectHelper.buildRedirectTarget(
+ new String[] {"/"}, URI.create("/callback"), conn,
oAuthCookieValue, cryptoService, audience);
+
+ assertNotNull(result);
+ assertNotNull(result.uri());
+ String uriString = result.uri().toString();
+ assertTrue(
+ uriString.contains("resource=https%3A%2F%2Fapi.example.com"),
+ "Expected resource parameter in URI but got: " + uriString);
+ }
+
+ @Test
+ void testBuildRedirectTargetWithMultipleAudiences() {
+ ResolvedConnection conn = createMockResolvedConnection();
+ CryptoService cryptoService = new StubCryptoService();
+ OAuthCookieValue oAuthCookieValue =
+ new OAuthCookieValue("perRequestKey", "connectionName",
"/redirect", new Nonce("nonce"), null);
+ String[] audience = new String[] {"https://api1.example.com",
"https://api2.example.com"};
+
+ RedirectTarget result = RedirectHelper.buildRedirectTarget(
+ new String[] {"/"}, URI.create("/callback"), conn,
oAuthCookieValue, cryptoService, audience);
+
+ assertNotNull(result);
+ assertNotNull(result.uri());
+ String uriString = result.uri().toString();
+ // Using Nimbus SDK resources() method properly handles multiple
resource values
+ assertTrue(
+ uriString.contains("resource=https%3A%2F%2Fapi1.example.com"),
+ "Expected first resource parameter in URI but got: " +
uriString);
+ assertTrue(
+ uriString.contains("resource=https%3A%2F%2Fapi2.example.com"),
+ "Expected second resource parameter in URI but got: " +
uriString);
+ }
+
+ @Test
+ void testBuildRedirectTargetWithEmptyAudience() {
+ ResolvedConnection conn = createMockResolvedConnection();
+ CryptoService cryptoService = new StubCryptoService();
+ OAuthCookieValue oAuthCookieValue =
+ new OAuthCookieValue("perRequestKey", "connectionName",
"/redirect", new Nonce("nonce"), null);
+ String[] audience = new String[] {};
+
+ RedirectTarget result = RedirectHelper.buildRedirectTarget(
+ new String[] {"/"}, URI.create("/callback"), conn,
oAuthCookieValue, cryptoService, audience);
+
+ assertRedirectTargetHasNoResourceParameter(result);
+ }
+
+ @Test
+ void testBuildRedirectTargetWithNullAudience() {
+ ResolvedConnection conn = createMockResolvedConnection();
+ CryptoService cryptoService = new StubCryptoService();
+ OAuthCookieValue oAuthCookieValue =
+ new OAuthCookieValue("perRequestKey", "connectionName",
"/redirect", new Nonce("nonce"), null);
+
+ RedirectTarget result = RedirectHelper.buildRedirectTarget(
+ new String[] {"/"}, URI.create("/callback"), conn,
oAuthCookieValue, cryptoService, null);
+
+ assertRedirectTargetHasNoResourceParameter(result);
+ }
+
+ @Test
+ void testBuildRedirectTargetWithAudienceContainingEmptyStrings() {
+ ResolvedConnection conn = createMockResolvedConnection();
+ CryptoService cryptoService = new StubCryptoService();
+ OAuthCookieValue oAuthCookieValue =
+ new OAuthCookieValue("perRequestKey", "connectionName",
"/redirect", new Nonce("nonce"), null);
+ // Array with empty strings, whitespace, and one valid value
+ String[] audience = new String[] {"", " ", "https://api.example.com",
null};
+
+ RedirectTarget result = RedirectHelper.buildRedirectTarget(
+ new String[] {"/"}, URI.create("/callback"), conn,
oAuthCookieValue, cryptoService, audience);
+
+ assertNotNull(result);
+ assertNotNull(result.uri());
+ String uriString = result.uri().toString();
+ assertTrue(
+ uriString.contains("resource=https%3A%2F%2Fapi.example.com"),
+ "Expected valid resource parameter in URI but got: " +
uriString);
+ // Count occurrences of "resource=" - should be exactly 1
+ int count = uriString.split("resource=", -1).length - 1;
+ assertEquals(1, count, "Expected exactly one resource parameter but
found " + count);
+ }
+
+ private ResolvedConnection createMockResolvedConnection() {
+ ResolvedConnection conn = mock(ResolvedConnection.class);
+
when(conn.authorizationEndpoint()).thenReturn("http://localhost:8080/authorize");
+ when(conn.tokenEndpoint()).thenReturn("http://localhost:8080/token");
+ when(conn.clientId()).thenReturn("client-id");
+ when(conn.clientSecret()).thenReturn("client-secret");
+ when(conn.scopes()).thenReturn(List.of("openid"));
+ when(conn.additionalAuthorizationParameters()).thenReturn(List.of());
+ return conn;
+ }
+
+ private void assertRedirectTargetHasNoResourceParameter(RedirectTarget
result) {
+ assertNotNull(result);
+ assertNotNull(result.uri());
+ String uriString = result.uri().toString();
+ assertFalse(uriString.contains("resource="), "Expected no resource
parameter in URI but got: " + uriString);
+ }
}