What's new in version 2 (part 3/4)
This article presents some improvements introduced in version 2 of FoalTS:
- the JWT utilities to manage secrets and RSA keys,
- the JWT utilities to manage cookies,
- and the new stateless CSRF protection.
This article is the part 3 of the series of articles What's new in version 2.0. Part 2 can be found here.
New JWT utilities
Accessing config secrets and public/private keys
Starting from version 2, there is a standardized way to provide and retrieve JWT secrets and RSA public/private keys: the functions getSecretOrPublicKey
and getSecretOrPrivateKey
.
Using secrets
In this example, a base64-encoded secret is provided in the configuration.
.env
JWT_SECRET="Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8="
- YAML
- JSON
- JS
settings:
jwt:
secret: "env(JWT_SECRET)"
secretEncoding: base64
{
"settings": {
"jwt": {
"secret": "env(JWT_SECRET)",
"secretEncoding": "base64"
}
}
}
module.exports = {
settings: {
jwt: {
secret: "env(JWT_SECRET)",
secretEncoding: "base64"
}
}
}
Both getSecretOrPublicKey
and getSecretOrPrivateKey
functions will return the secret.
In the case a secretEncoding
value is provided, the functions return a buffer which is the secret decoded with the provided encoding.
Using public and private keys
const { Env } = require('@foal/core');
const { readFileSync } = require('fs');
module.exports = {
settings: {
jwt: {
privateKey: Env.get('RSA_PRIVATE_KEY') || readFileSync('./id_rsa', 'utf8'),
publicKey: Env.get('RSA_PUBLIC_KEY') || readFileSync('./id_rsa.pub', 'utf8'),
}
}
}
In this case, getSecretOrPublicKey
and getSecretOrPrivateKey
return the keys from the environment variables RSA_PUBLIC_KEY
and RSA_PRIVATE_KEY
if they are defined or from the files id_rsa
and id_rsa.pub
otherwise.
Managing cookies
In version 2, Foal provides two dedicated functions to manage JWT with cookies. Using these functions instead of manually setting the cookie has three benefits:
- they include a CSRF protection (see section below),
- the function
setAuthCookie
automatically sets the cookie expiration based on the token expiration, - and cookie options can be provided through the configuration.
Example
api.controller.ts
import { JWTRequired } from '@foal/jwt';
@JWTRequired({ cookie: true })
export class ApiController {
// ...
}
auth.controller.ts
export class AuthController {
@Post('/login')
async login(ctx: Context) {
// ...
const response = new HttpResponseNoContent();
// Do not forget the "await" keyword.
await setAuthCookie(response, token);
return response;
}
@Post('/logout')
logout(ctx: Context) {
// ...
const response = new HttpResponseNoContent();
removeAuthCookie(response);
return response;
}
}
Cookie options
- YAML
- JSON
- JS
settings:
jwt:
cookie:
name: mycookiename # Default: auth
domain: example.com
httpOnly: true # Warning: unlike session tokens, the httpOnly directive has no default value.
path: /foo # Default: /
sameSite: strict # Default: lax if settings.jwt.csrf.enabled is true.
secure: true
{
"settings": {
"jwt": {
"cookie": {
"name": "mycookiename",
"domain": "example.com",
"httpOnly": true,
"path": "/foo",
"sameSite": "strict",
"secure": true
}
}
}
}
module.exports = {
settings: {
jwt: {
cookie: {
name: "mycookiename",
domain: "example.com",
httpOnly: true,
path: "/foo",
sameSite: "strict",
secure: true
}
}
}
}
Stateless CSRF protection simplified
In version 1, providing a CSRF protection was quite complex. We needed to provide another secret, generate a stateless token, manage the CSRF cookie (expiration, etc), use an additional hook, etc.
Starting from version 2, the CSRF protection is all managed by @JWTRequired
, setAuthCookie
and removeAuthCookie
.
The only thing that you have to do it to enable it through the configuration:
- YAML
- JSON
- JS
settings:
jwt:
csrf:
enabled: true
{
"settings": {
"jwt": {
"csrf": {
"enabled": true
}
}
}
}
module.exports = {
settings: {
jwt: {
csrf: {
enabled: true
}
}
}
}
When it is enabled, an additional XSRF-TOKEN
cookie is sent to the client at the same time as the auth cookie (containing your JWT). It contains a stateless CSRF token which is signed and has the same expiration date as your JWT.
When a request is made to the server, the @JWTRequired
hooks expects you to include its value in the XSRF-TOKEN
header.