Skip to content

Commit c6d20b5

Browse files
authored
JCL-337: End-to-end test of UMA auth for Access Grants endpoints (#520)
- Enforce OpenId session uses provided authenticator: The OpenID session was disregarding the provided authenticator, resulting in only ever using the ID Token as an Access Token instead of using the ID Token as a credential to get an UMA-issued Access Token. If no authenticator is provided, use the ID Token as an Access Token. - Enforce unit test accept both openid and uma authn - Add test to check UMA auth to VC endpoint: - Extend base test suite with an UMA-specific scenario verifying that UMA authentication is enforced. - Enable caching in tests - Enable UMA auth to mock VC issuer - Test OpenID authentication: This checks that the appropriate authenticator is being used in OpenID-specific tests
1 parent e69ed69 commit c6d20b5

10 files changed

Lines changed: 159 additions & 33 deletions

File tree

‎core/src/test/java/com/inrupt/client/core/MockHttpService.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ private void setupMocks() {
133133
.withHeader("User-Agent", equalTo(USER_AGENT))
134134
.withRequestBody(matching("Test String 1"))
135135
.withHeader(CONTENT_TYPE, containing(TEXT_PLAIN))
136-
.withHeader("Authorization", containing("Bearer eyJ"))
136+
.withHeader("Authorization", containing("Bearer"))
137137
.withHeader("DPoP", absent())
138138
.willReturn(aResponse()
139139
.withStatus(201)));

‎integration/base/pom.xml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
<artifactId>inrupt-client-webid</artifactId>
6969
<version>${project.version}</version>
7070
</dependency>
71+
<dependency>
72+
<groupId>com.inrupt.client</groupId>
73+
<artifactId>inrupt-client-caffeine</artifactId>
74+
<version>${project.version}</version>
75+
</dependency>
7176
<dependency>
7277
<groupId>com.fasterxml.jackson.core</groupId>
7378
<artifactId>jackson-annotations</artifactId>

‎integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,35 +85,35 @@ public class AccessGrantScenarios {
8585

8686
private static String podUrl;
8787
private static String issuer;
88-
private static String webidUrl;
88+
protected static String webidUrl;
8989
private static final String MOCK_USERNAME = "someuser";
9090

9191
private static final String CLIENT_ID = config.getValue("inrupt.test.client-id", String.class);
9292
private static final String CLIENT_SECRET = config.getValue("inrupt.test.client-secret", String.class);
9393
private static final String AUTH_METHOD = config
9494
.getOptionalValue("inrupt.test.auth-method", String.class)
9595
.orElse("client_secret_basic");
96-
private static String VC_PROVIDER;
96+
protected static String VC_PROVIDER;
9797
private static final String PRIVATE_RESOURCE_PATH = config
9898
.getOptionalValue("inrupt.test.private-resource-path", String.class)
9999
.orElse("private");
100100

101101
private static final URI ACCESS_GRANT = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant");
102102
private static final URI ACCESS_REQUEST = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessRequest");
103-
private static final String GRANT_MODE_READ = "Read";
103+
protected static final String GRANT_MODE_READ = "Read";
104104
private static final String GRANT_MODE_APPEND = "Append";
105105
private static final String GRANT_MODE_WRITE = "Write";
106106
private static final URI PURPOSE1 = URI.create("https://purpose.example/212efdf4-e1a4-4dcd-9d3b-d6eb92e0205f");
107107
private static final URI PURPOSE2 = URI.create("https://purpose.example/de605b08-76c7-4f04-9cec-a438810b0c03");
108-
private static final Set<URI> PURPOSES = new HashSet<>(Arrays.asList(PURPOSE1, PURPOSE2));
109-
private static final String GRANT_EXPIRATION = "2024-04-03T12:00:00Z";
108+
protected static final Set<URI> PURPOSES = new HashSet<>(Arrays.asList(PURPOSE1, PURPOSE2));
109+
protected static final String GRANT_EXPIRATION = "2024-04-03T12:00:00Z";
110110

111111
private static URI testContainerURI;
112112
private static String testRDFresourceName = "resource.ttl";
113113
private static URI testRDFresourceURI;
114114
private static String sharedTextFileName = "sharedFile.txt";
115115
private static String sharedResourceName = "sharedResource";
116-
private static URI sharedTextFileURI;
116+
protected static URI sharedTextFileURI;
117117
private static URI sharedResource;
118118
private static Session session;
119119

@@ -182,7 +182,12 @@ static void setup() throws IOException {
182182
prepareACPofResource(authClient, sharedTextFileURI);
183183
}
184184

185-
accessGrantServer = new MockAccessGrantServer(State.WEBID, sharedTextFileURI, sharedResource);
185+
accessGrantServer = new MockAccessGrantServer(
186+
State.WEBID,
187+
sharedTextFileURI,
188+
sharedResource,
189+
authServer.getMockServerUrl()
190+
);
186191
accessGrantServer.start();
187192

188193
VC_PROVIDER = config

‎integration/base/src/main/java/com/inrupt/client/integration/base/AuthenticationScenarios.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public class AuthenticationScenarios {
7272

7373
private static String testResourceName = "resource.ttl";
7474
private static URI publicResourceURL;
75-
private static URI privateResourceURL;
75+
protected static URI privateResourceURL;
7676
private static final Config config = ConfigProvider.getConfig();
7777

7878
private static final String CLIENT_ID = config.getValue("inrupt.test.client-id", String.class);

‎integration/base/src/main/java/com/inrupt/client/integration/base/MockAccessGrantServer.java‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,25 @@
3636
class MockAccessGrantServer {
3737

3838
private static final String DERIVE = "/derive";
39+
private static final String ISSUE = "/issue";
3940

4041
private final WireMockServer wireMockServer;
4142
private final String webId;
4243
private final String sharedFile;
4344
private final String sharedResource;
4445

45-
public MockAccessGrantServer(final URI webId, final URI sharedFile, final URI sharedResource) {
46+
private final String authorisationServerUrl;
47+
48+
public MockAccessGrantServer(
49+
final URI webId,
50+
final URI sharedFile,
51+
final URI sharedResource,
52+
final String authorisationServerUrl
53+
) {
4654
this.webId = webId.toString();
4755
this.sharedFile = sharedFile.toString();
4856
this.sharedResource = sharedResource.toString();
57+
this.authorisationServerUrl = authorisationServerUrl;
4958
wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort());
5059
}
5160

@@ -74,8 +83,16 @@ private void setupMocks() {
7483
.willReturn(aResponse()
7584
.withStatus(204)));
7685

77-
wireMockServer.stubFor(post(urlEqualTo("/issue"))
78-
.atPriority(2)
86+
wireMockServer.stubFor(post(urlEqualTo(ISSUE))
87+
.atPriority(2)
88+
.willReturn(aResponse()
89+
.withStatus(401)
90+
.withHeader("WWW-Authenticate", "Bearer, DPoP algs=\"ES256\", " +
91+
"UMA ticket=\"ticket-67890\", as_uri=\"" + authorisationServerUrl + "\"")));
92+
93+
wireMockServer.stubFor(post(urlEqualTo(ISSUE))
94+
.atPriority(3)
95+
.withHeader("Authorization", containing("Bearer"))
7996
.withRequestBody(containing("hasConsent"))
8097
.withRequestBody(containing(this.sharedResource))
8198
.willReturn(aResponse()
@@ -84,8 +101,9 @@ private void setupMocks() {
84101
.withBody(getResource("/vc-request.json", wireMockServer.baseUrl(),
85102
this.webId, this.sharedResource))));
86103

87-
wireMockServer.stubFor(post(urlEqualTo("/issue"))
88-
.atPriority(2)
104+
wireMockServer.stubFor(post(urlEqualTo(ISSUE))
105+
.atPriority(3)
106+
.withHeader("Authorization", containing("Bearer"))
89107
.withRequestBody(containing("providedConsent"))
90108
.withRequestBody(containing(this.sharedResource))
91109
.willReturn(aResponse()
@@ -94,17 +112,19 @@ private void setupMocks() {
94112
.withBody(getResource("/vc-grant.json", wireMockServer.baseUrl(),
95113
this.webId, this.sharedResource))));
96114

97-
wireMockServer.stubFor(post(urlEqualTo("/issue"))
115+
wireMockServer.stubFor(post(urlEqualTo(ISSUE))
98116
.atPriority(1)
117+
.withHeader("Authorization", containing("Bearer"))
99118
.withRequestBody(containing("hasConsent"))
100119
.willReturn(aResponse()
101120
.withStatus(Utils.SUCCESS)
102121
.withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON)
103122
.withBody(getResource("/vc-request.json", wireMockServer.baseUrl(),
104123
this.webId, this.sharedFile))));
105124

106-
wireMockServer.stubFor(post(urlEqualTo("/issue"))
125+
wireMockServer.stubFor(post(urlEqualTo(ISSUE))
107126
.atPriority(1)
127+
.withHeader("Authorization", containing("Bearer"))
108128
.withRequestBody(containing("providedConsent"))
109129
.willReturn(aResponse()
110130
.withStatus(Utils.SUCCESS)

‎integration/base/src/main/java/com/inrupt/client/integration/base/MockUMAAuthorizationServer.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ private void setupMocks() {
5757
.willReturn(aResponse()
5858
.withStatus(Utils.SUCCESS)
5959
.withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON)
60-
.withBody("{\"access_token\":\"token-67890\",\"token_type\":\"Bearer\"}")));
60+
.withBody(
61+
"{\"access_token\":\"token-67890\",\"token_type\":\"Bearer\",\"expires_in\":\"3600\"}"
62+
)));
6163
}
6264

6365
public void start() {

‎integration/base/src/main/java/com/inrupt/client/integration/base/Utils.java‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@
4141
import org.jose4j.lang.JoseException;
4242
import org.jose4j.lang.UncheckedJoseException;
4343

44-
final class Utils {
44+
public final class Utils {
4545

4646
static final String ACCEPT = "Accept";
47-
static final String CONTENT_TYPE = "Content-Type";
47+
public static final String CONTENT_TYPE = "Content-Type";
4848
static final String IF_NONE_MATCH = "If-None-Match";
4949
static final String TEXT_TURTLE = "text/turtle";
5050
static final String SPARQL_UPDATE = "application/sparql-update";
5151
static final String APPLICATION_JSON = "application/json";
5252
static final String APPLICATION_FORM = "application/x-www-form-urlencoded";
53-
static final String PLAIN_TEXT = "text/plain";
53+
public static final String PLAIN_TEXT = "text/plain";
5454
static final String APPLICATION_OCTET = "application/octet-stream";
5555
static final String PATCH = "PATCH";
5656
static final String WILDCARD = "*";
@@ -62,7 +62,7 @@ final class Utils {
6262
static final int SUCCESS = 200;
6363
static final int CREATED = 201;
6464
static final int NO_CONTENT = 204;
65-
static final int UNAUTHORIZED = 401;
65+
public static final int UNAUTHORIZED = 401;
6666
static final int NOT_FOUND = 404;
6767
static final int NOT_ALLOWED = 405;
6868
static final int CONFLICT = 409;

‎integration/openid/src/test/java/com/inrupt/client/integration/openid/OpenIdAuthenticationScenariosTest.java‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,47 @@
2020
*/
2121
package com.inrupt.client.integration.openid;
2222

23+
import static org.junit.jupiter.api.Assertions.*;
24+
25+
import com.inrupt.client.Request;
26+
import com.inrupt.client.auth.Session;
2327
import com.inrupt.client.integration.base.AuthenticationScenarios;
28+
import com.inrupt.client.solid.SolidRDFSource;
29+
import com.inrupt.client.solid.SolidSyncClient;
30+
31+
import java.net.URI;
32+
33+
import org.junit.jupiter.params.ParameterizedTest;
34+
import org.junit.jupiter.params.provider.MethodSource;
35+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
2437

2538
class OpenIdAuthenticationScenariosTest extends AuthenticationScenarios {
39+
private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdAuthenticationScenariosTest.class);
40+
41+
@ParameterizedTest
42+
@MethodSource("provideSessions")
43+
void openIdAuthenticationTest(final Session session) {
44+
LOGGER.info("Integration Test - Authenticated fetch of private resource uses OpenID authenticator");
45+
//create private resource
46+
final SolidSyncClient authClient = SolidSyncClient.getClient().session(session);
47+
try (final SolidRDFSource testResource = new SolidRDFSource(privateResourceURL, null, null)) {
48+
assertDoesNotThrow(() -> authClient.create(testResource));
49+
50+
assertDoesNotThrow(() -> authClient.read(privateResourceURL, SolidRDFSource.class));
51+
52+
// Lookup the session cache.
53+
final Request dummyRequest = Request.newBuilder(privateResourceURL).build();
54+
final var token = session.fromCache(dummyRequest);
55+
final var credential = session.getCredential(
56+
URI.create("http://openid.net/specs/openid-connect-core-1_0.html#IDToken"),
57+
null
58+
);
59+
// If OpenID authentication was successful, both the token and the credential (i.e. the ID Token) are issued
60+
// by the OpenID provider.
61+
assertEquals(token.get().getIssuer(), credential.get().getIssuer());
2662

63+
assertDoesNotThrow(() -> authClient.delete(testResource));
64+
}
65+
}
2766
}

‎integration/uma/src/test/java/com/inrupt/client/integration/uma/UmaAccessGrantScenarioTest.java‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,54 @@
2020
*/
2121
package com.inrupt.client.integration.uma;
2222

23+
import static org.junit.jupiter.api.Assertions.*;
24+
25+
import com.inrupt.client.Request;
26+
import com.inrupt.client.accessgrant.AccessGrantClient;
27+
import com.inrupt.client.auth.Session;
2328
import com.inrupt.client.integration.base.AccessGrantScenarios;
2429

30+
import java.net.URI;
31+
import java.time.Instant;
32+
import java.util.Arrays;
33+
import java.util.HashSet;
34+
35+
import org.junit.jupiter.params.ParameterizedTest;
36+
import org.junit.jupiter.params.provider.MethodSource;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
39+
2540
public class UmaAccessGrantScenarioTest extends AccessGrantScenarios {
2641

42+
private static final Logger LOGGER = LoggerFactory.getLogger(UmaAccessGrantScenarioTest.class);
43+
44+
@ParameterizedTest
45+
@MethodSource("provideSessions")
46+
void accessGrantUmaAuthentication(final Session session) {
47+
LOGGER.info("Integration Test - UMA Authentication to VC endpoint");
48+
49+
final AccessGrantClient accessGrantClient = new AccessGrantClient(
50+
URI.create(AccessGrantScenarios.VC_PROVIDER)
51+
).session(session);
52+
53+
// Make an authenticated request to the VC provider /issue endpoint to enforce a token is cached by the session.
54+
accessGrantClient.requestAccess(URI.create(webidUrl),
55+
new HashSet<>(Arrays.asList(sharedTextFileURI)),
56+
new HashSet<>(Arrays.asList(GRANT_MODE_READ)),
57+
PURPOSES,
58+
Instant.parse(GRANT_EXPIRATION))
59+
.toCompletableFuture().join();
60+
// Lookup the session cache.
61+
final Request dummyRequest = Request.newBuilder(
62+
URI.create(AccessGrantScenarios.VC_PROVIDER).resolve("/issue")
63+
).POST(Request.BodyPublishers.ofString("Not relevant")).build();
64+
final var token = session.fromCache(dummyRequest);
65+
final var credential = session.getCredential(
66+
URI.create("http://openid.net/specs/openid-connect-core-1_0.html#IDToken"),
67+
null
68+
);
69+
// If UMA authentication was successful, the token issuer will be the UMA server, while the credential (i.e. the
70+
// ID Token) is issued by the OpenID provider.
71+
assertNotEquals(token.get().getIssuer(), credential.get().getIssuer());
72+
}
2773
}

‎openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public final class OpenIdSession implements Session {
7979
private final AtomicReference<Credential> credential = new AtomicReference<>();
8080
private final ForkJoinPool executor = new ForkJoinPool(1);
8181
private final DPoP dpop;
82-
private final ClientCache<URI, Boolean> requestCache;
82+
private final ClientCache<URI, Credential> requestCache;
8383

8484
private OpenIdSession(final String id, final DPoP dpop,
8585
final Supplier<CompletionStage<Credential>> authenticator) {
@@ -222,10 +222,12 @@ public Optional<String> generateProof(final String jkt, final Request request) {
222222

223223
@Override
224224
public Optional<Credential> fromCache(final Request request) {
225-
final Credential c = credential.get();
226-
if (!hasExpired(c) && request != null && requestCache.get(cacheKey(request.uri())) != null) {
227-
LOGGER.debug("Using cached token for request: {}", request.uri());
228-
return Optional.of(c);
225+
if (request != null) {
226+
final Credential cachedToken = requestCache.get(cacheKey(request.uri()));
227+
if (!hasExpired(cachedToken)) {
228+
LOGGER.debug("Using cached token for request: {}", request.uri());
229+
return Optional.of(cachedToken);
230+
}
229231
}
230232
return Optional.empty();
231233
}
@@ -239,19 +241,26 @@ public void reset() {
239241
@Override
240242
public CompletionStage<Optional<Credential>> authenticate(final Authenticator auth,
241243
final Request request, final Set<String> algorithms) {
242-
final Optional<Credential> cred = getCredential(ID_TOKEN, null);
243-
if (cred.isPresent() && request != null) {
244-
LOGGER.debug("Setting cache entry for request: {}", request.uri());
245-
requestCache.put(cacheKey(request.uri()), Boolean.TRUE);
246-
}
247-
return CompletableFuture.completedFuture(cred);
244+
return auth.authenticate(this, request, algorithms)
245+
.thenApply(credential -> {
246+
if (credential != null) {
247+
LOGGER.debug("Setting cache entry for request: {}", request.uri());
248+
requestCache.put(cacheKey(request.uri()), credential);
249+
}
250+
return Optional.ofNullable(credential);
251+
});
248252
}
249253

250254
/* deprecated */
251255
@Override
252256
public CompletionStage<Optional<Credential>> authenticate(final Request request,
253257
final Set<String> algorithms) {
254-
return authenticate(null, request, algorithms);
258+
final Optional<Credential> cred = getCredential(ID_TOKEN, null);
259+
if (cred.isPresent() && request != null) {
260+
LOGGER.debug("Setting cache entry for request: {}", request.uri());
261+
requestCache.put(cacheKey(request.uri()), cred.get());
262+
}
263+
return CompletableFuture.completedFuture(cred);
255264
}
256265

257266
boolean hasExpired(final Credential credential) {

0 commit comments

Comments
 (0)