Skip to content

Commit ae5aa15

Browse files
authored
JCL-279: Implement token cache for session objects (#459)
* JCL-279: Implement token cache for session objects * Support fallback authenticator for malformed www-authenticate headers
1 parent 4f89974 commit ae5aa15

File tree

11 files changed

+154
-15
lines changed

11 files changed

+154
-15
lines changed

access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantSession.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,23 @@
2222

2323
import static java.nio.charset.StandardCharsets.UTF_8;
2424

25+
import com.inrupt.client.ClientCache;
2526
import com.inrupt.client.Request;
27+
import com.inrupt.client.auth.Authenticator;
2628
import com.inrupt.client.auth.Credential;
2729
import com.inrupt.client.auth.Session;
30+
import com.inrupt.client.spi.ServiceProvider;
2831

2932
import java.net.URI;
33+
import java.time.Duration;
34+
import java.time.Instant;
3035
import java.util.Arrays;
3136
import java.util.Base64;
3237
import java.util.Collection;
3338
import java.util.List;
3439
import java.util.Map;
3540
import java.util.NavigableMap;
41+
import java.util.Objects;
3642
import java.util.Optional;
3743
import java.util.Set;
3844
import java.util.UUID;
@@ -53,10 +59,13 @@ public final class AccessGrantSession implements Session {
5359
private final String id;
5460
private final Session session;
5561
private final NavigableMap<URI, AccessGrant> grants = new ConcurrentSkipListMap<>();
62+
private final ClientCache<URI, Credential> tokenCache;
5663

57-
private AccessGrantSession(final Session session, final List<AccessGrant> grants) {
64+
private AccessGrantSession(final Session session, final ClientCache<URI, Credential> cache,
65+
final List<AccessGrant> grants) {
5866
this.id = UUID.randomUUID().toString();
5967
this.session = session;
68+
this.tokenCache = Objects.requireNonNull(cache, "Cache may not be null!");
6069

6170
for (final AccessGrant grant : grants) {
6271
for (final URI uri : grant.getResources()) {
@@ -73,7 +82,21 @@ private AccessGrantSession(final Session session, final List<AccessGrant> grants
7382
* @return the Access Grant-based session
7483
*/
7584
public static AccessGrantSession ofAccessGrant(final Session session, final AccessGrant... accessGrants) {
76-
return new AccessGrantSession(session, Arrays.asList(accessGrants));
85+
return ofAccessGrant(session, ServiceProvider.getCacheBuilder().build(1000, Duration.ofMinutes(10)),
86+
accessGrants);
87+
}
88+
89+
/**
90+
* Create a session with a collection of known access grants.
91+
*
92+
* @param session the OpenID Session
93+
* @param cache a pre-configured cache
94+
* @param accessGrants the access grants
95+
* @return the Access Grant-based session
96+
*/
97+
public static AccessGrantSession ofAccessGrant(final Session session, final ClientCache<URI, Credential> cache,
98+
final AccessGrant... accessGrants) {
99+
return new AccessGrantSession(session, cache, Arrays.asList(accessGrants));
77100
}
78101

79102
@Override
@@ -117,6 +140,19 @@ public Optional<String> generateProof(final String jkt, final Request request) {
117140
return session.generateProof(jkt, request);
118141
}
119142

143+
@Override
144+
public CompletionStage<Optional<Credential>> authenticate(final Authenticator authenticator,
145+
final Request request, final Set<String> algorithms) {
146+
return authenticator.authenticate(this, request, algorithms)
147+
.thenApply(credential -> {
148+
if (credential != null) {
149+
tokenCache.put(request.uri(), credential);
150+
}
151+
return Optional.ofNullable(credential);
152+
});
153+
}
154+
155+
/* deprecated */
120156
@Override
121157
public CompletionStage<Optional<Credential>> authenticate(final Request request,
122158
final Set<String> algorithms) {
@@ -129,7 +165,10 @@ public CompletionStage<Optional<Credential>> authenticate(final Request request,
129165

130166
@Override
131167
public Optional<Credential> fromCache(final Request request) {
132-
// TODO add cache
168+
final Credential cachedToken = tokenCache.get(request.uri());
169+
if (cachedToken != null && cachedToken.getExpiration().isAfter(Instant.now())) {
170+
return Optional.of(cachedToken);
171+
}
133172
return Optional.empty();
134173
}
135174

api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
public class ReactiveAuthorization {
4848

4949
private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveAuthorization.class);
50+
private static final String BEARER = "Bearer";
5051

5152
private static final Comparator<Authenticator> comparator = Comparator
5253
.comparing(Authenticator::getPriority)
@@ -64,7 +65,9 @@ public ReactiveAuthorization() {
6465
ReactiveAuthorization.class.getClassLoader());
6566

6667
for (final AuthenticationProvider provider : loader) {
67-
registry.put(provider.getScheme(), provider);
68+
for (final String scheme : provider.getSchemes()) {
69+
registry.put(scheme, provider);
70+
}
6871
}
6972
}
7073

@@ -90,12 +93,19 @@ public CompletionStage<Optional<Credential>> negotiate(final Session session, fi
9093
}
9194
}
9295

93-
if (!authenticators.isEmpty()) {
96+
if (authenticators.isEmpty()) {
97+
// Fallback in case of missing or poorly formed www-authenticate header
98+
if (registry.containsKey(BEARER)) {
99+
final Authenticator auth = registry.get(BEARER).getAuthenticator(Challenge.of(BEARER));
100+
LOGGER.debug("Using fallback Bearer authenticator");
101+
return session.authenticate(auth, request, algorithms);
102+
}
103+
} else {
94104
// Use the first authenticator, sorted by priority
95105
authenticators.sort(comparator);
96106
final Authenticator auth = authenticators.get(0);
97107
LOGGER.debug("Using {} authenticator", auth.getName());
98-
return auth.authenticate(session, request, algorithms).thenApply(Optional::ofNullable);
108+
return session.authenticate(auth, request, algorithms);
99109
}
100110
return CompletableFuture.completedFuture(Optional.empty());
101111
}

api/src/main/java/com/inrupt/client/auth/Session.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,22 @@ public interface Session {
9999
* @param request the HTTP request
100100
* @param algorithms the supported DPoP algorithms
101101
* @return the next stage of completion, containing an access token, if present
102+
* @deprecated as of Beta3, this method is no longer used
102103
*/
104+
@Deprecated
103105
CompletionStage<Optional<Credential>> authenticate(Request request, Set<String> algorithms);
104106

107+
/**
108+
* Fetch an authentication token from session values.
109+
*
110+
* @param authenticator the authenticator in use
111+
* @param request the HTTP request
112+
* @param algorithms the supported DPoP algorithms
113+
* @return the next stage of completion, containing an access token, if present
114+
*/
115+
CompletionStage<Optional<Credential>> authenticate(Authenticator authenticator, Request request,
116+
Set<String> algorithms);
117+
105118
/**
106119
* Create a new anonymous session.
107120
*
@@ -146,6 +159,13 @@ public Optional<String> selectThumbprint(final Collection<String> algorithms) {
146159
return Optional.empty();
147160
}
148161

162+
@Override
163+
public CompletionStage<Optional<Credential>> authenticate(final Authenticator authenticator,
164+
final Request request, final Set<String> algorithms) {
165+
return authenticator.authenticate(this, request, algorithms).thenApply(Optional::ofNullable);
166+
}
167+
168+
/* deprecated */
149169
@Override
150170
public CompletionStage<Optional<Credential>> authenticate(final Request request,
151171
final Set<String> algorithms) {

api/src/main/java/com/inrupt/client/spi/AuthenticationProvider.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import com.inrupt.client.auth.Authenticator;
2424
import com.inrupt.client.auth.Challenge;
2525

26+
import java.util.Set;
27+
2628
/**
2729
* An authentication mechanism that knows how to authenticate over network connections.
2830
*/
@@ -32,9 +34,18 @@ public interface AuthenticationProvider {
3234
* Return the authorization scheme, such as Bearer or DPoP.
3335
*
3436
* @return the authorization scheme
37+
* @deprecated as of Beta3, please use the {@link #getSchemes()} method
3538
*/
39+
@Deprecated
3640
String getScheme();
3741

42+
/**
43+
* Return the set of supported authorization schemes, such as Bearer or DPoP.
44+
*
45+
* @return the authorization schemes
46+
*/
47+
Set<String> getSchemes();
48+
3849
/**
3950
* Return an authenticator for the supplied challenge.
4051
*

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 token-67890"))
136+
.withHeader("Authorization", containing("Bearer eyJ"))
137137
.withHeader("DPoP", absent())
138138
.willReturn(aResponse()
139139
.withStatus(201)));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424

2525
public class UmaAccessGrantScenarioTest extends AccessGrantScenarios {
2626

27-
}
27+
}

openid/src/main/java/com/inrupt/client/openid/OpenIdAuthenticationProvider.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import java.util.Optional;
3131
import java.util.Set;
32+
import java.util.TreeSet;
3233
import java.util.concurrent.CompletableFuture;
3334
import java.util.concurrent.CompletionStage;
3435

@@ -38,11 +39,15 @@
3839
public class OpenIdAuthenticationProvider implements AuthenticationProvider {
3940

4041
private static final String BEARER = "Bearer";
42+
private static final String DPOP = "DPoP";
4143

4244
private final int priorityLevel;
45+
private final Set<String> schemes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
4346

4447
public OpenIdAuthenticationProvider() {
4548
this(50);
49+
schemes.add(BEARER);
50+
schemes.add(DPOP);
4651
}
4752

4853
/**
@@ -59,15 +64,20 @@ public String getScheme() {
5964
return BEARER;
6065
}
6166

67+
@Override
68+
public Set<String> getSchemes() {
69+
return schemes;
70+
}
71+
6272
@Override
6373
public Authenticator getAuthenticator(final Challenge challenge) {
6474
validate(challenge);
6575
return new OpenIdAuthenticator(priorityLevel);
6676
}
6777

68-
static void validate(final Challenge challenge) {
78+
void validate(final Challenge challenge) {
6979
if (challenge == null ||
70-
!BEARER.equalsIgnoreCase(challenge.getScheme())) {
80+
!schemes.contains(challenge.getScheme())) {
7181
throw new OpenIdException("Invalid challenge for OpenID authentication");
7282
}
7383
}

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@
2222

2323
import static java.nio.charset.StandardCharsets.UTF_8;
2424

25+
import com.inrupt.client.ClientCache;
2526
import com.inrupt.client.Request;
27+
import com.inrupt.client.auth.Authenticator;
2628
import com.inrupt.client.auth.Credential;
2729
import com.inrupt.client.auth.DPoP;
2830
import com.inrupt.client.auth.Session;
31+
import com.inrupt.client.spi.ServiceProvider;
2932

3033
import java.net.URI;
3134
import java.security.MessageDigest;
35+
import java.time.Duration;
3236
import java.time.Instant;
3337
import java.util.Collection;
3438
import java.util.Collections;
@@ -75,16 +79,19 @@ public final class OpenIdSession implements Session {
7579
private final AtomicReference<Credential> credential = new AtomicReference<>();
7680
private final ForkJoinPool executor = new ForkJoinPool(1);
7781
private final DPoP dpop;
82+
private final ClientCache<URI, Boolean> requestCache;
7883

7984
private OpenIdSession(final String id, final DPoP dpop,
8085
final Supplier<CompletionStage<Credential>> authenticator) {
8186
this.id = Objects.requireNonNull(id, "Session id may not be null!");
8287
this.authenticator = Objects.requireNonNull(authenticator, "OpenID authenticator may not be null!");
8388
this.dpop = Objects.requireNonNull(dpop);
89+
this.requestCache = ServiceProvider.getCacheBuilder().build(1000, Duration.ofMinutes(5));
8490

8591
// Support case-insensitive lookups
8692
final Set<String> schemeNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
8793
schemeNames.add("Bearer");
94+
schemeNames.add("DPoP");
8895

8996
this.schemes = Collections.unmodifiableSet(schemeNames);
9097
}
@@ -216,16 +223,29 @@ public Optional<String> generateProof(final String jkt, final Request request) {
216223
@Override
217224
public Optional<Credential> fromCache(final Request request) {
218225
final Credential c = credential.get();
219-
if (!hasExpired(c)) {
226+
if (!hasExpired(c) && request != null && requestCache.get(request.uri()) != null) {
227+
LOGGER.debug("Using cached token for request: {}", request.uri());
220228
return Optional.of(c);
221229
}
222230
return Optional.empty();
223231
}
224232

233+
@Override
234+
public CompletionStage<Optional<Credential>> authenticate(final Authenticator auth,
235+
final Request request, final Set<String> algorithms) {
236+
final Optional<Credential> credential = getCredential(ID_TOKEN, null);
237+
if (credential.isPresent() && request != null) {
238+
LOGGER.debug("Setting cache entry for request: {}", request.uri());
239+
requestCache.put(request.uri(), Boolean.TRUE);
240+
}
241+
return CompletableFuture.completedFuture(credential);
242+
}
243+
244+
/* deprecated */
225245
@Override
226246
public CompletionStage<Optional<Credential>> authenticate(final Request request,
227247
final Set<String> algorithms) {
228-
return CompletableFuture.completedFuture(getCredential(ID_TOKEN, null));
248+
return authenticate(null, request, algorithms);
229249
}
230250

231251
boolean hasExpired(final Credential credential) {

openid/src/test/java/com/inrupt/client/openid/OpenIdSessionTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ void testClientCredentials() {
157157
assertFalse(session.fromCache(null).isPresent());
158158
final Optional<URI> principal = session.getPrincipal();
159159
assertEquals(Optional.of(URI.create(WEBID)), principal);
160-
assertTrue(session.fromCache(null).isPresent());
160+
assertFalse(session.fromCache(null).isPresent());
161161
final Optional<Credential> credential = session.authenticate(null, Collections.emptySet())
162162
.toCompletableFuture().join();
163163
assertEquals(Optional.of(URI.create(WEBID)), credential.flatMap(Credential::getPrincipal));
@@ -176,7 +176,7 @@ void testClientCredentialsWithConfig() {
176176
assertFalse(session.fromCache(null).isPresent());
177177
final Optional<URI> principal = session.getPrincipal();
178178
assertEquals(Optional.of(URI.create(WEBID)), principal);
179-
assertTrue(session.fromCache(null).isPresent());
179+
assertFalse(session.fromCache(null).isPresent());
180180
final Optional<Credential> credential = session.authenticate(null, Collections.emptySet())
181181
.toCompletableFuture().join();
182182
assertEquals(Optional.of(URI.create(WEBID)), credential.flatMap(Credential::getPrincipal));

uma/src/main/java/com/inrupt/client/uma/UmaAuthenticationProvider.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.Objects;
3737
import java.util.Optional;
3838
import java.util.Set;
39+
import java.util.TreeSet;
3940
import java.util.concurrent.CompletableFuture;
4041
import java.util.concurrent.CompletionStage;
4142

@@ -65,9 +66,11 @@ public class UmaAuthenticationProvider implements AuthenticationProvider {
6566
private final int priorityLevel;
6667
private final UmaClient umaClient;
6768
private final NeedInfoHandler claimHandler;
69+
private final Set<String> supportedSchemes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
6870

6971
public UmaAuthenticationProvider() {
7072
this(100);
73+
supportedSchemes.add(UMA);
7174
}
7275

7376
/**
@@ -92,11 +95,17 @@ public UmaAuthenticationProvider(final int priority, final UmaClient umaClient)
9295
this.claimHandler = new NeedInfoHandler();
9396
}
9497

98+
/* deprecated */
9599
@Override
96100
public String getScheme() {
97101
return UMA;
98102
}
99103

104+
@Override
105+
public Set<String> getSchemes() {
106+
return supportedSchemes;
107+
}
108+
100109
@Override
101110
public Authenticator getAuthenticator(final Challenge challenge) {
102111
validate(challenge);

0 commit comments

Comments
 (0)