Skip to content
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

authenticate on every request to broker when needed #1241

Merged
merged 8 commits into from
Dec 8, 2021

Conversation

flucivja
Copy link
Contributor

Fixes #1240

@Nevon
Copy link
Collaborator

Nevon commented Nov 29, 2021

Is this issue really limited to heartbeats? From reading the issue description, it sounds like the problem should occur on any request if the broker needs to reauthenticate.

If that's the case, then we could do the same thing in sendRequest, except when the request is apiVersions, since that happens before authentication.

src/broker/index.js Outdated Show resolved Hide resolved
@flucivja
Copy link
Contributor Author

Is this issue really limited to heartbeats? From reading the issue description, it sounds like the problem should occur on any request if the broker needs to reauthenticate.

If that's the case, then we could do the same thing in sendRequest, except when the request is apiVersions, since that happens before authentication.

yes, it can happend at any point. I will add it into [sendRequest].
btw. how to check if request is not apiVersions? By simply comparing apiName with string? Or better to get version protocol by requests.ApiVersions.protocol and compare names?

@Nevon
Copy link
Collaborator

Nevon commented Nov 29, 2021

btw. how to check if request is not apiVersions? By simply comparing apiName with string? Or better to get version protocol by requests.ApiVersions.protocol and compare names?

Not sure it matters much, but you could grab the api key of ApiVersions from src/protocol/requests/apiKeys.js and compare it with request.apiKey. The apiName shouldn't change, but apiKey definitely won't.

src/broker/index.js Outdated Show resolved Hide resolved
@flucivja
Copy link
Contributor Author

@Nevon everything should be ok now. Can you please check? :) Thanks.

@flucivja flucivja changed the title authenticate on heartbeat when needed authenticate on every request to broker when needed Nov 29, 2021
src/broker/index.js Outdated Show resolved Hide resolved
@Nevon
Copy link
Collaborator

Nevon commented Nov 30, 2021

I've been trying to figure out how to test this, and I wrote a test that looks something like this:

test('Reauthenticates if needed before making a request', async () => {
  const saslEntry = saslEntries.pop()
  const connection = createConnection(saslEntry.opts())
  broker = new Broker({
    connection,
    logger: newLogger(),
  })
  await broker.connect()
  const spy = jest.spyOn(connection, 'send')

  broker.authenticatedAt = null
  await broker.listGroups()

  // Assert that sasl authenticate requests happen
  console.log(spy.mock.calls)
})

But I ran into the problem that the server rejects the second authentication request with an ILLEGAL_SASL_STATE error. This got me thinking and reading the KIP about reauthentication.

In the current implementation, if you have multiple requests going on when you realize that you have to reauthenticate, each one is going to try to reauthenticate, which is gonna cause a problem. This is relatively easily solved by having the isAuthenticated method wait for the authLock to be released, so that you wait to check if you need to authenticate while there's an authentication going on.

async isAuthenticated() {
  try {
    await this.authLock.acquire()
    return this.authenticatedAt != null && !this[PRIVATE.SHOULD_REAUTHENTICATE]()
  } finally {
    await this.authLock.release()
  }
}

Unfortunately it makes the method async, which ripples through and affects a lot of things. A pain in the ass to fix, but doable.

But it doesn't explain why I fail to authenticate. In my test I'm not making concurrent requests that trigger multiple authentications. I'm just authenticating once, then making it appear that I need to reauthenticate, and then I trigger a request that will cause me to reauthenticate.

@flucivja
Copy link
Contributor Author

What do you think about putting isAuthenticated check into [PRIVATE.AUTHENTICATE]() after auth lock? And if it will be authenticated, then it will skip auth and release lock.

@t-d-d
Copy link
Contributor

t-d-d commented Nov 30, 2021 via email

@flucivja
Copy link
Contributor Author

sharedPromise looks handy for such case. But I am thinking if this is really an issue. I tried that test without my changes and it is failing too. @Nevon is that test passing for you without my changes (without check for re-authentication)?

@Nevon
Copy link
Collaborator

Nevon commented Nov 30, 2021

@Nevon is that test passing for you without my changes (without check for re-authentication)?

No, but I think it's an issue with the test. Maybe you can't authenticate again right after authenticating with sasl/scram like I'm doing. I'm guessing if you swap it out for oauthbearer with a short reauthentication timeout maybe it'll work. I just didn't have time to test it out.

But I am thinking if this is really an issue.

I would be very surprised if it isn't. Tossing broker.listGroups or something into a Promise.all should trigger it immediately.

@flucivja
Copy link
Contributor Author

Ok then, are you ok to handle it with sharedPromise? It will look like this in the broker constructor:

this[PRIVATE.AUTHENTICATE] = sharedPromiseTo(async () => {
  ... authentication logic
})

@Nevon
Copy link
Collaborator

Nevon commented Dec 1, 2021

Sounds like a good solution! Let's give it a try. I would also really like to test these scenarios. We do have a test suite that uses oauthbearer, which should certainly allow for reauthentication. See the test:group:oauthbearer yarn script, which runs a subset of test files. There's a function called describeIfOauthbearerEnabled in testHelpers that will run tests nested in it only if oauthbearer is enabled.

@flucivja
Copy link
Contributor Author

flucivja commented Dec 4, 2021

Hi @Nevon, I implemented tests and sorry it took longer time. I had issues with kafka SASL OAUTHBEARER authentication. Within test, the kafka returned me all the time that provided token created in test utils is invalid and I didn't know why. After debugging and looking to how other kafka clients are implemented, I found out that expiration time was missing in the token.

Btw. you were right with multiple parallel requests causing error: when I removed fix with sharedPromise my tests which runs multiple requests in parallel requiring authentication started failing with invalid SASL state.

Can you please check PR again when you will have time? If all will be good, can you merge it and release?

Thank you very much.

@flucivja
Copy link
Contributor Author

flucivja commented Dec 8, 2021

@Nevon just a gentle reminder :)

@Nevon
Copy link
Collaborator

Nevon commented Dec 8, 2021

Haven't forgotten about you. I left the notification sitting on my phone so it wouldn't get lost. 😅 The change looks good overall - I'd just like to get your thoughts on whether we can get rid of those sleeps in the test (or at least reduce them).

*/
this[PRIVATE.AUTHENTICATE] = sharedPromiseTo(async () => {
try {
await this.authLock.acquire()
Copy link
Collaborator

@Nevon Nevon Dec 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this lock anymore now right? If there's more callers, the first one will create a promise and start the authentication, the second one will just receive the promise from the first one so it'll just await the promise settlement from the first one. Since the promise creation itself is synchronous there shouldn't be any race condition there, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you are right. I will remove auth lock.

@flucivja
Copy link
Contributor Author

flucivja commented Dec 8, 2021

@Nevon done, now my test is faster by 30 seconds (14s vs 44s).

@Nevon Nevon merged commit e3c6d91 into tulios:master Dec 8, 2021
@Nevon
Copy link
Collaborator

Nevon commented Dec 8, 2021

Thanks for your patience and for taking the time to contribute!

@j-a-h-i-r
Copy link

Sorry for reviving a merged thread but I'm curious about one thing @Nevon. Does this fix #1039? As far as I can remember, that issue occurred due to re-authentication. I saw that you mentioned testing SASL OauthBearer flows. So I was wondering if the changes potentially fix that issue too.

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

Successfully merging this pull request may close these issues.

KafkaJS is not re-auhtenticating to all brokers
4 participants