From 4751815524e9f88fe03355471828245b0df6479f Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Wed, 17 May 2023 12:45:11 -0400 Subject: [PATCH 1/3] JCL-279: Implement token cache for session objects --- .../accessgrant/AccessGrantSession.java | 45 +++++++++++++++++-- .../client/auth/ReactiveAuthorization.java | 2 +- .../java/com/inrupt/client/auth/Session.java | 20 +++++++++ .../inrupt/client/core/MockHttpService.java | 2 +- .../inrupt/client/openid/OpenIdSession.java | 23 +++++++++- .../client/openid/OpenIdSessionTest.java | 4 +- .../com/inrupt/client/uma/UmaSession.java | 22 ++++++++- 7 files changed, 108 insertions(+), 10 deletions(-) diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantSession.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantSession.java index aabacfff75a..265c1959e31 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantSession.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantSession.java @@ -22,17 +22,23 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import com.inrupt.client.ClientCache; import com.inrupt.client.Request; +import com.inrupt.client.auth.Authenticator; import com.inrupt.client.auth.Credential; import com.inrupt.client.auth.Session; +import com.inrupt.client.spi.ServiceProvider; import java.net.URI; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.NavigableMap; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -53,10 +59,13 @@ public final class AccessGrantSession implements Session { private final String id; private final Session session; private final NavigableMap grants = new ConcurrentSkipListMap<>(); + private final ClientCache tokenCache; - private AccessGrantSession(final Session session, final List grants) { + private AccessGrantSession(final Session session, final ClientCache cache, + final List grants) { this.id = UUID.randomUUID().toString(); this.session = session; + this.tokenCache = Objects.requireNonNull(cache, "Cache may not be null!"); for (final AccessGrant grant : grants) { for (final URI uri : grant.getResources()) { @@ -73,7 +82,21 @@ private AccessGrantSession(final Session session, final List grants * @return the Access Grant-based session */ public static AccessGrantSession ofAccessGrant(final Session session, final AccessGrant... accessGrants) { - return new AccessGrantSession(session, Arrays.asList(accessGrants)); + return ofAccessGrant(session, ServiceProvider.getCacheBuilder().build(1000, Duration.ofMinutes(10)), + accessGrants); + } + + /** + * Create a session with a collection of known access grants. + * + * @param session the OpenID Session + * @param cache a pre-configured cache + * @param accessGrants the access grants + * @return the Access Grant-based session + */ + public static AccessGrantSession ofAccessGrant(final Session session, final ClientCache cache, + final AccessGrant... accessGrants) { + return new AccessGrantSession(session, cache, Arrays.asList(accessGrants)); } @Override @@ -117,6 +140,19 @@ public Optional generateProof(final String jkt, final Request request) { return session.generateProof(jkt, request); } + @Override + public CompletionStage> authenticate(final Authenticator authenticator, + final Request request, final Set algorithms) { + return authenticator.authenticate(this, request, algorithms) + .thenApply(credential -> { + if (credential != null) { + tokenCache.put(request.uri(), credential); + } + return Optional.ofNullable(credential); + }); + } + + /* deprecated */ @Override public CompletionStage> authenticate(final Request request, final Set algorithms) { @@ -129,7 +165,10 @@ public CompletionStage> authenticate(final Request request, @Override public Optional fromCache(final Request request) { - // TODO add cache + final Credential cachedToken = tokenCache.get(request.uri()); + if (cachedToken != null && cachedToken.getExpiration().isAfter(Instant.now())) { + return Optional.of(cachedToken); + } return Optional.empty(); } diff --git a/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java b/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java index 6192afbc63b..18c10e88303 100644 --- a/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java +++ b/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java @@ -95,7 +95,7 @@ public CompletionStage> negotiate(final Session session, fi authenticators.sort(comparator); final Authenticator auth = authenticators.get(0); LOGGER.debug("Using {} authenticator", auth.getName()); - return auth.authenticate(session, request, algorithms).thenApply(Optional::ofNullable); + return session.authenticate(auth, request, algorithms); } return CompletableFuture.completedFuture(Optional.empty()); } diff --git a/api/src/main/java/com/inrupt/client/auth/Session.java b/api/src/main/java/com/inrupt/client/auth/Session.java index 0e73584801b..ece51532993 100644 --- a/api/src/main/java/com/inrupt/client/auth/Session.java +++ b/api/src/main/java/com/inrupt/client/auth/Session.java @@ -99,9 +99,22 @@ public interface Session { * @param request the HTTP request * @param algorithms the supported DPoP algorithms * @return the next stage of completion, containing an access token, if present + * @deprecated as of Beta3, this method is no longer used */ + @Deprecated CompletionStage> authenticate(Request request, Set algorithms); + /** + * Fetch an authentication token from session values. + * + * @param authenticator the authenticator in use + * @param request the HTTP request + * @param algorithms the supported DPoP algorithms + * @return the next stage of completion, containing an access token, if present + */ + CompletionStage> authenticate(Authenticator authenticator, Request request, + Set algorithms); + /** * Create a new anonymous session. * @@ -146,6 +159,13 @@ public Optional selectThumbprint(final Collection algorithms) { return Optional.empty(); } + @Override + public CompletionStage> authenticate(final Authenticator authenticator, + final Request request, final Set algorithms) { + return authenticator.authenticate(this, request, algorithms).thenApply(Optional::ofNullable); + } + + /* deprecated */ @Override public CompletionStage> authenticate(final Request request, final Set algorithms) { diff --git a/core/src/test/java/com/inrupt/client/core/MockHttpService.java b/core/src/test/java/com/inrupt/client/core/MockHttpService.java index b46049949bd..7d77635c75c 100644 --- a/core/src/test/java/com/inrupt/client/core/MockHttpService.java +++ b/core/src/test/java/com/inrupt/client/core/MockHttpService.java @@ -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 token-67890")) + .withHeader("Authorization", containing("Bearer eyJ")) .withHeader("DPoP", absent()) .willReturn(aResponse() .withStatus(201))); diff --git a/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java b/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java index 11180fd3532..32537a0c0cd 100644 --- a/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java +++ b/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java @@ -22,13 +22,17 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import com.inrupt.client.ClientCache; import com.inrupt.client.Request; +import com.inrupt.client.auth.Authenticator; import com.inrupt.client.auth.Credential; import com.inrupt.client.auth.DPoP; import com.inrupt.client.auth.Session; +import com.inrupt.client.spi.ServiceProvider; import java.net.URI; import java.security.MessageDigest; +import java.time.Duration; import java.time.Instant; import java.util.Collection; import java.util.Collections; @@ -75,12 +79,14 @@ public final class OpenIdSession implements Session { private final AtomicReference credential = new AtomicReference<>(); private final ForkJoinPool executor = new ForkJoinPool(1); private final DPoP dpop; + private final ClientCache requestCache; private OpenIdSession(final String id, final DPoP dpop, final Supplier> authenticator) { this.id = Objects.requireNonNull(id, "Session id may not be null!"); this.authenticator = Objects.requireNonNull(authenticator, "OpenID authenticator may not be null!"); this.dpop = Objects.requireNonNull(dpop); + this.requestCache = ServiceProvider.getCacheBuilder().build(1000, Duration.ofMinutes(5)); // Support case-insensitive lookups final Set schemeNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); @@ -216,16 +222,29 @@ public Optional generateProof(final String jkt, final Request request) { @Override public Optional fromCache(final Request request) { final Credential c = credential.get(); - if (!hasExpired(c)) { + if (!hasExpired(c) && request != null && requestCache.get(request.uri()) != null) { + LOGGER.debug("Using cached token for request: {}", request.uri()); return Optional.of(c); } return Optional.empty(); } + @Override + public CompletionStage> authenticate(final Authenticator auth, + final Request request, final Set algorithms) { + final Optional credential = getCredential(ID_TOKEN, null); + if (credential.isPresent() && request != null) { + LOGGER.debug("Setting cache entry for request: {}", request.uri()); + requestCache.put(request.uri(), Boolean.TRUE); + } + return CompletableFuture.completedFuture(credential); + } + + /* deprecated */ @Override public CompletionStage> authenticate(final Request request, final Set algorithms) { - return CompletableFuture.completedFuture(getCredential(ID_TOKEN, null)); + return authenticate(null, request, algorithms); } boolean hasExpired(final Credential credential) { diff --git a/openid/src/test/java/com/inrupt/client/openid/OpenIdSessionTest.java b/openid/src/test/java/com/inrupt/client/openid/OpenIdSessionTest.java index 8c6cf749b17..5432fdb6367 100644 --- a/openid/src/test/java/com/inrupt/client/openid/OpenIdSessionTest.java +++ b/openid/src/test/java/com/inrupt/client/openid/OpenIdSessionTest.java @@ -157,7 +157,7 @@ void testClientCredentials() { assertFalse(session.fromCache(null).isPresent()); final Optional principal = session.getPrincipal(); assertEquals(Optional.of(URI.create(WEBID)), principal); - assertTrue(session.fromCache(null).isPresent()); + assertFalse(session.fromCache(null).isPresent()); final Optional credential = session.authenticate(null, Collections.emptySet()) .toCompletableFuture().join(); assertEquals(Optional.of(URI.create(WEBID)), credential.flatMap(Credential::getPrincipal)); @@ -176,7 +176,7 @@ void testClientCredentialsWithConfig() { assertFalse(session.fromCache(null).isPresent()); final Optional principal = session.getPrincipal(); assertEquals(Optional.of(URI.create(WEBID)), principal); - assertTrue(session.fromCache(null).isPresent()); + assertFalse(session.fromCache(null).isPresent()); final Optional credential = session.authenticate(null, Collections.emptySet()) .toCompletableFuture().join(); assertEquals(Optional.of(URI.create(WEBID)), credential.flatMap(Credential::getPrincipal)); diff --git a/uma/src/main/java/com/inrupt/client/uma/UmaSession.java b/uma/src/main/java/com/inrupt/client/uma/UmaSession.java index 89c73b0f1ac..9832b59c276 100644 --- a/uma/src/main/java/com/inrupt/client/uma/UmaSession.java +++ b/uma/src/main/java/com/inrupt/client/uma/UmaSession.java @@ -20,11 +20,16 @@ */ package com.inrupt.client.uma; +import com.inrupt.client.ClientCache; import com.inrupt.client.Request; +import com.inrupt.client.auth.Authenticator; import com.inrupt.client.auth.Credential; import com.inrupt.client.auth.Session; +import com.inrupt.client.spi.ServiceProvider; import java.net.URI; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -47,6 +52,7 @@ public final class UmaSession implements Session { private final String id; private final Set schemes; private final List internalSessions = new ArrayList<>(); + private final ClientCache tokenCache; private UmaSession(final Session... sessions) { this.id = UUID.randomUUID().toString(); @@ -57,6 +63,7 @@ private UmaSession(final Session... sessions) { } schemeTypes.add("UMA"); this.schemes = Collections.unmodifiableSet(schemeTypes); + this.tokenCache = ServiceProvider.getCacheBuilder().build(1000, Duration.ofMinutes(5)); } /** @@ -125,7 +132,20 @@ public Optional selectThumbprint(final Collection algorithms) { @Override public Optional fromCache(final Request request) { - return Optional.empty(); + return Optional.ofNullable(tokenCache.get(request.uri())) + .filter(credential -> credential.getExpiration().isAfter(Instant.now())); + } + + @Override + public CompletionStage> authenticate(final Authenticator authenticator, + final Request request, final Set algorithms) { + return authenticator.authenticate(this, request, algorithms) + .thenApply(credential -> { + if (credential != null) { + tokenCache.put(request.uri(), credential); + } + return Optional.ofNullable(credential); + }); } @Override From b9a6fb6edfc0da33a275b8e035716f2a4ea7c6d0 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Wed, 17 May 2023 16:49:00 -0400 Subject: [PATCH 2/3] Support fallback authenticator for malformed www-authenticate headers --- .../inrupt/client/auth/ReactiveAuthorization.java | 14 ++++++++++++-- .../inrupt/client/spi/AuthenticationProvider.java | 11 +++++++++++ .../uma/UmaAccessGrantScenarioTest.java | 2 +- .../openid/OpenIdAuthenticationProvider.java | 14 ++++++++++++-- .../com/inrupt/client/openid/OpenIdSession.java | 1 + .../client/uma/UmaAuthenticationProvider.java | 9 +++++++++ 6 files changed, 46 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java b/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java index 18c10e88303..d6ee133e1ca 100644 --- a/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java +++ b/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java @@ -47,6 +47,7 @@ public class ReactiveAuthorization { private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveAuthorization.class); + private static final String BEARER = "Bearer"; private static final Comparator comparator = Comparator .comparing(Authenticator::getPriority) @@ -64,7 +65,9 @@ public ReactiveAuthorization() { ReactiveAuthorization.class.getClassLoader()); for (final AuthenticationProvider provider : loader) { - registry.put(provider.getScheme(), provider); + for (final String scheme : provider.getSchemes()) { + registry.put(provider.getScheme(), provider); + } } } @@ -90,7 +93,14 @@ public CompletionStage> negotiate(final Session session, fi } } - if (!authenticators.isEmpty()) { + if (authenticators.isEmpty()) { + // Fallback in case of missing or poorly formed www-authenticate header + if (registry.containsKey(BEARER)) { + final Authenticator auth = registry.get(BEARER).getAuthenticator(Challenge.of(BEARER)); + LOGGER.debug("Using fallback Bearer authenticator"); + return session.authenticate(auth, request, algorithms); + } + } else { // Use the first authenticator, sorted by priority authenticators.sort(comparator); final Authenticator auth = authenticators.get(0); diff --git a/api/src/main/java/com/inrupt/client/spi/AuthenticationProvider.java b/api/src/main/java/com/inrupt/client/spi/AuthenticationProvider.java index ac5ab304b43..9648ed1c58d 100644 --- a/api/src/main/java/com/inrupt/client/spi/AuthenticationProvider.java +++ b/api/src/main/java/com/inrupt/client/spi/AuthenticationProvider.java @@ -23,6 +23,8 @@ import com.inrupt.client.auth.Authenticator; import com.inrupt.client.auth.Challenge; +import java.util.Set; + /** * An authentication mechanism that knows how to authenticate over network connections. */ @@ -32,9 +34,18 @@ public interface AuthenticationProvider { * Return the authorization scheme, such as Bearer or DPoP. * * @return the authorization scheme + * @deprecated as of Beta3, please use the {@link #getSchemes()} method */ + @Deprecated String getScheme(); + /** + * Return the set of supported authorization schemes, such as Bearer or DPoP. + * + * @return the authorization schemes + */ + Set getSchemes(); + /** * Return an authenticator for the supplied challenge. * diff --git a/integration/uma/src/test/java/com/inrupt/client/integration/uma/UmaAccessGrantScenarioTest.java b/integration/uma/src/test/java/com/inrupt/client/integration/uma/UmaAccessGrantScenarioTest.java index ff51ffcb4b9..a62ee007a02 100644 --- a/integration/uma/src/test/java/com/inrupt/client/integration/uma/UmaAccessGrantScenarioTest.java +++ b/integration/uma/src/test/java/com/inrupt/client/integration/uma/UmaAccessGrantScenarioTest.java @@ -24,4 +24,4 @@ public class UmaAccessGrantScenarioTest extends AccessGrantScenarios { -} \ No newline at end of file +} diff --git a/openid/src/main/java/com/inrupt/client/openid/OpenIdAuthenticationProvider.java b/openid/src/main/java/com/inrupt/client/openid/OpenIdAuthenticationProvider.java index e0abdec55ae..e36fe4fab2b 100644 --- a/openid/src/main/java/com/inrupt/client/openid/OpenIdAuthenticationProvider.java +++ b/openid/src/main/java/com/inrupt/client/openid/OpenIdAuthenticationProvider.java @@ -29,6 +29,7 @@ import java.util.Optional; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -38,11 +39,15 @@ public class OpenIdAuthenticationProvider implements AuthenticationProvider { private static final String BEARER = "Bearer"; + private static final String DPOP = "DPoP"; private final int priorityLevel; + private final Set schemes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); public OpenIdAuthenticationProvider() { this(50); + schemes.add(BEARER); + schemes.add(DPOP); } /** @@ -59,15 +64,20 @@ public String getScheme() { return BEARER; } + @Override + public Set getSchemes() { + return schemes; + } + @Override public Authenticator getAuthenticator(final Challenge challenge) { validate(challenge); return new OpenIdAuthenticator(priorityLevel); } - static void validate(final Challenge challenge) { + void validate(final Challenge challenge) { if (challenge == null || - !BEARER.equalsIgnoreCase(challenge.getScheme())) { + !schemes.contains(challenge.getScheme())) { throw new OpenIdException("Invalid challenge for OpenID authentication"); } } diff --git a/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java b/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java index 32537a0c0cd..395d2d7466c 100644 --- a/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java +++ b/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java @@ -91,6 +91,7 @@ private OpenIdSession(final String id, final DPoP dpop, // Support case-insensitive lookups final Set schemeNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); schemeNames.add("Bearer"); + schemeNames.add("DPoP"); this.schemes = Collections.unmodifiableSet(schemeNames); } diff --git a/uma/src/main/java/com/inrupt/client/uma/UmaAuthenticationProvider.java b/uma/src/main/java/com/inrupt/client/uma/UmaAuthenticationProvider.java index fe89c3b2b38..31172580905 100644 --- a/uma/src/main/java/com/inrupt/client/uma/UmaAuthenticationProvider.java +++ b/uma/src/main/java/com/inrupt/client/uma/UmaAuthenticationProvider.java @@ -36,6 +36,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -65,9 +66,11 @@ public class UmaAuthenticationProvider implements AuthenticationProvider { private final int priorityLevel; private final UmaClient umaClient; private final NeedInfoHandler claimHandler; + private final Set supportedSchemes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); public UmaAuthenticationProvider() { this(100); + supportedSchemes.add(UMA); } /** @@ -92,11 +95,17 @@ public UmaAuthenticationProvider(final int priority, final UmaClient umaClient) this.claimHandler = new NeedInfoHandler(); } + /* deprecated */ @Override public String getScheme() { return UMA; } + @Override + public Set getSchemes() { + return supportedSchemes; + } + @Override public Authenticator getAuthenticator(final Challenge challenge) { validate(challenge); From 9637928929bae6975aecd26debe7026b97ae1e71 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Wed, 17 May 2023 16:52:29 -0400 Subject: [PATCH 3/3] Correct loop var --- .../main/java/com/inrupt/client/auth/ReactiveAuthorization.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java b/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java index d6ee133e1ca..6af07618e07 100644 --- a/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java +++ b/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java @@ -66,7 +66,7 @@ public ReactiveAuthorization() { for (final AuthenticationProvider provider : loader) { for (final String scheme : provider.getSchemes()) { - registry.put(provider.getScheme(), provider); + registry.put(scheme, provider); } } }