Skip to content

Commit 9af810c

Browse files
committed
Update AuthToken rotation to support more auth types
This update extends the `AuthToken` rotation beyond supporting just the expiring (bearer) tokens to a more flexible solution that allows supporting 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)`, `Session.executeWrite(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 achieve 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.
1 parent 3958fce commit 9af810c

34 files changed

+769
-216
lines changed

driver/clirr-ignored-differences.xml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,4 +568,33 @@
568568
<method>org.neo4j.driver.AuthTokenAndExpiration expiringAt(long)</method>
569569
</difference>
570570

571+
<difference>
572+
<className>org/neo4j/driver/AuthTokenManager</className>
573+
<differenceType>7012</differenceType>
574+
<method>boolean handleSecurityException(org.neo4j.driver.AuthToken, org.neo4j.driver.exceptions.SecurityException)</method>
575+
</difference>
576+
577+
<difference>
578+
<className>org/neo4j/driver/AuthTokenManager</className>
579+
<differenceType>7002</differenceType>
580+
<method>void onExpired(org.neo4j.driver.AuthToken)</method>
581+
</difference>
582+
583+
<difference>
584+
<className>org/neo4j/driver/AuthTokenManagers</className>
585+
<differenceType>7002</differenceType>
586+
<method>org.neo4j.driver.AuthTokenManager expirationBased(java.util.function.Supplier)</method>
587+
</difference>
588+
589+
<difference>
590+
<className>org/neo4j/driver/AuthTokenManagers</className>
591+
<differenceType>7002</differenceType>
592+
<method>org.neo4j.driver.AuthTokenManager expirationBasedAsync(java.util.function.Supplier)</method>
593+
</difference>
594+
595+
<difference>
596+
<className>org/neo4j/driver/exceptions/TokenExpiredRetryableException</className>
597+
<differenceType>8001</differenceType>
598+
</difference>
599+
571600
</differences>

driver/src/main/java/org/neo4j/driver/AuthToken.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ public sealed interface AuthToken permits InternalAuthToken {
3636
/**
3737
* Returns a new instance of a type holding both the token and its UTC expiration timestamp.
3838
* <p>
39-
* This is used by the expiration-based implementation of the {@link AuthTokenManager} supplied by the
39+
* This is used by the bearer token implementation of the {@link AuthTokenManager} supplied by the
4040
* {@link AuthTokenManagers}.
4141
*
4242
* @param utcExpirationTimestamp the UTC expiration timestamp
4343
* @return a new instance of a type holding both the token and its UTC expiration timestamp
4444
* @since 5.8
45-
* @see AuthTokenManagers#expirationBased(Supplier)
46-
* @see AuthTokenManagers#expirationBasedAsync(Supplier)
45+
* @see AuthTokenManagers#bearer(Supplier)
46+
* @see AuthTokenManagers#bearerAsync(Supplier)
4747
*/
4848
@Preview(name = "AuthToken rotation and session auth support")
4949
default AuthTokenAndExpiration expiringAt(long utcExpirationTimestamp) {

driver/src/main/java/org/neo4j/driver/AuthTokenAndExpiration.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
* A container used by the expiration based {@link AuthTokenManager} implementation provided by the driver, it contains an
2727
* {@link AuthToken} and its UTC expiration timestamp.
2828
* <p>
29-
* This is used by the expiration-based implementation of the {@link AuthTokenManager} supplied by the
29+
* This is used by the bearer token implementation of the {@link AuthTokenManager} supplied by the
3030
* {@link AuthTokenManagers}.
3131
*
3232
* @since 5.8
33-
* @see AuthTokenManagers#expirationBased(Supplier)
34-
* @see AuthTokenManagers#expirationBasedAsync(Supplier)
33+
* @see AuthTokenManagers#bearer(Supplier)
34+
* @see AuthTokenManagers#bearerAsync(Supplier)
3535
*/
3636
@Preview(name = "AuthToken rotation and session auth support")
3737
public sealed interface AuthTokenAndExpiration permits InternalAuthTokenAndExpiration {

driver/src/main/java/org/neo4j/driver/AuthTokenManager.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
package org.neo4j.driver;
2020

2121
import java.util.concurrent.CompletionStage;
22+
import org.neo4j.driver.exceptions.AuthTokenManagerExecutionException;
23+
import org.neo4j.driver.exceptions.SecurityException;
24+
import org.neo4j.driver.exceptions.SecurityRetryableException;
2225
import org.neo4j.driver.util.Preview;
2326

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

5659
/**
57-
* Handles an error notification emitted by the server if the token is expired.
60+
* Handles {@link SecurityException} that is created based on the server's security error response by determining if
61+
* the given error may be resolved upon next {@link AuthTokenManager#getToken()} invokation.
5862
* <p>
59-
* This will be called when driver emits the {@link org.neo4j.driver.exceptions.TokenExpiredRetryableException}.
63+
* If this method returns {@code true}, the driver wraps the original {@link SecurityException} in
64+
* {@link SecurityRetryableException}. The managed transaction API (like
65+
* {@link Session#executeRead(TransactionCallback)}, etc.) automatically retries its unit of work if no other
66+
* condition is violated, while the other query execution APIs surface this error for external handling.
67+
* <p>
68+
* If this method returns {@code false}, the original error remains unchanged.
69+
* <p>
70+
* This method must not throw exceptions.
6071
*
61-
* @param authToken the expired token
72+
* @param authToken the token
73+
* @param exception the security exception
74+
* @return {@code true} if the exception should be marked as retryable or {@code false} if it should remain unchanged
75+
* @since 5.12
6276
*/
63-
void onExpired(AuthToken authToken);
77+
boolean handleSecurityException(AuthToken authToken, SecurityException exception);
6478
}

driver/src/main/java/org/neo4j/driver/AuthTokenManagers.java

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818
*/
1919
package org.neo4j.driver;
2020

21+
import static java.util.Objects.requireNonNull;
22+
2123
import java.time.Clock;
24+
import java.util.Set;
2225
import java.util.concurrent.CompletableFuture;
2326
import java.util.concurrent.CompletionStage;
2427
import java.util.concurrent.ForkJoinPool;
2528
import java.util.function.Supplier;
29+
import org.neo4j.driver.exceptions.AuthenticationException;
30+
import org.neo4j.driver.exceptions.SecurityException;
31+
import org.neo4j.driver.exceptions.TokenExpiredException;
2632
import org.neo4j.driver.internal.security.ExpirationBasedAuthTokenManager;
2733
import org.neo4j.driver.util.Preview;
2834

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

3844
/**
39-
* Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp.
45+
* Returns an {@link AuthTokenManager} that manages basic {@link AuthToken} instances.
46+
* <p>
47+
* The implementation will only use the token supplier when it needs a new token instance, which would happen if
48+
* the server rejects the current token with {@link AuthenticationException} (see
49+
* {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)}).
50+
* The provided supplier and its completion stages must be non-blocking as documented in the
51+
* {@link AuthTokenManager}.
52+
*
53+
* @param newTokenSupplier a new token stage supplier
54+
* @return a new token manager
55+
* @since 5.12
56+
*/
57+
public static AuthTokenManager basic(Supplier<AuthToken> newTokenSupplier) {
58+
requireNonNull(newTokenSupplier, "newTokenSupplier must not be null");
59+
return basicAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier));
60+
}
61+
62+
/**
63+
* Returns an {@link AuthTokenManager} that manages basic {@link AuthToken} instances.
64+
* <p>
65+
* The implementation will only use the token supplier when it needs a new token instance, which would happen if
66+
* the server rejects the current token with {@link AuthenticationException} (see
67+
* {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)}).
68+
* The provided supplier and its completion stages must be non-blocking as documented in the
69+
* {@link AuthTokenManager}.
70+
*
71+
* @param newTokenStageSupplier a new token stage supplier
72+
* @return a new token manager
73+
* @since 5.12
74+
*/
75+
public static AuthTokenManager basicAsync(Supplier<CompletionStage<AuthToken>> newTokenStageSupplier) {
76+
requireNonNull(newTokenStageSupplier, "newTokenStageSupplier must not be null");
77+
return new ExpirationBasedAuthTokenManager(
78+
() -> newTokenStageSupplier.get().thenApply(authToken -> authToken.expiringAt(Long.MAX_VALUE)),
79+
Set.of(AuthenticationException.class),
80+
Clock.systemUTC());
81+
}
82+
83+
/**
84+
* Returns an {@link AuthTokenManager} that manages bearer {@link AuthToken} instances with UTC expiration
85+
* timestamp.
4086
* <p>
4187
* The implementation will only use the token supplier when it needs a new token instance. This includes the
4288
* following conditions:
4389
* <ol>
4490
* <li>token's UTC timestamp is expired</li>
45-
* <li>server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})</li>
91+
* <li>server rejects the current token with either {@link TokenExpiredException} or
92+
* {@link AuthenticationException} (see
93+
* {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)})</li>
4694
* </ol>
4795
* <p>
4896
* The supplier will be called by a task running in the {@link ForkJoinPool#commonPool()} as documented in the
4997
* {@link CompletableFuture#supplyAsync(Supplier)}.
5098
*
5199
* @param newTokenSupplier a new token supplier
52100
* @return a new token manager
101+
* @since 5.12
53102
*/
54-
public static AuthTokenManager expirationBased(Supplier<AuthTokenAndExpiration> newTokenSupplier) {
55-
return expirationBasedAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier));
103+
public static AuthTokenManager bearer(Supplier<AuthTokenAndExpiration> newTokenSupplier) {
104+
requireNonNull(newTokenSupplier, "newTokenSupplier must not be null");
105+
return bearerAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier));
56106
}
57107

58108
/**
59-
* Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp.
109+
* Returns an {@link AuthTokenManager} that manages bearer {@link AuthToken} instances with UTC expiration
110+
* timestamp.
60111
* <p>
61112
* The implementation will only use the token supplier when it needs a new token instance. This includes the
62113
* following conditions:
63114
* <ol>
64115
* <li>token's UTC timestamp is expired</li>
65-
* <li>server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})</li>
116+
* <li>server rejects the current token with either {@link TokenExpiredException} or
117+
* {@link AuthenticationException} (see
118+
* {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)})</li>
66119
* </ol>
67120
* <p>
68121
* The provided supplier and its completion stages must be non-blocking as documented in the {@link AuthTokenManager}.
69122
*
70123
* @param newTokenStageSupplier a new token stage supplier
71124
* @return a new token manager
125+
* @since 5.12
72126
*/
73-
public static AuthTokenManager expirationBasedAsync(
127+
public static AuthTokenManager bearerAsync(
74128
Supplier<CompletionStage<AuthTokenAndExpiration>> newTokenStageSupplier) {
75-
return new ExpirationBasedAuthTokenManager(newTokenStageSupplier, Clock.systemUTC());
129+
requireNonNull(newTokenStageSupplier, "newTokenStageSupplier must not be null");
130+
return new ExpirationBasedAuthTokenManager(
131+
newTokenStageSupplier,
132+
Set.of(TokenExpiredException.class, AuthenticationException.class),
133+
Clock.systemUTC());
76134
}
77135
}

driver/src/main/java/org/neo4j/driver/exceptions/RetryableException.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@
2222
* A marker interface for retryable exceptions.
2323
* <p>
2424
* This indicates whether an operation that resulted in retryable exception is worth retrying.
25+
* @since 5.0
2526
*/
2627
public interface RetryableException {}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
package org.neo4j.driver.exceptions;
20+
21+
import java.io.Serial;
22+
import java.util.Objects;
23+
import org.neo4j.driver.AuthToken;
24+
import org.neo4j.driver.util.Experimental;
25+
import org.neo4j.driver.util.Preview;
26+
27+
/**
28+
* Indicates that the contained {@link SecurityException} is a {@link RetryableException}, which is determined by the
29+
* {@link org.neo4j.driver.AuthTokenManager#handleSecurityException(AuthToken, SecurityException)} method.
30+
* <p>
31+
* The original {@link java.lang.SecurityException} is always available as a {@link Throwable#getCause()}. The
32+
* {@link SecurityRetryableException#code()} and {@link SecurityRetryableException#getMessage()} supply the values from
33+
* the original exception.
34+
*
35+
* @since 5.12
36+
*/
37+
@Preview(name = "AuthToken rotation and session auth support")
38+
public class SecurityRetryableException extends SecurityException implements RetryableException {
39+
@Serial
40+
private static final long serialVersionUID = 3914900631374208080L;
41+
42+
/**
43+
* The original security exception.
44+
*/
45+
private final SecurityException exception;
46+
47+
/**
48+
* Creates a new instance.
49+
*
50+
* @param exception the original security exception, must not be {@code null}
51+
*/
52+
public SecurityRetryableException(SecurityException exception) {
53+
super(exception.getMessage(), exception);
54+
this.exception = Objects.requireNonNull(exception);
55+
}
56+
57+
@Override
58+
public String code() {
59+
return exception.code();
60+
}
61+
62+
@Override
63+
public String getMessage() {
64+
return exception.getMessage();
65+
}
66+
67+
/**
68+
* Returns the original security exception.
69+
*
70+
* @return the original security exception
71+
*/
72+
@Experimental
73+
public SecurityException securityException() {
74+
return exception;
75+
}
76+
}

driver/src/main/java/org/neo4j/driver/exceptions/TokenExpiredRetryableException.java

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)