Skip to content

Commit da5b47d

Browse files
committed
Introduce AuthToken rotation and session auth support
The main feature of this update is the support for `AuthToken` rotation, which might also be referred to as a refresh or re-auth. In practice, it allows replacing the current token with a new token during the driver's lifetime. The main objective of this feature is to allow token rotation for the same identity. As such, it is not intended for a change of identity. A new type called `AuthTokenManager` has the following 2 primary responsibilities: - supplying a valid token, which may be one of the following: - the current token - a new token, which instructs the driver to use the new token - handling a token expiration failure that originates from the server if it determines the current token to be expired (a timely rotation should generally reduce the likelihood of this happening) The driver does not make judgements on whether the current `AuthToken` should be updated. Instead, it calls the `AuthTokenManager` to check if the provided token is the same as the currently used token and takes action if not. The driver reserves the right to call the manager as often as it deems necessary. The manager implementations must be thread-safe and non-blocking for caller threads. For instance, IO operations must not be done on the calling thread. The `GraphDatabase` class has been updated to include a set of new methods that accept the `AuthTokenManager`. An example of the driver instantiation: ```java var manager = // the manager implementation var driver = GraphDatabase.driver(uri, manager); ``` The token rotation benefits from the new Bolt 5.1 version, but works on previous Bolt versions at the expence of replacing existing connections with new connections. An expiration based `AuthTokenManager` implementation is available via a new `AuthTokenManagers` factory. It manages `AuthToken` instances that come with a UTC expiration timestamp and calls a new token supplier, which is provided by the user, when a new token is required. An example of the expiration based manager instantiation: ```java var manager = AuthTokenManagers.expirationBased(() -> { var token = // get new token logic return token.expiringAt(timestamp); // a new method on AuthToken introduced for the supplied expiration based AuthTokenManager implementation }); ``` The new `LOGOFF` and `LOGON` Bolt protocol messages allow for auth management on active Bolt connections and are used by the features in this update. In addition to the token rotation support, this update also includes support for setting a static `AuthToken` instance on the driver session level. Unlike the rotation feature, this feature may be used for an identity change. As such, it might be referred to as user switching. It requires a minimum Bolt 5.1 version. The `Driver` interface has 2 new `session` methods that accept an `AuthToken` instance. A basic example: ```java var token = AuthTokens.bearer("token"); var session = driver.session(Session.class, token); ``` The `Driver` includes a new method that checks whether the session auth is supported. The implementation assumes all servers to be at the same version. Sample usage: ```java var supports = driver.supportsSessionAuth(); ``` The `Driver` includes a new method that verifies a given `AuthToken` instance by communicating with the server. It requires a minimum Bolt 5.1 version. Sample usage: ```java var token = AuthTokens.bearer("token"); var successful = driver.verifyAuthentication(token); ``` There are 2 new exceptions: - `AuthTokenManagerExecutionException` - Indicates that the `AuthTokenManager` execution has lead to an unexpected result. This includes invalid results and errors. - `TokenExpiredRetryableException` - Indicates that the token supplied by the `AuthTokenManager` has been deemed as expired by the server. This is a retryable variant of the `TokenExpiredException` used when the driver has an explicit `AuthTokenManager` that might supply a new token following this failure. If driver is instantiated with the static `AuthToken`, the `TokenExpiredException` will be used instead.
1 parent bf6c544 commit da5b47d

File tree

145 files changed

+5200
-466
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

145 files changed

+5200
-466
lines changed

driver/clirr-ignored-differences.xml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,4 +503,34 @@
503503
<method>org.neo4j.driver.BookmarkManager executableQueryBookmarkManager()</method>
504504
</difference>
505505

506+
<difference>
507+
<className>org/neo4j/driver/Driver</className>
508+
<differenceType>7012</differenceType>
509+
<method>org.neo4j.driver.BaseSession session(java.lang.Class, org.neo4j.driver.AuthToken)</method>
510+
</difference>
511+
512+
<difference>
513+
<className>org/neo4j/driver/Driver</className>
514+
<differenceType>7012</differenceType>
515+
<method>org.neo4j.driver.BaseSession session(java.lang.Class, org.neo4j.driver.SessionConfig, org.neo4j.driver.AuthToken)</method>
516+
</difference>
517+
518+
<difference>
519+
<className>org/neo4j/driver/Driver</className>
520+
<differenceType>7012</differenceType>
521+
<method>boolean verifyAuthentication(org.neo4j.driver.AuthToken)</method>
522+
</difference>
523+
524+
<difference>
525+
<className>org/neo4j/driver/Driver</className>
526+
<differenceType>7012</differenceType>
527+
<method>boolean supportsSessionAuth()</method>
528+
</difference>
529+
530+
<difference>
531+
<className>org/neo4j/driver/AuthToken</className>
532+
<differenceType>7012</differenceType>
533+
<method>org.neo4j.driver.AuthTokenAndExpiration expiringAt(long)</method>
534+
</difference>
535+
506536
</differences>

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
*/
1919
package org.neo4j.driver;
2020

21+
import java.util.function.Supplier;
2122
import org.neo4j.driver.internal.security.InternalAuthToken;
23+
import org.neo4j.driver.internal.security.InternalAuthTokenAndExpiration;
2224

2325
/**
2426
* Token for holding authentication details, such as <em>user name</em> and <em>password</em>.
@@ -29,4 +31,20 @@
2931
* @see GraphDatabase#driver(String, AuthToken)
3032
* @since 1.0
3133
*/
32-
public sealed interface AuthToken permits InternalAuthToken {}
34+
public sealed interface AuthToken permits InternalAuthToken {
35+
/**
36+
* Returns a new instance of a type holding both the token and its UTC expiration timestamp.
37+
* <p>
38+
* This is used by the expiration-based implementation of the {@link AuthTokenManager} supplied by the
39+
* {@link AuthTokenManagers}.
40+
*
41+
* @param utcExpirationTimestamp the UTC expiration timestamp
42+
* @return a new instance of a type holding both the token and its UTC expiration timestamp
43+
* @since 5.8
44+
* @see AuthTokenManagers#expirationBased(Supplier)
45+
* @see AuthTokenManagers#expirationBasedAsync(Supplier)
46+
*/
47+
default AuthTokenAndExpiration expiringAt(long utcExpirationTimestamp) {
48+
return new InternalAuthTokenAndExpiration(this, utcExpirationTimestamp);
49+
}
50+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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;
20+
21+
import java.util.function.Supplier;
22+
import org.neo4j.driver.internal.security.InternalAuthTokenAndExpiration;
23+
24+
/**
25+
* A container used by the temporal {@link AuthTokenManager} implementation provided by the driver, it contains an
26+
* {@link AuthToken} and its UTC expiration timestamp.
27+
* <p>
28+
* This is used by the expiration-based implementation of the {@link AuthTokenManager} supplied by the
29+
* {@link AuthTokenManagers}.
30+
*
31+
* @since 5.8
32+
* @see AuthTokenManagers#expirationBased(Supplier)
33+
* @see AuthTokenManagers#expirationBasedAsync(Supplier)
34+
*/
35+
public sealed interface AuthTokenAndExpiration permits InternalAuthTokenAndExpiration {
36+
/**
37+
* Returns the {@link AuthToken}.
38+
*
39+
* @return the token
40+
*/
41+
AuthToken authToken();
42+
43+
/**
44+
* Returns the token's UTC expiration timestamp.
45+
*
46+
* @return the token's UTC expiration timestamp
47+
*/
48+
long expirationTimestamp();
49+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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;
20+
21+
import java.util.concurrent.CompletionStage;
22+
23+
/**
24+
* A manager of {@link AuthToken} instances used by the driver.
25+
* <p>
26+
* The manager must manage tokens for the same identity. Therefore, it is not intended for a change of identity.
27+
* <p>
28+
* Implementations should supply the same token unless it needs to be updated since a change of token might result in
29+
* extra processing by the driver.
30+
* <p>
31+
* Driver initializes new connections with a token supplied by the manager. If token changes, driver action depends on
32+
* connection's Bolt protocol version:
33+
* <ul>
34+
* <li>Bolt 5.1 or above - {@code LOGOFF} and {@code LOGON} messages are dispatched to update the token on next interaction</li>
35+
* <li>Bolt 5.0 or below - connection is closed an a new one is initialized with the new token</li>
36+
* </ul>
37+
* <p>
38+
* All implementations of this interface must be thread-safe and non-blocking for caller threads. For instance, IO operations must not
39+
* be done on the calling thread.
40+
* @since 5.8
41+
*/
42+
public interface AuthTokenManager {
43+
/**
44+
* Returns a {@link CompletionStage} for a valid {@link AuthToken}.
45+
* <p>
46+
* Driver invokes this method often to check if token has changed.
47+
* <p>
48+
* Failures will surface via the driver API, like {@link Session#beginTransaction()} method and others.
49+
* @return a stage for a valid token, must not be {@code null} or complete with {@code null}
50+
* @see org.neo4j.driver.exceptions.AuthTokenManagerExecutionException
51+
*/
52+
CompletionStage<AuthToken> getToken();
53+
54+
/**
55+
* Handles an error notification emitted by the server if the token is expired.
56+
* <p>
57+
* This will be called when driver emits the {@link org.neo4j.driver.exceptions.TokenExpiredRetryableException}.
58+
*
59+
* @param authToken the expired token
60+
*/
61+
void onExpired(AuthToken authToken);
62+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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;
20+
21+
import java.time.Clock;
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.CompletionStage;
24+
import java.util.concurrent.ForkJoinPool;
25+
import java.util.function.Supplier;
26+
import org.neo4j.driver.internal.security.TemporalAuthTokenManager;
27+
28+
/**
29+
* Implementations of {@link AuthTokenManager}.
30+
*
31+
* @since 5.8
32+
*/
33+
public final class AuthTokenManagers {
34+
private AuthTokenManagers() {}
35+
36+
/**
37+
* Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp.
38+
* <p>
39+
* The implementation will only use the token supplier when it needs a new token instance. This includes the
40+
* following conditions:
41+
* <ol>
42+
* <li>token's UTC timestamp is expired</li>
43+
* <li>server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})</li>
44+
* </ol>
45+
* <p>
46+
* The supplier will be called by a task running in the {@link ForkJoinPool#commonPool()} as documented in the
47+
* {@link CompletableFuture#supplyAsync(Supplier)}.
48+
*
49+
* @param newTokenSupplier a new token supplier
50+
* @return a new token manager
51+
*/
52+
public static AuthTokenManager expirationBased(Supplier<AuthTokenAndExpiration> newTokenSupplier) {
53+
return expirationBasedAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier));
54+
}
55+
56+
/**
57+
* Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp.
58+
* <p>
59+
* The implementation will only use the token supplier when it needs a new token instance. This includes the
60+
* following conditions:
61+
* <ol>
62+
* <li>token's UTC timestamp is expired</li>
63+
* <li>server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})</li>
64+
* </ol>
65+
* <p>
66+
* The provided supplier and its completion stages must be non-blocking as documented in the {@link AuthTokenManager}.
67+
*
68+
* @param newTokenStageSupplier a new token stage supplier
69+
* @return a new token manager
70+
*/
71+
public static AuthTokenManager expirationBasedAsync(
72+
Supplier<CompletionStage<AuthTokenAndExpiration>> newTokenStageSupplier) {
73+
return new TemporalAuthTokenManager(newTokenStageSupplier, Clock.systemUTC());
74+
}
75+
}

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

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,42 @@ default <T extends BaseSession> T session(Class<T> sessionClass) {
145145
return session(sessionClass, SessionConfig.defaultConfig());
146146
}
147147

148+
/**
149+
* Instantiate a new session of a supported type with the supplied {@link AuthToken}.
150+
* <p>
151+
* This method allows creating a session with a different {@link AuthToken} to the one used on the driver level.
152+
* The minimum Bolt protocol version is 5.1. An {@link IllegalStateException} will be emitted on session interaction
153+
* for previous Bolt versions.
154+
* <p>
155+
* Supported types are:
156+
* <ul>
157+
* <li>{@link org.neo4j.driver.Session} - synchronous session</li>
158+
* <li>{@link org.neo4j.driver.async.AsyncSession} - asynchronous session</li>
159+
* <li>{@link org.neo4j.driver.reactive.ReactiveSession} - reactive session using Flow API</li>
160+
* <li>{@link org.neo4j.driver.reactivestreams.ReactiveSession} - reactive session using Reactive Streams
161+
* API</li>
162+
* <li>{@link org.neo4j.driver.reactive.RxSession} - deprecated reactive session using Reactive Streams
163+
* API, superseded by {@link org.neo4j.driver.reactivestreams.ReactiveSession}</li>
164+
* </ul>
165+
* <p>
166+
* Sample usage:
167+
* <pre>
168+
* {@code
169+
* var session = driver.session(AsyncSession.class);
170+
* }
171+
* </pre>
172+
*
173+
* @param sessionClass session type class, must not be null
174+
* @param sessionAuthToken a token, null will result in driver-level configuration being used
175+
* @return session instance
176+
* @param <T> session type
177+
* @throws IllegalArgumentException for unsupported session types
178+
* @since 5.8
179+
*/
180+
default <T extends BaseSession> T session(Class<T> sessionClass, AuthToken sessionAuthToken) {
181+
return session(sessionClass, SessionConfig.defaultConfig(), sessionAuthToken);
182+
}
183+
148184
/**
149185
* Create a new session of supported type with a specified {@link SessionConfig session configuration}.
150186
* <p>
@@ -173,7 +209,45 @@ default <T extends BaseSession> T session(Class<T> sessionClass) {
173209
* @throws IllegalArgumentException for unsupported session types
174210
* @since 5.2
175211
*/
176-
<T extends BaseSession> T session(Class<T> sessionClass, SessionConfig sessionConfig);
212+
default <T extends BaseSession> T session(Class<T> sessionClass, SessionConfig sessionConfig) {
213+
return session(sessionClass, sessionConfig, null);
214+
}
215+
216+
/**
217+
* Instantiate a new session of a supported type with the supplied {@link SessionConfig session configuration} and
218+
* {@link AuthToken}.
219+
* <p>
220+
* This method allows creating a session with a different {@link AuthToken} to the one used on the driver level.
221+
* The minimum Bolt protocol version is 5.1. An {@link IllegalStateException} will be emitted on session interaction
222+
* for previous Bolt versions.
223+
* <p>
224+
* Supported types are:
225+
* <ul>
226+
* <li>{@link org.neo4j.driver.Session} - synchronous session</li>
227+
* <li>{@link org.neo4j.driver.async.AsyncSession} - asynchronous session</li>
228+
* <li>{@link org.neo4j.driver.reactive.ReactiveSession} - reactive session using Flow API</li>
229+
* <li>{@link org.neo4j.driver.reactivestreams.ReactiveSession} - reactive session using Reactive Streams
230+
* API</li>
231+
* <li>{@link org.neo4j.driver.reactive.RxSession} - deprecated reactive session using Reactive Streams
232+
* API, superseded by {@link org.neo4j.driver.reactivestreams.ReactiveSession}</li>
233+
* </ul>
234+
* <p>
235+
* Sample usage:
236+
* <pre>
237+
* {@code
238+
* var session = driver.session(AsyncSession.class);
239+
* }
240+
* </pre>
241+
*
242+
* @param sessionClass session type class, must not be null
243+
* @param sessionConfig session config, must not be null
244+
* @param sessionAuthToken a token, null will result in driver-level configuration being used
245+
* @return session instance
246+
* @param <T> session type
247+
* @throws IllegalArgumentException for unsupported session types
248+
* @since 5.8
249+
*/
250+
<T extends BaseSession> T session(Class<T> sessionClass, SessionConfig sessionConfig, AuthToken sessionAuthToken);
177251

178252
/**
179253
* Create a new general purpose {@link RxSession} with default {@link SessionConfig session configuration}. The {@link RxSession} provides a reactive way to
@@ -326,6 +400,24 @@ default AsyncSession asyncSession(SessionConfig sessionConfig) {
326400
*/
327401
CompletionStage<Void> verifyConnectivityAsync();
328402

403+
/**
404+
* Verifies if the given {@link AuthToken} is valid.
405+
* <p>
406+
* This check works on Bolt 5.1 version or above only.
407+
* @param authToken the token
408+
* @return the verification outcome
409+
* @since 5.8
410+
*/
411+
boolean verifyAuthentication(AuthToken authToken);
412+
413+
/**
414+
* Checks if session auth is supported.
415+
* @return the check outcome
416+
* @since 5.8
417+
* @see Driver#session(Class, SessionConfig, AuthToken)
418+
*/
419+
boolean supportsSessionAuth();
420+
329421
/**
330422
* Returns true if the server or cluster the driver connects to supports multi-databases, otherwise false.
331423
*

0 commit comments

Comments
 (0)