Session Tokens & CSRF Protection
The main feature of FoalTS version 2 is the new session management. The new authentication system is intended to be more intuitive and to require less code and configuration. It also offers new functionalities.
Note that when upgrading to version 2, all your users will be automatically logged out.
Overview
Previously, we had to manage a lot of functions and hooks to authenticate users in FoalTS: @TokenRequired
, @TokenOptional
, removeSessionCookie
, setSessionCookie
, getCsrfToken
, @CsrfTokenRequired
, setCsrfCookie
.
Starting with version 2, they have all been removed and replaced with two unique hooks: @UseSessions
and @UserRequired
.
There is also no need for a session secret anymore. The config parameter settings.session.secret
can therefore be removed.
Session Tokens
Choosing and Configuring the Session Store
Specify the Store in The Configuration
Since v1.11.0, FoalTS allows you to globally specify in the configuration which session store to use. This is now the recommended approach and it is assumed that you use it in all examples in the documentation.
To specify the store globally, replace all references to TypeORMStore
(or redis, mongo, etc) with Store
and remove the store: TypeORMStore
option from your hooks.
Example
import { Store } from '@foal/core';
class AppController {
// Before
@dependency
store: TypeORMStore;
// After
@dependency
store: Store;
@Get('/products')
// Before
@TokenRequired({ store: TypeStore })
// After
@TokenRequired()
readProducts() {
// ...
}
}
Then, in the configuration, specify the package name of your session store (@foal/typeorm
, @foal/redis
, etc).
- YAML
- JSON
- JS
settings:
session:
store: "@foal/typeorm"
{
"settings": {
"session": {
"store": "@foal/typeorm",
}
}
}
module.exports = {
settings: {
session: {
store: "@foal/typeorm",
}
}
}
TypeORM Store
Warning: Starting from version 2, TypeORMStore
only support numbers (not strings) as user IDs.
In version 1, when launching the application, Foal was making a request to the database to create the session table if it does not exist.
This had two drawbacks:
- This may make too many undesirable requests to the database in a severless environment.
- The database schema is updated at runtime outside the classical migration process. This practice can be dangerous and it does not allow to keep a traceability of the modifications of the database schema unlike migrations (revert, etc).
Starting from version 2, you must generate and run migrations yourself to create the session table.
The easier way to achieve this is probably to export the DatabaseSession
entity from @foal/typeorm
and to run the following commands.
user.entity.ts
// ...
@Entity()
export class User extends BaseEntity {
}
export { DatabaseSession } from '@foal/typeorm';
npm run makemigrations
npm run migrations
Once you application is migrated to version 2 and works as expected, you will be able to manually delete the old
foal_session
table. The new table used by the framework is nameddatabase_session
.
Redis Store
The configuration key redis.uri
has been renamed to settings.redis.uri
.
See also this.
Note: In the Redis database, session keys now start with
sessions:
instead ofsession:
.
MongoDB Store
The configuration key mongodb.uri
has been renamed to settings.mongodb.uri
.
See also this.
Once you application is migrated to version 2 and works as expected, you will be able to manually delete the old
foalSessions
collection. The new collection used by the framework is namedsessions
.
Custom Store
Due to the complexity of implementing a store in version 1, it is unlikely that one has been implemented.
However, if it has, the best way to upgrade it to version 2 is to rewrite it from scratch using the documentation.
New Login
You may be interested in looking at the quick start page as well.
Example with the Authorization
header
Before
export class AuthController {
@dependency
store: TypeORMStore;
@Post('/login')
@ValidateBody(credentialsSchema)
async login(ctx: Context) {
const user = await getRepository(User).findOne({ email: ctx.request.body.email });
if (!user) {
return new HttpResponseUnauthorized();
}
if (!await verifyPassword(ctx.request.body.password, user.password)) {
return new HttpResponseUnauthorized();
}
const session = await this.store.createAndSaveSessionFromUser(user);
return new HttpResponseOK({
token: session.getToken()
});
}
}
After
import { UseSessions, Store } from '@foal/core';
@UseSessions()
export class AuthController {
@dependency
store: Store;
@Post('/login')
@ValidateBody(credentialsSchema)
async login(ctx: Context) {
const user = await getRepository(User).findOne({ email: ctx.request.body.email });
if (!user) {
return new HttpResponseUnauthorized();
}
if (!await verifyPassword(ctx.request.body.password, user.password)) {
return new HttpResponseUnauthorized();
}
ctx.session = await createSession(this.store);
ctx.session.setUser(user);
return new HttpResponseOK({
token: ctx.session.getToken()
});
}
}
Example with cookies
Before
export class AuthController {
@dependency
store: TypeORMStore;
@Post('/login')
@ValidateBody(credentialsSchema)
async login(ctx: Context) {
const user = await getRepository(User).findOne({ email: ctx.request.body.email });
if (!user) {
return new HttpResponseUnauthorized();
}
if (!await verifyPassword(ctx.request.body.password, user.password)) {
return new HttpResponseUnauthorized();
}
const session = await this.store.createAndSaveSessionFromUser(user);
const response = new HttpResponseNoContent();
const token = session.getToken();
setSessionCookie(response, token);
return response;
}
}
After
import { UseSessions, Store } from '@foal/core';
@UseSessions({
cookie: true,
// user: fetchUser(User)
})
export class AuthController {
// This line is required: the store must be injected in at least one controller.
@dependency
store: Store;
@Post('/login')
@ValidateBody(credentialsSchema)
async login(ctx: Context<any, Session>) {
const user = await getRepository(User).findOne({ email: ctx.request.body.email });
if (!user) {
return new HttpResponseUnauthorized();
}
if (!await verifyPassword(ctx.request.body.password, user.password)) {
return new HttpResponseUnauthorized();
}
ctx.session.setUser(user);
return new HttpResponseOK();
}
}
New Logout
You may be interested in looking at the quick start page as well.
In version 2, you don't need to talk directly to the store, use weird options (such as extendLifeTimeOrUpdate
) or manage cookies yourself.
Just call session.destroy()
and FoalTS will take care of everything else.
Example with the Authorization
header
Before
export class AuthController {
@dependency
store: TypeORMStore;
@Post('/logout')
@TokenRequired({
extendLifeTimeOrUpdate: false,
store: TypeORMStore,
})
async logout(ctx: Context<any, Session>) {
await this.store.destroy(ctx.session.sessionID);
return new HttpResponseNoContent();
}
}
After
export class AuthController {
@Post('/logout')
@UseSessions()
async logout(ctx: Context) {
if (ctx.session) {
await ctx.session.destroy();
}
return new HttpResponseNoContent();
}
}
Example with cookies
Before
export class AuthController {
@dependency
store: TypeORMStore;
@Post('/logout')
@TokenRequired({
cookie: true,
extendLifeTimeOrUpdate: false,
store: TypeORMStore,
})
async logout(ctx: Context<any, Session>) {
await this.store.destroy(ctx.session.sessionID);
const response = new HttpResponseNoContent();
removeSessionCookie(response);
return response;
}
}
After
export class AuthController {
@Post('/logout')
@UseSessions({
cookie: true,
})
async logout(ctx: Context) {
if (ctx.session) {
await ctx.session.destroy();
}
return new HttpResponseNoContent();
}
}
Access Control
You may be interested in looking at the quick start page as well.
Example with the Authorization
header
Before
@TokenRequired({ store: TypeORMStore, user: fetchUser(User) })
export class ApiController {
@Get('/products')
readProducts() {
return new HttpResponseOK([]);
}
}
After
// The `request` option returns a pretty message if the Authorization header is not here.
@UseSessions({
required: true,
user: fetchUser(User)
})
@UserRequired()
export class ApiController {
@Get('/products')
readProducts() {
return new HttpResponseOK([]);
}
}
Example with cookies
Before
@TokenRequired({ store: TypeORMStore, cookie: true, user: fetchUser(User) })
export class ApiController {
@Get('/products')
readProducts() {
return new HttpResponseOK([]);
}
}
After
@UseSessions({
cookie: true,
user: fetchUser(User)
})
@UserRequired()
export class ApiController {
@Get('/products')
readProducts() {
return new HttpResponseOK([]);
}
}
Send the CSRF token in a template
// Before
return render('templates/home.html', { csrfToken: await getCsrfToken(ctx.session) });
// After
return render('templates/home.html', { csrfToken: ctx.session.get('csrfToken') });
Read or create a session
To read or create a session manually, use the function createSession
and readSession
instead of the store directly.
Session.verifyTokenAndGetId(token)
is removed.
Revoking sessions
See session tokens
Breaking Changes that Should not Affect You
If you are affected, it's probably you do not use the component the right way.
- The Session constructor has changes. You should not instantiate it yourself. Use
readSession
orcreateSession
instead. - Except for the
get
andset
methods, the interface ofSession
has changed. - The methods
getRedisInstance
andgetMongoInstance
have been removed from the stores.
CSRF Protection
npm uninstall @foal/csrf
The package @foal/csrf
has been removed. In version 2, the CSRF protection is directly included in the @UseSessions
hook and can be enabled with settings.session.csrf.enabled
(the configuration key settings.csrf.enabled
does not exist anymore).
You do not need to take care of generating a CSRF token in the session. The framework handles it for you.
The best way to use the new CSRF protection is to go directly to the CSRF page.
Warning: In order to work better with some popular frontend frameworks, the default name of the CSRF cookie has been changed from csrfToken
to XSRF-TOKEN
.
New Features
Session Tokens
Query all sessions of a user (TypeORM only)
See session tokens
Query all connected users (TypeORM only)
See session tokens
Force the disconnection of a user (TypeORM only)
See session tokens
Flash sessions
See session tokens
Regenerate the session ID
See session tokens
Expired sessions clean up regularly (TypeORM and MongoDB)
See session tokens
Anonymous sessions and templates
See session tokens