Skip to content

Commit 89b6991

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 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](#bolt-51-support), but works on previous Bolt versions at the expence of replacing existing connections with new connections. A temporal `AuthTokenManager` implementation is available in 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 temporal manager instantiation: ```java import org.neo4j.driver.AuthTokenManagers.TemporalAuthData; var manager = AuthTokenManagers.temporal(() -> { var token = // get new token logic return TemporalAuthData.of(token, timestamp); }); ``` This update includes Bolt 5.1 support. The new `LOGOFF` and `LOGON` 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 3 new exceptions: - `AuthTokenManagerExecutionException` - Indicates that the `AuthTokenManager` execution has lead to an unexpected result. This includes invalid results and errors. - `UnsupportedFeatureException` - Indicates that a given feature is not supported in a particular setup. For instance, the session auth feature is not supported on Bolt versions below 5.1. - `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 c0737be commit 89b6991

File tree

139 files changed

+5748
-414
lines changed

Some content is hidden

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

139 files changed

+5748
-414
lines changed

driver/clirr-ignored-differences.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,28 @@
433433
<method>org.neo4j.driver.BookmarkManager queryTaskBookmarkManager()</method>
434434
</difference>
435435

436+
<difference>
437+
<className>org/neo4j/driver/Driver</className>
438+
<differenceType>7012</differenceType>
439+
<method>org.neo4j.driver.BaseSession session(java.lang.Class, org.neo4j.driver.AuthToken)</method>
440+
</difference>
441+
442+
<difference>
443+
<className>org/neo4j/driver/Driver</className>
444+
<differenceType>7012</differenceType>
445+
<method>org.neo4j.driver.BaseSession session(java.lang.Class, org.neo4j.driver.SessionConfig, org.neo4j.driver.AuthToken)</method>
446+
</difference>
447+
448+
<difference>
449+
<className>org/neo4j/driver/Driver</className>
450+
<differenceType>7012</differenceType>
451+
<method>boolean verifyAuthentication(org.neo4j.driver.AuthToken)</method>
452+
</difference>
453+
454+
<difference>
455+
<className>org/neo4j/driver/Driver</className>
456+
<differenceType>7012</differenceType>
457+
<method>boolean supportsSessionAuth()</method>
458+
</difference>
459+
436460
</differences>
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.7
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: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
* @since 5.7
31+
*/
32+
public final class AuthTokenManagers {
33+
private AuthTokenManagers() {}
34+
35+
/**
36+
* Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp.
37+
* <p>
38+
* The implementation will only use the token supplier when it needs a new token instance. This includes the
39+
* following conditions:
40+
* <ol>
41+
* <li>token's UTC timestamp is expired</li>
42+
* <li>server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})</li>
43+
* </ol>
44+
* <p>
45+
* The supplier will be called by a task running in the {@link ForkJoinPool#commonPool()} as documented in the
46+
* {@link CompletableFuture#supplyAsync(Supplier)}.
47+
*
48+
* @param newTokenSupplier a new token supplier
49+
* @return a new token manager
50+
*/
51+
public static AuthTokenManager temporal(Supplier<TemporalAuthData> newTokenSupplier) {
52+
return new TemporalAuthTokenManager(() -> CompletableFuture.supplyAsync(newTokenSupplier), Clock.systemUTC());
53+
}
54+
55+
/**
56+
* Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp.
57+
* <p>
58+
* The implementation will only use the token supplier when it needs a new token instance. This includes the
59+
* following conditions:
60+
* <ol>
61+
* <li>token's UTC timestamp is expired</li>
62+
* <li>server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})</li>
63+
* </ol>
64+
* <p>
65+
* The provided supplier and its completion stages must be non-blocking as documented in the {@link AuthTokenManager}.
66+
*
67+
* @param newTokenStageSupplier a new token stage supplier
68+
* @return a new token manager
69+
*/
70+
public static AuthTokenManager temporalAsync(Supplier<CompletionStage<TemporalAuthData>> newTokenStageSupplier) {
71+
return new TemporalAuthTokenManager(newTokenStageSupplier, Clock.systemUTC());
72+
}
73+
74+
/**
75+
* A container used by the temporal {@link AuthTokenManager} implementation provided by the driver, it contains an
76+
* {@link AuthToken} and its UTC expiration timestamp.
77+
* @since 5.7
78+
*/
79+
public interface TemporalAuthData {
80+
/**
81+
* Returns a new instance with the provided token and {@link Long#MAX_VALUE} expiration timestamp.
82+
* @param authToken the token
83+
* @return a new instance
84+
*/
85+
static TemporalAuthData of(AuthToken authToken) {
86+
return of(authToken, Long.MAX_VALUE);
87+
}
88+
89+
/**
90+
* Returns a new instance with the provided token and its expiration timestamp.
91+
* @param authToken the token
92+
* @param expirationTimestamp the expiration timestamp
93+
* @return a new instance
94+
*/
95+
static TemporalAuthData of(AuthToken authToken, long expirationTimestamp) {
96+
return new TemporalAuthTokenManager.InternalTemporalAuthData(authToken, expirationTimestamp);
97+
}
98+
99+
/**
100+
* Returns the {@link AuthToken}.
101+
*
102+
* @return the token
103+
*/
104+
AuthToken authToken();
105+
106+
/**
107+
* Returns the token's UTC expiration timestamp.
108+
*
109+
* @return the token's UTC expiration timestamp
110+
*/
111+
long expirationTimestamp();
112+
}
113+
}

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

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

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

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

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

0 commit comments

Comments
 (0)