msalar0454
04/11/2024, 6:50 AMNext.js
and the backend is using Nest.js
msalar0454
04/11/2024, 6:52 AMimport { 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
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;
}
}
msalar0454
04/11/2024, 6:52 AMALERT!!!! 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!rp_st
04/11/2024, 7:21 AMrp_st
04/11/2024, 7:24 AMrp_st
04/11/2024, 7:24 AMmsalar0454
04/11/2024, 8:11 AMimport 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.rp_st
04/11/2024, 8:13 AMmsalar0454
04/11/2024, 8:13 AMthis.superTokensHandler = errorHandler();
It is imported as
import { errorHandler } from 'supertokens-node/framework/express';
msalar0454
04/11/2024, 8:13 AMrp_st
04/11/2024, 8:13 AMmsalar0454
04/11/2024, 8:14 AMrp_st
04/11/2024, 8:14 AMrp_st
04/11/2024, 8:15 AMrp_st
04/11/2024, 8:16 AMmsalar0454
04/11/2024, 8:17 AMgetAccessTokenPayload
msalar0454
04/11/2024, 8:18 AMgetUserEmailFromSession
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.rp_st
04/11/2024, 8:19 AMrp_st
04/11/2024, 8:20 AMmsalar0454
04/11/2024, 8:21 AM401
rp_st
04/11/2024, 8:21 AMrp_st
04/11/2024, 8:22 AMmsalar0454
04/11/2024, 8:25 AMmsalar0454
04/11/2024, 8:26 AMrp_st
04/11/2024, 8:26 AMmsalar0454
04/11/2024, 8:26 AMmsalar0454
04/11/2024, 8:49 AMerr: {
"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.rp_st
04/11/2024, 8:50 AMrp_st
04/11/2024, 8:50 AMrp_st
04/11/2024, 8:51 AMmsalar0454
04/11/2024, 9:11 AMrp_st
04/11/2024, 9:13 AMmsalar0454
04/17/2024, 8:37 AMdiff --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.msalar0454
04/17/2024, 8:37 AMrp_st
04/17/2024, 8:38 AMmsalar0454
04/17/2024, 10:37 PMrp_st
04/18/2024, 2:53 AMrp_st
04/18/2024, 2:53 AMmsalar0454
04/22/2024, 10:56 AM{
"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.rp_st
04/22/2024, 10:57 AMrp_st
04/22/2024, 10:57 AMrp_st
04/22/2024, 10:57 AMrp_st
04/22/2024, 10:58 AMmsalar0454
04/22/2024, 10:59 AMrp_st
04/22/2024, 11:00 AMGot an undefined SessionContainer details of the request:
in the log output?msalar0454
04/22/2024, 11:00 AMrp_st
04/22/2024, 11:00 AMrp_st
04/22/2024, 11:00 AMrp_st
04/22/2024, 11:00 AMmsalar0454
04/22/2024, 11:02 AMmsalar0454
04/22/2024, 11:02 AMrp_st
04/22/2024, 11:03 AMrp_st
04/22/2024, 11:03 AMmsalar0454
04/22/2024, 11:04 AMmsalar0454
04/22/2024, 11:07 AMrp_st
04/22/2024, 11:08 AMmsalar0454
04/22/2024, 11:10 AMrp_st
04/22/2024, 11:11 AMrp_st
04/22/2024, 11:13 AMmsalar0454
04/22/2024, 11:14 AMverifySession
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.rp_st
04/22/2024, 11:15 AMnext(err);
which is not handled correctly in the nestjs integration guiderp_st
04/22/2024, 11:16 AMif (err) {
throw err;
}
In the auth guard. So hmm. I don't think thats the issue eithermsalar0454
04/22/2024, 4:36 PMmsalar0454
04/22/2024, 4:37 PMrp_st
04/22/2024, 5:18 PMrp_st
04/22/2024, 5:19 PMrp_st
04/22/2024, 5:19 PMporcellus
04/23/2024, 3:33 AMporcellus
04/23/2024, 4:27 AMverifySession
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.porcellus
04/23/2024, 4:27 AMporcellus
04/23/2024, 12:41 PMporcellus
04/23/2024, 12:42 PMporcellus
04/23/2024, 12:43 PMBasicAuthGuard
like this:
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;
}
}
porcellus
04/23/2024, 12:43 PMporcellus
04/23/2024, 12:47 PMheadersSent
(actually, it solves it regardless, because there is an explicit check for the session object, but that should never be hit)porcellus
04/23/2024, 12:54 PMrp_st
04/23/2024, 1:23 PMrp_st
04/23/2024, 1:23 PMporcellus
04/23/2024, 2:50 PM