Skip to content

[Feature Request] No ability to refresh authentication token #993

New issue

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

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

Already on GitHub? Sign in to your account

Closed
bmax opened this issue Sep 10, 2022 · 6 comments
Closed

[Feature Request] No ability to refresh authentication token #993

bmax opened this issue Sep 10, 2022 · 6 comments

Comments

@bmax
Copy link

bmax commented Sep 10, 2022

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

@bigmontz bigmontz changed the title [Bug] No ability to refresh authentication token No ability to refresh authentication token Sep 12, 2022
@bmax bmax changed the title No ability to refresh authentication token [Feature Request] No ability to refresh authentication token Sep 12, 2022
@asarkar
Copy link

asarkar commented Sep 14, 2022

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.

@germangamboa95
Copy link

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?

@bigmontz
Copy link
Contributor

bigmontz commented Oct 3, 2022

@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.

@cptflammin
Copy link

@bigmontz any update on this ? :)

@bigmontz
Copy link
Contributor

bigmontz commented Mar 8, 2023

@cptflammin, we already started the development.

bigmontz added a commit that referenced this issue Apr 5, 2023
**⚠️ This API is released as preview.**

This changes introduce two ways of changing the connection credentials in a driver instance, each of them solving a different use case. 

### Token Expiration / Change Credentials for the whole driver instance

This use case is related to the issue #993 in the repository. For solving this, the driver is now able to receive a `AuthTokenManager` in the driver creation. This interface enables the user code provide new auth tokens to the driver and be notified by token expiration failures. 

For simplifying the usage, the driver also provides a default implementation of `AuthTokenManager` which can be created with `neo4j.temporalAuthDataManager` and receives a function for renewing the auth token as parameters. 

Example:

```typescript
import neo4j, { AuthToken } from 'neo4j-driver'

/**
 * Method called whenever the driver needs to refresh the token.
 *
 * The refresh will happen if the driver is notified by the server
 * about a token expiration or if the `Date.now() > tokenData.expiry`
 *
 * Important, the driver will block all the connections creation until
 * this function resolves the new auth token.
 */
async function fetchAuthTokenFromMyProvider () {
   const bearer: string = await myProvider.getBearerToken()
   const token: AuthToken = neo4j.auth.bearer(bearer)
   const expiration: Date = myProvider.getExpiryDate()  
   return {
      token,
      // if expiration is not provided, 
      // the driver will only fetch a new token when a failure happens
      expiration 
   }
}

const driver = neo4j.driver(
    'neo4j://localhost:7687', 
    neo4j.expirationBasedAuthTokenManager({ 
        getAuthData: fetchAuthTokenFromMyProvider 
    })
)
```

### User Switching

In this scenario, different credentials can be configured in a session providing a way for change the user context for the session. For using this feature, it needed to check if your server supports session auth by calling `driver.supportsSessionAuth()`.

Example:

```typescript
import neo4j from 'neo4j-driver'


const driver =  neo4j.driver(
    'neo4j://localhost:7687', 
    neo4j.auth.basic('neo4j', 'password')
)


const sessionWithUserB = driver.session({
  database: 'neo4j',
  auth: neo4j.auth.basic('userB', 'userBpassword')
})


try {
  // run some queries as userB
  const result = await sessionWithUserB.executeRead(tx => tx.run('RETURN 1'))
} finally {
  // close the session as usual
  await sessionWithUserB.close()
}
```

**⚠️ This API is released as preview.**
@bigmontz
Copy link
Contributor

Problem solved by #1050.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants