π JSON Web Token signing and verification (HMAC, RSA, ECDSA, EdDSA) using SwiftCrypto and BoringSSL.
The table below shows a list of JWTKit major releases alongside their compatible Swift versions.
Version | Swift | SPM |
---|---|---|
4.0 | 5.6+ | from: "4.0.0" |
Use the SPM string to easily include the dependendency in your Package.swift
file.
.package(url: "https://github.com/vapor/jwt-kit.git", from: "4.0.0")
Note: Prior to version 4.0, this package was part of vapor/jwt.
JWTKit supports all platforms supported by Swift 5.6 and later, with the exception of Windows.
JWTKit provides APIs for signing and verifying JSON Web Tokens, as specified by RFC 7519. The following features are supported:
- Parsing
- Signature verification
- Payload signing
- Serialization
- Claim validation (
aud
,exp
,jti
,iss
,iat
,nbf
,sub
, and custom claims) - JSON Web Keys (
JWK
,JWKS
)
The following algorithms, as defined in RFC 7518 Β§ 3 and RFC 8037 Β§ 3, are supported for both signing and verification:
- HS256, HS384, HS512 (HMAC with SHA-2)
- RS256, RS384, RS512 (RSA with SHA-2)
- ES256, ES384, ES512 (ECDSA with SHA-2)
- EdDSA
- none (unsigned)
For those algorithms which specify a curve type (crv
), the following curves, as defined in RFC 7518 Β§ 6 and RFC 8037 Β§ 3, are supported:
- P-256 (ES256 algorithm only)
- P-384 (ES384 algorithm only)
- P-521 (ES512 algorithm only)
- Ed25519 (EdDSA algorithm only)
This package includes a vendored internal-only copy of BoringSSL, used for certain cryptographic operations not currently available via SwiftCrypto.
Note: The
P-521
elliptic curve used with the ES512 signing algorithm is often assumed to be a typo, but confusingly, it is not.
The vapor/jwt package provides first-class integration with Vapor and is recommended for all Vapor projects which want to use JWTKit.
A JWTSigners
object is used to load signing keys and keysets, and to sign and verify tokens:
import JWTKit
// Signs and verifies JWTs
let signers = JWTSigners()
The JWTSigner
class encapsulates a signature algorithm and an appropriate signing key. To use a signer, register it with the JWTSigners
object:
// Registers a HS256 (HMAC-SHA-256) signer.
signers.use(.hs256(key: "secret"))
This example uses the very secure key "secret"
.
Let's try to verify the following example JWT:
let exampleJWT = """
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
"""
You can inspect the contents of this token by visiting jwt.io and pasting the token in the debugger. Set the key in the "Verify Signature" section to secret
.
To verify a token, the format of the payload must be known. This is accomplished by defining a type conforming to the JWTPayload
protocol. Each property of the payload type corresponds to a claim in the token. JWTKit provides predefined types for all of the claims specified by RFC 7519, as well as some convenience types for working with custom claims. For the example token, the payload looks like this:
struct ExamplePayload: JWTPayload {
var sub: SubjectClaim
var exp: ExpirationClaim
var admin: BoolClaim
func verify(using signer: JWTSigner) throws {
try self.exp.verifyNotExpired()
}
}
Using this payload, the JWTSigners
object can process and verify the example JWT, returning its payload on success:
// Parses the JWT, verifies its signature, and decodes its content.
let payload = try signers.verify(exampleJWT, as: ExamplePayload.self)
print(payload)
If all works correctly, this code will print something like this:
TestPayload(
sub: SubjectClaim(value: "vapor"),
exp: ExpirationClaim(value: 4001-01-01 00:00:00 +0000),
admin: BoolClaim(value: true)
)
Note: The
admin
property of the example payload did not have to use theBoolClaim
type; a simpleBool
would have worked as well. TheBoolClaim
type is provided by JWTKit for convenience in working with the many JWT implementations which encode boolean values as JSON strings (e.g."true"
and"false"
) rather than using JSON'strue
andfalse
keywords.
We can also generate JWTs, also known as signing. To demonstrate this, let's use the TestPayload
from the previous section.
// Create a new instance of our JWTPayload
let payload = ExamplePayload(
subject: "vapor",
expiration: .init(value: .distantFuture),
isAdmin: true
)
Then, pass the payload to JWTSigners.sign
.
// Sign the payload, returning a JWT.
let jwt = try signers.sign(payload)
print(jwt)
You should see a JWT printed. This can be fed back into the verify
method to access the payload.
A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key, defined in 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 URL https://appleid.apple.com/auth/keys
.
You can add this JSON Web Key Set (JWKS) to your JWTSigners
:
import Foundation
import JWTKit
// Download the JWKS.
// This could be done asynchronously if needed.
let jwksData = try String(
contentsOf: URL(string: "https://appleid.apple.com/auth/keys")!
)
// Create signers and add JWKS.
let signers = JWTSigners()
try signers.use(jwksJSON: jwksData)
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. A JWKS may contain any of the key types supported by JWTKit.
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-256hs384
: HMAC with SHA-384hs512
: HMAC with SHA-512
// Add HMAC with SHA-256 signer.
signers.use(.hs256(key: "secret"))
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.
To create an RSA signer, first initialize an RSAKey
. This can be done by passing in the components:
// Initialize an RSA key with components.
let key = RSAKey(
modulus: "...",
exponent: "...",
// Only included in private keys.
privateExponent: "..."
)
You can also choose to load a PEM file:
let rsaPublicKey = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx
PmjXpbCkecAWLj/CcDWEcuTZkYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv
7FPo5Cq8FkvrdDzeacwRSxYuIq1LtYnd6I30qNaNthntjvbqyMmBulJ1mzLI+Xg/
aX4rbSL49Z3dAQn8vQIDAQAB
-----END PUBLIC KEY-----
"""
// Initialize an RSA key with public pem.
let key = RSAKey.public(pem: rsaPublicKey)
Use .private
for loading private RSA pem keys. These start with:
-----BEGIN RSA PRIVATE KEY-----
Use .certificate
for loading X.509 certificates. These start with:
-----BEGIN CERTIFICATE-----
Once you have the RSAKey, you can use it to create an RSA signer:
rs256
: RSA with SHA-256rs384
: RSA with SHA-384rs512
: RSA with SHA-512
// Add RSA with SHA-256 signer.
try signers.use(.rs256(key: .public(pem: rsaPublicKey)))
Important: RSA, despite still being the common algorithm in use, is no longer recommended for new applications. If possible, use EdDSA or ECDSA instead.
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. Infosec Insights' June 2020 blog post "ECDSA vs RSA: Everything You Need to Know" provides a detailed discussion on the differences between the two.
IMPORTANT: Cryptography is a complex topic, and the decision of algorithm can directly impact the integrity, security, and privacy of your data. This README does not attempt to offer a meaningful discussion of these concerns; the package authors recommend doing your own research before making a final decision.
Like RSA, you can load ECDSA keys using PEM files:
let ecdsaPublicKey = """
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx
C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ==
-----END PUBLIC KEY-----
"""
// Initialize an ECDSA key with public pem.
let key = ECDSAKey.public(pem: ecdsaPublicKey)
Use .private
for loading private ECDSA pem keys. These start with:
-----BEGIN PRIVATE KEY-----
Once you have the ECDSAKey, you can use it to create an ECDSA signer:
es256
: ECDSA with SHA-256 and P-256es384
: ECDSA with SHA-384 and P-384es512
: ECDSA with SHA-512 and P-521
// Add ECDSA with SHA-256 signer.
try signers.use(.es256(key: .public(pem: ecdsaPublicKey)))
JWTKit includes several helpers for implementing the "standard" JWT claims defined by RFC Β§ 4.1:
Claim | Type | Verify Method |
---|---|---|
aud |
AudienceClaim |
verifyIntendedAudience(includes:) |
exp |
ExpirationClaim |
verifyNotExpired(currentDate:) |
jti |
IDClaim |
n/a |
iat |
IssuedAtClaim |
n/a |
iss |
IssuerClaim |
n/a |
nbf |
NotBeforeClaim |
verifyNotBefore(currentDate:) |
sub |
SubjectClaim |
n/a |
Whenever possible, all of a payload's claims should be verified in the verify(using:)
method; those which do not have verification methods of their own may be verified manually.
Additional helpers are provided for common types of claims not defined by the RFC:
BoolClaim
: May be used for any claim whose value is a boolean flag. Will recognize both boolean JSON values and the strings"true"
and"false"
.GoogleHostedDomainClaim
: For use with theGoogleIdentityToken
vendor token type.JWTMultiValueClaim
: A protocol for claims, such asAudienceClaim
which can optionally be encoded as an array with multiple values.JWTUnixEpochClaim
: A protocol for claims, such asExpirationClaim
andIssuedAtClaim
, whose value is a count of seconds since the UNIX epoch (midnight of January 1, 1970).LocaleClaim
: A claim whose value is a BCP 47 language tag. Also used byGoogleIdentityToken
.
This package was originally authored by the wonderful @siemensikkema.