The following post is more a less a dump of code. Since version 5.8 the official Neo4j drivers supports expiration of authentication tokens (see Introduce AuthToken rotation and session auth support. The PR states: “The feature might also be referred to as a refresh or re-auth. In practice, it allows replacing the current token with a new token during the driver’s lifetime. The main objective of this feature is to allow token rotation for the same identity. As such, it is not intended for a change of identity.”
It’s up to you, if you are gonna change identities with it or not, but in theory you can. Personal opinion: It’s actually one of the main reasons I would integrate it into any backend application that is remotely doing anything multitenancy with it. Why? The impersonation feature of the driver that also exists does not work with credentials checking by default, so go figure: The one thing you want to have in a backend application (one driver instance transparently checking privileges for different tenants authenticated via a token (be it bearer or username/password), either is discouraged but works or does not work.
Normally, I would suggest using a org.springframework.boot.autoconfigure.neo4j.ConfigBuilderCustomizer
for changing anything related to the driver’s config, as it would spare me duplicating all the crap below (as described in my post Tailor-Made Neo4j Connectivity With Spring Boot 2.4+), but sadly, the org.neo4j.driver.AuthTokenManager
is not configurable via the config. I therefor have opened a pull request over at Spring Boot to allow the detection of an AuthTokenManager
bean which hopefully will make it into Spring Boot, rendering the stuff below unnecessary (See Spring Boot PR #36650). For now, I suggest duplicating a couple of pieces of from Spring Boot which turns environment properties into configuration, so that you can still completely rely on the standard properties. The relevant piece in the config is the driver
method that – for now – is required to add an AuthTokenManager
option to the driver. You are completely free to create one using the factory methods the driver provides or create a custom implementation of the interface. Some ideas are mentioned in the inline comments.
import java.io.File; import java.net.URI; import java.time.Duration; import java.time.ZonedDateTime; import java.util.Locale; import java.util.concurrent.TimeUnit; import org.neo4j.driver.AuthTokenManagers; import org.neo4j.driver.Config; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.springframework.boot.autoconfigure.neo4j.Neo4jConnectionDetails; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) public class Neo4jCustomAuthConfig { /** * * @param connectionDetails I'm using the Spring Boot 3.1 abstraction over all service connection details here, * so that the cool new container integration I described last week in * <a href="https://info.michael-simons.eu/2023/07/27/the-best-way-to-use-testcontainers-from-your-spring-boot-tests/">The best way to use Testcontainers from your Spring Boot Tests</a> still applies * If you are not on Spring Boot 3.1, this class is not available. Remove that argument and * just use {@link Neo4jProperties#getUri()} then. * @param neo4jProperties Injected so that pool and other connection settings are still configured from the default propertes / environment * @return The driver to be used in the application and further down the stack in Spring Data Neo4j */ @Bean Driver driver(Neo4jConnectionDetails connectionDetails, Neo4jProperties neo4jProperties) { // Right now, the factory for AuthTokenManagers only supports expiration based tokens. // This is mostly useful for anything token related. You could hook this into Spring Security // for example and pass on any JWT token. // Another option is using the username and password like here and grab an additional expiration date, i.e. from // the config or the environment. When the expiration date is reached, the supplier passed to the factory // method will be asked for a new token. This can be a new token or a new username and password configuration. // Take note that there is no way to actively trigger an expiration. // This would require changes in the Neo4j-Java-Driver: // <a href="https://github.com/neo4j/neo4j-java-driver/issues/new">Open an issue</a>. var authManager = AuthTokenManagers.expirationBased( // Here I'm just using the token from the connection. This must be ofc something else for anything that should make sense () -> connectionDetails.getAuthToken() .expiringAt(ZonedDateTime.now().plusHours(1).toInstant().toEpochMilli()) ); // You can totally run your own AuthManager, too /* authManager = new AuthTokenManager() { @Override public CompletionStage<AuthToken> getToken() { return CompletableFuture.completedFuture(connectionDetails.getAuthToken()); } @Override public void onExpired(AuthToken authToken) { // React accordingly } } */ var uri = connectionDetails.getUri(); // or for older boot versions neo4jProperties.getUri() var config = doAllTheStuffSpringBootCouldDoIfAuthManagerWasConfigurableViaConfig(uri, neo4jProperties); return GraphDatabase.driver(uri, authManager, config); } // Everything below is a verbatim copy from spring boot for the most relevant pieces // that can be configured via properties. // As of know, pick what you need or add what's missing. Config doAllTheStuffSpringBootCouldDoIfAuthManagerWasConfigurableViaConfig(URI uri, Neo4jProperties neo4jProperties) { var builder = Config.builder(); var scheme = uri.getScheme().toLowerCase(Locale.ROOT); if (scheme.equals("bolt") || scheme.equals("neo4j")) { var securityProperties = neo4jProperties.getSecurity(); if (securityProperties.isEncrypted()) { builder.withEncryption(); } else { builder.withoutEncryption(); } builder.withTrustStrategy(mapTrustStrategy(securityProperties)); if (securityProperties.isEncrypted()) { builder.withEncryption(); } else { builder.withoutEncryption(); } } builder.withConnectionTimeout(neo4jProperties.getConnectionTimeout().toMillis(), TimeUnit.MILLISECONDS); builder.withMaxTransactionRetryTime(neo4jProperties.getMaxTransactionRetryTime().toMillis(), TimeUnit.MILLISECONDS); var pool = neo4jProperties.getPool(); if (pool.isLogLeakedSessions()) { builder.withLeakedSessionsLogging(); } builder.withMaxConnectionPoolSize(pool.getMaxConnectionPoolSize()); Duration idleTimeBeforeConnectionTest = pool.getIdleTimeBeforeConnectionTest(); if (idleTimeBeforeConnectionTest != null) { builder.withConnectionLivenessCheckTimeout(idleTimeBeforeConnectionTest.toMillis(), TimeUnit.MILLISECONDS); } builder.withMaxConnectionLifetime(pool.getMaxConnectionLifetime().toMillis(), TimeUnit.MILLISECONDS); builder.withConnectionAcquisitionTimeout(pool.getConnectionAcquisitionTimeout().toMillis(), TimeUnit.MILLISECONDS); if (pool.isMetricsEnabled()) { builder.withDriverMetrics(); } else { builder.withoutDriverMetrics(); } return builder.build(); } private Config.TrustStrategy mapTrustStrategy(Neo4jProperties.Security securityProperties) { String propertyName = "spring.neo4j.security.trust-strategy"; Neo4jProperties.Security.TrustStrategy strategy = securityProperties.getTrustStrategy(); Config.TrustStrategy trustStrategy = createTrustStrategy(securityProperties, propertyName, strategy); if (securityProperties.isHostnameVerificationEnabled()) { trustStrategy.withHostnameVerification(); } else { trustStrategy.withoutHostnameVerification(); } return trustStrategy; } private Config.TrustStrategy createTrustStrategy(Neo4jProperties.Security securityProperties, String propertyName, Neo4jProperties.Security.TrustStrategy strategy) { switch (strategy) { case TRUST_ALL_CERTIFICATES: return Config.TrustStrategy.trustAllCertificates(); case TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: return Config.TrustStrategy.trustSystemCertificates(); case TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: File certFile = securityProperties.getCertFile(); if (certFile == null || !certFile.isFile()) { throw new InvalidConfigurationPropertyValueException(propertyName, strategy.name(), "Configured trust strategy requires a certificate file."); } return Config.TrustStrategy.trustCustomCertificateSignedBy(certFile); default: throw new InvalidConfigurationPropertyValueException(propertyName, strategy.name(), "Unknown strategy."); } } } |
Happy coding.
No comments yet
Post a Comment