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 @@ -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;
Expand All @@ -53,10 +59,13 @@ public final class AccessGrantSession implements Session {
private final String id;
private final Session session;
private final NavigableMap<URI, AccessGrant> grants = new ConcurrentSkipListMap<>();
private final ClientCache<URI, Credential> tokenCache;

private AccessGrantSession(final Session session, final List<AccessGrant> grants) {
private AccessGrantSession(final Session session, final ClientCache<URI, Credential> cache,
final List<AccessGrant> 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()) {
Expand All @@ -73,7 +82,21 @@ private AccessGrantSession(final Session session, final List<AccessGrant> 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<URI, Credential> cache,
final AccessGrant... accessGrants) {
return new AccessGrantSession(session, cache, Arrays.asList(accessGrants));
}

@Override
Expand Down Expand Up @@ -117,6 +140,19 @@ public Optional<String> generateProof(final String jkt, final Request request) {
return session.generateProof(jkt, request);
}

@Override
public CompletionStage<Optional<Credential>> authenticate(final Authenticator authenticator,
final Request request, final Set<String> 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<Optional<Credential>> authenticate(final Request request,
final Set<String> algorithms) {
Expand All @@ -129,7 +165,10 @@ public CompletionStage<Optional<Credential>> authenticate(final Request request,

@Override
public Optional<Credential> fromCache(final Request request) {
// TODO add cache
final Credential cachedToken = tokenCache.get(request.uri());
if (cachedToken != null && cachedToken.getExpiration().isAfter(Instant.now())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If the token is cached, but expired, should it be removed from cache?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I considered this, but I was concerned that there would be a race condition:

If multiple threads are reading/updating the cache, one thread could have fetched the token from the cache (line 168) and before line 169 another thread could have updated the cache. So if the first thread then invalidates the cache, it would have done so unnecessarily, as it would have already been updated by another thread.

return Optional.of(cachedToken);
}
return Optional.empty();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Authenticator> comparator = Comparator
.comparing(Authenticator::getPriority)
Expand All @@ -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(scheme, provider);
}
}
}

Expand All @@ -90,12 +93,19 @@ public CompletionStage<Optional<Credential>> 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);
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());
}
Expand Down
20 changes: 20 additions & 0 deletions api/src/main/java/com/inrupt/client/auth/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Optional<Credential>> authenticate(Request request, Set<String> 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<Optional<Credential>> authenticate(Authenticator authenticator, Request request,
Set<String> algorithms);

/**
* Create a new anonymous session.
*
Expand Down Expand Up @@ -146,6 +159,13 @@ public Optional<String> selectThumbprint(final Collection<String> algorithms) {
return Optional.empty();
}

@Override
public CompletionStage<Optional<Credential>> authenticate(final Authenticator authenticator,
final Request request, final Set<String> algorithms) {
return authenticator.authenticate(this, request, algorithms).thenApply(Optional::ofNullable);
}

/* deprecated */
@Override
public CompletionStage<Optional<Credential>> authenticate(final Request request,
final Set<String> algorithms) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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<String> getSchemes();

/**
* Return an authenticator for the supplied challenge.
*
Expand Down
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 token-67890"))
.withHeader("Authorization", containing("Bearer eyJ"))
Copy link
Contributor

Choose a reason for hiding this comment

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

For my own understanding, why was this required?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This allows us to vary the mock server response based on the presence of an auth token.

.withHeader("DPoP", absent())
.willReturn(aResponse()
.withStatus(201)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@

public class UmaAccessGrantScenarioTest extends AccessGrantScenarios {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<String> schemes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

public OpenIdAuthenticationProvider() {
this(50);
schemes.add(BEARER);
schemes.add(DPOP);
}

/**
Expand All @@ -59,15 +64,20 @@ public String getScheme() {
return BEARER;
}

@Override
public Set<String> 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())) {
Comment on lines -70 to +80
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes the test case-sensitive, is it a problem?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually, it's not case sensitive:

private final Set<String> schemes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

throw new OpenIdException("Invalid challenge for OpenID authentication");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,16 +79,19 @@ 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 OpenIdSession(final String id, final DPoP dpop,
final Supplier<CompletionStage<Credential>> 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<String> schemeNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
schemeNames.add("Bearer");
schemeNames.add("DPoP");
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it worth using constants from OpenIdAuthenticationProvider instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Possibly -- I've generally been moving away from storing string constants in a single file and using them elsewhere. It makes no difference to the JVM, since there will only ever be a single instance of the string in memory.


this.schemes = Collections.unmodifiableSet(schemeNames);
}
Expand Down Expand Up @@ -216,16 +223,29 @@ 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)) {
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<Optional<Credential>> authenticate(final Authenticator auth,
final Request request, final Set<String> algorithms) {
final Optional<Credential> 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<Optional<Credential>> authenticate(final Request request,
final Set<String> algorithms) {
return CompletableFuture.completedFuture(getCredential(ID_TOKEN, null));
return authenticate(null, request, algorithms);
}

boolean hasExpired(final Credential credential) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ void testClientCredentials() {
assertFalse(session.fromCache(null).isPresent());
final Optional<URI> principal = session.getPrincipal();
assertEquals(Optional.of(URI.create(WEBID)), principal);
assertTrue(session.fromCache(null).isPresent());
assertFalse(session.fromCache(null).isPresent());
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicates assertion from 3 lines above

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In the past, the cache would be populated at this point (which was incorrect). This assertion tests that the cache is still not populated.

final Optional<Credential> credential = session.authenticate(null, Collections.emptySet())
.toCompletableFuture().join();
assertEquals(Optional.of(URI.create(WEBID)), credential.flatMap(Credential::getPrincipal));
Expand All @@ -176,7 +176,7 @@ void testClientCredentialsWithConfig() {
assertFalse(session.fromCache(null).isPresent());
final Optional<URI> principal = session.getPrincipal();
assertEquals(Optional.of(URI.create(WEBID)), principal);
assertTrue(session.fromCache(null).isPresent());
assertFalse(session.fromCache(null).isPresent());
final Optional<Credential> credential = session.authenticate(null, Collections.emptySet())
.toCompletableFuture().join();
assertEquals(Optional.of(URI.create(WEBID)), credential.flatMap(Credential::getPrincipal));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

public UmaAuthenticationProvider() {
this(100);
supportedSchemes.add(UMA);
}

/**
Expand All @@ -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<String> getSchemes() {
return supportedSchemes;
}

@Override
public Authenticator getAuthenticator(final Challenge challenge) {
validate(challenge);
Expand Down
Loading