Authentication with JWT
You are reading the documentation for version 2 of FoalTS. Instructions for upgrading to this version are available here. The old documentation can be found here.
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Source: https://jwt.io/introduction/
Foal offers a package, named @foal/jwt
, to manage authentication / authorization with JSON Web Tokens. When the user logs in, a token is generated and sent to the client. Then, each subsequent request must include this JWT, allowing the user to access routes, services, and resources that are permitted with that token.
#
Generate & Provide a SecretIn order to use JWTs, you must provide a secret to sign your tokens. If you do not already have your own, you can generate one with the foal createsecret
command.
Alternatively you can use a public/private key pair to sign your tokens. In this case, please refer to the advanced section below.
Once the secret is in hand, there are several ways to provide it to the future hooks:
- YAML
- JSON
- JS
#
Generate & Send Temporary TokensJSON Web Tokens are generated from JavaScript objects that usually contain information about the user.
The below example shows how to generate a one-hour token using a secret.
The
getSecretOrPrivateKey
function tries to read the configurationssettings.jwt.secret
andsettings.jwt.privateKey
. It throws an error if not value is provided. The functiongetSecretOrPublicKey
works similarly.
- The
subject
property (orsub
) is only required when making a database call to get more user properties. - Each token should have an expiration time. Otherwise, the JWT will be valid indefinitely, which will raise security issues.
LoginController
#
Example of a The below example shows how to implement a login controller with an email and a password.
login.controller.ts
user.entity.ts
#
Receive & Verify TokensFoal provides two hooks to authenticate users upon subsequent requests: JWTOptional
and JWTRequired
. They both expect the client to send the JWT in the Authorization header using the Bearer schema.
In other words, the content of the header should look like the following:
If no token is provided, the JWTRequired
hook returns an error 400 - BAD REQUEST while JWTOptional
does nothing.
If a token is provided and valid, the hooks set the Context.user
with the decoded payload (default behavior).
Example
#
Advanced#
Blacklist TokensIn the event that a jwt has been stolen by an attacker, the application must be able to revoke the compromised token. This can be done by establishing a black list. Revoked tokens are no longer considered valid and the hooks return a 401 error - UNAUTHORIZED when they receive one.
The isInFile
function takes a token and returns a boolean specifying if the token is revoked or not.
You can provide your own function (in the case you want to use a cache database for example). This function must have this signature:
#
Refresh the tokensHaving a too-long expiration date for JSON Web Tokens is not recommend as it increases exposure to attacks based on token hijacking. If an attacker succeeds in stealing a token with an insufficient expiration date, he/she will have plenty of time to make other attacks and harm your application.
In order to minimize the exposure, it is recommend to set a short expiration date (15 minutes for common applications) to quickly invalidate tokens. In this way, even if a token is stolen, it will quickly become unusable since it will have expired.
One of the disadvantages of having short expiration dates, however, is that users get logged out too often which is not very user-friendly.
One way to get around this problem is to generate and send a new token on each request. The client then saves this new token and uses it on further requests. In this way, if users are inactive more than 15 minutes, they are disconnected. Otherwise, the user will still be connected but the application will use a different token.
The below code shows how to implement this technique with a hook. On each request, the client will receive a new token in the Authorization
header of the response. Other implementations are still possible (especially if you use cookies).
Note that when a new token is generated, the previous one is still valid until its expiration date.
refresh-jwt.hook.ts (example)
api.controller.ts (example)
#
Make a Database Call to Get More User PropertiesIn several cases, the decoded payload is not sufficient. We may need to fetch extra properties from the database, such as the user permissions for example, or simply want the Context.user
to a be a model instance instead of a plain object.
In these cases, the two hooks JWTRequired
and JWTOptional
offer a user
option to transform the decoded payload into something else. To do this,
Each JSON Web Token must have a
subject
property (orsub
) which is a string containing the user id. If the id is a number, it must be converted to a string using, for example, thetoString()
method.The hook must be provided a function that takes a string id (the
subject
) as parameter and returns the value of theContext.user
. If the function returnsundefined
, the hooks returns an error 401 - UNAUTHORIZED.Example with TypeORM (SQL database)
Example with TypeORM (MongoDB)
Example with a custom function
#
Specifying a Different Encoding for SecretsBy default, UTF-8 is used to encode the secret string into bytes when verifying the token. However, you can use another character encoding with the settings.jwt.secretEncoding
configuration key.
Available encodings are listed here.
- YAML
- JSON
- JS
#
Usage with CookiesBe aware that if you use cookies, your application must provide a CSRF defense.
By default, the hooks expect the token to be sent in the Authorization header using the Bearer schema. But it is also possible to send the token in a cookie with the cookie
option.
api.controller.ts
auth.controller.ts
Note: the cookie expire date is equal to the JWT expire date.
#
Cookie options- YAML
- JSON
- JS
#
Use RSA or ECDSA public/private keysJWTs can also be signed using a public/private key pair using RSA or ECDSA.
#
Provide the Public/Private KeyExample with a .env
file
#
Generate Temporary TokensExample
#
Receive & Verify TokensExample with RSA
#
Audience, Issuer and Other OptionsThe second parameter of JWTOptional
and JWTRequired
allows to specify the required audience or issuer as well as other properties. It is passed as options to the verify
function of the jsonwebtoken library.
Example checking the audience
Example checking the issuer
#
Retreive a Dynamic Secret Or Public KeyBy default JWTRequired
and JWTOptional
use the value of the configuration keys settings.jwt.secret
or settings.jwt.publicKey
as a static secret (or public key).
But it is also possible to dynamically retrieve a key to verify the token. To do so, you can specify a function with the below signature to the secretOrPublicKey
option.
Example
If needed, this function can throw an InvalidTokenError
to return a 401 error to the client.
Example
In the above example, if the algorithm specified in the token is not RS256
, then the server will respond a 401 - UNAUTHORIZED
error with this content:
#
Retreive a Public Key from a JWKS endpointThe getRSAPublicKeyFromJWKS
allows you to retreive a public key from a JWKS endpoint. It is based on the jwks-rsa
library.
Example
#
Auth0 and AWS Cognito (examples)Auth0 & AWS Cognito are both platforms to manage authentication & authorization.
This section provides examples on how to decode and verify JWTs generated by these platforms (the id_token
). It assumes that you are already familiar with them.
Auth0
AWS Cognito
Note: The above example does not use a secret for simplicity.
#
Hook ErrorsError | Response Status | Response Body | WWW-Authenticate Response Header |
---|---|---|---|
No secret or public key is provided in default.json or as environment variable. | 500 | ||
The Authorization header does not exist (only for JWTRequired ). | 400 | { code: 'invalid_request', description: 'Authorization header not found.' } | |
The auth cookie does not exist (only for JWTRequired ). | 400 | { code: 'invalid_request', description: 'Auth cookie not found.' } | |
The Authorization header does use the Bearer scheme. | 400 | { code: 'invalid_request', description: 'Expected a bearer token. Scheme is Authorization: Bearer <token>.' } | |
The token is black listed. | 401 | { code: 'invalid_token', description: 'jwt revoked' } | error="invalid_token", error_description="jwt revoked" |
The token is not a JWT. | 401 | { code: 'invalid_token', description: 'jwt malformed' } | error="invalid_token", error_description="jwt malformed" |
The signature is invalid. | 401 | { code: 'invalid_token', description: 'jwt signature' } | error="invalid_token", error_description="jwt signature" |
The token is expired. | 401 | { code: 'invalid_token', description: 'jwt expired' } | error="invalid_token", error_description="jwt expired" |
The audience is not expected. | 401 | { code: 'invalid_token', description: 'jwt audience invalid. expected: xxx' } | error="invalid_token", error_description="jwt audience invalid. expected: xxx" |
The issuer is not expected. | 401 | { code: 'invalid_token', description: 'jwt issuer invalid. expected: xxx' } | error="invalid_token", error_description="jwt issuer invalid. expected: xxx" |
There is no subject claim and options.user is defined. | 401 | { code: 'invalid_token', description: 'The token must include a subject which is the id of the user.' } | error="invalid_token", error_description="The token must include a subject which is the id of the user." |
options.user is defined and no user was found from its value (function). | 401 | { code: 'invalid_token', description: 'The token subject does not match any user.' } | error="invalid_token", error_description="The token subject does not match any user." |