Hi Everyone. I have a problem regarding sessions t...
# support-questions-legacy
m
Hi Everyone. I have a problem regarding sessions that I'm running into I would like to post the complete thing with the code as well. So I will be posting this in parts as a thread. For starters we are a YC startup and we're using SuperTokens' managed instance as our authentication layer. The frontend is based on
Next.js
and the backend is using
Nest.js
I have a controller defined as follows:
Copy code
import { Controller, Get, Query, Session, UseGuards } from '@nestjs/common';
import { SessionContainer } from 'supertokens-node/recipe/session';

import { BasicAuthGuard } from '../auth/guard/base-auth.guard';
import { GetSubscriptionDetailsRequestDto } from './dto/get-subscription-details.dto';
import { PaymentsService } from './payments.service';

function getUserEmailFromSession(session: SessionContainer) {
  const { email } = session.getAccessTokenPayload();
  return email;
}

@UseGuards(BasicAuthGuard)
@Controller('payments')
export class PaymentsController {
  constructor(private readonly paymentsService: PaymentsService) {}


  @Get('details')
  public async getSubscriptionDetails(
    @Session() session: SessionContainer,
    @Query() input: GetSubscriptionDetailsRequestDto,
  ) {
    const subscriptionDetails = await this.getSubscriptionDetailsFromEmail(
      getUserEmailFromSession(session),
    );
    return subscriptionDetails;
  }
}
Here's the code for the auth guard
Copy code
import type { CanActivate, ExecutionContext } from '@nestjs/common';
import { Error as STError } from 'supertokens-node';
import { verifySession } from 'supertokens-node/recipe/session/framework/express';

export class BasicAuthGuard implements CanActivate {
  constructor() {}

  public async canActivate(context: ExecutionContext): Promise<boolean> {
    const ctx = context.switchToHttp();

    let err = undefined;
    const resp = ctx.getResponse();
    // You can create an optional version of this by passing {sessionRequired: false} to verifySession
    await verifySession({ checkDatabase: true, sessionRequired: true })(
      ctx.getRequest(),
      resp,
      (res) => {
        err = res;
      },
    );

    if (resp.headersSent) {
      throw new STError({
        message: 'RESPONSE_SENT',
        type: 'RESPONSE_SENT',
      });
    }

    if (err) {
      throw err;
    }

    return true;
  }
}
This controller runs fine 95% of the time. But we're seeing a weird log on our servers that shouldn't be there.
Copy code
ALERT!!!! INTERNAL SERVER ERROR!!!
            STATUS: 500
            METHOD: GET
            PATH: /payments/details?checkoutCancelPath=%2Fbilling&checkoutSuccessPath=%2Fbilling&portalReturnPath=%2Fbilling
            REQUEST-ID: 492f101b-92e4-4762-8a82-4f131c995c3f
            MESSAGE: Cannot read properties of undefined (reading 'getAccessTokenPayload')
            ERROR: 'Unhandled Rejection'
            STACK: TypeError: Cannot read properties of undefined (reading 'getAccessTokenPayload')
    at PaymentsService.getUserEmailFromSession (/app/dist/modules/payments/payments.service.js:187:35)
    at PaymentsController.getSubscriptionDetails (/app/dist/modules/payments/payments.controller.js:27:37)
    at /app/node_modules/@nestjs/core/router/router-execution-context.js:38:29
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /app/node_modules/@nestjs/core/router/router-execution-context.js:46:28
    at async /app/node_modules/@nestjs/core/router/router-proxy.js:9:17
And I have no clue as to what is happening on the FE that's causing this. We've been unable to identify which user is having this issue. The authentication method is cookies, the access token is sent in the 'cookie' header. I would really appreciate if anyone can give insights into why we're getting a
session
that's
undefined
. In my limited knowledge, if the cookie header is not present the BasicAuthGuard should already throw a 401, and I've tested this manually and it is indeed the case. What escapes me is how can a request pass through the auth guard successfully and end up having a session that is
undefined
Thanks allot!
r
hey @msalar0454 !
Have you added the supertokens error handler to your application?
m
@rp_st yes I have added it. It does not have an exception handler of its own, but I've incorporated it into the all-exceptions handler of our app
Copy code
import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import { Catch, HttpException } from '@nestjs/common';
import * as express from 'express';
import { Error as STError } from 'supertokens-node';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly superTokensHandler: express.ErrorRequestHandler;

  constructor(
    // ...
  ) {
  }

  catch(exception: unknown, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<express.Response>();
    const request = ctx.getRequest<
      express.Request & { session?: { userId?: string } }
    >();
     // ...


    if (exception instanceof HttpException) {
      // ...
    } else if (exception instanceof STError) {
      if (response.headersSent) {
        this.logger.error('Headers already sent');
        return;
      }
      return this.superTokensHandler(
        exception,
        ctx.getRequest<express.Request>(),
        response,
        ctx.getNext<express.NextFunction>(),
      );
    } else {
      // ...
    }


  }
}
I have redacted the un-related code.
r
So operations like auto session refresh work as expected right?
m
the supertokens handler is defined like this
Copy code
this.superTokensHandler = errorHandler();
It is imported as
Copy code
import { errorHandler } from 'supertokens-node/framework/express';
Absolutely! they work perfectly.
r
Hmm ok
m
What I'm blown away by is how can a request pass the authentication guard, and not have a session defined...
r
Yea I’m not sure how this type of error is possible
Cause verifySession will send a response in case the access token is expired or doesn’t exist. In which case, the if(resp.headersSent) should be true
Where are you calling the getAccessTokenPayload function? Maybe there is some other issue? Like the session does exist, and is passed in the API as an input, but somewhere in your logic it gets set to undefined?
m
This could be a script that someon is running against our server. Let me share where and how we're using
getAccessTokenPayload
@rp_st if you look at the code I have pasted. There's a function called
getUserEmailFromSession
it uses the access token payload to get the email of the user. That's where we're calling
getAccessTokenPayload
Look at the stack trace to get a clear picture.
r
I see
Can you make an api call to this endpoint without an access token? What happens?
m
@rp_st I get a
401
r
Right.
Can you create a session, save the access token somewhere, then revoke the session in offline mode, and then call the api with the saved access token? What do you get?
m
Hmmm... I don't know. Let me try it. Can you guide me on how do I revoke the access token in offline mode?? If I understand this correctly would this be equivalent to using an access token for a session that was just logged out??
@rp_st ^
r
Yes.
m
Thanks allot for this @rp_st Let me try and get back to you right away.
hi @rp_st So this is what I did. I logged in using the FE. And I went to the page that makes this api call. I coped the api call as curl and tried it in postman, it worked and returned a 200. I then logged out of the FE. And went back to Postman to try this again, and I got a 401. And the server logs just show this log.
Copy code
err: {
      "type": "SuperTokensError",
      "message": "RESPONSE_SENT",
      "stack":
          Error: RESPONSE_SENT
              at BasicAuthGuard.canActivate (/Users/muhammadsalarkhan/Repos/Apollo/venus-api/src/modules/auth/guard/base-auth.guard.ts:27:13)
              at processTicksAndRejections (node:internal/process/task_queues:95:5)
              at GuardsConsumer.tryActivate (/Users/muhammadsalarkhan/Repos/Apollo/venus-api/node_modules/@nestjs/core/guards/guards-consumer.js:16:17)
              at canActivateFn (/Users/muhammadsalarkhan/Repos/Apollo/venus-api/node_modules/@nestjs/core/router/router-execution-context.js:134:33)
              at /Users/muhammadsalarkhan/Repos/Apollo/venus-api/node_modules/@nestjs/core/router/router-execution-context.js:42:31
              at /Users/muhammadsalarkhan/Repos/Apollo/venus-api/node_modules/@nestjs/core/router/router-proxy.js:9:17
      "errMagic": "ndskajfasndlfkj435234krjdsa"
    }
Which means the request was declined by the auth guard and it didn't even goto the service.
r
Yes. So this is expected
Hmm. So the code seems fine
The best course of action now may be to just add some debug logs perhaps. So in the place where you use the getAccessTokenPayload function, before that, check if session is undefined, and if it is, log some helpful messages: like the headers in the request for example.
m
@rp_st sure. Let me do this.
r
You can always ping me here when u have more details about this issue.
m
Hi @rp_st Just letting you in on the progress. This issue is still happening on our production servers. So I've added the following piece of code to the auth-guard.
Copy code
diff --git a/src/modules/auth/guard/base-auth.guard.ts b/src/modules/auth/guard/base-auth.guard.ts
index 001c385d5..78b2aa0fd 100644
--- a/src/modules/auth/guard/base-auth.guard.ts
+++ b/src/modules/auth/guard/base-auth.guard.ts
@@ -1,5 +1,6 @@
 import type { CanActivate, ExecutionContext } from '@nestjs/common';
 import { Error as STError } from 'supertokens-node';
+import type { SessionContainer } from 'supertokens-node/recipe/session';
 import { verifySession } from 'supertokens-node/recipe/session/framework/express';
 
 /**
@@ -34,6 +35,16 @@ export class BasicAuthGuard implements CanActivate {
       throw err;
     }
 
+    const request = ctx.getRequest();
+    const session = request?.session as SessionContainer;
+    if (!session) {
+      console.error(
+        `Got an undefined SessionContainer details of the request: ${JSON.stringify(
+          ctx.getRequest().headers,
+        )}`,
+      );
+    }
+
     return true;
   }
 }
Basically, when
verifySession
passes the request, and we cannot get a session object. We log the headers of the request. I think this is what you were suggesting. Let me know if this looks okay. Once this happens again I will be sharing the details with you of the logs.
Should I also log the current unix timestamp for some reason?
r
yup! i think this works. You are still calling verifySession above this right?
m
@rp_st yep
r
Okay. Let me know what it logs out the next time this issue happens :£
🙂 *
m
Hi @rp_st so I went into the logs and I saw one. I have redacted the sensitive information from the log. This did not happen on production, this was another environment. Here is a list of all the headers.
Copy code
{
  "x-forwarded-for": "20.75.94.90",
  "x-forwarded-proto": "https",
  "x-forwarded-port": "443",
  "host": "api-dev.app.so",
  "x-amzn-trace-id": "Root=1-66263963-08af103c211ec8020b499e96",
  "rid": "anti-csrf",
  "st-auth-mode": "cookie",
  "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.28 Safari/537.36",
  "accept-language": "en-US",
  "accept": "*/*",
  "origin": "https://local.app.so:3001",
  "sec-fetch-site": "same-site",
  "sec-fetch-mode": "cors",
  "sec-fetch-dest": "empty",
  "referer": "https://local.app.so:3001/",
  "accept-encoding": "gzip, deflate, br",
  "cookie": "sAccessToken=< .... redacted ... >; st-last-access-token-update=1713781088016; sFrontToken=< ... redacted ... >; mp_xxxx_mixpanel=%7B%22distinct_id%22%3A%20%22tech%40app-group.io%22%2C%22%24device_id%22%3A%20%2218f055020e11ce-0ce29e85749ab-1739317d-e1000-18f055020e21ce%22%2C%22%24initial_referrer%22%3A%20%22https%3A%2F%2Flocal.app.so%3A3001%2Flogin%22%2C%22%24initial_referring_domain%22%3A%20%22local.app.so%3A3001%22%2C%22__mps%22%3A%20%7B%7D%2C%22__mpso%22%3A%20%7B%7D%2C%22__mpus%22%3A%20%7B%7D%2C%22__mpa%22%3A%20%7B%7D%2C%22__mpu%22%3A%20%7B%7D%2C%22__mpr%22%3A%20%5B%5D%2C%22__mpap%22%3A%20%5B%5D%2C%22%24user_id%22%3A%20%22tech%40app-group.io%22%7D"
}
We are using mixpanel so the cookie has some data for that as well but the sAccessToken was a valid token, it parsed out nicely on
jwt.io
Got any hints?? Looks pretty normal to me.
r
hmm. Seems about fine
So this makes sense as to why your API was called.
But, still, req.session should always be there
did anything get logged based from this change?
m
@rp_st the log that I showed you is after this change ^. It detected that the session is not there and it logged the headers. And then it let the request go to the next middleware, where it crashed because we were calling the session object.
r
right. So you did see the text
Got an undefined SessionContainer details of the request:
in the log output?
m
Yes. Apologies for not pointing that out, I removed that part of the log for posting here.
r
ah ok
hmm
can you send another request to that env with the exact same request headers?
m
Let me try.
But I think its moot at this point the access token must have expired
r
right. You can try and see anyway
or maybe use a new access token and keep the other headers the same
m
Sure.
@rp_st I got a 200 OK response with the new access token
r
hmmm. This is very very strange 😯
m
Do you know the code in the nestjs supertokens library that appends the session to the request object?? If you can point me to it, I'd like to read it to know what specifically constructs the session object. I have a feeling the supertokens core is invoked in there and the data returned from there is used for the session object. And it might be that call that is intermittently failing.
r
right. Maybe that's the issue. Maybe the core is returning a 500 or something, and that's not being handled by the auth guard properly.
m
I think the issue is a bit more subtle. I'm pretty sure the
verifySession
call should have handled if it were a simple 500 from the core. I believe the core returns a 2xx but the response has something missing for that specific api call that is not yet handled by
verifySession
implementation so it does not create a session object. The rest of the authguard works perfectly. For now I can patch it such that if I detect an
undefined
session container I will return a
401
from my backend.
r
the verifySession function handles all cases, and is well tested. So unlikely that we have missed something. Whats most likely is that the core is returning a 500 error and verifySession calls
next(err);
which is not handled correctly in the nestjs integration guide
but actually, our docs does have (https://supertokens.com/docs/thirdpartyemailpassword/nestjs/guide#add-a-session-verification-guard):
Copy code
if (err) {
      throw err;
    }
In the auth guard. So hmm. I don't think thats the issue either
m
So the auth guard that I have shared with you is exactly as per the nestjs integration docs.
and if verifySession is doing its job correctly, why is it able to come to the conclusion that the request is authenticated, but is not able to attach a proper session object to the request. I believe it is `verifySession`'s responsibility to attach the session container to the context right??
r
That is correct. It is.
I still think the issue might be that the core is throwing a 500 error and that’s not being handled properly in the nestjs integration we have. Even though it seems like it is.
@porcellus could you test this out please?
p
I'll check this out, but a 500 error should be covered.
I'll test this out manually a bit later today, but as far as I can see the only path where
verifySession
doesn't attach the session object is where it encounters an error, but then handles it by sending a response. Right now I have 3 main theories: 1.
ctx.getRequest
returns different objects in some cases (which would be very weird from NestJS) 2.
verifySession
encountered an error and then handled it by sending a response. By some race condition/timing issue, however it hasn't been actually sent, so the
headersSent
check doesn't catch it. (I'll do further checks to see if/how this can happen, but it'd explain all symptoms) 3. there is some bug in
verifySession
that only happens in some weird timing.
As far as I can see this cannot happen because of the core returning a 2XX as we handle anything non-200 as an error and a 200 response with missing props would handled as either as a TRY_REFRESH_TOKEN error or construct the session object (or throw while constructing the session object)
I've manually tested: 1. Core responding with 400&500 (the handling is the same for any non-200): this always results in a 500 error returned from the backend (the AuthGuard throws) 2. Modifying core responses to remove some/all props from the getSession response: this also works as expected: in some cases it continues working, in other cases we get a 401 response.
As my current best guess is #2 from the above list, I've reviewed the code to check if we missed any awaits or ignored any promises returned during error handling and I could find anything. Still, this is my most likely candidate.
One way you could try to get around it is to update
BasicAuthGuard
like this:
Copy code
ts
import type { CanActivate, ExecutionContext } from '@nestjs/common';
import { getSession } from 'supertokens-node/recipe/session';

export class BasicAuthGuard implements CanActivate {
  constructor() {}

  public async canActivate(context: ExecutionContext): Promise<boolean> {
    const ctx = context.switchToHttp();

    const req = ctx.getRequest();
    const resp = ctx.getResponse();

    const session = await getSession(req, resp, { checkDatabase: true, sessionRequired: true })

    req.session = session;

    if (!req.session) {
      // This should never happen, because sessionRequired is set to true above
      // which makes getSession throw if there is no active session
      return false;
    }
    return true;
  }
}
as a disclaimer: while this has been somewhat tested, it's not tested to the level we usually do it.
What this changes is that it removes the error handling part from the guard (by changing from verifySession to getSession) and rely on the exception filter handling errors. If I'm correct, this should fix your issue since it doesn't rely on
headersSent
(actually, it solves it regardless, because there is an explicit check for the session object, but that should never be hit)
If you try this, I'd be very interested if this: 1. solves your issue 2. the "this should never happen" comment holds true, as that would indicate another type of bug
r
What happens if getSession here throws?
(cause maybe the access token has expired)
p
it relies on the exception filter catching and handling the error
99 Views