Skip to content

Commit fc6bdf6

Browse files
committed
Retry on token expired when token is static token
1 parent 8b20615 commit fc6bdf6

File tree

15 files changed

+273
-81
lines changed

15 files changed

+273
-81
lines changed

packages/bolt-connection/src/connection-provider/connection-provider-direct.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider {
7777
`Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'`
7878
)
7979

80-
this._authenticationProvider.handleError({ connection, code: error.code })
81-
82-
if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') {
83-
this._connectionPool.apply(address, (conn) => { conn.authToken = null })
84-
}
85-
86-
connection.close().catch(() => undefined)
87-
88-
return error
80+
return super._handleAuthorizationExpired(error, address, connection)
8981
}
9082

9183
async _hasProtocolVersion (versionPredicate) {

packages/bolt-connection/src/connection-provider/connection-provider-pooled.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

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

@@ -41,6 +41,7 @@ export default class PooledConnectionProvider extends ConnectionProvider {
4141
this._id = id
4242
this._config = config
4343
this._log = log
44+
this._authTokenManager = authTokenManager
4445
this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent })
4546
this._createChannelConnection =
4647
createChannelConnectionHook ||
@@ -227,4 +228,22 @@ export default class PooledConnectionProvider extends ConnectionProvider {
227228
static _removeIdleObserverOnConnection (conn) {
228229
conn._updateCurrentObserver()
229230
}
231+
232+
_handleAuthorizationExpired (error, address, connection) {
233+
this._authenticationProvider.handleError({ connection, code: error.code })
234+
235+
if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') {
236+
this._connectionPool.apply(address, (conn) => { conn.authToken = null })
237+
}
238+
239+
if (connection) {
240+
connection.close().catch(() => undefined)
241+
}
242+
243+
if (error.code === 'Neo.ClientError.Security.TokenExpired' && !isStaticAuthTokenManger(this._authTokenManager)) {
244+
error.retriable = true
245+
}
246+
247+
return error
248+
}
230249
}

packages/bolt-connection/src/connection-provider/connection-provider-routing.js

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
117117
`Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'`
118118
)
119119

120-
this._authenticationProvider.handleError({ connection, code: error.code })
121-
122-
if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') {
123-
this._connectionPool.apply(address, (conn) => { conn.authToken = null })
124-
}
125-
126-
if (connection) {
127-
connection.close().catch(() => undefined)
128-
}
129-
130-
return error
120+
return super._handleAuthorizationExpired(error, address, connection, database)
131121
}
132122

133123
_handleWriteFailure (error, address, database) {

packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import DirectConnectionProvider from '../../src/connection-provider/connection-provider-direct'
2121
import { Pool } from '../../src/pool'
2222
import { Connection, DelegateConnection } from '../../src/connection'
23-
import { internal, newError, ServerInfo } from 'neo4j-driver-core'
23+
import { internal, newError, ServerInfo, staticAuthTokenManager, temporalAuthDataManager } from 'neo4j-driver-core'
2424
import AuthenticationProvider from '../../src/connection-provider/authentication-provider'
2525
import { functional } from '../../src/lang'
2626

@@ -206,6 +206,46 @@ it('should call authenticationAuthProvider.handleError when TokenExpired happens
206206
expect(handleError).toBeCalledWith({ connection: conn, code: 'Neo.ClientError.Security.TokenExpired' })
207207
})
208208

209+
it('should change error to retriable when error when TokenExpired happens and staticAuthTokenManager is not being used', async () => {
210+
const address = ServerAddress.fromUrl('localhost:123')
211+
const pool = newPool()
212+
const connectionProvider = newDirectConnectionProvider(address, pool, temporalAuthDataManager({ getAuthData: () => null }))
213+
214+
const conn = await connectionProvider.acquireConnection({
215+
accessMode: 'READ',
216+
database: ''
217+
})
218+
219+
const expectedError = newError(
220+
'Message',
221+
'Neo.ClientError.Security.TokenExpired'
222+
)
223+
224+
const error = conn.handleAndTransformError(expectedError, address)
225+
226+
expect(error.retriable).toBe(true)
227+
})
228+
229+
it('should not change error to retriable when error when TokenExpired happens and staticAuthTokenManager is being used', async () => {
230+
const address = ServerAddress.fromUrl('localhost:123')
231+
const pool = newPool()
232+
const connectionProvider = newDirectConnectionProvider(address, pool, staticAuthTokenManager({ authToken: null }))
233+
234+
const conn = await connectionProvider.acquireConnection({
235+
accessMode: 'READ',
236+
database: ''
237+
})
238+
239+
const expectedError = newError(
240+
'Message',
241+
'Neo.ClientError.Security.TokenExpired'
242+
)
243+
244+
const error = conn.handleAndTransformError(expectedError, address)
245+
246+
expect(error.retriable).toBe(false)
247+
})
248+
209249
describe('constructor', () => {
210250
describe('newPool', () => {
211251
const server0 = ServerAddress.fromUrl('localhost:123')
@@ -762,12 +802,13 @@ describe('.verifyConnectivityAndGetServerInfo()', () => {
762802
})
763803
})
764804

765-
function newDirectConnectionProvider (address, pool) {
805+
function newDirectConnectionProvider (address, pool, authTokenManager) {
766806
const connectionProvider = new DirectConnectionProvider({
767807
id: 0,
768808
config: {},
769809
log: Logger.noOp(),
770-
address: address
810+
address: address,
811+
authTokenManager
771812
})
772813
connectionProvider._connectionPool = pool
773814
return connectionProvider

packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import {
2424
Integer,
2525
int,
2626
internal,
27-
ServerInfo
27+
ServerInfo,
28+
staticAuthTokenManager,
29+
temporalAuthDataManager
2830
} from 'neo4j-driver-core'
2931
import { RoutingTable } from '../../src/rediscovery/'
3032
import { Pool } from '../../src/pool'
@@ -1703,6 +1705,84 @@ describe.each([
17031705
expect(error).toBe(expectedError)
17041706
})
17051707

1708+
it.each(usersDataSet)('should change error to retriable when error when TokenExpired happens and staticAuthTokenManager is not being used [user=%s]', async (user) => {
1709+
const pool = newPool()
1710+
const connectionProvider = newRoutingConnectionProvider(
1711+
[
1712+
newRoutingTable(
1713+
null,
1714+
[server1, server2],
1715+
[server3, server2],
1716+
[server2, server4]
1717+
)
1718+
],
1719+
pool
1720+
)
1721+
connectionProvider._authTokenManager = temporalAuthDataManager({ getAuthData: () => null })
1722+
1723+
const error = newError(
1724+
'Message',
1725+
'Neo.ClientError.Security.TokenExpired'
1726+
)
1727+
1728+
const server2Connection = await connectionProvider.acquireConnection({
1729+
accessMode: 'WRITE',
1730+
database: null,
1731+
impersonatedUser: user
1732+
})
1733+
1734+
const server3Connection = await connectionProvider.acquireConnection({
1735+
accessMode: 'READ',
1736+
database: null,
1737+
impersonatedUser: user
1738+
})
1739+
1740+
const error1 = server3Connection.handleAndTransformError(error, server3)
1741+
const error2 = server2Connection.handleAndTransformError(error, server2)
1742+
1743+
expect(error1.retriable).toBe(true)
1744+
expect(error2.retriable).toBe(true)
1745+
})
1746+
1747+
it.each(usersDataSet)('should not change error to retriable when error when TokenExpired happens and staticAuthTokenManager is being used [user=%s]', async (user) => {
1748+
const pool = newPool()
1749+
const connectionProvider = newRoutingConnectionProvider(
1750+
[
1751+
newRoutingTable(
1752+
null,
1753+
[server1, server2],
1754+
[server3, server2],
1755+
[server2, server4]
1756+
)
1757+
],
1758+
pool
1759+
)
1760+
connectionProvider._authTokenManager = staticAuthTokenManager({ authToken: null })
1761+
1762+
const error = newError(
1763+
'Message',
1764+
'Neo.ClientError.Security.TokenExpired'
1765+
)
1766+
1767+
const server2Connection = await connectionProvider.acquireConnection({
1768+
accessMode: 'WRITE',
1769+
database: null,
1770+
impersonatedUser: user
1771+
})
1772+
1773+
const server3Connection = await connectionProvider.acquireConnection({
1774+
accessMode: 'READ',
1775+
database: null,
1776+
impersonatedUser: user
1777+
})
1778+
1779+
const error1 = server3Connection.handleAndTransformError(error, server3)
1780+
const error2 = server2Connection.handleAndTransformError(error, server2)
1781+
1782+
expect(error1.retriable).toBe(false)
1783+
expect(error2.retriable).toBe(false)
1784+
})
1785+
17061786
it.each(usersDataSet)('should use resolved seed router after accepting table with no writers [user=%s]', (user, done) => {
17071787
const routingTable1 = newRoutingTable(
17081788
null,

packages/core/src/auth-token-manager.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,30 @@ export function temporalAuthDataManager ({ getAuthData }: { getAuthData: () => P
9090
return new TemporalAuthDataManager(getAuthData)
9191
}
9292

93+
/**
94+
* Create a {@link AuthTokenManager} for handle static {@link AuthToken}
95+
*
96+
* @private
97+
* @param {param} args - The args
98+
* @param {AuthToken} args.authToken - The static auth token which will always used in the driver.
99+
* @returns {AuthTokenManager} The temporal auth data manager.
100+
*/
101+
export function staticAuthTokenManager ({ authToken }: { authToken: AuthToken }): AuthTokenManager {
102+
return new StaticAuthTokenManager(authToken)
103+
}
104+
105+
/**
106+
* Checks if the manager is a StaticAuthTokenManager
107+
*
108+
* @private
109+
* @experimental
110+
* @param {AuthTokenManager} manager The auth token manager to be checked.
111+
* @returns {boolean} Manager is StaticAuthTokenManager
112+
*/
113+
export function isStaticAuthTokenManger (manager: AuthTokenManager): manager is StaticAuthTokenManager {
114+
return manager instanceof StaticAuthTokenManager
115+
}
116+
93117
interface TokenRefreshObserver {
94118
onCompleted: (data: TemporalAuthData) => void
95119
onError: (error: Error) => void
@@ -171,3 +195,19 @@ class TemporalAuthDataManager implements AuthTokenManager {
171195
})
172196
}
173197
}
198+
199+
class StaticAuthTokenManager implements AuthTokenManager {
200+
constructor (
201+
private readonly _authToken: AuthToken
202+
) {
203+
204+
}
205+
206+
getToken (): AuthToken {
207+
return this._authToken
208+
}
209+
210+
onTokenExpired (_: AuthToken): void {
211+
// nothing to do here
212+
}
213+
}

packages/core/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ import Session, { TransactionConfig } from './session'
8787
import Driver, * as driver from './driver'
8888
import auth from './auth'
8989
import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager'
90-
import AuthTokenManager, { temporalAuthDataManager, TemporalAuthData } from './auth-token-manager'
90+
import AuthTokenManager, { temporalAuthDataManager, staticAuthTokenManager, isStaticAuthTokenManger, TemporalAuthData } from './auth-token-manager'
9191
import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver'
9292
import * as types from './types'
9393
import * as json from './json'
@@ -233,6 +233,8 @@ export {
233233
auth,
234234
bookmarkManager,
235235
temporalAuthDataManager,
236+
staticAuthTokenManager,
237+
isStaticAuthTokenManger,
236238
routing,
237239
resultTransformers,
238240
notificationCategory,

packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider {
7777
`Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'`
7878
)
7979

80-
this._authenticationProvider.handleError({ connection, code: error.code })
81-
82-
if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') {
83-
this._connectionPool.apply(address, (conn) => { conn.authToken = null })
84-
}
85-
86-
connection.close().catch(() => undefined)
87-
88-
return error
80+
return super._handleAuthorizationExpired(error, address, connection)
8981
}
9082

9183
async _hasProtocolVersion (versionPredicate) {

packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import { createChannelConnection, ConnectionErrorHandler } from '../connection/index.js'
2121
import Pool, { PoolConfig } from '../pool/index.js'
22-
import { error, ConnectionProvider, ServerInfo, newError } from '../../core/index.ts'
22+
import { error, ConnectionProvider, ServerInfo, newError, isStaticAuthTokenManger } from '../../core/index.ts'
2323
import AuthenticationProvider from './authentication-provider.js'
2424
import { object } from '../lang/index.js'
2525

@@ -41,6 +41,7 @@ export default class PooledConnectionProvider extends ConnectionProvider {
4141
this._id = id
4242
this._config = config
4343
this._log = log
44+
this._authTokenManager = authTokenManager
4445
this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent })
4546
this._createChannelConnection =
4647
createChannelConnectionHook ||
@@ -227,4 +228,22 @@ export default class PooledConnectionProvider extends ConnectionProvider {
227228
static _removeIdleObserverOnConnection (conn) {
228229
conn._updateCurrentObserver()
229230
}
231+
232+
_handleAuthorizationExpired (error, address, connection) {
233+
this._authenticationProvider.handleError({ connection, code: error.code })
234+
235+
if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') {
236+
this._connectionPool.apply(address, (conn) => { conn.authToken = null })
237+
}
238+
239+
if (connection) {
240+
connection.close().catch(() => undefined)
241+
}
242+
243+
if (error.code === 'Neo.ClientError.Security.TokenExpired' && !isStaticAuthTokenManger(this._authTokenManager)) {
244+
error.retriable = true
245+
}
246+
247+
return error
248+
}
230249
}

packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
117117
`Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'`
118118
)
119119

120-
this._authenticationProvider.handleError({ connection, code: error.code })
121-
122-
if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') {
123-
this._connectionPool.apply(address, (conn) => { conn.authToken = null })
124-
}
125-
126-
if (connection) {
127-
connection.close().catch(() => undefined)
128-
}
129-
130-
return error
120+
return super._handleAuthorizationExpired(error, address, connection, database)
131121
}
132122

133123
_handleWriteFailure (error, address, database) {

0 commit comments

Comments
 (0)