diff --git a/docs/security/authentication.md b/docs/security/authentication.md index 4ed8115f..1a5fd4e0 100644 --- a/docs/security/authentication.md +++ b/docs/security/authentication.md @@ -841,7 +841,7 @@ struct SessionToken: Content, Authenticatable, JWTPayload { self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime)) } - func verify(using signer: JWTSigner) throws { + func verify(using algorithm: some JWTAlgorithm) throws { try expiration.verifyNotExpired() } } @@ -859,20 +859,20 @@ Using our model for the JWT token and response, we can use a password protected ```swift let passwordProtected = app.grouped(User.authenticator(), User.guardMiddleware()) -passwordProtected.post("login") { req -> ClientTokenReponse in +passwordProtected.post("login") { req async throws -> ClientTokenReponse in let user = try req.auth.require(User.self) let payload = try SessionToken(with: user) - return ClientTokenReponse(token: try req.jwt.sign(payload)) + return ClientTokenReponse(token: try await req.jwt.sign(payload)) } ``` Alternatively, if you don't want to use an authenticator you can have something that looks like the following. ```swift -app.post("login") { req -> ClientTokenReponse in +app.post("login") { req async throws -> ClientTokenReponse in // Validate provided credential for user // Get userId for provided user let payload = try SessionToken(userId: userId) - return ClientTokenReponse(token: try req.jwt.sign(payload)) + return ClientTokenReponse(token: try await req.jwt.sign(payload)) } ``` diff --git a/docs/security/jwt.md b/docs/security/jwt.md index 2e22b464..7250a96d 100644 --- a/docs/security/jwt.md +++ b/docs/security/jwt.md @@ -1,6 +1,5 @@ # 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. ## Getting Started @@ -15,7 +14,7 @@ let package = Package( name: "my-app", dependencies: [ // Other dependencies... - .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"), + .package(url: "https://github.com/vapor/jwt.git", from: "5.0.0-beta"), ], targets: [ .target(name: "App", dependencies: [ @@ -31,15 +30,18 @@ If you edit the manifest directly inside Xcode, it will automatically pick up th ### 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 signer. The simplest signing algorithm is `HS256` or HMAC with SHA-256. +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. ```swift import JWT // Add HMAC with SHA-256 signer. -app.jwt.signers.use(.hs256(key: "secret")) +await app.jwt.keys.addHMAC(key: "secret", digestAlgorithm: .sha256) ``` +!!! note + 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. ### Payload @@ -81,7 +83,7 @@ struct TestPayload: JWTPayload { // signature verification here. // Since we have an ExpirationClaim, we will // call its verify method. - func verify(using signer: JWTSigner) throws { + func verify(using algorithm: some JWTAlgorithm) async throws { try self.expiration.verifyNotExpired() } } @@ -93,8 +95,8 @@ Now that we have a `JWTPayload`, we can attach the JWT above to a request and us ```swift // Fetch and verify JWT from incoming request. -app.get("me") { req -> HTTPStatus in - let payload = try req.jwt.verify(as: TestPayload.self) +app.get("me") { req async throws -> HTTPStatus in + let payload = try await req.jwt.verify(as: TestPayload.self) print(payload) return .ok } @@ -125,7 +127,7 @@ This package can also _generate_ JWTs, also known as signing. To demonstrate thi ```swift // Generate and return a new JWT. -app.post("login") { req -> [String: String] in +app.post("login") { req async throws -> [String: String] in // Create a new instance of our JWTPayload let payload = TestPayload( subject: "vapor", @@ -133,8 +135,8 @@ app.post("login") { req -> [String: String] in isAdmin: true ) // Return the signed JWT - return try [ - "token": req.jwt.sign(payload) + return try await [ + "token": req.jwt.sign(payload, kid: "a"), ] } ``` @@ -167,28 +169,39 @@ Vapor's JWT API supports verifying and signing tokens using the following algori 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. -- `hs256`: HMAC with SHA-256 -- `hs384`: HMAC with SHA-384 -- `hs512`: HMAC with SHA-512 +- `HS256`: HMAC with SHA-256 +- `HS384`: HMAC with SHA-384 +- `HS512`: HMAC with SHA-512 ```swift // Add HMAC with SHA-256 signer. -app.jwt.signers.use(.hs256(key: "secret")) +await app.jwt.keys.addHMAC(key: "secret", digestAlgorithm: .sha256) ``` ### 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. +!!! 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. + To create an RSA signer, first initialize an `RSAKey`. This can be done by passing in the components. ```swift -// Initialize an RSA key with components. -let key = RSAKey( - modulus: "...", - exponent: "...", - // Only included in private keys. - privateExponent: "..." +// 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 ) ``` @@ -205,24 +218,34 @@ aX4rbSL49Z3dAQn8vQIDAQAB """ // Initialize an RSA key with public pem. -let key = RSAKey.public(pem: rsaPublicKey) +let key = try Insecure.RSA.PublicKey(pem: rsaPublicKey) ``` -Use `.private` for loading private RSA PEM keys. These start with: +Use `Insecure.RSA.PrivateKey` for loading private RSA PEM keys. These start with: ``` -----BEGIN RSA PRIVATE KEY----- ``` -Once you have the RSAKey, you can use it to create an RSA signer. - -- `rs256`: RSA with SHA-256 -- `rs384`: RSA with SHA-384 -- `rs512`: RSA with SHA-512 +Once you have the RSA key, you can add it using the `addRSA` method. ```swift // Add RSA with SHA-256 signer. -try app.jwt.signers.use(.rs256(key: .public(pem: rsaPublicKey))) +try await app.jwt.keys.addRSA( + key: Insecure.RSA.PublicKey(pem: rsaPublicKey), + digestAlgorithm: .sha256 +) +``` + +### PSS + +In addition to standard RSA, Vapor's JWT package also supports RSA with PSS padding. +This is considered more secure than standard RSA, however it is still discouraged in favor of other asymmetric algorithms like ECDSA. +While PSS just uses a different padding scheme than standard RSA, the key generation and usage is the same as RSA. + +```swift +let key = Insecure.RSA.PublicKey(pem: publicKey) +try app.jwt.keys.addPSS(key: key, digestAlgorithm: .sha256) ``` ### ECDSA @@ -242,30 +265,28 @@ C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ== """ // Initialize an ECDSA key with public PEM. -let key = ECDSAKey.public(pem: ecdsaPublicKey) +let key = try ES256PublicKey(pem: ecdsaPublicKey) ``` -Use `.private` for loading private ECDSA PEM keys. These start with: +There are three ECDSA algorithms available, depending on the curve used: +- `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 -``` ------BEGIN PRIVATE KEY----- -``` +All algorithms provide botha public key and a private key, +such as `ES256PublicKey` and `ES256PrivateKey`. -You can also generate random ECDSA using the `generate()` method. This is useful for testing. +You can also generate random ECDSA using the empty initializer. This is useful for testing. ```swift -let key = try ECDSAKey.generate() +let key = ES256PrivateKey() ``` -Once you have the ECDSAKey, you can use it to create an ECDSA signer. - -- `es256`: ECDSA with SHA-256 -- `es384`: ECDSA with SHA-384 -- `es512`: ECDSA with SHA-512 +Once you have the ECDSAKey, you can add it to the key collection using the `addECDSA` method. ```swift // Add ECDSA with SHA-256 signer. -try app.jwt.signers.use(.es256(key: .public(pem: ecdsaPublicKey))) +try await app.jwt.keys.addECDSA(key: ES256PublicKey(pem: ecdsaPublicKey)) ``` ### Key Identifier (kid) @@ -274,16 +295,16 @@ If you are using multiple algorithms, you can use key identifiers (`kid`s) to di ```swift // Add HMAC with SHA-256 signer named "a". -app.jwt.signers.use(.hs256(key: "foo"), kid: "a") +await app.jwt.keys.addHMAC(key: "foo", digestAlgorithm: .sha256, kid: "a") // Add HMAC with SHA-256 signer named "b". -app.jwt.signers.use(.hs256(key: "bar"), kid: "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" -req.jwt.sign(payload, kid: "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. @@ -291,7 +312,7 @@ This will automatically include the signer's name in the JWT header's `"kid"` fi ```swift // Verify using signer specified by "kid" header. // If no "kid" header is present, default signer will be used. -let payload = try req.jwt.verify(as: TestPayload.self) +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. @@ -299,7 +320,7 @@ Since [JWKs](#jwk) already contain `kid` values, you do not need to specify them ```swift // JWKs already contain the "kid" field. let jwk: JWK = ... -app.jwt.signers.use(jwk: jwk) +try await app.jwt.keys.use(jwk: jwk) ``` ## Claims @@ -330,27 +351,9 @@ GET https://appleid.apple.com/auth/keys ``` You can add this JSON Web Key Set (JWKS) to your `JWTSigners`. +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. -```swift -import JWT -import Vapor - -// Download the JWKS. -// This could be done asynchronously if needed. -let jwksData = try Data( - contentsOf: URL(string: "https://appleid.apple.com/auth/keys")! -) - -// Decode the downloaded JSON. -let jwks = try JSONDecoder().decode(JWKS.self, from: jwksData) - -// Create signers and add JWKS. -try app.jwt.signers.use(jwks: jwks) -``` - -You can now 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. - -As of writing, JWK only supports RSA keys. Additionally, 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. +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. ## Vendors @@ -369,15 +372,6 @@ Then, use the `req.jwt.apple` helper to fetch and verify an Apple JWT. ```swift // Fetch and verify Apple JWT from Authorization header. -app.get("apple") { req -> EventLoopFuture in - req.jwt.apple.verify().map { token in - print(token) // AppleIdentityToken - return .ok - } -} - -// Or - app.get("apple") { req async throws -> HTTPStatus in let token = try await req.jwt.apple.verify() print(token) // AppleIdentityToken @@ -399,15 +393,6 @@ Then, use the `req.jwt.google` helper to fetch and verify a Google JWT. ```swift // Fetch and verify Google JWT from Authorization header. -app.get("google") { req -> EventLoopFuture in - req.jwt.google.verify().map { token in - print(token) // GoogleIdentityToken - return .ok - } -} - -// or - app.get("google") { req async throws -> HTTPStatus in let token = try await req.jwt.google.verify() print(token) // GoogleIdentityToken @@ -428,15 +413,6 @@ Then, use the `req.jwt.microsoft` helper to fetch and verify a Microsoft JWT. ```swift // Fetch and verify Microsoft JWT from Authorization header. -app.get("microsoft") { req -> EventLoopFuture in - req.jwt.microsoft.verify().map { token in - print(token) // MicrosoftIdentityToken - return .ok - } -} - -// Or - app.get("microsoft") { req async throws -> HTTPStatus in let token = try await req.jwt.microsoft.verify() print(token) // MicrosoftIdentityToken