JWT docs overhaul (#1001)

Update JWT docs for v5
This commit is contained in:
Paul Toffoloni 2024-10-03 17:51:13 +02:00 committed by GitHub
parent 4b0e25d369
commit 15f64e2eb5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 197 additions and 186 deletions

View File

@ -14,10 +14,12 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Create virtual environment
run: python3 -m venv venv
- name: Install dependencies - name: Install dependencies
run: pip install -r requirements.txt run: venv/bin/pip install -r requirements.txt
- name: Build documentation - name: Build documentation
run: mkdocs build run: venv/bin/mkdocs build
- name: Setup cloudformation linter - name: Setup cloudformation linter
uses: ScottBrenner/cfn-lint-action@v2 uses: ScottBrenner/cfn-lint-action@v2
- name: Run cloudformation lint - name: Run cloudformation lint

View File

@ -17,11 +17,13 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Create virtual environment
run: python3 -m venv venv
- name: Install dependencies - name: Install dependencies
run: pip install -r requirements.txt run: venv/bin/pip install -r requirements.txt
- name: Build the website - name: Build the website
run: | run: |
mkdocs build venv/bin/mkdocs build
swift fixSearchIndex.swift swift fixSearchIndex.swift
cp googlefc012e5d94cfa05f.html site/googlefc012e5d94cfa05f.html cp googlefc012e5d94cfa05f.html site/googlefc012e5d94cfa05f.html
swift setUpRedirects.swift swift setUpRedirects.swift

View File

@ -1,20 +1,23 @@
# JWT # JWT
JSON Web Token (JWT) is an open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) 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. JSON Web Token (JWT) is an open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) 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 are particularly useful in web applications, where they are commonly used for stateless authentication/authorization and information exchange. You can read more about the theory behind JWTs in the spec linked above or on [jwt.io](https://jwt.io/introduction).
Vapor provides first-class support for JWTs through the `JWT` module. This module is built on top of the `JWTKit` library, which is a Swift implementation of the JWT standard based on [SwiftCrypto](https://github.com/apple/swift-crypto). JWTKit provides signers and verifiers for a variety of algorithms, including HMAC, ECDSA, EdDSA, and RSA.
## Getting Started ## Getting Started
The first step to using JWT is adding the dependency to your [Package.swift](../getting-started/spm.md#package-manifest). The first step to using JWTs in your Vapor application is to add the `JWT` dependency to your project's `Package.swift` file:
```swift ```swift
// swift-tools-version:5.2 // swift-tools-version:5.10
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "my-app", name: "my-app",
dependencies: [ dependencies: [
// Other dependencies... // Other dependencies...
.package(url: "https://github.com/vapor/jwt.git", from: "5.0.0-beta"), .package(url: "https://github.com/vapor/jwt.git", from: "5.0.0-rc"),
], ],
targets: [ targets: [
.target(name: "App", dependencies: [ .target(name: "App", dependencies: [
@ -26,35 +29,32 @@ let package = Package(
) )
``` ```
If you edit the manifest directly inside Xcode, it will automatically pick up the changes and fetch the new dependency when the file is saved. Otherwise, run `swift package resolve` to fetch the new dependency.
### Configuration ### Configuration
The JWT module adds a new property `jwt` to `Application` that is used for configuration. To sign or verify JWTs, you will need to add a key. The simplest signing algorithm is `HS256` or HMAC with SHA-256. After adding the dependency, you can start using the `JWT` module in your application. The JWT module adds a new `jwt` property to `Application` that is used for configuration, of which the internals are provided by the [JWTKit](https://github.com/vapor/jwt-kit) library.
#### Key Collection
The `jwt` object comes with a `keys` property, which is an instance of JWTKit's `JWTKeyCollection`. This collection is used to store and manage the keys used to sign and verify JWTs. The `JWTKeyCollection` is an `actor`, which means that all operations on the collection are serialized and thread-safe.
To sign or verify JWTs, you will need to add a key to the collection. This is usually done in your `configure.swift` file:
```swift ```swift
import JWT import JWT
// Add HMAC with SHA-256 signer. // Add HMAC with SHA-256 signer.
await app.jwt.keys.addHMAC(key: "secret", digestAlgorithm: .sha256) await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)
``` ```
!!! note This adds an HMAC key with SHA-256 as the digest algorithm to the keychain, or HS256 in JWA notation. Check out the [algorithms](#algorithms) section below for more information on the available algorithms.
The `await` keyword is required because the key collection is an `actor`.
The `HS256` signer requires a key to initialize. Unlike other signers, this single key is used for both signing _and_ verifying tokens. Learn more about the available [algorithms](#algorithms) below. !!! note
Be sure to replace `"secret"` with an actual secret key. This key should be kept secure, ideally in a configuration file or environment variable.
### Payload ### Signing
Let's try to verify the following example JWT. The added key can then be used to sign JWTs. To do this,
you first of all need _something_ to sign, namely a 'payload'.
```swift This payload is simply a JSON object containing the data you want to transmit. You can create your custom payload by conforming your structure to the `JWTPayload` protocol:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
```
You can inspect the contents of this token by visiting [jwt.io](https://jwt.io) and pasting the token in the debugger. Set the key in the "Verify Signature" section to `secret`.
We need to create a struct conforming to `JWTPayload` that represents the JWT's structure. We'll use JWT's included [claims](#claims) to handle common fields like `sub` and `exp`.
```swift ```swift
// JWT payload structure. // JWT payload structure.
@ -89,9 +89,32 @@ struct TestPayload: JWTPayload {
} }
``` ```
### Verify Signing the payload is done by calling the `sign` method on the `JWT` module, for example inside of a route handler:
Now that we have a `JWTPayload`, we can attach the JWT above to a request and use `req.jwt` to fetch and verify it. Add the following route to your project. ```swift
app.post("login") { req async throws -> [String: String]
let payload = TestPayload(
subject: "vapor",
expiration: .init(value: .distantFuture),
isAdmin: true
)
return try await ["token": req.jwt.sign(payload)]
}
```
When a request is made to this endpoint, it will return the signed JWT as a `String` in the response body, and if everything went according to plan, you'll see something like this:
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo"
}
```
You can decode and verify this token using the [`jwt.io` debugger](https://jwt.io/#debugger). The debugger will show you the payload (which should be the data you specified earlier) and header of the JWT, and you can verify the signature using the secret key you used to sign the JWT.
### Verifying
When a token is instead sent _to_ your application, you can verify the authenticity of the token by calling the `verify` method on the `JWT` module:
```swift ```swift
// Fetch and verify JWT from incoming request. // Fetch and verify JWT from incoming request.
@ -102,16 +125,15 @@ app.get("me") { req async throws -> HTTPStatus in
} }
``` ```
The `req.jwt.verify` helper will check the `Authorization` header for a bearer token. If one exists, it will parse the JWT and verify its signature and claims. If any of these steps fail, a _401 Unauthorized_ error will be thrown. The `req.jwt.verify` helper will check the `Authorization` header for a bearer token. If one exists, it will parse the JWT and verify its signature and claims. If any of these steps fail, a 401 Unauthorized error will be thrown.
Test the route by sending the following HTTP request:
Test the route by sending the following HTTP request.
```http ```http
GET /me HTTP/1.1 GET /me HTTP/1.1
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
``` ```
If everything worked, a _200 OK_ response will be returned and the payload printed: If everything worked, a `200 OK` response will be returned and the payload printed:
```swift ```swift
TestPayload( TestPayload(
@ -121,91 +143,96 @@ TestPayload(
) )
``` ```
### Signing The whole authentication flow can be found at [Authentication → JWT](authentication.md#jwt).
This package can also _generate_ JWTs, also known as signing. To demonstrate this, let's use the `TestPayload` from the previous section. Add the following route to your project.
```swift
// Generate and return a new JWT.
app.post("login") { req async throws -> [String: String] in
// Create a new instance of our JWTPayload
let payload = TestPayload(
subject: "vapor",
expiration: .init(value: .distantFuture),
isAdmin: true
)
// Return the signed JWT
return try await [
"token": req.jwt.sign(payload, kid: "a"),
]
}
```
The `req.jwt.sign` helper will use the default configured signer to serialize and sign the `JWTPayload`. The encoded JWT is returned as a `String`.
Test the route by sending the following HTTP request.
```http
POST /login HTTP/1.1
```
You should see the newly generated token returned in a _200 OK_ response.
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo"
}
```
## Authentication
For more information on using JWT with Vapor's authentication API, visit [Authentication → JWT](authentication.md#jwt).
## Algorithms ## Algorithms
Vapor's JWT API supports verifying and signing tokens using the following algorithms. JWTs can be signed using a variety of algorithms.
To add a key to the keychain, an overload of the `add` method is available for each of the following algorithms:
### HMAC ### HMAC
HMAC is the simplest JWT signing algorithm. It uses a single key that can both sign and verify tokens. The key can be any length. HMAC (Hash-based Message Authentication Code) is a symmetric algorithm that uses a secret key to sign and verify the JWT. Vapor supports the following HMAC algorithms:
- `HS256`: HMAC with SHA-256 - `HS256`: HMAC with SHA-256
- `HS384`: HMAC with SHA-384 - `HS384`: HMAC with SHA-384
- `HS512`: HMAC with SHA-512 - `HS384`: HMAC with SHA-384
```swift ```swift
// Add HMAC with SHA-256 signer. // Add an HS256 key.
await app.jwt.keys.addHMAC(key: "secret", digestAlgorithm: .sha256) await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)
```
### ECDSA
ECDSA (Elliptic Curve Digital Signature Algorithm) is an asymmetric algorithm that uses a public/private key pair to sign and verify the JWT. It's reliance is based on the math around elliptic curves. Vapor supports the following ECDSA algorithms:
- `ES256`: ECDSA with a P-256 curve and SHA-256
- `ES384`: ECDSA with a P-384 curve and SHA-384
- `ES512`: ECDSA with a P-521 curve and SHA-512
All algorithms provide botha public key and a private key, such as `ES256PublicKey` and `ES256PrivateKey`. You can add ECDSA keys using the PEM format:
```swift
let ecdsaPublicKey = """
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx
C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ==
-----END PUBLIC KEY-----
"""
// Initialize an ECDSA key with public PEM.
let key = try ES256PublicKey(pem: ecdsaPublicKey)
```
or generate random ones (useful for testing):
```swift
let key = ES256PrivateKey()
```
To add the key to the keychain:
```swift
await app.jwt.keys.add(ecdsa: key)
```
### EdDSA
EdDSA (Edwards-curve Digital Signature Algorithm) is an asymmetric algorithm that uses a public/private key pair to sign and verify the JWT. It's similar to ECDSA in that both rely on the DSA algorithm, but EdDSA is based on the Edwards-curve, a different family of elliptic curves, and has slight performance improvements. It's however also newer and therefore less widely supported. Vapor only supports the `EdDSA` algorithm which uses the `Ed25519` curve.
You can create an EdDSA key using its (base-64 encoded `String`) coordinate, so `x` if it's a public key and `d` if it's a private key:
```swift
let publicKey = try EdDSA.PublicKey(x: "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE", curve: .ed25519)
let privateKey = try EdDSA.PrivateKey(d: "d1H3/dcg0V3XyAuZW2TE5Z3rhY20M+4YAfYu/HUQd8w=", curve: .ed25519)
```
You can also generate random ones:
```swift
let key = EdDSA.PrivateKey(curve: .ed25519)
```
To add the key to the keychain:
```swift
await app.jwt.keys.add(eddsa: key)
``` ```
### RSA ### RSA
RSA is the most commonly used JWT signing algorithm. It supports distinct public and private keys. This means that a public key can be distributed for verifying JWTs are authentic while the private key that generates them is kept secret. RSA (Rivest-Shamir-Adleman) is an asymmetric algorithm that uses a public/private key pair to sign and verify the JWT.
!!! warning !!! warning
Vapor's JWT package does not support RSA keys with a size less than 2048 bits. In addition to this, since RSA is no longer recommended by NIST due to security reasons, RSA keys are gated behind an `Insecure` namespace to discourage their use. As you'll see, RSA keys are gated behind an `Insecure` namespace to discourage new users from using them. This is because RSA is considered less secure than ECDSA and EdDSA, and should only be used for compatibility reasons.
If possible, use any of the other algorithms instead.
To create an RSA signer, first initialize an `RSAKey`. This can be done by passing in the components. Vapor supports the following RSA algorithms:
- `RS256`: RSA with SHA-256
- `RS384`: RSA with SHA-384
- `RS512`: RSA with SHA-512
```swift You can create an RSA key using its PEM format:
// Initialize an RSA private key with components.
let key = try Insecure.RSA.PrivateKey(
modulus: modulus,
exponent: publicExponent,
privateExponent: privateExponent
)
```
The initializer for the public key is similar.
```swift
// Initialize an RSA public key with components.
let key = try Insecure.RSA.PublicKey(
modulus: modulus,
exponent: publicExponent
)
```
You can also choose to load a PEM file:
```swift ```swift
let rsaPublicKey = """ let rsaPublicKey = """
@ -221,107 +248,56 @@ aX4rbSL49Z3dAQn8vQIDAQAB
let key = try Insecure.RSA.PublicKey(pem: rsaPublicKey) let key = try Insecure.RSA.PublicKey(pem: rsaPublicKey)
``` ```
Use `Insecure.RSA.PrivateKey` for loading private RSA PEM keys. These start with: or usign its components:
```
-----BEGIN RSA PRIVATE KEY-----
```
Once you have the RSA key, you can add it using the `addRSA` method.
```swift ```swift
// Add RSA with SHA-256 signer. // Initialize an RSA private key with components.
try await app.jwt.keys.addRSA( let key = try Insecure.RSA.PrivateKey(
key: Insecure.RSA.PublicKey(pem: rsaPublicKey), modulus: modulus,
digestAlgorithm: .sha256 exponent: publicExponent,
privateExponent: privateExponent
) )
``` ```
!!! warning
The package does not support RSA keys smaller than 2048 bits.
Then you can add the key to the key collection:
```swift
await app.jwt.keys.add(rsa: key, digestAlgorithm: .sha256)
```
### PSS ### PSS
In addition to standard RSA, Vapor's JWT package also supports RSA with PSS padding. In addition to the RSA-PKCS1v1.5 algorithm, Vapor also supports the RSA-PSS algorithm. PSS (Probabilistic Signature Scheme) is a more secure padding scheme for RSA signatures. It is recommended to use PSS over PKCS1v1.5 when possible.
This is considered more secure than standard RSA, however it is still discouraged in favor of other asymmetric algorithms like ECDSA. The algorithm only differs in the signature phase, which means that the keys are the same as RSA, however, you need to specify the padding scheme when adding them to the key collection:
While PSS just uses a different padding scheme than standard RSA, the key generation and usage is the same as RSA.
```swift ```swift
let key = Insecure.RSA.PublicKey(pem: publicKey) await app.jwt.keys.add(pss: key, digestAlgorithm: .sha256)
try app.jwt.keys.addPSS(key: key, digestAlgorithm: .sha256)
``` ```
### ECDSA ## Key Identifier (kid)
ECDSA is a more modern algorithm that is similar to RSA. It is considered to be more secure for a given key length than RSA[^1]. However, you should do your own research before deciding. When adding a key to the key collection, you can also specify a key identifier (kid). This is a unique identifier for the key that can be used to look up the key in the collection.
[^1]: https://www.ssl.com/article/comparing-ecdsa-vs-rsa/
Like RSA, you can load ECDSA keys using PEM files:
```swift ```swift
let ecdsaPublicKey = """ // Add HMAC with SHA-256 key named "a".
-----BEGIN PUBLIC KEY----- await app.jwt.keys.add(hmac: "foo", digestAlgorithm: .sha256, kid: "a")
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx
C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ==
-----END PUBLIC KEY-----
"""
// Initialize an ECDSA key with public PEM.
let key = try ES256PublicKey(pem: ecdsaPublicKey)
``` ```
There are three ECDSA algorithms available, depending on the curve used: If you don't specify a `kid`, the key will be assigned as the default key.
- `ES256`: ECDSA with a P-256 curve and SHA-256
- `ES384`: ECDSA with a P-384 curve and SHA-384
- `ES512`: ECDSA with a P-521 curve and SHA-512
All algorithms provide botha public key and a private key, !!! note
such as `ES256PublicKey` and `ES256PrivateKey`. The default key will be overridden if you add another key without a `kid`.
You can also generate random ECDSA using the empty initializer. This is useful for testing. When signing a JWT, you can specify the `kid` to use:
```swift ```swift
let key = ES256PrivateKey() let token = try await req.jwt.sign(payload, kid: "a")
``` ```
Once you have the ECDSAKey, you can add it to the key collection using the `addECDSA` method. When verifying on the other hand, the `kid` is automatically extracted from the JWT header and used to look up the key in the collection. There's also a `iteratingKeys` parameter on the verify method that allows you to specify whether to iterate over all keys in the collection if the `kid` is not found.
```swift
// Add ECDSA with SHA-256 signer.
try await app.jwt.keys.addECDSA(key: ES256PublicKey(pem: ecdsaPublicKey))
```
### Key Identifier (kid)
If you are using multiple algorithms, you can use key identifiers (`kid`s) to differentiate them. When configuring an algorithm, pass the `kid` parameter.
```swift
// Add HMAC with SHA-256 signer named "a".
await app.jwt.keys.addHMAC(key: "foo", digestAlgorithm: .sha256, kid: "a")
// Add HMAC with SHA-256 signer named "b".
await app.jwt.keys.addHMAC(key: "bar", digestAlgorithm: .sha256, kid: "b")
```
When signing JWTs, pass the `kid` parameter for the desired signer.
```swift
// Sign using signer "a"
try await req.jwt.sign(payload, kid: "a")
```
This will automatically include the signer's name in the JWT header's `"kid"` field. When verifying the JWT, this field will be used to look up the appropriate signer.
```swift
// Verify using signer specified by "kid" header.
// If no "kid" header is present, default signer will be used.
let payload = try await req.jwt.verify(as: TestPayload.self)
```
Since [JWKs](#jwk) already contain `kid` values, you do not need to specify them during configuration.
```swift
// JWKs already contain the "kid" field.
let jwk: JWK = ...
try await app.jwt.keys.use(jwk: jwk)
```
## Claims ## Claims
@ -341,19 +317,50 @@ Vapor's JWT package includes several helpers for implementing common [JWT claims
All claims should be verified in the `JWTPayload.verify` method. If the claim has a special verify method, you can use that. Otherwise, access the value of the claim using `value` and check that it is valid. All claims should be verified in the `JWTPayload.verify` method. If the claim has a special verify method, you can use that. Otherwise, access the value of the claim using `value` and check that it is valid.
## JWK ## JWK
A JSON Web Key (JWK) is a JSON data structure that represents a cryptographic key ([RFC7517](https://datatracker.ietf.org/doc/html/rfc7517)). These are commonly used to supply clients with keys for verifying JWTs.
A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key ([RFC7517](https://tools.ietf.org/html/rfc7517)). These are commonly used to supply clients with keys for verifying JWTs. For example, Apple hosts their Sign in with Apple JWKS at the following URL.
For example, Apple hosts their _Sign in with Apple_ JWKS at the following URL.
```http ```http
GET https://appleid.apple.com/auth/keys GET https://appleid.apple.com/auth/keys
``` ```
You can add this JSON Web Key Set (JWKS) to your `JWTSigners`. Vapor provides utilities to add JWKs to the key collection:
You can then pass JWTs from Apple to the `verify` method. The key identifier (`kid`) in the JWT header will be used to automatically select the correct key for verification.
JWT issuers may rotate their JWKS meaning you need to re-download occasionally. See Vapor's supported JWT [Vendors](#vendors) list below for APIs that do this automatically. ```swift
let privateKey = """
{
"kty": "RSA",
"d": "\(rsaPrivateExponent)",
"e": "AQAB",
"use": "sig",
"kid": "1234",
"alg": "RS256",
"n": "\(rsaModulus)"
}
"""
let jwk = try JWK(json: privateKey)
try await app.jwt.keys.use(jwk: jwk)
```
This will add the JWK to the key collection, and you can use it to sign and verify JWTs as you would with any other key.
### JWKs
If you have multiple JWKs, you can add them just as well:
```swift
let json = """
{
"keys": [
{"kty": "RSA", "alg": "RS256", "kid": "a", "n": "\(rsaModulus)", "e": "AQAB"},
{"kty": "RSA", "alg": "RS512", "kid": "b", "n": "\(rsaModulus)", "e": "AQAB"},
]
}
"""
try await app.jwt.keys.use(jwksJSON: json)
```
## Vendors ## Vendors