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

fix auth races for react native #29

Closed
wants to merge 8 commits into from

Conversation

erquhart
Copy link
Contributor

@erquhart erquhart commented Feb 10, 2025

This PR addresses three race conditions, which can technically appear on the web, but are very likely to appear for native. They are technically isolated, but it helps to evaluate as a whole, so I've opened a single PR for fixing all of them.

All of these were observed and fixed using Convex Auth, but only one was suitable for fixing directly in that library: get-convex/convex-auth#163

The race conditions present in one of two ways:

  • False unauth state - client falls into unauth, but on refresh/reload, the user is authenticated
  • Lost ws connection - client connection fails and makes no further attempts to reconnect

Race conditions

  • False unauth state: Authenticate ws message followed by unrelated auth error
    When sending a fresh token for server validation, any subsequent auth error triggers unauth state in the client. It is possible for a query or mutation to trigger an auth error response while the client is awaiting token validation, leading to an erroneous unauth state in the client. This PR checks the auth error message in this case to ensure the error is in response to an Authenticate request, and ignores the error if not.

  • False unauth state: Reauth attempt disrupted by app moved to background
    When a native app is moved to background during reauth, the response may not be processed at all, or could be queued and executed after the token has expired. This PR allows up to two retries when token validation returns an error.

  • Lost ws connection: Reauth stops connection and doesn't restart
    Reauth stops the connection and restarts it at the end of the function, but the function can return before the restart if another token fetch has initiated while the reauth was running. If a regular refetch, such as a scheduled refetch, runs while reauth is running, reauth will return early and refetch does not restart the connection. This PR attempts to restart the connection at the end of both refetch and reauth to ensure the connection recovers.

@erquhart erquhart changed the title Fix auth races fix auth races Feb 10, 2025
@erquhart erquhart changed the title fix auth races fix auth races for react native Feb 10, 2025
@sshader sshader self-requested a review February 20, 2025 16:36
Copy link
Contributor

@sshader sshader left a comment

Choose a reason for hiding this comment

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

(submitting drafted comments from a while ago)

// is waiting for the server to confirm a token, ignore.
// TODO: This shouldn't rely on a specific error text, make less brittle.
// May require backend changes.
if (
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a server-side change where the AuthError should have a field authUpdateAttempted: boolean. It's true when we're attempting to update to a new auth token, and false otherwise, so I believe you can switch this condition to checking for authUpdateAttempted is false

@@ -206,12 +222,13 @@ export class AuthenticationManager {
// in that we pause the WebSocket so that mutations
// don't retry with bad auth.
private async tryToReauthenticate(serverMessage: AuthError) {
// We only retry once, to avoid infinite retries
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add this comment back? I understand that there are retries now to deal with interruptions during the re-authentication, but I think the point that we should really only be calling tryToReauthenticate a fixed number of times seems nice to document

@@ -176,13 +179,26 @@ export class AuthenticationManager {
if (this.authState.state === "waitingForServerConfirmationOfFreshToken") {
this._logVerbose("server confirmed new auth token is valid");
this.scheduleTokenRefetch(this.authState.token);
this.tokenConfirmationRetries = TOKEN_CONFIRMATION_RETRIES;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: It was unclear to me whether this was supposed to be tries remaining or tries attempted. maybe this.tokenConfirmationAttempts and set it at 0 initially?

);
this.resumeSocket();
this.restartSocket();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also be resetting this.tokenConfirmationRetries?

];
const tokenFetcher = vi.fn(async (_opts) => tokens.shift()!);
const onChange = vi.fn();
client.setAuth(tokenFetcher, onChange);
Copy link
Contributor

Choose a reason for hiding this comment

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

To make this more realistic, we should be subscribing to a query, right? Otherwise there's no reason we'd get the AuthError back if there are no functions to execute.

If that's hard to actually do in this test, we can just add a comment that in the real world we'd need a query subscription here

client.setAuth(tokenFetcher, onChange);

expect((await receive()).type).toEqual("Connect");
expect((await receive()).type).toEqual("Authenticate");
Copy link
Contributor

Choose a reason for hiding this comment

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

This is Authenticate with identity version 1, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

(I'll start my own branch adding some more comments since that seems more efficient than telling you to add them)

@sshader
Copy link
Contributor

sshader commented Mar 5, 2025

Ok it took me forever, but I built off of this here (sorry for weird branching).

Would love a look at those changes + some manual testing, and then we can either port them over here or merge the other one

@erquhart
Copy link
Contributor Author

Cool, I'll go ahead and close this, we could always reopen if need be.

@erquhart erquhart closed this Mar 10, 2025
convex-copybara bot pushed a commit to get-convex/convex-backend that referenced this pull request Mar 14, 2025
Most of this PR is from get-convex/convex-js#29 from @erquhart

There's one more commit I added to (1) use `authUpdateAttempted` on `AuthError` instead of needing to string match now that the backend feature is in -- we should double check that we're seeing this field come through on the WS with any repros we have and (2) add even more comments to the tests.

(pushed up here because GH permissions are hard)

Co-authored-by: Sarah Shader <sarahshader@gmail.com>
Co-authored-by: Shawn Erquhart <shawn@erquh.art>
GitOrigin-RevId: 446f5ade8dee52d8b8c045e80105c26200458a8c
convex-copybara bot pushed a commit that referenced this pull request Mar 14, 2025
Most of this PR is from #29 from @erquhart

There's one more commit I added to (1) use `authUpdateAttempted` on `AuthError` instead of needing to string match now that the backend feature is in -- we should double check that we're seeing this field come through on the WS with any repros we have and (2) add even more comments to the tests.

(pushed up here because GH permissions are hard)

Co-authored-by: Sarah Shader <sarahshader@gmail.com>
Co-authored-by: Shawn Erquhart <shawn@erquh.art>
GitOrigin-RevId: 446f5ade8dee52d8b8c045e80105c26200458a8c
@sshader
Copy link
Contributor

sshader commented Mar 17, 2025

merged in here

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.

None yet

2 participants