diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index 77d06e11d..131eb5125 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import { expirationBasedAuthTokenManager } from 'neo4j-driver-core' +import { staticAuthTokenManager } from 'neo4j-driver-core' import { object } from '../lang' /** @@ -25,9 +25,7 @@ import { object } from '../lang' */ export default class AuthenticationProvider { constructor ({ authTokenManager, userAgent, boltAgent }) { - this._authTokenManager = authTokenManager || expirationBasedAuthTokenManager({ - tokenProvider: () => {} - }) + this._authTokenManager = authTokenManager || staticAuthTokenManager({}) this._userAgent = userAgent this._boltAgent = boltAgent } @@ -56,12 +54,10 @@ export default class AuthenticationProvider { handleError ({ connection, code }) { if ( connection && - [ - 'Neo.ClientError.Security.Unauthorized', - 'Neo.ClientError.Security.TokenExpired' - ].includes(code) + code.startsWith('Neo.ClientError.Security.') ) { - this._authTokenManager.onTokenExpired(connection.authToken) + return this._authTokenManager.handleSecurityException(connection.authToken, code) } + return false } } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 0f2b5ae8a..fe3a663ee 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -50,8 +50,8 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { async acquireConnection ({ accessMode, database, bookmarks, auth, forceReAuth } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, - handleAuthorizationExpired: (error, address, conn) => - this._handleAuthorizationExpired(error, address, conn, database) + handleSecurityError: (error, address, conn) => + this._handleSecurityError(error, address, conn, database) }) const connection = await this._connectionPool.acquire({ auth, forceReAuth }, this._address) @@ -68,12 +68,12 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { return new DelegateConnection(connection, databaseSpecificErrorHandler) } - _handleAuthorizationExpired (error, address, connection, database) { + _handleSecurityError (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}'` ) - return super._handleAuthorizationExpired(error, address, connection) + return super._handleSecurityError(error, address, connection) } async _hasProtocolVersion (versionPredicate) { diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index f7bf6577b..e07a4e1bf 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -19,7 +19,7 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection' import Pool, { PoolConfig } from '../pool' -import { error, ConnectionProvider, ServerInfo, newError, isStaticAuthTokenManger } from 'neo4j-driver-core' +import { error, ConnectionProvider, ServerInfo, newError } from 'neo4j-driver-core' import AuthenticationProvider from './authentication-provider' import { object } from '../lang' @@ -41,7 +41,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log - this._authTokenManager = authTokenManager this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent, boltAgent }) this._userAgent = userAgent this._boltAgent = boltAgent @@ -224,8 +223,12 @@ export default class PooledConnectionProvider extends ConnectionProvider { conn._updateCurrentObserver() } - _handleAuthorizationExpired (error, address, connection) { - this._authenticationProvider.handleError({ connection, code: error.code }) + _handleSecurityError (error, address, connection) { + const handled = this._authenticationProvider.handleError({ connection, code: error.code }) + + if (handled) { + error.retriable = true + } if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { this._connectionPool.apply(address, (conn) => { conn.authToken = null }) @@ -235,10 +238,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { connection.close().catch(() => undefined) } - if (error.code === 'Neo.ClientError.Security.TokenExpired' && !isStaticAuthTokenManger(this._authTokenManager)) { - error.retriable = true - } - return error } } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index ed653ebaa..4102adf78 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -116,12 +116,12 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return error } - _handleAuthorizationExpired (error, address, connection, database) { + _handleSecurityError (error, address, connection, database) { this._log.warn( `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - return super._handleAuthorizationExpired(error, address, connection, database) + return super._handleSecurityError(error, address, connection, database) } _handleWriteFailure (error, address, database) { @@ -150,7 +150,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider (error, address) => this._handleUnavailability(error, address, context.database), (error, address) => this._handleWriteFailure(error, address, context.database), (error, address, conn) => - this._handleAuthorizationExpired(error, address, conn, context.database) + this._handleSecurityError(error, address, conn, context.database) ) const routingTable = await this._freshRoutingTable({ @@ -584,7 +584,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SESSION_EXPIRED, - handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn) + handleSecurityError: (error, address, conn) => this._handleSecurityError(error, address, conn) }) const delegateConnection = !connection._sticky diff --git a/packages/bolt-connection/src/connection/connection-error-handler.js b/packages/bolt-connection/src/connection/connection-error-handler.js index 91f855c11..6ba87d482 100644 --- a/packages/bolt-connection/src/connection/connection-error-handler.js +++ b/packages/bolt-connection/src/connection/connection-error-handler.js @@ -26,25 +26,25 @@ export default class ConnectionErrorHandler { errorCode, handleUnavailability, handleWriteFailure, - handleAuthorizationExpired + handleSecurityError ) { this._errorCode = errorCode this._handleUnavailability = handleUnavailability || noOpHandler this._handleWriteFailure = handleWriteFailure || noOpHandler - this._handleAuthorizationExpired = handleAuthorizationExpired || noOpHandler + this._handleSecurityError = handleSecurityError || noOpHandler } static create ({ errorCode, handleUnavailability, handleWriteFailure, - handleAuthorizationExpired + handleSecurityError }) { return new ConnectionErrorHandler( errorCode, handleUnavailability, handleWriteFailure, - handleAuthorizationExpired + handleSecurityError ) } @@ -63,8 +63,8 @@ export default class ConnectionErrorHandler { * @return {Neo4jError} new error that should be propagated to the user. */ handleAndTransformError (error, address, connection) { - if (isAutorizationExpiredError(error)) { - return this._handleAuthorizationExpired(error, address, connection) + if (isSecurityError(error)) { + return this._handleSecurityError(error, address, connection) } if (isAvailabilityError(error)) { return this._handleUnavailability(error, address, connection) @@ -76,11 +76,10 @@ export default class ConnectionErrorHandler { } } -function isAutorizationExpiredError (error) { - return error && ( - error.code === 'Neo.ClientError.Security.AuthorizationExpired' || - error.code === 'Neo.ClientError.Security.TokenExpired' - ) +function isSecurityError (error) { + return error != null && + error.code != null && + error.code.startsWith('Neo.ClientError.Security.') } function isAvailabilityError (error) { diff --git a/packages/bolt-connection/test/connection-provider/authentication-provider.test.js b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js index 0b0a70ae2..cc74bb4e5 100644 --- a/packages/bolt-connection/test/connection-provider/authentication-provider.test.js +++ b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { expirationBasedAuthTokenManager } from 'neo4j-driver-core' +import { authTokenManagers } from 'neo4j-driver-core' import AuthenticationProvider from '../../src/connection-provider/authentication-provider' describe('AuthenticationProvider', () => { @@ -642,8 +642,15 @@ describe('AuthenticationProvider', () => { authenticationProvider } = createScenario() + const handleSecurityExceptionSpy = jest.spyOn(authenticationProvider._authTokenManager, 'handleSecurityException') + authenticationProvider.handleError({ code, connection }) + if (code.startsWith('Neo.ClientError.Security.')) { + expect(handleSecurityExceptionSpy).toBeCalledWith(connection.authToken, code) + } else { + expect(handleSecurityExceptionSpy).not.toBeCalled() + } expect(authTokenProvider).not.toHaveBeenCalled() }) @@ -785,7 +792,7 @@ describe('AuthenticationProvider', () => { }) function createAuthenticationProvider (authTokenProvider, mocks) { - const authTokenManager = expirationBasedAuthTokenManager({ tokenProvider: authTokenProvider }) + const authTokenManager = authTokenManagers.bearer({ tokenProvider: authTokenProvider }) const provider = new AuthenticationProvider({ authTokenManager, userAgent: USER_AGENT, diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index 83ff25abf..311d228ef 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -20,7 +20,7 @@ import DirectConnectionProvider from '../../src/connection-provider/connection-provider-direct' import { Pool } from '../../src/pool' import { Connection, DelegateConnection } from '../../src/connection' -import { internal, newError, ServerInfo, staticAuthTokenManager, expirationBasedAuthTokenManager } from 'neo4j-driver-core' +import { authTokenManagers, internal, newError, ServerInfo, staticAuthTokenManager } from 'neo4j-driver-core' import AuthenticationProvider from '../../src/connection-provider/authentication-provider' import { functional } from '../../src/lang' @@ -209,7 +209,7 @@ it('should call authenticationAuthProvider.handleError when TokenExpired happens it('should change error to retriable when error when TokenExpired happens and staticAuthTokenManager is not being used', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connectionProvider = newDirectConnectionProvider(address, pool, expirationBasedAuthTokenManager({ tokenProvider: () => null })) + const connectionProvider = newDirectConnectionProvider(address, pool, authTokenManagers.bearer({ tokenProvider: () => null })) const conn = await connectionProvider.acquireConnection({ accessMode: 'READ', @@ -246,6 +246,26 @@ it('should not change error to retriable when error when TokenExpired happens an expect(error.retriable).toBe(false) }) +it('should not change error to retriable when error when TokenExpired happens and authTokenManagers.basic is being used', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connectionProvider = newDirectConnectionProvider(address, pool, authTokenManagers.basic({ tokenProvider: () => null })) + + const conn = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: '' + }) + + const expectedError = newError( + 'Message', + 'Neo.ClientError.Security.TokenExpired' + ) + + const error = conn.handleAndTransformError(expectedError, address) + + expect(error.retriable).toBe(false) +}) + describe('constructor', () => { describe('newPool', () => { const server0 = ServerAddress.fromUrl('localhost:123') diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 2ec3512a1..69bdcfb4f 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -26,7 +26,7 @@ import { internal, ServerInfo, staticAuthTokenManager, - expirationBasedAuthTokenManager + authTokenManagers } from 'neo4j-driver-core' import { RoutingTable } from '../../src/rediscovery/' import { Pool } from '../../src/pool' @@ -1718,7 +1718,8 @@ describe.each([ ], pool ) - connectionProvider._authTokenManager = expirationBasedAuthTokenManager({ tokenProvider: () => null }) + + setupAuthTokenManager(connectionProvider, authTokenManagers.bearer({ tokenProvider: () => null })) const error = newError( 'Message', @@ -1756,8 +1757,51 @@ describe.each([ ) ], pool + + ) + + setupAuthTokenManager(connectionProvider, staticAuthTokenManager({ authToken: null })) + + const error = newError( + 'Message', + 'Neo.ClientError.Security.TokenExpired' + ) + + const server2Connection = await connectionProvider.acquireConnection({ + accessMode: 'WRITE', + database: null, + impersonatedUser: user + }) + + const server3Connection = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: null, + impersonatedUser: user + }) + + const error1 = server3Connection.handleAndTransformError(error, server3) + const error2 = server2Connection.handleAndTransformError(error, server2) + + expect(error1.retriable).toBe(false) + expect(error2.retriable).toBe(false) + }) + + it.each(usersDataSet)('should not change error to retriable when error when TokenExpired happens and authTokenManagers.basic is being used [user=%s]', async (user) => { + const pool = newPool() + const connectionProvider = newRoutingConnectionProvider( + [ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) - connectionProvider._authTokenManager = staticAuthTokenManager({ authToken: null }) + + setupAuthTokenManager(connectionProvider, authTokenManagers.basic({ tokenProvider: () => {} })) const error = newError( 'Message', @@ -3944,3 +3988,7 @@ class FakeDnsResolver { return Promise.resolve(this._addresses ? this._addresses : [seedRouter]) } } + +function setupAuthTokenManager (provider, authTokenManager) { + provider._authenticationProvider._authTokenManager = authTokenManager +} diff --git a/packages/bolt-connection/test/connection/connection-error-handler.test.js b/packages/bolt-connection/test/connection/connection-error-handler.test.js index ba299be38..f3cd84de0 100644 --- a/packages/bolt-connection/test/connection/connection-error-handler.test.js +++ b/packages/bolt-connection/test/connection/connection-error-handler.test.js @@ -79,35 +79,7 @@ describe('#unit ConnectionErrorHandler', () => { ]) }) - it('should handle and transform authorization expired error', () => { - const errors = [] - const addresses = [] - const transformedError = newError('Message', 'Code') - const handler = ConnectionErrorHandler.create({ - errorCode: SERVICE_UNAVAILABLE, - handleAuthorizationExpired: (error, address) => { - errors.push(error) - addresses.push(address) - return transformedError - } - }) - - const error1 = newError( - 'C', - 'Neo.ClientError.Security.AuthorizationExpired' - ) - - const errorTransformed1 = handler.handleAndTransformError( - error1, - ServerAddress.fromUrl('localhost:0') - ) - - expect(errorTransformed1).toEqual(transformedError) - - expect(addresses).toEqual([ServerAddress.fromUrl('localhost:0')]) - }) - - it('should return original erro if authorization expired handler is not informed', () => { + it('should return original error if authorization expired handler is not informed', () => { const errors = [] const addresses = [] const transformedError = newError('Message', 'Code') @@ -140,13 +112,18 @@ describe('#unit ConnectionErrorHandler', () => { expect(addresses).toEqual([]) }) - it('should handle and transform token expired error', () => { + it.each([ + 'Neo.ClientError.Security.TokenExpired', + 'Neo.ClientError.Security.AuthorizationExpired', + 'Neo.ClientError.Security.Unauthorized', + 'Neo.ClientError.Security.MadeUp' + ])('should handle and transform "%s"', (code) => { const errors = [] const addresses = [] const transformedError = newError('Message', 'Code') const handler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, - handleAuthorizationExpired: (error, address) => { + handleSecurityError: (error, address) => { errors.push(error) addresses.push(address) return transformedError @@ -155,7 +132,7 @@ describe('#unit ConnectionErrorHandler', () => { const error1 = newError( 'C', - 'Neo.ClientError.Security.TokenExpired' + code ) const errorTransformed1 = handler.handleAndTransformError( @@ -168,7 +145,12 @@ describe('#unit ConnectionErrorHandler', () => { expect(addresses).toEqual([ServerAddress.fromUrl('localhost:0')]) }) - it('should return original erro if token expired handler is not informed', () => { + it.each([ + 'Neo.ClientError.Security.TokenExpired', + 'Neo.ClientError.Security.AuthorizationExpired', + 'Neo.ClientError.Security.Unauthorized', + 'Neo.ClientError.Security.MadeUp' + ])('should return original error code equals "%s" if security error handler is not informed', (code) => { const errors = [] const addresses = [] const transformedError = newError('Message', 'Code') @@ -188,7 +170,7 @@ describe('#unit ConnectionErrorHandler', () => { const error1 = newError( 'C', - 'Neo.ClientError.Security.TokenExpired' + code ) const errorTransformed1 = handler.handleAndTransformError( diff --git a/packages/core/src/auth-token-manager.ts b/packages/core/src/auth-token-manager.ts index 77a2961cc..4d6522f1e 100644 --- a/packages/core/src/auth-token-manager.ts +++ b/packages/core/src/auth-token-manager.ts @@ -21,6 +21,8 @@ import auth from './auth' import { AuthToken } from './types' import { util } from './internal' +export type SecurityErrorCode = `Neo.ClientError.Security.${string}` + /** * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. * @interface @@ -41,12 +43,13 @@ export default class AuthTokenManager { } /** - * Called to notify a token expiration. + * Handles an error notification emitted by the server if a security error happened. * * @param {AuthToken} token The expired token. - * @return {void} + * @param {`Neo.ClientError.Security.${string}`} securityErrorCode the security error code returned by the server + * @return {boolean} whether the exception was handled by the manager, so the driver knows if it can be retried.. */ - onTokenExpired (token: AuthToken): void { + handleSecurityException (token: AuthToken, securityErrorCode: SecurityErrorCode): boolean { throw new Error('Not implemented') } } @@ -74,7 +77,7 @@ export class AuthTokenAndExpiration { * The expected expiration date of the auth token. * * This information will be used for triggering the auth token refresh - * in managers created with {@link expirationBasedAuthTokenManager}. + * in managers created with {@link authTokenManagers#bearer}. * * If this value is not defined, the {@link AuthToken} will be considered valid * until a `Neo.ClientError.Security.TokenExpired` error happens. @@ -86,22 +89,69 @@ export class AuthTokenAndExpiration { } /** - * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. - * - * **Warning**: `tokenProvider` must only ever return auth information belonging to the same identity. - * Switching identities using the `AuthTokenManager` is undefined behavior. + * Defines the object which holds the common {@link AuthTokenManager} used in the Driver + */ +class AuthTokenManagers { + /** + * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. + * + * **Warning**: `tokenProvider` must only ever return auth information belonging to the same identity. + * Switching identities using the `AuthTokenManager` is undefined behavior. + * + * @param {object} param0 - The params + * @param {function(): Promise} param0.tokenProvider - Retrieves a new valid auth token. + * Must only ever return auth information belonging to the same identity. + * @returns {AuthTokenManager} The temporal auth data manager. + * @experimental Exposed as preview feature. + */ + bearer ({ tokenProvider }: { tokenProvider: () => Promise }): AuthTokenManager { + if (typeof tokenProvider !== 'function') { + throw new TypeError(`tokenProvider should be function, but got: ${typeof tokenProvider}`) + } + return new ExpirationBasedAuthTokenManager(tokenProvider, [ + 'Neo.ClientError.Security.Unauthorized', + 'Neo.ClientError.Security.TokenExpired' + ]) + } + + /** + * Creates a {@link AuthTokenManager} for handle {@link AuthToken} and password rotation. + * + * **Warning**: `tokenProvider` must only ever return auth information belonging to the same identity. + * Switching identities using the `AuthTokenManager` is undefined behavior. + * + * @param {object} param0 - The params + * @param {function(): Promise} param0.tokenProvider - Retrieves a new valid auth token. + * Must only ever return auth information belonging to the same identity. + * @returns {AuthTokenManager} The basic auth data manager. + * @experimental Exposed as preview feature. + */ + basic ({ tokenProvider }: { tokenProvider: () => Promise }): AuthTokenManager { + if (typeof tokenProvider !== 'function') { + throw new TypeError(`tokenProvider should be function, but got: ${typeof tokenProvider}`) + } + + return new ExpirationBasedAuthTokenManager(async () => { + return { token: await tokenProvider() } + }, ['Neo.ClientError.Security.Unauthorized']) + } +} + +/** + * Holds the common {@link AuthTokenManagers} used in the Driver * - * @param {object} param0 - The params - * @param {function(): Promise} param0.tokenProvider - Retrieves a new valid auth token. - * Must only ever return auth information belonging to the same identity. - * @returns {AuthTokenManager} The temporal auth data manager. * @experimental Exposed as preview feature. */ -export function expirationBasedAuthTokenManager ({ tokenProvider }: { tokenProvider: () => Promise }): AuthTokenManager { - if (typeof tokenProvider !== 'function') { - throw new TypeError(`tokenProvider should be function, but got: ${typeof tokenProvider}`) - } - return new ExpirationBasedAuthTokenManager(tokenProvider) +const authTokenManagers: AuthTokenManagers = new AuthTokenManagers() + +Object.freeze(authTokenManagers) + +export { + authTokenManagers +} + +export type { + AuthTokenManagers } /** @@ -116,18 +166,6 @@ export function staticAuthTokenManager ({ authToken }: { authToken: AuthToken }) return new StaticAuthTokenManager(authToken) } -/** - * Checks if the manager is a StaticAuthTokenManager - * - * @private - * @experimental - * @param {AuthTokenManager} manager The auth token manager to be checked. - * @returns {boolean} Manager is StaticAuthTokenManager - */ -export function isStaticAuthTokenManger (manager: AuthTokenManager): manager is StaticAuthTokenManager { - return manager instanceof StaticAuthTokenManager -} - interface TokenRefreshObserver { onCompleted: (data: AuthTokenAndExpiration) => void onError: (error: Error) => void @@ -154,6 +192,7 @@ class TokenRefreshObservable implements TokenRefreshObserver { class ExpirationBasedAuthTokenManager implements AuthTokenManager { constructor ( private readonly _tokenProvider: () => Promise, + private readonly _handledSecurityCodes: SecurityErrorCode[], private _currentAuthData?: AuthTokenAndExpiration, private _refreshObservable?: TokenRefreshObservable) { @@ -171,10 +210,14 @@ class ExpirationBasedAuthTokenManager implements AuthTokenManager { return this._currentAuthData?.token as AuthToken } - onTokenExpired (token: AuthToken): void { - if (util.equals(token, this._currentAuthData?.token)) { - this._scheduleRefreshAuthToken() + handleSecurityException (token: AuthToken, securityErrorCode: SecurityErrorCode): boolean { + if (this._handledSecurityCodes.includes(securityErrorCode)) { + if (util.equals(token, this._currentAuthData?.token)) { + this._scheduleRefreshAuthToken() + } + return true } + return false } private _scheduleRefreshAuthToken (observer?: TokenRefreshObserver): void { @@ -221,7 +264,7 @@ class StaticAuthTokenManager implements AuthTokenManager { return this._authToken } - onTokenExpired (_: AuthToken): void { - // nothing to do here + handleSecurityException (_: AuthToken, __: SecurityErrorCode): boolean { + return false } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 34acbcc60..82f0c76cd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -87,7 +87,7 @@ import Session, { TransactionConfig } from './session' import Driver, * as driver from './driver' import auth from './auth' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager' -import AuthTokenManager, { expirationBasedAuthTokenManager, staticAuthTokenManager, isStaticAuthTokenManger, AuthTokenAndExpiration } from './auth-token-manager' +import AuthTokenManager, { authTokenManagers, AuthTokenManagers, staticAuthTokenManager, AuthTokenAndExpiration } from './auth-token-manager' import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver' import { Config } from './types' import * as types from './types' @@ -108,6 +108,7 @@ const error = { * @private */ const forExport = { + authTokenManagers, newError, Neo4jError, isRetriableError, @@ -165,7 +166,6 @@ const forExport = { json, auth, bookmarkManager, - expirationBasedAuthTokenManager, routing, resultTransformers, notificationCategory, @@ -175,6 +175,7 @@ const forExport = { } export { + authTokenManagers, newError, Neo4jError, isRetriableError, @@ -233,9 +234,7 @@ export { json, auth, bookmarkManager, - expirationBasedAuthTokenManager, staticAuthTokenManager, - isStaticAuthTokenManger, routing, resultTransformers, notificationCategory, @@ -254,6 +253,7 @@ export type { BookmarkManager, BookmarkManagerConfig, AuthTokenManager, + AuthTokenManagers, AuthTokenAndExpiration, Config, SessionConfig, diff --git a/packages/core/test/auth-token-manager.test.ts b/packages/core/test/auth-token-manager.test.ts new file mode 100644 index 000000000..9998a8dad --- /dev/null +++ b/packages/core/test/auth-token-manager.test.ts @@ -0,0 +1,370 @@ +/** + * 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 { auth } from '../src' +import AuthTokenManager, { AuthTokenAndExpiration, SecurityErrorCode, authTokenManagers } from '../src/auth-token-manager' +import { AuthToken } from '../src/types' + +describe('authTokenManagers', () => { + const SECURITY_ERROR_CODES = [ + 'Neo.ClientError.Security.TokenExpired', + 'Neo.ClientError.Security.MadeUp', + 'Neo.ClientError.Security.AuthorizationExpired', + 'Neo.ClientError.Security.Unauthorized' + ] + + describe('.basic()', () => { + const BASIC_HANDLED_ERROR_CODES = Object.freeze(['Neo.ClientError.Security.Unauthorized']) + const BASIC_NOT_HANDLED_ERROR_CODES = Object.freeze(SECURITY_ERROR_CODES.filter(code => !BASIC_HANDLED_ERROR_CODES.includes(code))) + + it.each([ + undefined, + null, + {}, + { tokenProvider: null }, + { tokenProvider: undefined }, + { tokenProvider: false }, + { tokenProvider: auth.basic('user', 'password') } + ])('should throw when instantiate with wrong parameters (params=%o)', (param) => { + // @ts-expect-error + expect(() => authTokenManagers.basic(param)).toThrowError(TypeError) + }) + + it('should create an AuthTokenManager instance', () => { + const basic: AuthTokenManager = authTokenManagers.basic({ tokenProvider: async () => auth.basic('user', 'password') }) + + expect(basic).toBeDefined() + expect(basic.getToken).toBeInstanceOf(Function) + expect(basic.handleSecurityException).toBeInstanceOf(Function) + }) + + describe('.handleSecurityException()', () => { + let basic: AuthTokenManager + let tokenProvider: jest.Mock> + + beforeEach(async () => { + tokenProvider = jest.fn(async () => auth.basic('user', 'password')) + basic = authTokenManagers.basic({ tokenProvider }) + // init auth token + await basic.getToken() + tokenProvider.mockReset() + }) + + describe.each(BASIC_HANDLED_ERROR_CODES)('when error code equals to "%s"', (code: SecurityErrorCode) => { + describe('and same auth token', () => { + const authToken = auth.basic('user', 'password') + + it('should call tokenProvider and return true', () => { + const handled = basic.handleSecurityException(authToken, code) + + expect(handled).toBe(true) + expect(tokenProvider).toHaveBeenCalled() + }) + + it('should call tokenProvider only if there is not an ongoing call', async () => { + const promise = { resolve: (_: AuthToken) => { } } + tokenProvider.mockReturnValue(new Promise((resolve) => { promise.resolve = resolve })) + + expect(basic.handleSecurityException(authToken, code)).toBe(true) + expect(tokenProvider).toHaveBeenCalled() + + await triggerEventLoop() + + expect(basic.handleSecurityException(authToken, code)).toBe(true) + expect(tokenProvider).toHaveBeenCalledTimes(1) + + promise.resolve(authToken) + await triggerEventLoop() + + expect(basic.handleSecurityException(authToken, code)).toBe(true) + expect(tokenProvider).toHaveBeenCalledTimes(2) + + promise.resolve(authToken) + }) + }) + + describe('and different auth token', () => { + const authToken = auth.basic('other_user', 'other_password') + + it('should return true and not call the provider', () => { + const handled = basic.handleSecurityException(authToken, code) + + expect(handled).toBe(true) + expect(tokenProvider).not.toHaveBeenCalled() + }) + }) + }) + + it.each(BASIC_NOT_HANDLED_ERROR_CODES)('should not handle "%s"', (code: SecurityErrorCode) => { + const handled = basic.handleSecurityException(auth.basic('user', 'password'), code) + + expect(handled).toBe(false) + expect(tokenProvider).not.toHaveBeenCalled() + }) + }) + + describe('.getToken()', () => { + let basic: AuthTokenManager + let tokenProvider: jest.Mock> + let authToken: AuthToken + + beforeEach(async () => { + authToken = auth.basic('user', 'password') + tokenProvider = jest.fn(async () => authToken) + basic = authTokenManagers.basic({ tokenProvider }) + }) + + it('should call tokenProvider once and return the provided token many times', async () => { + await expect(basic.getToken()).resolves.toBe(authToken) + + expect(tokenProvider).toHaveBeenCalledTimes(1) + + await expect(basic.getToken()).resolves.toBe(authToken) + await expect(basic.getToken()).resolves.toBe(authToken) + + expect(tokenProvider).toHaveBeenCalledTimes(1) + }) + + it.each(BASIC_HANDLED_ERROR_CODES)('should reflect the authToken refreshed by handleSecurityException(authToken, "%s")', async (code: SecurityErrorCode) => { + const newAuthToken = auth.basic('other_user', 'other_password') + await expect(basic.getToken()).resolves.toBe(authToken) + + expect(tokenProvider).toHaveBeenCalledTimes(1) + + tokenProvider.mockReturnValueOnce(Promise.resolve(newAuthToken)) + + basic.handleSecurityException(authToken, code) + expect(tokenProvider).toHaveBeenCalledTimes(2) + + await expect(basic.getToken()).resolves.toBe(newAuthToken) + await expect(basic.getToken()).resolves.toBe(newAuthToken) + + expect(tokenProvider).toHaveBeenCalledTimes(2) + }) + }) + }) + + describe('.bearer()', () => { + const BEARER_HANDLED_ERROR_CODES = Object.freeze(['Neo.ClientError.Security.Unauthorized', 'Neo.ClientError.Security.TokenExpired']) + const BEARER_NOT_HANDLED_ERROR_CODES = Object.freeze(SECURITY_ERROR_CODES.filter(code => !BEARER_HANDLED_ERROR_CODES.includes(code))) + + it.each([ + undefined, + null, + {}, + { tokenProvider: null }, + { tokenProvider: undefined }, + { tokenProvider: false }, + { tokenProvider: auth.bearer('THE BEAR') } + ])('should throw when instantiate with wrong parameters (params=%o)', (param) => { + // @ts-expect-error + expect(() => authTokenManagers.bearer(param)).toThrowError(TypeError) + }) + + it('should create an AuthTokenManager instance', () => { + const bearer: AuthTokenManager = authTokenManagers.bearer({ + tokenProvider: async () => { + return { + token: auth.bearer('bearer my_bear') + } + } + }) + + expect(bearer).toBeDefined() + expect(bearer.getToken).toBeInstanceOf(Function) + expect(bearer.handleSecurityException).toBeInstanceOf(Function) + }) + + describe('.handleSecurityException()', () => { + let bearer: AuthTokenManager + let tokenProvider: jest.Mock> + let authToken: AuthToken + + beforeEach(async () => { + authToken = auth.bearer('bearer my_bear') + tokenProvider = jest.fn(async () => ({ token: authToken })) + bearer = authTokenManagers.bearer({ tokenProvider }) + // init auth token + await bearer.getToken() + tokenProvider.mockReset() + }) + + describe.each(BEARER_HANDLED_ERROR_CODES)('when error code equals to "%s"', (code: SecurityErrorCode) => { + describe('and same auth token', () => { + it('should call tokenProvider and return true', () => { + const handled = bearer.handleSecurityException(authToken, code) + + expect(handled).toBe(true) + expect(tokenProvider).toHaveBeenCalled() + }) + + it('should call tokenProvider only if there is not an ongoing call', async () => { + const promise = { resolve: (_: AuthTokenAndExpiration) => { } } + tokenProvider.mockReturnValue(new Promise((resolve) => { promise.resolve = resolve })) + + expect(bearer.handleSecurityException(authToken, code)).toBe(true) + expect(tokenProvider).toHaveBeenCalled() + + await triggerEventLoop() + + expect(bearer.handleSecurityException(authToken, code)).toBe(true) + expect(tokenProvider).toHaveBeenCalledTimes(1) + + promise.resolve({ token: authToken }) + await triggerEventLoop() + + expect(bearer.handleSecurityException(authToken, code)).toBe(true) + expect(tokenProvider).toHaveBeenCalledTimes(2) + + promise.resolve({ token: authToken }) + }) + }) + + describe('and different auth token', () => { + const otherAuthToken = auth.bearer('bearer another_bear') + + it('should return true and not call the provider', () => { + const handled = bearer.handleSecurityException(otherAuthToken, code) + + expect(handled).toBe(true) + expect(tokenProvider).not.toHaveBeenCalled() + }) + }) + }) + + it.each(BEARER_NOT_HANDLED_ERROR_CODES)('should not handle "%s"', (code: SecurityErrorCode) => { + const handled = bearer.handleSecurityException(authToken, code) + + expect(handled).toBe(false) + expect(tokenProvider).not.toHaveBeenCalled() + }) + }) + + describe('.getToken()', () => { + describe('when token has no expiration', () => { + let bearer: AuthTokenManager + let tokenProvider: jest.Mock> + let authToken: AuthToken + + beforeEach(async () => { + authToken = auth.bearer('bearer my_bear') + tokenProvider = jest.fn(async () => ({ token: authToken })) + bearer = authTokenManagers.bearer({ tokenProvider }) + }) + + it('should call tokenProvider once and return the provided token many times', async () => { + await expect(bearer.getToken()).resolves.toBe(authToken) + + expect(tokenProvider).toHaveBeenCalledTimes(1) + + await expect(bearer.getToken()).resolves.toBe(authToken) + await expect(bearer.getToken()).resolves.toBe(authToken) + + expect(tokenProvider).toHaveBeenCalledTimes(1) + }) + + it.each(BEARER_HANDLED_ERROR_CODES)('should reflect the authToken refreshed by handleSecurityException(authToken, "%s")', async (code: SecurityErrorCode) => { + const newAuthToken = auth.bearer('bearer another_bear') + await expect(bearer.getToken()).resolves.toBe(authToken) + + expect(tokenProvider).toHaveBeenCalledTimes(1) + + tokenProvider.mockReturnValueOnce(Promise.resolve({ token: newAuthToken })) + + bearer.handleSecurityException(authToken, code) + expect(tokenProvider).toHaveBeenCalledTimes(2) + + await expect(bearer.getToken()).resolves.toBe(newAuthToken) + await expect(bearer.getToken()).resolves.toBe(newAuthToken) + + expect(tokenProvider).toHaveBeenCalledTimes(2) + }) + }) + + describe('when token has expiration', () => { + let bearer: AuthTokenManager + let tokenProvider: jest.Mock> + let authToken: AuthToken + + beforeEach(() => { + jest.useFakeTimers() + authToken = auth.bearer('bearer my_bearer') + tokenProvider = jest.fn(async () => ({ + token: authToken, + expiration: expirationIn({ milliseconds: 2000 }) + })) + bearer = authTokenManagers.bearer({ tokenProvider }) + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should call tokenProvider if the existing token is expired', async () => { + await expect(bearer.getToken()).resolves.toBe(authToken) + expect(tokenProvider).toHaveBeenCalledTimes(1) + + await expect(bearer.getToken()).resolves.toBe(authToken) + expect(tokenProvider).toHaveBeenCalledTimes(1) + + // the time passed, the token changed + jest.advanceTimersByTime(2001) + const newAuthToken = auth.bearer('bearer the_other_bear') + tokenProvider.mockReturnValueOnce(Promise.resolve({ + token: newAuthToken, + expiration: expirationIn({ milliseconds: 2000 }) + })) + + await expect(bearer.getToken()).resolves.toBe(newAuthToken) + expect(tokenProvider).toHaveBeenCalledTimes(2) + + await expect(bearer.getToken()).resolves.toBe(newAuthToken) + expect(tokenProvider).toHaveBeenCalledTimes(2) + }) + + it.each(BEARER_HANDLED_ERROR_CODES)('should reflect the authToken refreshed by handleSecurityException(authToken, "%s")', async (code: SecurityErrorCode) => { + const newAuthToken = auth.bearer('bearer another_bear') + await expect(bearer.getToken()).resolves.toBe(authToken) + + expect(tokenProvider).toHaveBeenCalledTimes(1) + + tokenProvider.mockReturnValueOnce(Promise.resolve({ + token: newAuthToken, + expiration: expirationIn({ milliseconds: 2000 }) + })) + + bearer.handleSecurityException(authToken, code) + expect(tokenProvider).toHaveBeenCalledTimes(2) + + await expect(bearer.getToken()).resolves.toBe(newAuthToken) + await expect(bearer.getToken()).resolves.toBe(newAuthToken) + + expect(tokenProvider).toHaveBeenCalledTimes(2) + }) + }) + }) + }) +}) + +async function triggerEventLoop (): Promise { + return await new Promise(resolve => setTimeout(resolve, 0)) +} + +function expirationIn ({ milliseconds }: { milliseconds: number}): Date { + return new Date(Date.now() + milliseconds) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index 30e5fa92b..c01f70412 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import { expirationBasedAuthTokenManager } from '../../core/index.ts' +import { staticAuthTokenManager } from '../../core/index.ts' import { object } from '../lang/index.js' /** @@ -25,9 +25,7 @@ import { object } from '../lang/index.js' */ export default class AuthenticationProvider { constructor ({ authTokenManager, userAgent, boltAgent }) { - this._authTokenManager = authTokenManager || expirationBasedAuthTokenManager({ - tokenProvider: () => {} - }) + this._authTokenManager = authTokenManager || staticAuthTokenManager({}) this._userAgent = userAgent this._boltAgent = boltAgent } @@ -56,12 +54,10 @@ export default class AuthenticationProvider { handleError ({ connection, code }) { if ( connection && - [ - 'Neo.ClientError.Security.Unauthorized', - 'Neo.ClientError.Security.TokenExpired' - ].includes(code) + code.startsWith('Neo.ClientError.Security.') ) { - this._authTokenManager.onTokenExpired(connection.authToken) + return this._authTokenManager.handleSecurityException(connection.authToken, code) } + return false } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 46bb5c0e6..fe7190255 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -50,8 +50,8 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { async acquireConnection ({ accessMode, database, bookmarks, auth, forceReAuth } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, - handleAuthorizationExpired: (error, address, conn) => - this._handleAuthorizationExpired(error, address, conn, database) + handleSecurityError: (error, address, conn) => + this._handleSecurityError(error, address, conn, database) }) const connection = await this._connectionPool.acquire({ auth, forceReAuth }, this._address) @@ -68,12 +68,12 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { return new DelegateConnection(connection, databaseSpecificErrorHandler) } - _handleAuthorizationExpired (error, address, connection, database) { + _handleSecurityError (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}'` ) - return super._handleAuthorizationExpired(error, address, connection) + return super._handleSecurityError(error, address, connection) } async _hasProtocolVersion (versionPredicate) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 5e4cadd21..631889fcf 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -19,7 +19,7 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection/index.js' import Pool, { PoolConfig } from '../pool/index.js' -import { error, ConnectionProvider, ServerInfo, newError, isStaticAuthTokenManger } from '../../core/index.ts' +import { error, ConnectionProvider, ServerInfo, newError } from '../../core/index.ts' import AuthenticationProvider from './authentication-provider.js' import { object } from '../lang/index.js' @@ -41,7 +41,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log - this._authTokenManager = authTokenManager this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent, boltAgent }) this._userAgent = userAgent this._boltAgent = boltAgent @@ -224,8 +223,12 @@ export default class PooledConnectionProvider extends ConnectionProvider { conn._updateCurrentObserver() } - _handleAuthorizationExpired (error, address, connection) { - this._authenticationProvider.handleError({ connection, code: error.code }) + _handleSecurityError (error, address, connection) { + const handled = this._authenticationProvider.handleError({ connection, code: error.code }) + + if (handled) { + error.retriable = true + } if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { this._connectionPool.apply(address, (conn) => { conn.authToken = null }) @@ -235,10 +238,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { connection.close().catch(() => undefined) } - if (error.code === 'Neo.ClientError.Security.TokenExpired' && !isStaticAuthTokenManger(this._authTokenManager)) { - error.retriable = true - } - return error } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 8bd671dc5..79a23b81a 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -116,12 +116,12 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return error } - _handleAuthorizationExpired (error, address, connection, database) { + _handleSecurityError (error, address, connection, database) { this._log.warn( `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - return super._handleAuthorizationExpired(error, address, connection, database) + return super._handleSecurityError(error, address, connection, database) } _handleWriteFailure (error, address, database) { @@ -150,7 +150,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider (error, address) => this._handleUnavailability(error, address, context.database), (error, address) => this._handleWriteFailure(error, address, context.database), (error, address, conn) => - this._handleAuthorizationExpired(error, address, conn, context.database) + this._handleSecurityError(error, address, conn, context.database) ) const routingTable = await this._freshRoutingTable({ @@ -584,7 +584,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SESSION_EXPIRED, - handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn) + handleSecurityError: (error, address, conn) => this._handleSecurityError(error, address, conn) }) const delegateConnection = !connection._sticky diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js index ebe305e26..6ccfb4b1e 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js @@ -26,25 +26,25 @@ export default class ConnectionErrorHandler { errorCode, handleUnavailability, handleWriteFailure, - handleAuthorizationExpired + handleSecurityError ) { this._errorCode = errorCode this._handleUnavailability = handleUnavailability || noOpHandler this._handleWriteFailure = handleWriteFailure || noOpHandler - this._handleAuthorizationExpired = handleAuthorizationExpired || noOpHandler + this._handleSecurityError = handleSecurityError || noOpHandler } static create ({ errorCode, handleUnavailability, handleWriteFailure, - handleAuthorizationExpired + handleSecurityError }) { return new ConnectionErrorHandler( errorCode, handleUnavailability, handleWriteFailure, - handleAuthorizationExpired + handleSecurityError ) } @@ -63,8 +63,8 @@ export default class ConnectionErrorHandler { * @return {Neo4jError} new error that should be propagated to the user. */ handleAndTransformError (error, address, connection) { - if (isAutorizationExpiredError(error)) { - return this._handleAuthorizationExpired(error, address, connection) + if (isSecurityError(error)) { + return this._handleSecurityError(error, address, connection) } if (isAvailabilityError(error)) { return this._handleUnavailability(error, address, connection) @@ -76,11 +76,10 @@ export default class ConnectionErrorHandler { } } -function isAutorizationExpiredError (error) { - return error && ( - error.code === 'Neo.ClientError.Security.AuthorizationExpired' || - error.code === 'Neo.ClientError.Security.TokenExpired' - ) +function isSecurityError (error) { + return error != null && + error.code != null && + error.code.startsWith('Neo.ClientError.Security.') } function isAvailabilityError (error) { diff --git a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts index c692be877..3db32ed71 100644 --- a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts +++ b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts @@ -21,6 +21,8 @@ import auth from './auth.ts' import { AuthToken } from './types.ts' import { util } from './internal/index.ts' +export type SecurityErrorCode = `Neo.ClientError.Security.${string}` + /** * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. * @interface @@ -41,12 +43,13 @@ export default class AuthTokenManager { } /** - * Called to notify a token expiration. + * Handles an error notification emitted by the server if a security error happened. * * @param {AuthToken} token The expired token. - * @return {void} + * @param {`Neo.ClientError.Security.${string}`} securityErrorCode the security error code returned by the server + * @return {boolean} whether the exception was handled by the manager, so the driver knows if it can be retried.. */ - onTokenExpired (token: AuthToken): void { + handleSecurityException (token: AuthToken, securityErrorCode: SecurityErrorCode): boolean { throw new Error('Not implemented') } } @@ -74,7 +77,7 @@ export class AuthTokenAndExpiration { * The expected expiration date of the auth token. * * This information will be used for triggering the auth token refresh - * in managers created with {@link expirationBasedAuthTokenManager}. + * in managers created with {@link authTokenManagers#bearer}. * * If this value is not defined, the {@link AuthToken} will be considered valid * until a `Neo.ClientError.Security.TokenExpired` error happens. @@ -86,22 +89,69 @@ export class AuthTokenAndExpiration { } /** - * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. - * - * **Warning**: `tokenProvider` must only ever return auth information belonging to the same identity. - * Switching identities using the `AuthTokenManager` is undefined behavior. + * Defines the object which holds the common {@link AuthTokenManager} used in the Driver + */ +class AuthTokenManagers { + /** + * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. + * + * **Warning**: `tokenProvider` must only ever return auth information belonging to the same identity. + * Switching identities using the `AuthTokenManager` is undefined behavior. + * + * @param {object} param0 - The params + * @param {function(): Promise} param0.tokenProvider - Retrieves a new valid auth token. + * Must only ever return auth information belonging to the same identity. + * @returns {AuthTokenManager} The temporal auth data manager. + * @experimental Exposed as preview feature. + */ + bearer ({ tokenProvider }: { tokenProvider: () => Promise }): AuthTokenManager { + if (typeof tokenProvider !== 'function') { + throw new TypeError(`tokenProvider should be function, but got: ${typeof tokenProvider}`) + } + return new ExpirationBasedAuthTokenManager(tokenProvider, [ + 'Neo.ClientError.Security.Unauthorized', + 'Neo.ClientError.Security.TokenExpired' + ]) + } + + /** + * Creates a {@link AuthTokenManager} for handle {@link AuthToken} and password rotation. + * + * **Warning**: `tokenProvider` must only ever return auth information belonging to the same identity. + * Switching identities using the `AuthTokenManager` is undefined behavior. + * + * @param {object} param0 - The params + * @param {function(): Promise} param0.tokenProvider - Retrieves a new valid auth token. + * Must only ever return auth information belonging to the same identity. + * @returns {AuthTokenManager} The basic auth data manager. + * @experimental Exposed as preview feature. + */ + basic ({ tokenProvider }: { tokenProvider: () => Promise }): AuthTokenManager { + if (typeof tokenProvider !== 'function') { + throw new TypeError(`tokenProvider should be function, but got: ${typeof tokenProvider}`) + } + + return new ExpirationBasedAuthTokenManager(async () => { + return { token: await tokenProvider() } + }, ['Neo.ClientError.Security.Unauthorized']) + } +} + +/** + * Holds the common {@link AuthTokenManagers} used in the Driver * - * @param {object} param0 - The params - * @param {function(): Promise} param0.tokenProvider - Retrieves a new valid auth token. - * Must only ever return auth information belonging to the same identity. - * @returns {AuthTokenManager} The temporal auth data manager. * @experimental Exposed as preview feature. */ -export function expirationBasedAuthTokenManager ({ tokenProvider }: { tokenProvider: () => Promise }): AuthTokenManager { - if (typeof tokenProvider !== 'function') { - throw new TypeError(`tokenProvider should be function, but got: ${typeof tokenProvider}`) - } - return new ExpirationBasedAuthTokenManager(tokenProvider) +const authTokenManagers: AuthTokenManagers = new AuthTokenManagers() + +Object.freeze(authTokenManagers) + +export { + authTokenManagers +} + +export type { + AuthTokenManagers } /** @@ -116,18 +166,6 @@ export function staticAuthTokenManager ({ authToken }: { authToken: AuthToken }) return new StaticAuthTokenManager(authToken) } -/** - * Checks if the manager is a StaticAuthTokenManager - * - * @private - * @experimental - * @param {AuthTokenManager} manager The auth token manager to be checked. - * @returns {boolean} Manager is StaticAuthTokenManager - */ -export function isStaticAuthTokenManger (manager: AuthTokenManager): manager is StaticAuthTokenManager { - return manager instanceof StaticAuthTokenManager -} - interface TokenRefreshObserver { onCompleted: (data: AuthTokenAndExpiration) => void onError: (error: Error) => void @@ -154,6 +192,7 @@ class TokenRefreshObservable implements TokenRefreshObserver { class ExpirationBasedAuthTokenManager implements AuthTokenManager { constructor ( private readonly _tokenProvider: () => Promise, + private readonly _handledSecurityCodes: SecurityErrorCode[], private _currentAuthData?: AuthTokenAndExpiration, private _refreshObservable?: TokenRefreshObservable) { @@ -171,10 +210,14 @@ class ExpirationBasedAuthTokenManager implements AuthTokenManager { return this._currentAuthData?.token as AuthToken } - onTokenExpired (token: AuthToken): void { - if (util.equals(token, this._currentAuthData?.token)) { - this._scheduleRefreshAuthToken() + handleSecurityException (token: AuthToken, securityErrorCode: SecurityErrorCode): boolean { + if (this._handledSecurityCodes.includes(securityErrorCode)) { + if (util.equals(token, this._currentAuthData?.token)) { + this._scheduleRefreshAuthToken() + } + return true } + return false } private _scheduleRefreshAuthToken (observer?: TokenRefreshObserver): void { @@ -221,7 +264,7 @@ class StaticAuthTokenManager implements AuthTokenManager { return this._authToken } - onTokenExpired (_: AuthToken): void { - // nothing to do here + handleSecurityException (_: AuthToken, __: SecurityErrorCode): boolean { + return false } } diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 5b5d1cd94..0242df6c3 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -87,7 +87,7 @@ import Session, { TransactionConfig } from './session.ts' import Driver, * as driver from './driver.ts' import auth from './auth.ts' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager.ts' -import AuthTokenManager, { expirationBasedAuthTokenManager, staticAuthTokenManager, isStaticAuthTokenManger, AuthTokenAndExpiration } from './auth-token-manager.ts' +import AuthTokenManager, { authTokenManagers, AuthTokenManagers, staticAuthTokenManager, AuthTokenAndExpiration } from './auth-token-manager.ts' import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver.ts' import { Config } from './types.ts' import * as types from './types.ts' @@ -108,6 +108,7 @@ const error = { * @private */ const forExport = { + authTokenManagers, newError, Neo4jError, isRetriableError, @@ -165,7 +166,6 @@ const forExport = { json, auth, bookmarkManager, - expirationBasedAuthTokenManager, routing, resultTransformers, notificationCategory, @@ -175,6 +175,7 @@ const forExport = { } export { + authTokenManagers, newError, Neo4jError, isRetriableError, @@ -233,9 +234,7 @@ export { json, auth, bookmarkManager, - expirationBasedAuthTokenManager, staticAuthTokenManager, - isStaticAuthTokenManger, routing, resultTransformers, notificationCategory, @@ -254,6 +253,7 @@ export type { BookmarkManager, BookmarkManagerConfig, AuthTokenManager, + AuthTokenManagers, AuthTokenAndExpiration, Config, SessionConfig, diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 2b919e5e3..d32a84300 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -21,6 +21,8 @@ import { logging } from './logging.ts' import { auth, + AuthTokenManagers, + authTokenManagers, BookmarkManager, bookmarkManager, BookmarkManagerConfig, @@ -63,7 +65,6 @@ import { NotificationFilterDisabledCategory, notificationFilterDisabledCategory, AuthTokenManager, - expirationBasedAuthTokenManager, AuthTokenAndExpiration, staticAuthTokenManager, NotificationFilterMinimumSeverityLevel, @@ -126,11 +127,11 @@ function isAuthTokenManager (value: unknown): value is AuthTokenManager { if (typeof value === 'object' && value != null && 'getToken' in value && - 'onTokenExpired' in value) { + 'handleSecurityException' in value) { const manager = value as AuthTokenManager return typeof manager.getToken === 'function' && - typeof manager.onTokenExpired === 'function' + typeof manager.handleSecurityException === 'function' } return false @@ -362,6 +363,7 @@ const graph = { * @private */ const forExport = { + authTokenManagers, driver, hasReachableServer, int, @@ -424,11 +426,11 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel, - expirationBasedAuthTokenManager + notificationFilterMinimumSeverityLevel } export { + authTokenManagers, driver, hasReachableServer, int, @@ -491,13 +493,13 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel, - expirationBasedAuthTokenManager + notificationFilterMinimumSeverityLevel } export type { QueryResult, AuthToken, AuthTokenManager, + AuthTokenManagers, AuthTokenAndExpiration, Config, EncryptionLevel, diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index 7007e1cab..994491996 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -21,6 +21,8 @@ import { logging } from './logging' import { auth, + AuthTokenManagers, + authTokenManagers, BookmarkManager, bookmarkManager, BookmarkManagerConfig, @@ -63,7 +65,6 @@ import { NotificationFilterDisabledCategory, notificationFilterDisabledCategory, AuthTokenManager, - expirationBasedAuthTokenManager, AuthTokenAndExpiration, staticAuthTokenManager, NotificationFilterMinimumSeverityLevel, @@ -125,11 +126,11 @@ function isAuthTokenManager (value: unknown): value is AuthTokenManager { if (typeof value === 'object' && value != null && 'getToken' in value && - 'onTokenExpired' in value) { + 'handleSecurityException' in value) { const manager = value as AuthTokenManager return typeof manager.getToken === 'function' && - typeof manager.onTokenExpired === 'function' + typeof manager.handleSecurityException === 'function' } return false @@ -361,6 +362,7 @@ const graph = { * @private */ const forExport = { + authTokenManagers, driver, hasReachableServer, int, @@ -423,11 +425,11 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel, - expirationBasedAuthTokenManager + notificationFilterMinimumSeverityLevel } export { + authTokenManagers, driver, hasReachableServer, int, @@ -490,13 +492,13 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel, - expirationBasedAuthTokenManager + notificationFilterMinimumSeverityLevel } export type { QueryResult, AuthToken, AuthTokenManager, + AuthTokenManagers, AuthTokenAndExpiration, Config, EncryptionLevel, diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 3dfce7777..26aa5fe71 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -20,6 +20,7 @@ import { Driver, READ, WRITE } from './driver' import VERSION from './version' import { + authTokenManagers, Neo4jError, isRetryableError, error, @@ -74,7 +75,6 @@ import { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - expirationBasedAuthTokenManager, staticAuthTokenManager } from 'neo4j-driver-core' import { @@ -99,9 +99,9 @@ function isAuthTokenManager (value) { return typeof value === 'object' && value != null && 'getToken' in value && - 'onTokenExpired' in value && + 'handleSecurityException' in value && typeof value.getToken === 'function' && - typeof value.onTokenExpired === 'function' + typeof value.handleSecurityException === 'function' } function createAuthManager (authTokenOrManager) { @@ -332,6 +332,7 @@ const graph = { * @private */ const forExport = { + authTokenManagers, driver, hasReachableServer, int, @@ -395,11 +396,11 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel, - expirationBasedAuthTokenManager + notificationFilterMinimumSeverityLevel } export { + authTokenManagers, driver, hasReachableServer, int, @@ -463,7 +464,6 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel, - expirationBasedAuthTokenManager + notificationFilterMinimumSeverityLevel } export default forExport diff --git a/packages/neo4j-driver/test/types/index.test.ts b/packages/neo4j-driver/test/types/index.test.ts index eedcb478a..163c2e621 100644 --- a/packages/neo4j-driver/test/types/index.test.ts +++ b/packages/neo4j-driver/test/types/index.test.ts @@ -45,7 +45,7 @@ import { NotificationFilterMinimumSeverityLevel, NotificationFilterDisabledCategory, notificationFilterDisabledCategory, - expirationBasedAuthTokenManager + authTokenManagers } from '../../types/index' import Driver from '../../types/driver' @@ -88,14 +88,20 @@ const driver4: Driver = driver( } ) -const driver5: Driver = driver('bolt://localhost:7687', expirationBasedAuthTokenManager({ +const driver5: Driver = driver('bolt://localhost:7687', authTokenManagers.bearer({ tokenProvider: async () => { return { - token: auth.basic('neo4j', 'password') + token: auth.bearer('bearer token') } } })) +const driver6: Driver = driver('bolt://localhost:7687', authTokenManagers.basic({ + tokenProvider: async () => { + return auth.basic('neo4j', 'password') + } +})) + const readMode1: string = session.READ const writeMode1: string = session.WRITE diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index e0f1489af..490ba1cfb 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -18,6 +18,8 @@ */ import { + authTokenManagers, + AuthTokenManagers, Neo4jError, isRetriableError, error, @@ -87,7 +89,6 @@ import { notificationFilterMinimumSeverityLevel, AuthTokenManager, AuthTokenAndExpiration, - expirationBasedAuthTokenManager, types as coreTypes } from 'neo4j-driver-core' import { @@ -211,6 +212,7 @@ declare const graph: { */ declare const forExport: { + authTokenManagers: typeof authTokenManagers driver: typeof driver hasReachableServer: typeof hasReachableServer int: typeof int @@ -282,11 +284,11 @@ declare const forExport: { notificationSeverityLevel: typeof notificationSeverityLevel notificationFilterDisabledCategory: typeof notificationFilterDisabledCategory notificationFilterMinimumSeverityLevel: typeof notificationFilterMinimumSeverityLevel - expirationBasedAuthTokenManager: typeof expirationBasedAuthTokenManager logging: typeof logging } export { + authTokenManagers, driver, hasReachableServer, int, @@ -358,11 +360,11 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - expirationBasedAuthTokenManager, logging } export type { + AuthTokenManagers, BookmarkManager, BookmarkManagerConfig, SessionConfig, diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index 3fb58bc76..511c75d6c 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -37,6 +37,8 @@ function newWire(context: Context, reply: Reply): Wire { msg: e.message, // @ts-ignore Code Neo4jError does have code code: e.code, + // @ts-ignore Code Neo4jError does retryable + retryable: e.retriable, }, }); } diff --git a/packages/testkit-backend/src/context.js b/packages/testkit-backend/src/context.js index f38520c76..4cdc99d74 100644 --- a/packages/testkit-backend/src/context.js +++ b/packages/testkit-backend/src/context.js @@ -15,7 +15,8 @@ export default class Context { this._authTokenManagers = {} this._authTokenManagerGetAuthRequests = {} this._authTokenManagerOnAuthExpiredRequests = {} - this._expirationBasedAuthTokenProviderRequests = {} + this._bearerAuthTokenProviderRequests = {} + this._basicAuthTokenProviderRequests = {} this._binder = binder this._environmentLogLevel = environmentLogLevel } @@ -201,16 +202,28 @@ export default class Context { delete this._authTokenManagerOnAuthExpiredRequests[id] } - addExpirationBasedAuthTokenProviderRequest (resolve, reject) { - return this._add(this._expirationBasedAuthTokenProviderRequests, { resolve, reject }) + addBearerAuthTokenProviderRequest (resolve, reject) { + return this._add(this._bearerAuthTokenProviderRequests, { resolve, reject }) } - getExpirationBasedAuthTokenProviderRequest (id) { - return this._expirationBasedAuthTokenProviderRequests[id] + getBearerAuthTokenProviderRequest (id) { + return this._bearerAuthTokenProviderRequests[id] } - removeExpirationBasedAuthTokenProviderRequest (id) { - delete this._expirationBasedAuthTokenProviderRequests[id] + removeBearerAuthTokenProviderRequest (id) { + delete this._bearerAuthTokenProviderRequests[id] + } + + addBasicAuthTokenProviderRequest (resolve, reject) { + return this._add(this._basicAuthTokenProviderRequests, { resolve, reject }) + } + + getBasicAuthTokenProviderRequest (id) { + return this._basicAuthTokenProviderRequests[id] + } + + removeBasicAuthTokenProviderRequest (id) { + delete this._basicAuthTokenProviderRequests[id] } _add (map, object) { diff --git a/packages/testkit-backend/src/controller/local.js b/packages/testkit-backend/src/controller/local.js index 9513eab95..255a47546 100644 --- a/packages/testkit-backend/src/controller/local.js +++ b/packages/testkit-backend/src/controller/local.js @@ -70,7 +70,8 @@ export default class LocalController extends Controller { this._writeResponse(contextId, newResponse('DriverError', { id, msg: e.message, - code: e.code + code: e.code, + retryable: e.retriable })) } return diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index a1505376c..81bc24fef 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -6,6 +6,7 @@ const features = [ 'Feature:Auth:Bearer', 'Feature:Auth:Managed', 'Feature:API:BookmarkManager', + 'Feature:API:RetryableExceptions', 'Feature:API:Session:AuthConfig', 'Feature:API:SSLConfig', 'Feature:API:SSLSchemes', @@ -27,6 +28,7 @@ const features = [ 'Feature:API:Driver.ExecuteQuery', 'Feature:API:Driver:NotificationsConfig', 'Feature:API:Driver:GetServerInfo', + 'Feature:API:Driver.SupportsSessionAuth', 'Feature:API:Driver.VerifyAuthentication', 'Feature:API:Driver.VerifyConnectivity', 'Feature:API:Session:NotificationsConfig', diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index 65a62b6af..c093932be 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -28,9 +28,11 @@ export { NewAuthTokenManager, AuthTokenManagerClose, AuthTokenManagerGetAuthCompleted, - AuthTokenManagerOnAuthExpiredCompleted, - NewExpirationBasedAuthTokenManager, - ExpirationBasedAuthTokenProviderCompleted, + AuthTokenManagerHandleSecurityExceptionCompleted, + NewBearerAuthTokenManager, + BearerAuthTokenProviderCompleted, + NewBasicAuthTokenManager, + BasicAuthTokenProviderCompleted, FakeTimeInstall, FakeTimeTick, FakeTimeUninstall diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 471aae150..47b17b876 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -528,9 +528,17 @@ export function NewAuthTokenManager (_, context, _data, wire) { const id = context.addAuthTokenManagerGetAuthRequest(resolve, reject) wire.writeResponse(responses.AuthTokenManagerGetAuthRequest({ id, authTokenManagerId })) }), - onTokenExpired: (auth) => { + handleSecurityException: (auth, code) => { + // The handle security exception should wait for the response + // to return the error as handle. + // However, testkit doesn't enable sync communication and + // the `handleSecurityException` must be sync since the + // part of the code which get called depends on it be sync. + // Send the message and return true is a naive implementation + // for making some of the tests valid. const id = context.addAuthTokenManagerOnAuthExpiredRequest() - wire.writeResponse(responses.AuthTokenManagerOnAuthExpiredRequest({ id, authTokenManagerId, auth })) + wire.writeResponse(responses.AuthTokenManagerHandleSecurityExceptionRequest({ id, authTokenManagerId, auth, code })) + return true } } }) @@ -549,32 +557,52 @@ export function AuthTokenManagerGetAuthCompleted (_, context, { requestId, auth context.removeAuthTokenManagerGetAuthRequest(requestId) } -export function AuthTokenManagerOnAuthExpiredCompleted (_, context, { requestId }) { +export function AuthTokenManagerHandleSecurityExceptionCompleted (_, context, { requestId }) { + // See NewAuthTokenManager context.removeAuthTokenManagerOnAuthExpiredRequest(requestId) } -export function NewExpirationBasedAuthTokenManager ({ neo4j }, context, _, wire) { - const id = context.addAuthTokenManager((expirationBasedAuthTokenManagerId) => { - return neo4j.expirationBasedAuthTokenManager({ +export function NewBearerAuthTokenManager ({ neo4j }, context, _, wire) { + const id = context.addAuthTokenManager((bearerAuthTokenManagerId) => { + return neo4j.authTokenManagers.bearer({ tokenProvider: () => new Promise((resolve, reject) => { - const id = context.addExpirationBasedAuthTokenProviderRequest(resolve, reject) - wire.writeResponse(responses.ExpirationBasedAuthTokenProviderRequest({ id, expirationBasedAuthTokenManagerId })) + const id = context.addBearerAuthTokenProviderRequest(resolve, reject) + wire.writeResponse(responses.BearerAuthTokenProviderRequest({ id, bearerAuthTokenManagerId })) }) }) }) - wire.writeResponse(responses.ExpirationBasedAuthTokenManager({ id })) + wire.writeResponse(responses.BearerAuthTokenManager({ id })) } -export function ExpirationBasedAuthTokenProviderCompleted (_, context, { requestId, auth }) { - const request = context.getExpirationBasedAuthTokenProviderRequest(requestId) +export function BearerAuthTokenProviderCompleted (_, context, { requestId, auth }) { + const request = context.getBearerAuthTokenProviderRequest(requestId) request.resolve({ expiration: auth.data.expiresInMs != null ? new Date(new Date().getTime() + auth.data.expiresInMs) : undefined, token: context.binder.parseAuthToken(auth.data.auth.data) }) - context.removeExpirationBasedAuthTokenProviderRequest(requestId) + context.removeBearerAuthTokenProviderRequest(requestId) +} + +export function NewBasicAuthTokenManager ({ neo4j }, context, _, wire) { + const id = context.addAuthTokenManager((basicAuthTokenManagerId) => { + return neo4j.authTokenManagers.basic({ + tokenProvider: () => new Promise((resolve, reject) => { + const id = context.addBasicAuthTokenProviderRequest(resolve, reject) + wire.writeResponse(responses.BasicAuthTokenProviderRequest({ id, basicAuthTokenManagerId })) + }) + }) + }) + + wire.writeResponse(responses.BasicAuthTokenManager({ id })) +} + +export function BasicAuthTokenProviderCompleted (_, context, { requestId, auth }) { + const request = context.getBasicAuthTokenProviderRequest(requestId) + request.resolve(context.binder.parseAuthToken(auth.data)) + context.removeBasicAuthTokenProviderRequest(requestId) } export function GetRoutingTable (_, context, { driverId, database }, wire) { diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index 9d223f0e2..3d3405fa3 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -115,16 +115,24 @@ export function AuthorizationToken (data) { return response('AuthorizationToken', data) } -export function AuthTokenManagerOnAuthExpiredRequest ({ id, authTokenManagerId, auth }) { - return response('AuthTokenManagerOnAuthExpiredRequest', { id, authTokenManagerId, auth: AuthorizationToken(auth) }) +export function AuthTokenManagerHandleSecurityExceptionRequest ({ id, authTokenManagerId, auth, code }) { + return response('AuthTokenManagerHandleSecurityExceptionRequest', { id, authTokenManagerId, auth: AuthorizationToken(auth), error_code: code }) } -export function ExpirationBasedAuthTokenManager ({ id }) { - return response('ExpirationBasedAuthTokenManager', { id }) +export function BearerAuthTokenManager ({ id }) { + return response('BearerAuthTokenManager', { id }) } -export function ExpirationBasedAuthTokenProviderRequest ({ id, expirationBasedAuthTokenManagerId }) { - return response('ExpirationBasedAuthTokenProviderRequest', { id, expirationBasedAuthTokenManagerId }) +export function BearerAuthTokenProviderRequest ({ id, bearerAuthTokenManagerId }) { + return response('BearerAuthTokenProviderRequest', { id, bearerAuthTokenManagerId }) +} + +export function BasicAuthTokenManager ({ id }) { + return response('BasicAuthTokenManager', { id }) +} + +export function BasicAuthTokenProviderRequest ({ id, basicAuthTokenManagerId }) { + return response('BasicAuthTokenProviderRequest', { id, basicAuthTokenManagerId }) } export function DriverIsAuthenticated ({ id, authenticated }) { diff --git a/packages/testkit-backend/src/skipped-tests/common.js b/packages/testkit-backend/src/skipped-tests/common.js index 4efb70b43..6f767f0c7 100644 --- a/packages/testkit-backend/src/skipped-tests/common.js +++ b/packages/testkit-backend/src/skipped-tests/common.js @@ -1,4 +1,4 @@ -import skip, { ifEquals, ifEndsWith, endsWith, ifStartsWith, startsWith, not } from './skip.js' +import skip, { ifEquals, ifEndsWith, endsWith, ifStartsWith, startsWith, not, or } from './skip.js' const skippedTests = [ skip( @@ -184,6 +184,20 @@ const skippedTests = [ ifEquals( 'stub.driver_parameters.test_connection_acquisition_timeout_ms.TestConnectionAcquisitionTimeoutMs.test_does_not_encompass_router_handshake' ) + ), + skip( + 'Backend does not support async AuthTokenManager.handleSecurityException', + ifStartsWith('stub.authorization.test_auth_token_manager.TestAuthTokenManager') + .and( + or( + endsWith('test_error_on_pull_using_session_run'), + endsWith('test_error_on_begin_using_tx_run'), + endsWith('test_error_on_run_using_tx_run'), + endsWith('test_error_on_pull_using_tx_run'), + endsWith('test_error_on_commit_using_tx_run'), + endsWith('test_error_on_rollback_using_tx_run') + ) + ) ) ]