FoalTS / foal

Full-featured Node.js framework, with no complexity. 🚀 Simple and easy to use, TypeScript-based and well-documented.
https://foalts.org/
MIT License
1.88k stars 137 forks source link

FoalTS V2: UseSessions - Generates session when requesting static files e.g. favicon.ico #855

Closed crossinghoods closed 3 years ago

crossinghoods commented 3 years ago

Been trying to migrate my project from V1 to V2, have found that the new UseSessions generates a session when I'm requesting a static asset, not sure if I'm using it wrongly.

My front-end controller is as follows:-

@UseSessions({ store: MongoDBStore, cookie: true })
export class DefaultController {
    public subControllers = [
        // Checkout - uses cookies
        controller("/checkout", CheckoutController),
    ]

    @Get("/*")
    public async defaultView(ctx: Context) {
       // Serves a single page application - using JWT and no cookies
        return new HttpResponseRedirect("/admin");
    }

}
LoicPoullain commented 3 years ago

Starting from version 2, @UseSessions always generates a session when using cookies if none already exists. This behavior is similar to what Django and Laravel do and was introduced in order to support more types of applications (SSR, etc).

However, if you prefer not to auto-create a new session when none already exists, you can disable this feature with @UseSessions({ cookie: true, create: false }). In this case, you will have to create the session yourself when a user logs in like this:

ctx.session = await createSession(this.store);
ctx.session.setUser(user);

In you example, I would suggest to move UseSessions to decorate the top of CheckoutController as it seems to only be used there.

Does it help?

crossinghoods commented 3 years ago

Makes sense, think it's a bit of a pain when doing angular development because it keeps generating so many new sessions... but not the end of the world :)

LoicPoullain commented 3 years ago

angular development because it keeps generating so many new sessions...

Are you sure of this? This theoretically should not happen. Could you give more details on how your application is structured?

crossinghoods commented 3 years ago

A boiled down version of my application is as follows:-

Angular 11.0.5 front-end (ng serve) - development phase generates a new sessions on each new api request FoalTS 2.0 backend (yarn develop) - REST APIs

app.controller loads => frontend.controller => backend.controller

General flow localhost:4200 > loads Login page > submit login details to Auth controller on port 3001 > Auth controller generates JWT > Angular app goes to Dashboard page (calls settings api and other micro api end-points for info with JWT Bearer Token).

During each of these micro end-points, FoalTS seems to have generated a new session for each one of them. However if it's in the published version where Angular is compiled and put in the public folder, the new session issue goes away as expected.

The way I've gone around this is to move the the @UseSessions decorator from app.controller to the frontend, since my backend relies solely on JWT and I try to keep the backend stateless but have a custom session management in case I need to use it.

LoicPoullain commented 3 years ago

Thank you for the details. How is your Angular dev app wired to your backend? Have you used foal connect angular? Do you use another approach (with CORS, etc)?

crossinghoods commented 3 years ago

This project was started in FoalTS 1.0 when it was still using Mongoose, don't think I remember running foal connect angular.

I then manually updated and migrated the project to now the latest FoalTS 2.0.

In terms of CORS etc.
I have :-

@Cors() //  <--- example from your site
@SessionHandler() // <-- custom session handler for JWT verification and fetching JWT session to ctx.sessions
export class AppController {
.....
}

For the Angular side of things, I have environment.ts to setup where to connect.

Am I the only one facing this issue of multiple sessions with Angular at the moment, or has it become a bit more widespread? :0

LoicPoullain commented 3 years ago

I think the problem comes from CORS configuration. When @UseSessions creates a cookie on the client, the cookie is probably not sent back on subsequent requests due to the same-origin policy. This would explain why sessions are re-recreated on each request and why we don't have this issue in production when we use one single origin (i.e. we don't have two ports).

Can you take a look at https://foalts.org/docs/api-section/public-api-and-cors-requests#cors-requests-and-cookies and see how it goes?

You might also be interested in the connect command which will configure Angular CLI so you do not have to manage CORS requests or deal with two different ports in development.

crossinghoods commented 3 years ago

Thanks Loic,

Appreciate you spending time to help me debug this issue! Tried the solution in your link you gave, have added { withCredentials:true } and can see it being passed on the browser to FoalTS however as soon as it reaches @UseSessions({ store: MongoDBStore, cookie: true }) it just creates a new session again.

The @Hook sequence on app.controller is @Cors() // <-- this just sets the header to allow origin and credentials @UseSessions({ store: MongoDBStore, cookie: true }) @SessionHandler() // <--- I've put debug in to inspect it on node inspector

Request Cookie

sessionID | cXoN6LufIn15HQxkpK5aA4I0K2l8KPOpUYYi1hxYmm8 | localhost | / | 2021-01-15T16:17:51.062Z | 52 | ✓ |   |   | Medium

Response Cookie

sessionID | cXoN6LufIn15HQxkpK5aA4I0K2l8KPOpUYYi1hxYmm8 | localhost | / | 2021-01-15T16:17:51.062Z | 52 | ✓ |   |   | Medium

General Request Info

Request URL: http://localhost:3001/admin/api/products/status
Request Method: GET
Status Code: 200 OK
Remote Address: [::1]:3001
Referrer Policy: strict-origin-when-cross-origin

Request Header

GET /admin/api/products/status HTTP/1.1
Host: localhost:3001
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Google Chrome";v="87", " Not;A Brand";v="99", "Chromium";v="87"
Accept: application/json, text/plain, */*
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZWU5NGUxMWQ4OGY1ZjM1YmJlNDA1YjgiLCJhdmF0YXIiOiIvYWRtaW4vYXNzZXRzL2ltZ3MvYXZhdGFyLnBuZyIsImVtYWlsIjoiYWRtaW5AZ21haWwuY29tIiwibmFtZSI6IlN1cGVyIEFkbWluIiwicm9sZSI6MSwic2Vzc2lvblRva2VuIjoiZDdpaEVJb2F5TkdwUjI2ZkNaODJhck5BcHNrOGF1WlJkcFcxaUhMRWM0TSIsInN1YiI6IjVlZTk0ZTExZDg4ZjVmMzViYmU0MDViOCIsImlhdCI6MTYxMDcyNjQ2NiwiZXhwIjoxNjEwNzI3MzY2fQ.-Hs58oitxBOarCSv1EfrUyFSMILWqsMjKL3od3mngyA
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Origin: http://localhost:4200
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:4200/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: sessionID=cXoN6LufIn15HQxkpK5aA4I0K2l8KPOpUYYi1hxYmm8
dnt: 1
sec-gpc: 1

Response Header

HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=15552000; includeSubDomains
Access-Control-Allow-Origin: http://localhost:4200
Access-Control-Allow-Credentials: true
Set-Cookie: sessionID=cXoN6LufIn15HQxkpK5aA4I0K2l8KPOpUYYi1hxYmm8; Path=/; Expires=Fri, 15 Jan 2021 16:31:44 GMT; HttpOnly
Content-Type: application/json; charset=utf-8
Content-Length: 169
ETag: W/"a9-xG3GF4tgZ2gTV+wfUmnGDzcKcX8"
Date: Fri, 15 Jan 2021 16:01:44 GMT
Connection: keep-alive
Keep-Alive: timeout=5
LoicPoullain commented 3 years ago

Are you sure it re-creates a new session? It looks like there is only one whose ID is cXoN6LufIn15HQxkpK5aA4I0K2l8KPOpUYYi1hxYmm8 (cookie value). What's in your MongoDB collection named sessions?

crossinghoods commented 3 years ago

Yeah I cleared the table "session" which was created by @UseSession and did the login > then replayed one of the request.

Each time I reply the same request, a new entry is created in the sessions table.

LoicPoullain commented 3 years ago

Do you use @UseSessions at several points of your applications? Could it be possible that @UseSessions is applied twice (for example it decorates a controller and a subcontroller or a method)?

crossinghoods commented 3 years ago

I only have @UseSession in app.controller which then loads frontend and backend (but as mentioned before it's been shifted to frontend to avoid the extra session generation. As soon as I move it to the frontend, it doesn't generate anymore extra sessions.

app.controller ---> frontend.controller ---> backend.controller

LoicPoullain commented 3 years ago

@crossinghoods would you have some time to create an example Github repo that reproduces the problem? It will helps me debug the code and figure out what's going on.

crossinghoods commented 3 years ago

Was thinking about that about when I was responding but was a bit tied up with work, I'll try to get it uploaded here as soon as I can

crossinghoods commented 3 years ago

Did stripped down version with angular and { WithCredentials: true} but am not able to recreate the session duplication issue.

Think I'll have to call it an issue within my code :(