Skip to content

Commit

Permalink
Update AuthToken rotation to support more auth types
Browse files Browse the repository at this point in the history
This update extends the `AuthToken` rotation beyound supporting just the expiring (bearer) tokens to a more versatile solution that allows support of various auth types.

An important part of the `AuthToken` rotation support is retryability, which is either managed automatically by the driver via the Managed Transaction API (Session.executeRead(TransactionCallback), etc.) or explicitly by the user. The rotation support relies on the existing retry approach used for the other retryable conditions. If a given unit of work fails due to credentials rejection by the server and the `AuthTokenManager` is able to supply valid credentials, the failure must be marked as retryable to indicate that the given unit of work is worth retrying. The driver provides the `RetryableException` marker interface for that.

However, the credentials rejection server security error depends on auth type used. Therefore, it was decided that the `AuthTokenManager` implementations should have access to the security error details and should make a decision on whether such error should be considered as retryable or not. To achive this, the `AuthTokenManager.onExpired(AuthToken)` method is replaced with a new `AuthTokenManager.handleSecurityException(AuthToken authToken, SecurityException exception)` method that returns a `boolean` value to determine if the error is retryable.

By default, the `SecurityException` and its subclasses are not retryable. If an error is determined to be retryable by the `AuthTokenManager`, then the driver wraps the current error into a new `SecurityRetryableException`. The latter is a subclass of the `SecurityException` and is also a `RetryableException`. It contains the original exception as a cause and transparently proxies calls of both the `SecurityRetryableException.code()` and `SecurityRetryableException.getMessage()` methods to the original exception. The `SecurityRetryableException` has been introduced to allow marking `SecurityException` and its subclasses as retryable and is not currently meant to fork a separate class hierarchy as a single class is sufficient for this purpose at this point. Following these updates, the `TokenExpiredRetryableException` has been deleted as no longer needed.

The `AuthTokenManagers.expirationBased(Supplier<AuthTokenAndExpiration>)` and `AuthTokenManagers.expirationBasedAsync(Supplier<CompletionStage<AuthTokenAndExpiration>>)` methods have been replaced with `AuthTokenManagers.bearer(Supplier<AuthTokenAndExpiration>)` and `AuthTokenManagers.bearerAsync(Supplier<CompletionStage<AuthTokenAndExpiration>>)` methods respectively. The new medhods are tailored for the bearer token auth support specifically. In addition, 2 new methods have been introduced for the basic (type) `AuthToken` rotation support: `AuthTokenManagers.basic(Supplier<AuthToken>)` and `AuthTokenManagers.basicAsync(Supplier<CompletionStage<AuthToken>>)`.

The code inspection profile has been updated to enable the `SerializableHasSerialVersionUIDField` warning.
  • Loading branch information
injectives committed Aug 16, 2023
1 parent 3958fce commit 238a7b2
Show file tree
Hide file tree
Showing 34 changed files with 769 additions and 216 deletions.
29 changes: 29 additions & 0 deletions driver/clirr-ignored-differences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -568,4 +568,33 @@
<method>org.neo4j.driver.AuthTokenAndExpiration expiringAt(long)</method>
</difference>

<difference>
<className>org/neo4j/driver/AuthTokenManager</className>
<differenceType>7012</differenceType>
<method>boolean handleSecurityException(org.neo4j.driver.AuthToken, org.neo4j.driver.exceptions.SecurityException)</method>
</difference>

<difference>
<className>org/neo4j/driver/AuthTokenManager</className>
<differenceType>7002</differenceType>
<method>void onExpired(org.neo4j.driver.AuthToken)</method>
</difference>

<difference>
<className>org/neo4j/driver/AuthTokenManagers</className>
<differenceType>7002</differenceType>
<method>org.neo4j.driver.AuthTokenManager expirationBased(java.util.function.Supplier)</method>
</difference>

<difference>
<className>org/neo4j/driver/AuthTokenManagers</className>
<differenceType>7002</differenceType>
<method>org.neo4j.driver.AuthTokenManager expirationBasedAsync(java.util.function.Supplier)</method>
</difference>

<difference>
<className>org/neo4j/driver/exceptions/TokenExpiredRetryableException</className>
<differenceType>8001</differenceType>
</difference>

</differences>
6 changes: 3 additions & 3 deletions driver/src/main/java/org/neo4j/driver/AuthToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ public sealed interface AuthToken permits InternalAuthToken {
/**
* Returns a new instance of a type holding both the token and its UTC expiration timestamp.
* <p>
* This is used by the expiration-based implementation of the {@link AuthTokenManager} supplied by the
* This is used by the bearer token implementation of the {@link AuthTokenManager} supplied by the
* {@link AuthTokenManagers}.
*
* @param utcExpirationTimestamp the UTC expiration timestamp
* @return a new instance of a type holding both the token and its UTC expiration timestamp
* @since 5.8
* @see AuthTokenManagers#expirationBased(Supplier)
* @see AuthTokenManagers#expirationBasedAsync(Supplier)
* @see AuthTokenManagers#bearer(Supplier)
* @see AuthTokenManagers#bearerAsync(Supplier)
*/
@Preview(name = "AuthToken rotation and session auth support")
default AuthTokenAndExpiration expiringAt(long utcExpirationTimestamp) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
* A container used by the expiration based {@link AuthTokenManager} implementation provided by the driver, it contains an
* {@link AuthToken} and its UTC expiration timestamp.
* <p>
* This is used by the expiration-based implementation of the {@link AuthTokenManager} supplied by the
* This is used by the bearer token implementation of the {@link AuthTokenManager} supplied by the
* {@link AuthTokenManagers}.
*
* @since 5.8
* @see AuthTokenManagers#expirationBased(Supplier)
* @see AuthTokenManagers#expirationBasedAsync(Supplier)
* @see AuthTokenManagers#bearer(Supplier)
* @see AuthTokenManagers#bearerAsync(Supplier)
*/
@Preview(name = "AuthToken rotation and session auth support")
public sealed interface AuthTokenAndExpiration permits InternalAuthTokenAndExpiration {
Expand Down
24 changes: 19 additions & 5 deletions driver/src/main/java/org/neo4j/driver/AuthTokenManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
package org.neo4j.driver;

import java.util.concurrent.CompletionStage;
import org.neo4j.driver.exceptions.AuthTokenManagerExecutionException;
import org.neo4j.driver.exceptions.SecurityException;
import org.neo4j.driver.exceptions.SecurityRetryableException;
import org.neo4j.driver.util.Preview;

/**
Expand Down Expand Up @@ -49,16 +52,27 @@ public interface AuthTokenManager {
* <p>
* Failures will surface via the driver API, like {@link Session#beginTransaction()} method and others.
* @return a stage for a valid token, must not be {@code null} or complete with {@code null}
* @see org.neo4j.driver.exceptions.AuthTokenManagerExecutionException
* @see AuthTokenManagerExecutionException
*/
CompletionStage<AuthToken> getToken();

/**
* Handles an error notification emitted by the server if the token is expired.
* Handles {@link SecurityException} that is created based on the server's security error response by determining if
* the given error may be resolved upon next {@link AuthTokenManager#getToken()} invokation.
* <p>
* This will be called when driver emits the {@link org.neo4j.driver.exceptions.TokenExpiredRetryableException}.
* If this method returns {@code true}, the driver wraps the original {@link SecurityException} in
* {@link SecurityRetryableException}. The managed transaction API (like
* {@link Session#executeRead(TransactionCallback)}, etc.) automatically retries its unit of work if no other
* condition is violated, while the other query execution APIs surface this error for external handling.
* <p>
* If this method returns {@code false}, the original error remains unchanged.
* <p>
* This method must not throw exceptions.
*
* @param authToken the expired token
* @param authToken the token
* @param exception the security exception
* @return {@code true} if the exception should be marked as retryable or {@code false} if it should remain unchanged
* @since 5.12
*/
void onExpired(AuthToken authToken);
boolean handleSecurityException(AuthToken authToken, SecurityException exception);
}
74 changes: 66 additions & 8 deletions driver/src/main/java/org/neo4j/driver/AuthTokenManagers.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@
*/
package org.neo4j.driver;

import static java.util.Objects.requireNonNull;

import java.time.Clock;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ForkJoinPool;
import java.util.function.Supplier;
import org.neo4j.driver.exceptions.AuthenticationException;
import org.neo4j.driver.exceptions.SecurityException;
import org.neo4j.driver.exceptions.TokenExpiredException;
import org.neo4j.driver.internal.security.ExpirationBasedAuthTokenManager;
import org.neo4j.driver.util.Preview;

Expand All @@ -36,42 +42,94 @@ public final class AuthTokenManagers {
private AuthTokenManagers() {}

/**
* Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp.
* Returns an {@link AuthTokenManager} that manages basic {@link AuthToken} instances.
* <p>
* The implementation will only use the token supplier when it needs a new token instance, which would happen if
* the server rejects the current token with {@link AuthenticationException} (see
* {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)}).
* The provided supplier and its completion stages must be non-blocking as documented in the
* {@link AuthTokenManager}.
*
* @param newTokenSupplier a new token stage supplier
* @return a new token manager
* @since 5.12
*/
public static AuthTokenManager basic(Supplier<AuthToken> newTokenSupplier) {
requireNonNull(newTokenSupplier, "newTokenSupplier must not be null");
return basicAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier));
}

/**
* Returns an {@link AuthTokenManager} that manages basic {@link AuthToken} instances.
* <p>
* The implementation will only use the token supplier when it needs a new token instance, which would happen if
* the server rejects the current token with {@link AuthenticationException} (see
* {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)}).
* The provided supplier and its completion stages must be non-blocking as documented in the
* {@link AuthTokenManager}.
*
* @param newTokenStageSupplier a new token stage supplier
* @return a new token manager
* @since 5.12
*/
public static AuthTokenManager basicAsync(Supplier<CompletionStage<AuthToken>> newTokenStageSupplier) {
requireNonNull(newTokenStageSupplier, "newTokenStageSupplier must not be null");
return new ExpirationBasedAuthTokenManager(
() -> newTokenStageSupplier.get().thenApply(authToken -> authToken.expiringAt(Long.MAX_VALUE)),
Set.of(AuthenticationException.class),
Clock.systemUTC());
}

/**
* Returns an {@link AuthTokenManager} that manages bearer {@link AuthToken} instances with UTC expiration
* timestamp.
* <p>
* The implementation will only use the token supplier when it needs a new token instance. This includes the
* following conditions:
* <ol>
* <li>token's UTC timestamp is expired</li>
* <li>server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})</li>
* <li>server rejects the current token with either {@link TokenExpiredException} or
* {@link AuthenticationException} (see
* {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)})</li>
* </ol>
* <p>
* The supplier will be called by a task running in the {@link ForkJoinPool#commonPool()} as documented in the
* {@link CompletableFuture#supplyAsync(Supplier)}.
*
* @param newTokenSupplier a new token supplier
* @return a new token manager
* @since 5.12
*/
public static AuthTokenManager expirationBased(Supplier<AuthTokenAndExpiration> newTokenSupplier) {
return expirationBasedAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier));
public static AuthTokenManager bearer(Supplier<AuthTokenAndExpiration> newTokenSupplier) {
requireNonNull(newTokenSupplier, "newTokenSupplier must not be null");
return bearerAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier));
}

/**
* Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp.
* Returns an {@link AuthTokenManager} that manages bearer {@link AuthToken} instances with UTC expiration
* timestamp.
* <p>
* The implementation will only use the token supplier when it needs a new token instance. This includes the
* following conditions:
* <ol>
* <li>token's UTC timestamp is expired</li>
* <li>server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})</li>
* <li>server rejects the current token with either {@link TokenExpiredException} or
* {@link AuthenticationException} (see
* {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)})</li>
* </ol>
* <p>
* The provided supplier and its completion stages must be non-blocking as documented in the {@link AuthTokenManager}.
*
* @param newTokenStageSupplier a new token stage supplier
* @return a new token manager
* @since 5.12
*/
public static AuthTokenManager expirationBasedAsync(
public static AuthTokenManager bearerAsync(
Supplier<CompletionStage<AuthTokenAndExpiration>> newTokenStageSupplier) {
return new ExpirationBasedAuthTokenManager(newTokenStageSupplier, Clock.systemUTC());
requireNonNull(newTokenStageSupplier, "newTokenStageSupplier must not be null");
return new ExpirationBasedAuthTokenManager(
newTokenStageSupplier,
Set.of(TokenExpiredException.class, AuthenticationException.class),
Clock.systemUTC());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
* A marker interface for retryable exceptions.
* <p>
* This indicates whether an operation that resulted in retryable exception is worth retrying.
* @since 5.0
*/
public interface RetryableException {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.driver.exceptions;

import java.io.Serial;
import java.util.Objects;
import org.neo4j.driver.AuthToken;
import org.neo4j.driver.util.Experimental;
import org.neo4j.driver.util.Preview;

/**
* Indicates that the contained {@link SecurityException} is a {@link RetryableException}, which is determined by the
* {@link org.neo4j.driver.AuthTokenManager#handleSecurityException(AuthToken, SecurityException)} method.
* <p>
* The original {@link java.lang.SecurityException} is always available as a {@link Throwable#getCause()}. The
* {@link SecurityRetryableException#code()} and {@link SecurityRetryableException#getMessage()} supply the values from
* the original exception.
*
* @since 5.12
*/
@Preview(name = "AuthToken rotation and session auth support")
public class SecurityRetryableException extends SecurityException implements RetryableException {
@Serial
private static final long serialVersionUID = 3914900631374208080L;

/**
* The original security exception.
*/
private final SecurityException exception;

/**
* Creates a new instance.
*
* @param exception the original security exception, must not be {@code null}
*/
public SecurityRetryableException(SecurityException exception) {
super(exception.getMessage(), exception);
this.exception = Objects.requireNonNull(exception);
}

@Override
public String code() {
return exception.code();
}

@Override
public String getMessage() {
return exception.getMessage();
}

/**
* Returns the original security exception.
*
* @return the original security exception
*/
@Experimental
public SecurityException securityException() {
return exception;
}
}

This file was deleted.

Loading

0 comments on commit 238a7b2

Please sign in to comment.