neo4j/neo4j-javascript-driver

[Feature Request] No ability to refresh authentication token

bmax opened this issue ยท 6 comments

bmax commented

I am unsure whether to call this a bug or a feature request. We're using opencypher with AWS Neptune and want to use the neo4j driver with the bolt protocol to allow us in the future to make an easier switch to neo4j enterprise. We are generating an AWS V4 Signature and passing it in as a basic auth to the driver.

Unfortunately, we can't figure out a way to gracefully handle the signature expiring (whether it's 5m or 24h). I imagine a lot of things would be involved in handling this gracefully (lost queries, reconnecting, transactions, etc), we would love for the driver to be able to handle the regeneration of the signature for us.

Thanks in advance!

  • Neo4j version: AWS Neptune OpenCypher
  • Neo4j Mode: N/A
  • Driver version: JS Driver 4.4.7
  • Operating system: Linux

Steps to reproduce

Here is the code that we've used to generate the neo4j driver w/ an AWS V4 Signature:

import neo4j, { Driver } from 'neo4j-driver'
import { HttpRequest } from '@aws-sdk/protocol-http'
import { defaultProvider } from '@aws-sdk/credential-provider-node'
import { SignatureV4 } from '@aws-sdk/signature-v4'
const { Sha256 } = require('@aws-crypto/sha256-js')
import { Credentials, LogLevel } from '@aws-sdk/types'
import { STS } from '@aws-sdk/client-sts'

const region = 'us-east-1'
const serviceName = 'neptune-db'
const host = process.env.AWS_NEPTUNE_URL
const port = 8182
const protocol = 'bolt+s'
const hostPort = host + ':' + port
const url = protocol + '://' + hostPort

const log_level = process.env.LOG as Exclude<LogLevel, 'all' | 'log' | 'off'>

async function assume(params): Promise<Credentials> {
  const sts = new STS({ region })
  const result = await sts.assumeRoleWithWebIdentity(params)
  if (!result.Credentials) {
    throw new Error('unable to assume credentials - empty credential object')
  }
  return {
    accessKeyId: String(result.Credentials.AccessKeyId),
    secretAccessKey: String(result.Credentials.SecretAccessKey),
    sessionToken: result.Credentials.SessionToken,
  }
}

async function signedHeader() {
  const req = new HttpRequest({
    method: 'GET',
    protocol: protocol,
    hostname: host,
    port: port,
    headers: {
      Host: hostPort,
    },
  })

  const signer = new SignatureV4({
    credentials: defaultProvider({ roleAssumerWithWebIdentity: assume }),
    region: region,
    service: serviceName,
    sha256: Sha256,
  })

  return signer
    .sign(req, { unsignableHeaders: new Set(['x-amz-content-sha256']) })
    .then(signedRequest => {
      const authInfo = {
        Authorization: signedRequest.headers['authorization'],
        HttpMethod: signedRequest.method,
        'X-Amz-Date': signedRequest.headers['x-amz-date'],
        Host: signedRequest.headers['Host'],
        'X-Amz-Security-Token': signedRequest.headers['x-amz-security-token'],
      }
      return JSON.stringify(authInfo)
    })
}

export async function createDriver() {
  const credentials = await signedHeader()
  let authToken = {
    scheme: 'basic',
    realm: 'realm',
    principal: 'username',
    credentials: credentials,
  }
  return neo4j.driver(url, authToken, {
    maxConnectionPoolSize: 10,
    disableLosslessIntegers: true,
    logging: {
      level: log_level,
      logger: (level, message) => console.log(message)
    },
  })
}

Expected behavior

Driver should be able to know when a token expires and have stored functionality to regenerate specific token.

Actual behavior

Driver fails to authenticate and all subsequent requests fail

I believe the problem lies in the fact that the authToken is a constant object, and not something that is reevaluated. In Java, the authToken comes from InternalAuthToken.toMap() method, so, InternalAuthToken can be subclassed to generate a fresh auth token every time toMap is called. AFAICT, this isn't possible using the JS API.

We could achieve this by allowing an async function to be passed in as the auth token.
Changes that would need to be made.

  // neo4j-driver/src/index.js 
  
  if (authToken.constructor.name !== "AsyncFunction") {
    // Sanitize authority token. Nicer error from server when a scheme is set.
    authToken = authToken || {};
    authToken.scheme = authToken.scheme || "none";
  }

  // Use default user agent or user agent specified by user.
  config.userAgent = config.userAgent || USER_AGENT;
  const address = ServerAddress.fromUrl(parsedUrl.hostAndPort);
// bolt-connection/src/connection/connection-channel.js 

 _initialize (userAgent, authToken) {
    const self = this

    return new Promise(async (resolve, reject) => {
      let token = authToken

      if (authToken.constructor.name === 'AsyncFunction') {
        token = await authToken()
      }

      this._protocol.initialize({
        userAgent,
        token,
        onError: (err) => reject(err),
// core/src/auth.ts


const auth = {
  ..., 
  awsv4: async (cb: () => Promise<any>) => {
    return await cb()
  }
}
 

With that we can pass in this as the auth token and fix the issue

   neo4j.driver(url, async () => {
    const credentials = await signedHeader()
    let authToken = {
      scheme: 'basic',
      realm: 'realm',
      principal: 'username',
      credentials: credentials,
    }
    return authToken
  })

@bigmontz is this a change that would be allowed to merge?

@germangamboa95, we are evaluating the solution for the feature request internally. We are drafting some designs, whenever I have some concrete I will post here.

@bigmontz any update on this ? :)

@cptflammin, we already started the development.

Problem solved by #1050.