dbAuth authentication succeeds but with null currentUser

I’m noticing a behavior that I think should be classified as a bug:

After a successful logIn() on a page useAuth() returns isAuthenticated, but with null for currentUser and userMetadata.

This results in additional unnecessary authentication(s) when code assumes !currentUser means not authenticated. I believe there are places in the AuthProvider code that makes this assumption and trigger additional authentications.

After some debugging I noticed that this is what is happening:

  1. The initial call to useAuth() on a page calls getToken() which tries to fetch the token from the server.
  2. The server response is a null token because the user is not yet logged in (no session cookie).
  3. dbAuth caches the null token
  4. The page calls logIn() which succeeds. The server sends a set-cookie for the session.
  5. logIn() next calls reauthenticate() internally, which kicks off another getToken()
  6. That call to getToken() never goes to the server because dbAuth finds the previously cached null token and returns it instead of fetching from from the server and getting the actual token
  7. The page re-renders with isAuthenticated: true, currentUser: null, userMetadata: null
  8. Calling navigate() to another route at this point triggers another authentication because calls to useAuth() have currentUser: null

It seems to me that if the cached token is null, getToken should fetch from the server. I don’t know the code very well but I was thinking the code below could be a fix.

Can someone who knows the code better determine the most appropriate fix and include it in the next release?

IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/packages/auth-providers/dbAuth/web/src/dbAuth.ts b/packages/auth-providers/dbAuth/web/src/dbAuth.ts
--- a/packages/auth-providers/dbAuth/web/src/dbAuth.ts	(revision 1075258d60cc0d0fdda0257576f999b26ac5a07d)
+++ b/packages/auth-providers/dbAuth/web/src/dbAuth.ts	(date 1704231152678)
@@ -86,7 +86,7 @@
       return getTokenPromise
     }
 
-    if (isTokenCacheExpired()) {
+    if (isTokenCacheExpired() || cachedToken === null) {
       getTokenPromise = fetch(`${getApiDbAuthUrl()}?method=getToken`, {
         credentials,
       })

I’m getting this behavior as well.

I get two calls to the auth endpoint. The first one sends a SetCookie with the proper value, but the second one resets it for some reason (changed api port to 8921 to rule out hidden cookies on standard 8911 port):

Any idea on what could be happening?

@rob Any thoughts on the cachedToken case here with dbAuth and session cookies?

It’s been a while since I’ve been in this code! I’m out this week for spring break, but will take a look next week.

I think this is something different…can you verify that the cookie set in the first request is actually being sent up to the server in the second request? There should be a Cookie header in the second request that matches the Set-Cookie from the response of the first.

What browser are you using? There are some quirks with some browsers that won’t let you set a Secure cookie on localhost, (Chrome allows you to do this). We have a check in the auth.js function that should only set Secure if you’re in a non-development environment to prevent this, but something to look into.

Before we added this ^ check, this is the exact behavior we saw in Safari: first request set a cookie, second request cleared it, because the second request wouldn’t sent up the Secure cookie in a non-secure environment. To be safe the server removes the cookie on any request that doesn’t appear as authenticated already (like the cookie not being set).

We only cache the token for 5 seconds, to make sure it isn’t requested multiple times in a single page load (any call to useAuth() would trigger a call to the server otherwise). We keep this as short as possible (instead of something like 6 hours) to be sure that if you revoke access to your site you can ban/remove them effectively instantly.

Is it possible that’s the source of the request pattern you’re seeing? If you click on a link to another page that uses useAuth(), and it’s been more than 5 seconds, you’ll get another check with the server to verify the currentUser.

Hey, I know I’m not OP but I’m having the same issue. It looks like the cookie is being set after the first request to /auth. I can confirm that it is sent back on the second request (to getToken) which unsets the cookie, and then a third call (webAuthnAuthOptions) fails with HTTP 400. This is with nothing other than me setting up dbAuth with webAuthn.

Check your Secure cookie setting: dbAuth on serve using ip address doesn’t work - #5 by rob That post and my following one was the solution to that person having a similar issue!

That was one of my first thoughts as well, but this is immediately after generating all the code for dbAuth on the newest version of Redwood, so it includes the check for a development environment to make it non-secure. I confirmed that by looking in the initial request, and it was non-secure.

From the initial request:

session=;HttpOnly;Path=/;SameSite=Strict;Expires=Sun, 16 Jun 2024 15:23:50 GMT

From the second:

session=;HttpOnly;Path=/;SameSite=Strict;Expires=Thu, 01 Jan 1970 00:00:00 GMT

It looks like Discourse mangled that. Here it is again without triggering markdown issues. Note the session ID is contained in the first request.

First:

session=(removed);HttpOnly;Path=/;SameSite=Strict;Expires=Sun, 16 Jun 2024 15:23:50 GMT

Second:

session=;HttpOnly;Path=/;SameSite=Strict;Expires=Thu, 01 Jan 1970 00:00:00 GMT

If you look at the Request headers on the second request (the one that wipes out the cookie) can you see that the Cookie header is being sent up, and should be the same value as the cookie that was just set in the previous response? Or is there no Cookie header at all? That’s my guess.

If dbAuth can’t decrypt the cookie, or the cookie isn’t sent in the request, it’ll return a “logout” response, which is what you’re seeing in that second request: remove the cookie completely. This is what usually happens with the Secure attribute, the cookie isn’t sent up in the second request (because it’s HTTP on localhost) and so the server thinks you’re not logged in and sends the logout response.

The cookie sent on the second request is the one originally saved. It’s the response to that one that wipes the cookie. I remember that because it struck me as very odd, but I’m away from home for the next week and can’t do any more in depth testing.

Hi.

We recently did some rework to auth. See if the following issue and PR might help you. But not sure this is the exact same issue.

This issue:

And this PR:

Hmm, so the second request actually creates the cookie and any subsequent request wipes it? That’s the first time I’ve heard of that one!

Getting screenshots showing the request and response headers for each of these requests would definitely help with debugging. Also if you’re using Chrome, the Applications tab or Network tab may show a little exclamation point warning next to cookies and if you hover it’ll give you a reason why Chrome doesn’t like it. That could offer more hints…

Maybe I phrased that a little poorly on rereading it. To clarify: the cookie is created on the first response and sent back on the second request. The second response wipes the cookie.

Note that I have upgraded to version 7.7.3 of Redwood.

Request 1: sent after hitting the login button

POST /.redwood/functions/auth HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Length: 84
Content-Type: application/json
Host: localhost:8910
Origin: http://localhost:8910
Referer: http://localhost:8910/login?redirectTo=/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"
sec-ch-ua-mobile: ?1
sec-ch-ua-platform: "Android"

Response 1:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
content-type: application/json; charset=utf-8
csrf-token: 9b512b1d-8090-46c0-b3a5-d6314c463a7a
set-cookie: session=[hash removed];HttpOnly;Path=/;SameSite=Strict;Expires=Sun, 14 Jul 2024 17:13:54 GMT
content-length: 38
date: Sun, 14 Jul 2024 16:13:54 GMT
connection: close

Request 2: sent on loading the WebAuthn prompt

GET /.redwood/functions/auth?method=getToken HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Cookie: session=[hash removed]
Host: localhost:8910
Referer: http://localhost:8910/login?redirectTo=/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"
sec-ch-ua-mobile: ?1
sec-ch-ua-platform: "Android"

Response 2:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
content-type: application/json; charset=utf-8
set-cookie: session=;HttpOnly;Path=/;SameSite=Strict;Expires=Thu, 01 Jan 1970 00:00:00 GMT
content-length: 0
date: Sun, 14 Jul 2024 16:13:54 GMT
connection: close

Request 3: WebAuthn options

GET /.redwood/functions/auth?method=webAuthnAuthOptions HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: localhost:8910
Referer: http://localhost:8910/login?redirectTo=/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36
content-type: application/json
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"
sec-ch-ua-mobile: ?1
sec-ch-ua-platform: "Android"

Response 3:

HTTP/1.1 400 Bad Request
Access-Control-Allow-Origin: *
content-type: application/json; charset=utf-8
content-length: 64
date: Sun, 14 Jul 2024 16:13:54 GMT
connection: close

I’m pretty sure there are supposed to be a couple of additional headers that are sent up, telling the backend what auth strategy is being used…Authorization and maybe auth-provider? I don’t see those here. Are these requests going up as a result of a GraphQL call, or are you doing something custom?

What’s the expiration time on your cookie? You made that post on July 14th, which is also the date the cookie is set to expire…I wonder if something weird is happening there? If you make sure the cookie is set to the far future, does that make a difference?

Otherwise the only thing I can think to do is go into node_modules/@redwoodjs and start adding console statements to see where things break down. :frowning: