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
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ private void setupMocks() {
.withHeader("User-Agent", equalTo(USER_AGENT))
.withRequestBody(matching("Test String 1"))
.withHeader(CONTENT_TYPE, containing(TEXT_PLAIN))
.withHeader("Authorization", containing("Bearer eyJ"))
.withHeader("Authorization", containing("Bearer"))
.withHeader("DPoP", absent())
.willReturn(aResponse()
.withStatus(201)));
Expand Down
5 changes: 5 additions & 0 deletions integration/base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
<artifactId>inrupt-client-webid</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.inrupt.client</groupId>
<artifactId>inrupt-client-caffeine</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,35 +85,35 @@ public class AccessGrantScenarios {

private static String podUrl;
private static String issuer;
private static String webidUrl;
protected static String webidUrl;
private static final String MOCK_USERNAME = "someuser";

private static final String CLIENT_ID = config.getValue("inrupt.test.client-id", String.class);
private static final String CLIENT_SECRET = config.getValue("inrupt.test.client-secret", String.class);
private static final String AUTH_METHOD = config
.getOptionalValue("inrupt.test.auth-method", String.class)
.orElse("client_secret_basic");
private static String VC_PROVIDER;
protected static String VC_PROVIDER;
private static final String PRIVATE_RESOURCE_PATH = config
.getOptionalValue("inrupt.test.private-resource-path", String.class)
.orElse("private");

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

private static URI testContainerURI;
private static String testRDFresourceName = "resource.ttl";
private static URI testRDFresourceURI;
private static String sharedTextFileName = "sharedFile.txt";
private static String sharedResourceName = "sharedResource";
private static URI sharedTextFileURI;
protected static URI sharedTextFileURI;
private static URI sharedResource;
private static Session session;

Expand Down Expand Up @@ -182,7 +182,12 @@ static void setup() throws IOException {
prepareACPofResource(authClient, sharedTextFileURI);
}

accessGrantServer = new MockAccessGrantServer(State.WEBID, sharedTextFileURI, sharedResource);
accessGrantServer = new MockAccessGrantServer(
State.WEBID,
sharedTextFileURI,
sharedResource,
authServer.getMockServerUrl()
);
accessGrantServer.start();

VC_PROVIDER = config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class AuthenticationScenarios {

private static String testResourceName = "resource.ttl";
private static URI publicResourceURL;
private static URI privateResourceURL;
protected static URI privateResourceURL;
private static final Config config = ConfigProvider.getConfig();

private static final String CLIENT_ID = config.getValue("inrupt.test.client-id", String.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,25 @@
class MockAccessGrantServer {

private static final String DERIVE = "/derive";
private static final String ISSUE = "/issue";

private final WireMockServer wireMockServer;
private final String webId;
private final String sharedFile;
private final String sharedResource;

public MockAccessGrantServer(final URI webId, final URI sharedFile, final URI sharedResource) {
private final String authorisationServerUrl;

public MockAccessGrantServer(
final URI webId,
final URI sharedFile,
final URI sharedResource,
final String authorisationServerUrl
) {
this.webId = webId.toString();
this.sharedFile = sharedFile.toString();
this.sharedResource = sharedResource.toString();
this.authorisationServerUrl = authorisationServerUrl;
wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort());
}

Expand Down Expand Up @@ -74,8 +83,16 @@ private void setupMocks() {
.willReturn(aResponse()
.withStatus(204)));

wireMockServer.stubFor(post(urlEqualTo("/issue"))
.atPriority(2)
wireMockServer.stubFor(post(urlEqualTo(ISSUE))
.atPriority(2)
.willReturn(aResponse()
.withStatus(401)
.withHeader("WWW-Authenticate", "Bearer, DPoP algs=\"ES256\", " +
"UMA ticket=\"ticket-67890\", as_uri=\"" + authorisationServerUrl + "\"")));

wireMockServer.stubFor(post(urlEqualTo(ISSUE))
.atPriority(3)
.withHeader("Authorization", containing("Bearer"))
.withRequestBody(containing("hasConsent"))
.withRequestBody(containing(this.sharedResource))
.willReturn(aResponse()
Expand All @@ -84,8 +101,9 @@ private void setupMocks() {
.withBody(getResource("/vc-request.json", wireMockServer.baseUrl(),
this.webId, this.sharedResource))));

wireMockServer.stubFor(post(urlEqualTo("/issue"))
.atPriority(2)
wireMockServer.stubFor(post(urlEqualTo(ISSUE))
.atPriority(3)
.withHeader("Authorization", containing("Bearer"))
.withRequestBody(containing("providedConsent"))
.withRequestBody(containing(this.sharedResource))
.willReturn(aResponse()
Expand All @@ -94,17 +112,19 @@ private void setupMocks() {
.withBody(getResource("/vc-grant.json", wireMockServer.baseUrl(),
this.webId, this.sharedResource))));

wireMockServer.stubFor(post(urlEqualTo("/issue"))
wireMockServer.stubFor(post(urlEqualTo(ISSUE))
.atPriority(1)
.withHeader("Authorization", containing("Bearer"))
.withRequestBody(containing("hasConsent"))
.willReturn(aResponse()
.withStatus(Utils.SUCCESS)
.withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON)
.withBody(getResource("/vc-request.json", wireMockServer.baseUrl(),
this.webId, this.sharedFile))));

wireMockServer.stubFor(post(urlEqualTo("/issue"))
wireMockServer.stubFor(post(urlEqualTo(ISSUE))
.atPriority(1)
.withHeader("Authorization", containing("Bearer"))
.withRequestBody(containing("providedConsent"))
.willReturn(aResponse()
.withStatus(Utils.SUCCESS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ private void setupMocks() {
.willReturn(aResponse()
.withStatus(Utils.SUCCESS)
.withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON)
.withBody("{\"access_token\":\"token-67890\",\"token_type\":\"Bearer\"}")));
.withBody(
"{\"access_token\":\"token-67890\",\"token_type\":\"Bearer\",\"expires_in\":\"3600\"}"
)));
}

public void start() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@
import org.jose4j.lang.JoseException;
import org.jose4j.lang.UncheckedJoseException;

final class Utils {
public final class Utils {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're making this public, should we also make the other fields public? (That could also be a follow-on PR to keep this one simpler)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no reason not to, and it would probably look more consistent. I'll open a follow-up PR.


static final String ACCEPT = "Accept";
static final String CONTENT_TYPE = "Content-Type";
public static final String CONTENT_TYPE = "Content-Type";
static final String IF_NONE_MATCH = "If-None-Match";
static final String TEXT_TURTLE = "text/turtle";
static final String SPARQL_UPDATE = "application/sparql-update";
static final String APPLICATION_JSON = "application/json";
static final String APPLICATION_FORM = "application/x-www-form-urlencoded";
static final String PLAIN_TEXT = "text/plain";
public static final String PLAIN_TEXT = "text/plain";
static final String APPLICATION_OCTET = "application/octet-stream";
static final String PATCH = "PATCH";
static final String WILDCARD = "*";
Expand All @@ -62,7 +62,7 @@ final class Utils {
static final int SUCCESS = 200;
static final int CREATED = 201;
static final int NO_CONTENT = 204;
static final int UNAUTHORIZED = 401;
public static final int UNAUTHORIZED = 401;
static final int NOT_FOUND = 404;
static final int NOT_ALLOWED = 405;
static final int CONFLICT = 409;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,47 @@
*/
package com.inrupt.client.integration.openid;

import static org.junit.jupiter.api.Assertions.*;

import com.inrupt.client.Request;
import com.inrupt.client.auth.Session;
import com.inrupt.client.integration.base.AuthenticationScenarios;
import com.inrupt.client.solid.SolidRDFSource;
import com.inrupt.client.solid.SolidSyncClient;

import java.net.URI;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class OpenIdAuthenticationScenariosTest extends AuthenticationScenarios {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdAuthenticationScenariosTest.class);

@ParameterizedTest
@MethodSource("provideSessions")
void openIdAuthenticationTest(final Session session) {
LOGGER.info("Integration Test - Authenticated fetch of private resource uses OpenID authenticator");
//create private resource
final SolidSyncClient authClient = SolidSyncClient.getClient().session(session);
try (final SolidRDFSource testResource = new SolidRDFSource(privateResourceURL, null, null)) {
assertDoesNotThrow(() -> authClient.create(testResource));

assertDoesNotThrow(() -> authClient.read(privateResourceURL, SolidRDFSource.class));

// Lookup the session cache.
final Request dummyRequest = Request.newBuilder(privateResourceURL).build();
final var token = session.fromCache(dummyRequest);
final var credential = session.getCredential(
URI.create("http://openid.net/specs/openid-connect-core-1_0.html#IDToken"),
null
);
// If OpenID authentication was successful, both the token and the credential (i.e. the ID Token) are issued
// by the OpenID provider.
assertEquals(token.get().getIssuer(), credential.get().getIssuer());

assertDoesNotThrow(() -> authClient.delete(testResource));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,54 @@
*/
package com.inrupt.client.integration.uma;

import static org.junit.jupiter.api.Assertions.*;

import com.inrupt.client.Request;
import com.inrupt.client.accessgrant.AccessGrantClient;
import com.inrupt.client.auth.Session;
import com.inrupt.client.integration.base.AccessGrantScenarios;

import java.net.URI;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashSet;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UmaAccessGrantScenarioTest extends AccessGrantScenarios {

private static final Logger LOGGER = LoggerFactory.getLogger(UmaAccessGrantScenarioTest.class);

@ParameterizedTest
@MethodSource("provideSessions")
void accessGrantUmaAuthentication(final Session session) {
LOGGER.info("Integration Test - UMA Authentication to VC endpoint");

final AccessGrantClient accessGrantClient = new AccessGrantClient(
URI.create(AccessGrantScenarios.VC_PROVIDER)
).session(session);

// Make an authenticated request to the VC provider /issue endpoint to enforce a token is cached by the session.
accessGrantClient.requestAccess(URI.create(webidUrl),
new HashSet<>(Arrays.asList(sharedTextFileURI)),
new HashSet<>(Arrays.asList(GRANT_MODE_READ)),
PURPOSES,
Instant.parse(GRANT_EXPIRATION))
.toCompletableFuture().join();
// Lookup the session cache.
final Request dummyRequest = Request.newBuilder(
URI.create(AccessGrantScenarios.VC_PROVIDER).resolve("/issue")
).POST(Request.BodyPublishers.ofString("Not relevant")).build();
final var token = session.fromCache(dummyRequest);
final var credential = session.getCredential(
URI.create("http://openid.net/specs/openid-connect-core-1_0.html#IDToken"),
null
);
// If UMA authentication was successful, the token issuer will be the UMA server, while the credential (i.e. the
// ID Token) is issued by the OpenID provider.
assertNotEquals(token.get().getIssuer(), credential.get().getIssuer());
}
}
33 changes: 21 additions & 12 deletions openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public final class OpenIdSession implements Session {
private final AtomicReference<Credential> credential = new AtomicReference<>();
private final ForkJoinPool executor = new ForkJoinPool(1);
private final DPoP dpop;
private final ClientCache<URI, Boolean> requestCache;
private final ClientCache<URI, Credential> requestCache;

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

@Override
public Optional<Credential> fromCache(final Request request) {
final Credential c = credential.get();
if (!hasExpired(c) && request != null && requestCache.get(cacheKey(request.uri())) != null) {
LOGGER.debug("Using cached token for request: {}", request.uri());
return Optional.of(c);
if (request != null) {
final Credential cachedToken = requestCache.get(cacheKey(request.uri()));
if (!hasExpired(cachedToken)) {
LOGGER.debug("Using cached token for request: {}", request.uri());
return Optional.of(cachedToken);
}
}
return Optional.empty();
}
Expand All @@ -239,19 +241,26 @@ public void reset() {
@Override
public CompletionStage<Optional<Credential>> authenticate(final Authenticator auth,
final Request request, final Set<String> algorithms) {
final Optional<Credential> cred = getCredential(ID_TOKEN, null);
if (cred.isPresent() && request != null) {
LOGGER.debug("Setting cache entry for request: {}", request.uri());
requestCache.put(cacheKey(request.uri()), Boolean.TRUE);
}
return CompletableFuture.completedFuture(cred);
return auth.authenticate(this, request, algorithms)
.thenApply(credential -> {
if (credential != null) {
LOGGER.debug("Setting cache entry for request: {}", request.uri());
requestCache.put(cacheKey(request.uri()), credential);
}
return Optional.ofNullable(credential);
});
}

/* deprecated */
@Override
public CompletionStage<Optional<Credential>> authenticate(final Request request,
final Set<String> algorithms) {
return authenticate(null, request, algorithms);
final Optional<Credential> cred = getCredential(ID_TOKEN, null);
if (cred.isPresent() && request != null) {
LOGGER.debug("Setting cache entry for request: {}", request.uri());
requestCache.put(cacheKey(request.uri()), cred.get());
}
return CompletableFuture.completedFuture(cred);
}

boolean hasExpired(final Credential credential) {
Expand Down