Skip to content

Commit ad1ba27

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), 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 ad1ba27

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)