Skip to content

Re-authentication #1050

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 70 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
88398b1
Add AuthTokenProvider configuration
bigmontz Dec 14, 2022
4fb19db
Handling auth error
bigmontz Dec 14, 2022
89b0b44
Add support for impersonation
bigmontz Dec 14, 2022
beac651
Add holder
bigmontz Jan 9, 2023
759fec2
Re-Authentication
bigmontz Jan 9, 2023
a7f441a
Fix tests
bigmontz Jan 9, 2023
44c891b
Adjust test
bigmontz Jan 10, 2023
b41b729
Add login/logoff concepts
bigmontz Jan 10, 2023
dab7f38
Add basic testkit tests for AuthProvider
bigmontz Jan 11, 2023
dbdc8ed
Ajust the re-auth flow and support FakeTime in tk
bigmontz Jan 11, 2023
89e27d3
Session config
bigmontz Jan 13, 2023
f5f66c8
Pool: separate validation on acquire and on release and add the acqui…
bigmontz Jan 13, 2023
faa5a6d
Close connection, pipeline re-auth and passing to test_renewable_auth…
bigmontz Jan 16, 2023
068309b
Ajust user switching
bigmontz Jan 16, 2023
6d0c296
Add tests to DenoJS
bigmontz Jan 17, 2023
cbce3c8
Testing pool changes
bigmontz Jan 17, 2023
fb05d79
Add 5.1 base protocol test and supportLogoff test
bigmontz Jan 17, 2023
f640d8d
Add test for login/logoff in a non supported protocol
bigmontz Jan 17, 2023
cae34c9
Add test to the connection-channel
bigmontz Jan 17, 2023
1b6950e
Add tests for AuthenticationProvider#authenticate, no user-switching
bigmontz Jan 17, 2023
49f34ac
Add AuthenticationProvider.authenticate when auth is provider
bigmontz Jan 18, 2023
a9bf704
Test AuthenticationProvider.handleError
bigmontz Jan 18, 2023
e0ad1a0
Add tests for direct provider error handle
bigmontz Jan 19, 2023
e1354d2
Add tests for handle error in the RoutingConnectionProvider
bigmontz Jan 19, 2023
cb3aec1
Add test for 5.1 in RoutingProvider
bigmontz Jan 19, 2023
19f3633
Add test for RoutingConnectionProvider resource creation setup (pool)
bigmontz Jan 19, 2023
a80b869
Add tests to the RoutingConnectionProvider pool configuration
bigmontz Jan 19, 2023
4663468
Add tests to the DirectConnectionProvider pool configuration
bigmontz Jan 19, 2023
b5a6b6c
Small fix and sync deno
bigmontz Jan 20, 2023
1951038
Fix merge issues
bigmontz Jan 27, 2023
792c4a5
Implement user-switch doesn't allow sticky connection
bigmontz Feb 1, 2023
dc8e45a
Improve tests
bigmontz Feb 1, 2023
4e7537b
Improve tests around non-sticky connections
bigmontz Feb 1, 2023
2b24f15
Mark connections created with a new auth and without support to re-au…
bigmontz Feb 2, 2023
31447be
Killing sticky connections on-release
bigmontz Feb 2, 2023
ec560ba
Enable require a new connection from the pool
bigmontz Feb 2, 2023
00287ec
Acquire new sticky connection if backwards compatible and protocol do…
bigmontz Feb 2, 2023
d7bdab4
Add `supportsSessionAuth`
bigmontz Feb 6, 2023
2bbc14a
Add CheckSessionAuthSupport to testkit
bigmontz Feb 6, 2023
733e4e2
Add tests for session configuration
bigmontz Feb 6, 2023
ce4806b
Add user switching to RxSession
bigmontz Feb 6, 2023
f9933f8
VerifyAuthorization
bigmontz Feb 6, 2023
46f99ed
Add backwardsCompatibleAuth flag to the driver configuration
bigmontz Feb 8, 2023
368f841
VerifyAuth: refresh auth if needed
bigmontz Feb 8, 2023
0e4e7c0
Add AuthTokenManager
bigmontz Feb 21, 2023
ed4a56d
Adjust testkit
bigmontz Feb 21, 2023
19a332c
Release sticky connection and only drop connection when needed
bigmontz Feb 22, 2023
2350c7c
Fix pool size comparison
bigmontz Feb 22, 2023
97f59cd
VerifyAuthentication ajusts
bigmontz Feb 23, 2023
18187b9
Clean and test
bigmontz Feb 27, 2023
b74617e
sync deno
bigmontz Feb 27, 2023
90dd4ea
Implement changes from testkit
bigmontz Mar 6, 2023
efc22aa
Add temporal token tests
bigmontz Mar 6, 2023
7a80acf
rename login => logon
bigmontz Mar 8, 2023
1eb29b6
rename supportsLogoff => supportsReAuth
bigmontz Mar 8, 2023
95ee035
Enable AuthTokenManager tests for testkit
bigmontz Mar 21, 2023
8b20615
Marking re-auth and user-switch as experimental/preview
bigmontz Mar 21, 2023
fc6bdf6
Retry on token expired when token is static token
bigmontz Mar 21, 2023
bfb40c3
Adjust documentation
bigmontz Mar 21, 2023
302d43d
Remove backwardsCompatibilityAuth code
bigmontz Mar 22, 2023
78c2970
Fix rebase issues
bigmontz Apr 3, 2023
e911411
Rename TemporalAuthTokenManager to ExpirationBasedAuthTokenManager
bigmontz Apr 4, 2023
0a02a2a
Fix names in testkit-backend
bigmontz Apr 4, 2023
6054069
Ajust docs
bigmontz Apr 4, 2023
7e6b9b2
testkit-backend: Fix `authToken` param in VerifyAuthentication
bigmontz Apr 5, 2023
5df0cef
Remove file added by acident
bigmontz Apr 5, 2023
346b334
testkit-backend: Adjust authToken name in
bigmontz Apr 5, 2023
7548e5a
Sync DenoJS
bigmontz Apr 5, 2023
7306980
PolyfillFlatMap
bigmontz Apr 5, 2023
731dbdd
polyfill flatmap
bigmontz Apr 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/bolt-connection/src/bolt/bolt-protocol-v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export default class BoltProtocol {
onError: onError
})

// TODO: Verify the Neo4j version in the message
const error = newError(
'Driver is connected to a database that does not support logoff. ' +
'Please upgrade to Neo4j 5.5.0 or later in order to use this functionality.'
Expand Down Expand Up @@ -233,6 +234,7 @@ export default class BoltProtocol {
onError: (error) => this._onLoginError(error, onError)
})

// TODO: Verify the Neo4j version in the message
const error = newError(
'Driver is connected to a database that does not support logon. ' +
'Please upgrade to Neo4j 5.5.0 or later in order to use this functionality.'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expirationBasedAuthTokenManager } from 'neo4j-driver-core'
import { object } from '../lang'

/**
* Class which provides Authorization for {@link Connection}
*/
export default class AuthenticationProvider {
constructor ({ authTokenManager, userAgent }) {
this._authTokenManager = authTokenManager || expirationBasedAuthTokenManager({
tokenProvider: () => {}
})
this._userAgent = userAgent
}

async authenticate ({ connection, auth, skipReAuth, waitReAuth, forceReAuth }) {
if (auth != null) {
const shouldReAuth = connection.supportsReAuth === true && (
(!object.equals(connection.authToken, auth) && skipReAuth !== true) ||
forceReAuth === true
)
if (connection.authToken == null || shouldReAuth) {
return await connection.connect(this._userAgent, auth, waitReAuth || false)
}
return connection
}

const authToken = await this._authTokenManager.getToken()

if (!object.equals(authToken, connection.authToken)) {
return await connection.connect(this._userAgent, authToken)
}

return connection
}

handleError ({ connection, code }) {
if (
connection &&
[
'Neo.ClientError.Security.Unauthorized',
'Neo.ClientError.Security.TokenExpired'
].includes(code)
) {
this._authTokenManager.onTokenExpired(connection.authToken)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,19 @@ import {
import { internal, error } from 'neo4j-driver-core'

const {
constants: { BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, BOLT_PROTOCOL_V4_4 }
constants: {
BOLT_PROTOCOL_V3,
BOLT_PROTOCOL_V4_0,
BOLT_PROTOCOL_V4_4,
BOLT_PROTOCOL_V5_1
}
} = internal

const { SERVICE_UNAVAILABLE } = error

export default class DirectConnectionProvider extends PooledConnectionProvider {
constructor ({ id, config, log, address, userAgent, authToken }) {
super({ id, config, log, userAgent, authToken })
constructor ({ id, config, log, address, userAgent, authTokenManager, newPool }) {
super({ id, config, log, userAgent, authTokenManager, newPool })

this._address = address
}
Expand All @@ -42,27 +47,33 @@ export default class DirectConnectionProvider extends PooledConnectionProvider {
* See {@link ConnectionProvider} for more information about this method and
* its arguments.
*/
acquireConnection ({ accessMode, database, bookmarks } = {}) {
async acquireConnection ({ accessMode, database, bookmarks, auth, forceReAuth } = {}) {
const databaseSpecificErrorHandler = ConnectionErrorHandler.create({
errorCode: SERVICE_UNAVAILABLE,
handleAuthorizationExpired: (error, address) =>
this._handleAuthorizationExpired(error, address, database)
handleAuthorizationExpired: (error, address, conn) =>
this._handleAuthorizationExpired(error, address, conn, database)
})

return this._connectionPool
.acquire(this._address)
.then(
connection =>
new DelegateConnection(connection, databaseSpecificErrorHandler)
)
const connection = await this._connectionPool.acquire({ auth, forceReAuth }, this._address)

if (auth) {
await this._verifyStickyConnection({
auth,
connection,
address: this._address
})
return connection
}

return new DelegateConnection(connection, databaseSpecificErrorHandler)
}

_handleAuthorizationExpired (error, address, database) {
_handleAuthorizationExpired (error, address, connection, database) {
this._log.warn(
`Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'`
)
this._connectionPool.purge(address).catch(() => {})
return error

return super._handleAuthorizationExpired(error, address, connection)
}

async _hasProtocolVersion (versionPredicate) {
Expand Down Expand Up @@ -111,6 +122,19 @@ export default class DirectConnectionProvider extends PooledConnectionProvider {
)
}

async supportsSessionAuth () {
return await this._hasProtocolVersion(
version => version >= BOLT_PROTOCOL_V5_1
)
}

async verifyAuthentication ({ auth }) {
return this._verifyAuthentication({
auth,
getAddress: () => this._address
})
}

async verifyConnectivityAndGetServerInfo () {
return await this._verifyConnectivityAndGetServerVersion({ address: this._address })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,30 @@

import { createChannelConnection, ConnectionErrorHandler } from '../connection'
import Pool, { PoolConfig } from '../pool'
import { error, ConnectionProvider, ServerInfo } from 'neo4j-driver-core'
import { error, ConnectionProvider, ServerInfo, newError, isStaticAuthTokenManger } from 'neo4j-driver-core'
import AuthenticationProvider from './authentication-provider'
import { object } from '../lang'

const { SERVICE_UNAVAILABLE } = error
const AUTHENTICATION_ERRORS = [
'Neo.ClientError.Security.CredentialsExpired',
'Neo.ClientError.Security.Forbidden',
'Neo.ClientError.Security.TokenExpired',
'Neo.ClientError.Security.Unauthorized'
]

export default class PooledConnectionProvider extends ConnectionProvider {
constructor (
{ id, config, log, userAgent, authToken },
{ id, config, log, userAgent, authTokenManager, newPool = (...args) => new Pool(...args) },
createChannelConnectionHook = null
) {
super()

this._id = id
this._config = config
this._log = log
this._userAgent = userAgent
this._authToken = authToken
this._authTokenManager = authTokenManager
this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent })
this._createChannelConnection =
createChannelConnectionHook ||
(address => {
Expand All @@ -44,10 +53,11 @@ export default class PooledConnectionProvider extends ConnectionProvider {
this._log
)
})
this._connectionPool = new Pool({
this._connectionPool = newPool({
create: this._createConnection.bind(this),
destroy: this._destroyConnection.bind(this),
validate: this._validateConnection.bind(this),
validateOnAcquire: this._validateConnectionOnAcquire.bind(this),
validateOnRelease: this._validateConnectionOnRelease.bind(this),
installIdleObserver: PooledConnectionProvider._installIdleObserverOnConnection.bind(
this
),
Expand All @@ -57,6 +67,7 @@ export default class PooledConnectionProvider extends ConnectionProvider {
config: PoolConfig.fromDriverConfig(config),
log: this._log
})
this._userAgent = userAgent
this._openConnections = {}
}

Expand All @@ -69,14 +80,13 @@ export default class PooledConnectionProvider extends ConnectionProvider {
* @return {Promise<Connection>} promise resolved with a new connection or rejected when failed to connect.
* @access private
*/
_createConnection (address, release) {
_createConnection ({ auth }, address, release) {
return this._createChannelConnection(address).then(connection => {
connection._release = () => {
return release(address, connection)
}
this._openConnections[connection.id] = connection
return connection
.connect(this._userAgent, this._authToken)
return this._authenticationProvider.authenticate({ connection, auth })
.catch(error => {
// let's destroy this connection
this._destroyConnection(connection)
Expand All @@ -86,6 +96,26 @@ export default class PooledConnectionProvider extends ConnectionProvider {
})
}

async _validateConnectionOnAcquire ({ auth, skipReAuth }, conn) {
if (!this._validateConnection(conn)) {
return false
}

try {
await this._authenticationProvider.authenticate({ connection: conn, auth, skipReAuth })
return true
} catch (error) {
this._log.debug(
`The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'`
)
return false
}
}

_validateConnectionOnRelease (conn) {
return conn._sticky !== true && this._validateConnection(conn)
}

/**
* Check that a connection is usable
* @return {boolean} true if the connection is open
Expand All @@ -98,7 +128,11 @@ export default class PooledConnectionProvider extends ConnectionProvider {

const maxConnectionLifetime = this._config.maxConnectionLifetime
const lifetime = Date.now() - conn.creationTimestamp
return lifetime <= maxConnectionLifetime
if (lifetime > maxConnectionLifetime) {
return false
}

return true
}

/**
Expand All @@ -118,7 +152,7 @@ export default class PooledConnectionProvider extends ConnectionProvider {
* @return {Promise<ServerInfo>} the server info
*/
async _verifyConnectivityAndGetServerVersion ({ address }) {
const connection = await this._connectionPool.acquire(address)
const connection = await this._connectionPool.acquire({}, address)
const serverInfo = new ServerInfo(connection.server, connection.protocol().version)
try {
if (!connection.protocol().isLastMessageLogon()) {
Expand All @@ -130,6 +164,47 @@ export default class PooledConnectionProvider extends ConnectionProvider {
return serverInfo
}

async _verifyAuthentication ({ getAddress, auth }) {
const connectionsToRelease = []
try {
const address = await getAddress()
const connection = await this._connectionPool.acquire({ auth, skipReAuth: true }, address)
connectionsToRelease.push(connection)

const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogon()

if (!connection.supportsReAuth) {
throw newError('Driver is connected to a database that does not support user switch.')
}
if (lastMessageIsNotLogin && connection.supportsReAuth) {
await this._authenticationProvider.authenticate({ connection, auth, waitReAuth: true, forceReAuth: true })
} else if (lastMessageIsNotLogin && !connection.supportsReAuth) {
const stickyConnection = await this._connectionPool.acquire({ auth }, address, { requireNew: true })
stickyConnection._sticky = true
connectionsToRelease.push(stickyConnection)
}
return true
} catch (error) {
if (AUTHENTICATION_ERRORS.includes(error.code)) {
return false
}
throw error
} finally {
await Promise.all(connectionsToRelease.map(conn => conn._release()))
}
}

async _verifyStickyConnection ({ auth, connection, address }) {
const connectionWithSameCredentials = object.equals(auth, connection.authToken)
const shouldCreateStickyConnection = !connectionWithSameCredentials
connection._sticky = connectionWithSameCredentials && !connection.supportsReAuth

if (shouldCreateStickyConnection || connection._sticky) {
await connection._release()
throw newError('Driver is connected to a database that does not support user switch.')
}
}

async close () {
// purge all idle connections in the connection pool
await this._connectionPool.close()
Expand All @@ -146,4 +221,22 @@ export default class PooledConnectionProvider extends ConnectionProvider {
static _removeIdleObserverOnConnection (conn) {
conn._updateCurrentObserver()
}

_handleAuthorizationExpired (error, address, connection) {
this._authenticationProvider.handleError({ connection, code: error.code })

if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') {
this._connectionPool.apply(address, (conn) => { conn.authToken = null })
}

if (connection) {
connection.close().catch(() => undefined)
}

if (error.code === 'Neo.ClientError.Security.TokenExpired' && !isStaticAuthTokenManger(this._authTokenManager)) {
error.retriable = true
}

return error
}
}
Loading