Skip to content

Commit 442ceb3

Browse files
committed
Introduce AuthToken rotation and session auth support (neo4j#1380)
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 cbd9e63 commit 442ceb3

36 files changed

+464
-159
lines changed

driver/clirr-ignored-differences.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,28 @@
6262
<to>org.neo4j.driver.Config$TrustStrategy trustCustomCertificateSignedBy(java.io.File[])</to>
6363
</difference>
6464

65+
<difference>
66+
<className>org/neo4j/driver/Driver</className>
67+
<differenceType>7012</differenceType>
68+
<method>org.neo4j.driver.BaseSession session(java.lang.Class)</method>
69+
</difference>
70+
71+
<difference>
72+
<className>org/neo4j/driver/Driver</className>
73+
<differenceType>7012</differenceType>
74+
<method>org.neo4j.driver.BaseSession session(java.lang.Class, org.neo4j.driver.SessionConfig)</method>
75+
</difference>
76+
77+
<difference>
78+
<className>org/neo4j/driver/Driver</className>
79+
<differenceType>7012</differenceType>
80+
<method>org.neo4j.driver.BaseSession session(java.lang.Class, org.neo4j.driver.AuthToken)</method>
81+
</difference>
82+
83+
<difference>
84+
<className>org/neo4j/driver/Driver</className>
85+
<differenceType>7012</differenceType>
86+
<method>org.neo4j.driver.BaseSession session(java.lang.Class, org.neo4j.driver.SessionConfig, org.neo4j.driver.AuthToken)</method>
87+
</difference>
88+
6589
</differences>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
/**
22+
* A base interface for sessions, used by {@link Driver#session(Class)} and {@link Driver#session(Class, SessionConfig)}.
23+
*/
24+
public interface BaseSession {}

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

Lines changed: 137 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ public interface Driver extends AutoCloseable {
7676
*
7777
* @return a new {@link Session} object.
7878
*/
79-
Session session();
79+
default Session session() {
80+
return session(Session.class);
81+
}
8082

8183
/**
8284
* Create a new {@link Session} with a specified {@link SessionConfig session configuration}.
@@ -85,7 +87,128 @@ public interface Driver extends AutoCloseable {
8587
* @return a new {@link Session} object.
8688
* @see SessionConfig
8789
*/
88-
Session session(SessionConfig sessionConfig);
90+
default Session session(SessionConfig sessionConfig) {
91+
return session(Session.class, sessionConfig);
92+
}
93+
94+
/**
95+
* Instantiate a new session of supported type with default {@link SessionConfig session configuration}.
96+
* <p>
97+
* Supported types are:
98+
* <ul>
99+
* <li>{@link org.neo4j.driver.Session} - synchronous session</li>
100+
* <li>{@link org.neo4j.driver.async.AsyncSession} - asynchronous session</li>
101+
* <li>{@link org.neo4j.driver.reactive.RxSession} - reactive session using Reactive Streams API</li>
102+
* </ul>
103+
* <p>
104+
* Sample usage:
105+
* <pre>
106+
* {@code
107+
* var session = driver.session(AsyncSession.class);
108+
* }
109+
* </pre>
110+
*
111+
* @param sessionClass session type class, must not be null
112+
* @return session instance
113+
* @param <T> session type
114+
* @throws IllegalArgumentException for unsupported session types
115+
* @since 5.2
116+
*/
117+
default <T extends BaseSession> T session(Class<T> sessionClass) {
118+
return session(sessionClass, SessionConfig.defaultConfig());
119+
}
120+
121+
/**
122+
* Instantiate a new session of a supported type with the supplied {@link AuthToken}.
123+
* <p>
124+
* This method allows creating a session with a different {@link AuthToken} to the one used on the driver level.
125+
* The minimum Bolt protocol version is 5.1. An {@link IllegalStateException} will be emitted on session interaction
126+
* for previous Bolt versions.
127+
* <p>
128+
* Supported types are:
129+
* <ul>
130+
* <li>{@link org.neo4j.driver.Session} - synchronous session</li>
131+
* <li>{@link org.neo4j.driver.async.AsyncSession} - asynchronous session</li>
132+
* <li>{@link org.neo4j.driver.reactive.RxSession} - reactive session using Reactive Streams API</li>
133+
* </ul>
134+
* <p>
135+
* Sample usage:
136+
* <pre>
137+
* {@code
138+
* var session = driver.session(AsyncSession.class);
139+
* }
140+
* </pre>
141+
*
142+
* @param sessionClass session type class, must not be null
143+
* @param sessionAuthToken a token, null will result in driver-level configuration being used
144+
* @return session instance
145+
* @param <T> session type
146+
* @throws IllegalArgumentException for unsupported session types
147+
* @since 5.8
148+
*/
149+
default <T extends BaseSession> T session(Class<T> sessionClass, AuthToken sessionAuthToken) {
150+
return session(sessionClass, SessionConfig.defaultConfig(), sessionAuthToken);
151+
}
152+
153+
/**
154+
* Create a new session of supported type with a specified {@link SessionConfig session configuration}.
155+
* <p>
156+
* Supported types are:
157+
* <ul>
158+
* <li>{@link org.neo4j.driver.Session} - synchronous session</li>
159+
* <li>{@link org.neo4j.driver.async.AsyncSession} - asynchronous session</li>
160+
* <li>{@link org.neo4j.driver.reactive.RxSession} - reactive session using Reactive Streams API</li>
161+
* </ul>
162+
* <p>
163+
* Sample usage:
164+
* <pre>
165+
* {@code
166+
* var session = driver.session(AsyncSession.class);
167+
* }
168+
* </pre>
169+
*
170+
* @param sessionClass session type class, must not be null
171+
* @param sessionConfig session config, must not be null
172+
* @return session instance
173+
* @param <T> session type
174+
* @throws IllegalArgumentException for unsupported session types
175+
* @since 5.2
176+
*/
177+
default <T extends BaseSession> T session(Class<T> sessionClass, SessionConfig sessionConfig) {
178+
return session(sessionClass, sessionConfig, null);
179+
}
180+
181+
/**
182+
* Instantiate a new session of a supported type with the supplied {@link SessionConfig session configuration} and
183+
* {@link AuthToken}.
184+
* <p>
185+
* This method allows creating a session with a different {@link AuthToken} to the one used on the driver level.
186+
* The minimum Bolt protocol version is 5.1. An {@link IllegalStateException} will be emitted on session interaction
187+
* for previous Bolt versions.
188+
* <p>
189+
* Supported types are:
190+
* <ul>
191+
* <li>{@link org.neo4j.driver.Session} - synchronous session</li>
192+
* <li>{@link org.neo4j.driver.async.AsyncSession} - asynchronous session</li>
193+
* <li>{@link org.neo4j.driver.reactive.RxSession} - reactive session using Reactive Streams API</li>
194+
* </ul>
195+
* <p>
196+
* Sample usage:
197+
* <pre>
198+
* {@code
199+
* var session = driver.session(AsyncSession.class);
200+
* }
201+
* </pre>
202+
*
203+
* @param sessionClass session type class, must not be null
204+
* @param sessionConfig session config, must not be null
205+
* @param sessionAuthToken a token, null will result in driver-level configuration being used
206+
* @return session instance
207+
* @param <T> session type
208+
* @throws IllegalArgumentException for unsupported session types
209+
* @since 5.8
210+
*/
211+
<T extends BaseSession> T session(Class<T> sessionClass, SessionConfig sessionConfig, AuthToken sessionAuthToken);
89212

90213
/**
91214
* Create a new general purpose {@link RxSession} with default {@link SessionConfig session configuration}.
@@ -95,7 +218,9 @@ public interface Driver extends AutoCloseable {
95218
*
96219
* @return a new {@link RxSession} object.
97220
*/
98-
RxSession rxSession();
221+
default RxSession rxSession() {
222+
return session(RxSession.class);
223+
}
99224

100225
/**
101226
* Create a new {@link RxSession} with a specified {@link SessionConfig session configuration}.
@@ -104,7 +229,9 @@ public interface Driver extends AutoCloseable {
104229
* @param sessionConfig used to customize the session.
105230
* @return a new {@link RxSession} object.
106231
*/
107-
RxSession rxSession(SessionConfig sessionConfig);
232+
default RxSession rxSession(SessionConfig sessionConfig) {
233+
return session(RxSession.class, sessionConfig);
234+
}
108235

109236
/**
110237
* Create a new general purpose {@link AsyncSession} with default {@link SessionConfig session configuration}.
@@ -114,7 +241,9 @@ public interface Driver extends AutoCloseable {
114241
*
115242
* @return a new {@link AsyncSession} object.
116243
*/
117-
AsyncSession asyncSession();
244+
default AsyncSession asyncSession() {
245+
return session(AsyncSession.class);
246+
}
118247

119248
/**
120249
* Create a new {@link AsyncSession} with a specified {@link SessionConfig session configuration}.
@@ -124,7 +253,9 @@ public interface Driver extends AutoCloseable {
124253
* @param sessionConfig used to customize the session.
125254
* @return a new {@link AsyncSession} object.
126255
*/
127-
AsyncSession asyncSession(SessionConfig sessionConfig);
256+
default AsyncSession asyncSession(SessionConfig sessionConfig) {
257+
return session(AsyncSession.class, sessionConfig);
258+
}
128259

129260
/**
130261
* Close all the resources assigned to this driver, including open connections and IO threads.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
*
5252
* @since 1.0 (Removed async API to {@link AsyncSession} in 4.0)
5353
*/
54-
public interface Session extends Resource, QueryRunner {
54+
public interface Session extends BaseSession, Resource, QueryRunner {
5555
/**
5656
* Begin a new <em>unmanaged {@linkplain Transaction transaction}</em>. At
5757
* most one transaction may exist in a session at any point in time. To

driver/src/main/java/org/neo4j/driver/async/AsyncSession.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.concurrent.Executor;
2525
import java.util.function.Function;
2626
import org.neo4j.driver.AccessMode;
27+
import org.neo4j.driver.BaseSession;
2728
import org.neo4j.driver.Bookmark;
2829
import org.neo4j.driver.Query;
2930
import org.neo4j.driver.Transaction;
@@ -68,7 +69,7 @@
6869
*
6970
* @since 4.0
7071
*/
71-
public interface AsyncSession extends AsyncQueryRunner {
72+
public interface AsyncSession extends BaseSession, AsyncQueryRunner {
7273
/**
7374
* Begin a new <em>unmanaged {@linkplain Transaction transaction}</em>. At
7475
* most one transaction may exist in a session at any point in time. To

driver/src/main/java/org/neo4j/driver/internal/DirectConnectionProvider.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
package org.neo4j.driver.internal;
2020

2121
import static org.neo4j.driver.internal.async.ConnectionContext.PENDING_DATABASE_NAME_EXCEPTION_SUPPLIER;
22-
import static org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil.supportsMultiDatabase;
2322

2423
import java.util.concurrent.CompletableFuture;
2524
import java.util.concurrent.CompletionStage;
25+
import java.util.function.Function;
26+
import org.neo4j.driver.AuthToken;
2627
import org.neo4j.driver.internal.async.ConnectionContext;
2728
import org.neo4j.driver.internal.async.connection.DirectConnection;
29+
import org.neo4j.driver.internal.messaging.request.MultiDatabaseUtil;
2830
import org.neo4j.driver.internal.spi.Connection;
2931
import org.neo4j.driver.internal.spi.ConnectionPool;
3032
import org.neo4j.driver.internal.spi.ConnectionProvider;
@@ -46,7 +48,7 @@ public class DirectConnectionProvider implements ConnectionProvider {
4648
public CompletionStage<Connection> acquireConnection(ConnectionContext context) {
4749
CompletableFuture<DatabaseName> databaseNameFuture = context.databaseNameFuture();
4850
databaseNameFuture.complete(DatabaseNameUtil.defaultDatabase());
49-
return acquireConnection()
51+
return acquirePooledConnection(context.overrideAuthToken())
5052
.thenApply(connection -> new DirectConnection(
5153
connection,
5254
Futures.joinNowOrElseThrow(databaseNameFuture, PENDING_DATABASE_NAME_EXCEPTION_SUPPLIER),
@@ -56,7 +58,7 @@ public CompletionStage<Connection> acquireConnection(ConnectionContext context)
5658

5759
@Override
5860
public CompletionStage<Void> verifyConnectivity() {
59-
return acquireConnection().thenCompose(Connection::release);
61+
return acquirePooledConnection(null).thenCompose(Connection::release);
6062
}
6163

6264
@Override
@@ -66,9 +68,13 @@ public CompletionStage<Void> close() {
6668

6769
@Override
6870
public CompletionStage<Boolean> supportsMultiDb() {
69-
return acquireConnection().thenCompose(conn -> {
70-
boolean supportsMultiDatabase = supportsMultiDatabase(conn);
71-
return conn.release().thenApply(ignored -> supportsMultiDatabase);
71+
return detectFeature(MultiDatabaseUtil::supportsMultiDatabase);
72+
}
73+
74+
private CompletionStage<Boolean> detectFeature(Function<Connection, Boolean> featureDetectionFunction) {
75+
return acquirePooledConnection(null).thenCompose(conn -> {
76+
boolean featureDetected = featureDetectionFunction.apply(conn);
77+
return conn.release().thenApply(ignored -> featureDetected);
7278
});
7379
}
7480

@@ -80,7 +86,7 @@ public BoltServerAddress getAddress() {
8086
* Used only for grabbing a connection with the server after hello message.
8187
* This connection cannot be directly used for running any queries as it is missing necessary connection context
8288
*/
83-
private CompletionStage<Connection> acquireConnection() {
84-
return connectionPool.acquire(address);
89+
private CompletionStage<Connection> acquirePooledConnection(AuthToken authToken) {
90+
return connectionPool.acquire(address, authToken);
8591
}
8692
}

0 commit comments

Comments
 (0)