neo4j / graphql

A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
https://neo4j.com/docs/graphql-manual/current/
Apache License 2.0
498 stars 147 forks source link

@neo4j/graphql:auth TypeError: Key for the RS256 algorithm must be one of type KeyObject or CryptoKey. Received an instance of Buffer #5347

Closed DanielAtCosmicDNA closed 3 weeks ago

DanielAtCosmicDNA commented 3 weeks ago

Running the following code as an api route within NextJS pages/api/graphql/endpoint.js:

import { ApolloServer } from '@apollo/server'
import { Neo4jGraphQL } from '@neo4j/graphql'
import { OGM } from '@neo4j/graphql-ogm'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { readFileSync } from 'fs'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import driver from '@/app/libs/neo4j'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const typeDefs = readFileSync(join(__dirname, 'schema.graphql'), 'utf-8').toString()

const neoSchema = new Neo4jGraphQL({
  typeDefs,
  driver,
  features: {
    authorization: {
      key: process.env.AUTH_SECRET
    }
  }
})

const [schema] = await Promise.all([
  neoSchema.getSchema(),
  ogm.init()
])

const server = new ApolloServer({
  schema
})

export default startServerAndCreateNextHandler(server, {
  context: async ({ headers }) => {
    const token = headers.authorization
    return {
      token
    }
  }
})

with a schema.graphql file in the same folder:

type User {
    id: ID! @id @unique
    name: String!
}

And running the server with:

DEBUG=@neo4j/graphql:auth yarn dev

After adding the following line to node_modules/jose/dist/node/cjs/lib/check_key_type.js just to add this console.log:

....
const asymmetricTypeCheck = (alg, key, usage) => {
    console.log({ alg, key, usage })
    if (!(0, is_key_like_js_1.default)(key)) {
        throw new TypeError((0, invalid_key_input_js_1.withAlg)(alg, key, ...is_key_like_js_1.types));
    }
...

where @/app/libs/neo4j.js is:

import neo4j from 'neo4j-driver'

const {
  NEO4J_URI,
  NEO4J_USERNAME,
  NEO4J_PASSWORD
} = process.env

const driver = neo4j.driver(
  // "neo4j://localhost:7687",
  NEO4J_URI,
  neo4j.auth.basic(NEO4J_USERNAME, NEO4J_PASSWORD)
)

export default driver

results in:

image

after making any queries or mutations at the endpoint: http://localhost:3000/api/graphql/endpoint

Additionally, if @authentication marker is added to the field name, a query results in unauthenticated error.

Expected behavior It was expected that the JWT authentication would work without any problem with or without authentication mark in the name field.

System (please complete the following information):

Additional context "@apollo/server": "^4.10.4", "@neo4j/graphql": "^5.4.4", "@neo4j/graphql-ogm": "^5.4.5", "@as-integrations/next": "^3.0.0", "neo4j-driver": "^5.22.0",

neo4j-team-graphql commented 3 weeks ago

Many thanks for raising this bug report @DanielAtCosmicDNA. :bug: We will now attempt to reproduce the bug based on the steps you have provided.

Please ensure that you've provided the necessary information for a minimal reproduction, including but not limited to:

If you have a support agreement with Neo4j, please link this GitHub issue to a new or existing Zendesk ticket.

Thanks again! :pray:

mjfwebb commented 3 weeks ago

Hi @DanielAtCosmicDNA!

At first look it seems that this is highly related to your usage of startServerAndCreateNextHandler. Do you see the same behaviour without using this?

DanielAtCosmicDNA commented 3 weeks ago

DEBUG=@neo4j/graphql:auth

I have just created a reproduction repo.

So answering your question, yes. The same behaviour is happening without nextjs and @as-integrations/next.

mjfwebb commented 3 weeks ago

@DanielAtCosmicDNA I don't see any problems when using your reproduction code.

Can you specify a particular combination of type definitions and query/mutation you use which can showcase the issue?

DanielAtCosmicDNA commented 3 weeks ago

@DanielAtCosmicDNA I don't see any problems when using your reproduction code.

Can you specify a particular combination of type definitions and query/mutation you use which can showcase the issue?

When I run a simple query:

query Query {
  users {
    id
  }
}

With Authorization header: Bearer eyJh.....

and then you simply run:

npm run start

The output in the terminal is:

  @neo4j/graphql:auth Verifying JWT using secret +0ms
  @neo4j/graphql:auth TypeError: Key for the RS256 algorithm must be one of type KeyObject or CryptoKey. Received an instance of Buffer
    at asymmetricTypeCheck
DanielAtCosmicDNA commented 3 weeks ago

The next step would be to add @authentication to name field here.

And performing a simple query with and without the authorisation header:

query Query {
  users {
    id
    name
  }
}

It should behave differently as expected, only allowing the name to be displayed in case of successful authentication.

DanielAtCosmicDNA commented 3 weeks ago

@DanielAtCosmicDNA I don't see any problems when using your reproduction code.

Can you spot a difference in our setups?

mjfwebb commented 3 weeks ago

@DanielAtCosmicDNA I don't see any problems when using your reproduction code.

Can you spot a difference in our setups?

I cloned your repo and tried your code and it worked fine. How are you creating your JWT?

mjfwebb commented 3 weeks ago

@DanielAtCosmicDNA if you make a dummy key and JWT using https://jwt.io/ do you still encounter the same error?

DanielAtCosmicDNA commented 3 weeks ago

@DanielAtCosmicDNA I don't see any problems when using your reproduction code.

Can you spot a difference in our setups?

I cloned your repo and tried your code and it worked fine. How are you creating your JWT?

NextAuth created it for me. Now that you pointed that out, I have just tested with http://jwtbuilder.jamiekurtz.com/. And the JWT here is decoded correctly.

DanielAtCosmicDNA commented 3 weeks ago

@DanielAtCosmicDNA if you make a dummy key and JWT using https://jwt.io/ do you still encounter the same error?

What is weird, though is that the original JWT I used and which has a problem is decoded correctly by https://jwt.io/. It seems to me there is something amiss here.

mjfwebb commented 3 weeks ago

@DanielAtCosmicDNA https://medium.com/@fatih969692/understanding-the-incompatibility-between-jwt-verify-and-nextauth-js-c41adcd16fd0 this seems highly relevant

DanielAtCosmicDNA commented 3 weeks ago

I created a new branch JWT decryption and when I run:

NEXTAUTH_JWT_TOKEN=eyJh... npm run convert

I get the following error:

neo4j-graphql-authentication/node_modules/jose/dist/node/esm/jwe/compact/decrypt.js:13
        throw new JWEInvalid('Invalid Compact JWE');
mjfwebb commented 3 weeks ago

@DanielAtCosmicDNA I wonder if you can use the built-in NextJS getToken https://next-auth.js.org/configuration/options#jwt-helper ?

DanielAtCosmicDNA commented 3 weeks ago

@DanielAtCosmicDNA I wonder if you can use the built-in NextJS getToken https://next-auth.js.org/configuration/options#jwt-helper ?

I can get the JWT token with the following route handler:

import { NextResponse } from 'next/server'
import { getToken } from 'next-auth/jwt'

const secret = process.env.NEXTAUTH_SECRET

const GET = async (request) => {
  // Get the token from the request
  const token = await getToken({ req: request, secret, raw: true })
  return NextResponse.json({ token })
}

export { GET }

But it results in the following error:

  @neo4j/graphql:auth Verifying JWT using secret +1s
  @neo4j/graphql:auth JWSInvalid: Invalid Compact JWS

Now if I try to decode with convert script I have the following error:

node_modules/jose/dist/node/esm/runtime/decrypt.js:65
        throw new JWEDecryptionFailed();
mjfwebb commented 3 weeks ago

How about something like this, where you just pass the result in the context?

export default startServerAndCreateNextHandler(server, {
  context: async ({ req }) => {
    const token = await getToken({ req, secret });
    return {
      token
    }
  }
})

Not sure of the exact values.

DanielAtCosmicDNA commented 3 weeks ago

How about something like this, where you just pass the result in the context?

export default startServerAndCreateNextHandler(server, {
  context: async ({ req }) => {
    const token = await getToken({ req, secret });
    return {
      token
    }
  }
})

Not sure of the exact values.

Following https://neo4j.com/docs/graphql/current/security/configuration/ I changed the code to:

export default startServerAndCreateNextHandler(server, {
  context: async (req) => {
    // const token = headers.authorization
    const secret = process.env.NEXTAUTH_SECRET
    const jwt = await getToken({ req, secret })
    return {
      jwt
    }
  }
})

And it is now working perfectly fine. Many thanks for your insights in this regard @mjfwebb !