Session Tokens
Introduction
This document assumes that you have alread read the Quick Start page.
In FoalTS, web sessions are temporary states associated with a specific user. They are identified by a token and are mainly used to keep users authenticated between several HTTP requests (the client sends the token on each request to authenticate the user).
A session usually begins when the user logs in and ends after a period of inactivity or when the user logs out. By inactivity, we mean that the server no longer receives requests from the authenticated user for a certain period of time.
Get Started
Provide a Secret
In order to use sessions, you must provide a base64-encoded secret in either:
-
a configuration file
Example with config/default.yml
settings:
session:
secret: xxx -
or in a
.env
file or in an environment variable:SETTINGS_SESSION_SECRET=xxx
You can generate such a secret with the CLI command:
foal createsecret
Choose a Session Store
Then you have to choose where the temporary session state will be stored. FoalTS provides several session stores for this. For example, you can use the TypeORMStore
to save the sessions in your SQL database or the RedisStore
to save them in a redis cache.
These session stores are services and can therefore be injected into your controllers and services as such:
export class AuthController {
@dependency
store: TypeORMStore;
@Post('/login')
// ...
login() {
// ...
const store = this.store;
}
}
Create the Session and Get the Token (Log In)
Sessions are created using the method createAndSaveSessionFromUser
of the session store. It takes one parameter: an object that must have an id
attribute (the user id). At login time, the user is usually retrieved upstream when checking credentials.
const session = await store.createAndSaveSessionFromUser(user);
// Alternatively, you can also call the `createAndSaveSession` method as follows:
const session = await store.createAndSaveSession({ userId: user.id });
The session token then can be read with the method getToken()
to send it back to the client. This token identifies the session.
const token = session.getToken();
Use the Session Token to Retrieve the Session
On each subsequent request, the client must send this token in order to retrieve the session and authenticate the user. It must be included in the Authorization
header using the bearer scheme (unless you use cookies, see section below).
Authorization: Bearer my-session-token
The hooks @TokenRequired
and @TokenOptional
will then check the token and retrieve the associated session and user.
import { Context, Get, HttpResponseOK, TokenRequired } from '@foal/core';
import { TypeORMStore } from '@foal/typeorm';
@TokenRequired({ store: TypeORMStore })
class ApiController {
@Get('/products')
readProducts(ctx: Context) {
// ctx.user and ctx.session are defined.
return new HttpResponseOK();
}
}
If the header Authorization
is not found or does not use the bearer
scheme, the hook @TokenRequired
returns an error 400 - BAD REQUEST. The @TokenOptional
hook does nothing.
If the token is present and not valid or if the associated session has expired, both hooks return an error 401 - UNAUTHORIZED.
In other cases, the hooks retrieve the session from the store and assign it to the Context.session
property. As for the session user ID, it is assigned to Context.user
.
If you want the ctx.user
to be an object or an instance of the User
class, you must pass to the hook user
option a function whose signature is:
(id: string|number) => Promise<any|undefined>
The hooks will assign the value it returns to ctx.user
.
For example, you can use the fetchUser
function to retrieve the user from the database:
import { Context, Get, HttpResponseOK, TokenRequired } from '@foal/core';
import { fetchUser, TypeORMStore } from '@foal/typeorm';
import { User } from '../entities';
@TokenRequired({
store: TypeORMStore,
user: fetchUser(User)
})
class ApiController {
@Get('/products')
readProducts(ctx: Context) {
// ctx.user is an instance of User
return new HttpResponseOK();
}
}
Note: The hooks @TokenRequired
and @TokenOptional
are responsible for extending the session life each time a request is received.
Alternatively, you can also manually verify a session token and read its associated session. The code below shows how to do so.
const token = // ...
const sessionID = Session.verifyTokenAndGetId(token);
if (!sessionID) {
throw new Error('Invalid session token.');
}
const session = await store.read(sessionID);
if (!session) {
throw new Error('Session does not exist or has expired.')
}
const userId = session.get('userId');
Destroy the Session (Log Out)
Sessions are can be destroyed (i.e users can be logged out) using the destroy
method of the session store.
import { Context, dependency, HttpResponseNoContent, TokenRequired, Session } from '@foal/core';
import { TypeORMStore } from '@foal/typeorm';
export class AuthController {
@dependency
store: TypeORMStore;
@Post('/logout')
@TokenRequired({ store: TypeORMStore, extendLifeTimeOrUpdate: false })
async logout(ctx: Context<any, Session>) {
await this.store.destroy(ctx.session.sessionID);
return new HttpResponseNoContent();
}
}