JWTKit

repository·main·Indexed 43 days ago

https://github.com/vapor/jwt-kit

A Swift library for signing and verifying JSON Web Tokens (JWT) built on top of SwiftCrypto. It supports various algorithms including HMAC, RSA, ECDSA, EdDSA, and post-quantum MLDSA. Features include JWTKeyCollection for managing keysets, JWKS import support, and built-in helpers for standard RFC claims.

Tokens
4.2K
Snippets
13
Records
14
Agent score
5%

What's inside JWTKit

  1. Standard JWT Claims and Verification

    main

    JWTKit provides helpers for standard claims defined in RFC § 4.1. Whenever possible, verify these claims within the verify(using:) method. For claims without built-in verification methods, you must verify them manually.

    ClaimTypeVerify Method
    audAudienceClaimverifyIntendedAudience(includes:)
    expExpirationClaimverifyNotExpired(currentDate:)
    jtiIDClaimn/a
    iatIssuedAtClaimn/a
    issIssuerClaimn/a
    nbfNotBeforeClaimverifyNotBefore(currentDate:)
    subSubjectClaimn/a

    Additional Claim Helpers

    • BoolClaim: Handles boolean flags (recognizes JSON booleans and strings "true"/"false").
    • GoogleHostedDomainClaim: For GoogleIdentityToken vendor tokens.
    • JWTMultiValueClaim: Protocol for claims that can be encoded as arrays (e.g., AudienceClaim).
    • JWTUnixEpochClaim: Protocol for claims using UNIX epoch seconds (e.g., ExpirationClaim, IssuedAtClaim).
    • LocaleClaim: For BCP 47 language tags.
  2. How JWTKeyCollection works

    main

    The JWTKeyCollection is an actor used to manage signing keys and keysets. It is responsible for both signing new tokens and verifying existing ones. Because it is an actor, all its methods are async and must be awaited to ensure thread safety.

    To use it, initialize a collection and add keys using algorithm-specific methods (e.g., add(hmac:...), add(ecdsa:...)). If you provide a kid (Key ID), JWTKit will use that ID to select the correct key during verification based on the kid in the JWT header. If no kid is provided, the key is treated as the default.

    Warning: If you add multiple keys without a kid, only the last one will be stored; previous ones will be overwritten.

    import JWTKit
    
    // Signs and verifies JWTs
    let keys = JWTKeyCollection()
  3. Implement custom JWT parsing and serialization

    main

    If you need to handle non-standard JWT formats (for example, controlling whether the payload is base64 encoded via a b64 header), implement the JWTParser and JWTSerializer protocols.

    Custom Serializer

    Implement serialize(_:header:) to define how the payload is converted to Data. You can check header.b64 to decide whether to apply base64URLEncodedBytes().

    Custom Parser

    Implement parse(_:as:) to define how a token is decomposed. Use the getTokenParts(token) helper to split the token into encoded header, payload, and signature components.

    Usage

    Pass your custom implementations to the JWTKeyCollection.add(...) method.

    struct CustomSerializer: JWTSerializer {
        var jsonEncoder: JWTJSONEncoder = .defaultForJWT
        func serialize(_ payload: some JWTPayload, header: JWTHeader) throws -> Data {
            if header.b64?.asBool == true {
                try Data(jsonEncoder.encode(payload).base64URLEncodedBytes())
            } else {
                try jsonEncoder.encode(payload)
            }
        }
    }
    
    struct CustomParser: JWTParser {
        var jsonDecoder: JWTJSONDecoder = .defaultForJWT
        func parse<Payload>(_ token: some DataProtocol, as: Payload.Type) throws -> (header: JWTHeader, payload: Payload, signature: Data) where Payload: JWTPayload {
            let (encodedHeader, encodedPayload, encodedSignature) = try getTokenParts(token)
            let header = try jsonDecoder.decode(JWTHeader.self, from: .init(encodedHeader.base64URLDecodedBytes()))
            let payload = if header.b64?.asBool ?? true {
                try jsonDecoder.decode(Payload.self, from: .init(encodedPayload.base64URLDecodedBytes()))
            } else {
                try jsonDecoder.decode(Payload.self, from: .init(encodedPayload))
            }
            let signature = Data(encodedSignature.base64URLDecodedBytes())
            return (header: header, payload: payload, signature: signature)
        }
    }
    
    // Usage
    let keyCollection = await JWTKeyCollection().add(
        hmac: "secret", 
        digestAlgorithm: .sha256,
        parser: CustomParser(), 
        serializer: CustomSerializer()
    )
  4. Sign a JWT payload

    main

    To sign a token, first define a payload type that conforms to JWTPayload. Each property in your struct represents a claim. You must implement the verify(using:) method to handle claim validation (e.g., checking expiration).

    Once your payload is ready, use JWTKeyCollection.sign(_:kid:) to generate the signed JWT string.

    struct ExamplePayload: JWTPayload {
        var sub: SubjectClaim
        var exp: ExpirationClaim
        var admin: BoolClaim
    
        func verify(using key: some JWTAlgorithm) throws {
            try self.exp.verifyNotExpired()
        }
    }
    
    let payload = ExamplePayload(
        subject: "vapor",
        expiration: .init(value: .distantFuture),
        isAdmin: true
    )
    
    // Sign the payload, returning the JWT as String
    let jwt = try await keys.sign(payload, kid: "my-key")
  5. Verify a JWT

    main

    To verify a token, you must provide the type that the payload should be decoded into. Use JWTKeyCollection.verify(_:as:). This method parses the JWT, verifies the signature using the keys in the collection, and returns the decoded payload on success.

    let exampleJWT = "..."
    
    // Parse the JWT, verify its signature and decode its content
    let payload = try await keys.verify(exampleJWT, as: ExamplePayload.self)
    print(payload)
  6. Install JWTKit via Swift Package Manager

    main

    You can install JWTKit by manually adding it to your Package.swift file or by using the SwiftPM command line.

    Manual Configuration

    Add the following to your Package.swift:

    dependencies: [
        .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0")
    ],
    targets: [
        .target(
            name: "MyTarget",
            dependencies: [
                .product(name: "JWTKit", package: "jwt-kit"),
            ]),
    ]

    CLI Installation

    Run these commands in your project root, replacing MyTarget with your actual target name:

    swift package add-dependency https://github.com/vapor/jwt-kit.git --from 5.0.0
    swift package add-target-dependency JWTKit MyTarget --package jwt-kit
    swift package add-dependency https://github.com/vapor/jwt-kit.git --from 5.0.0
    swift package add-target-dependency JWTKit MyTarget --package jwt-kit
  7. Use custom JSON Encoder and Decoder

    main

    If you do not need to change the parsing/serialization logic itself, but only need to customize how JSON is encoded or decoded (e.g., changing dateEncodingStrategy), use DefaultJWTParser and DefaultJWTSerializer to wrap your custom JSONDecoder and JSONEncoder.

    let encoder = JSONEncoder()
    encoder.dateEncodingStrategy = .iso8601
    let decoder = JSONDecoder() 
    decoder.dateDecodingStrategy = .iso8601
    
    let parser = DefaultJWTParser(jsonDecoder: decoder)
    let serializer = DefaultJWTSerializer(jsonEncoder: encoder)
    
    let keyCollection = await JWTKeyCollection().add(
        hmac: "secret",
        digestAlgorithm: .sha256,
        parser: parser, 
        serializer: serializer
    )
  8. Configure MLDSA (Post-Quantum) signing keys

    main

    MLDSA is a post-quantum signature scheme. Note: MLDSA requires macOS 26+. You can initialize an MLDSA key using a seed representation and add it to the collection.

    // Initialize an MLDSA key with its seed
    let seedRepresentation = Data("...".utf8)
    let privateKey = try MLDSA87PrivateKey(seedRepresentation: seedRepresentation)
    
    // Add private key to the key collection
    await keys.add(mldsa: privateKey)
  9. Add a JSON Web Key Set (JWKS) to JWTKeyCollection

    main

    You can import a full JSON Web Key Set (JWKS) into a JWTKeyCollection using the add(jwksJSON:) method. This is useful for integrating with providers like Apple that host public keys at a specific URL. JWTKit will automatically use the kid from the JWT header to select the appropriate key from the set during verification.

    let json = """
    {
        "keys": [
            {"kty": "RSA", "alg": "RS256", "kid": "a", "n": "...", "e": "AQAB"},
            {"kty": "RSA", "alg": "RS512", "kid": "b", "n": "...", "e": "AQAB"}
        ]
    }
    """
    
    // Create key collection and add JWKS
    let keys = try await JWTKeyCollection().add(jwksJSON: json)
  10. Configure ECDSA signing keys

    main

    ECDSA is an asymmetric algorithm. You can initialize an ECDSA key using a public PEM string and then add it to the collection using add(ecdsa:).

    let ecdsaPublicKey = "-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----"
    
    // Initialize an ECDSA key with public pem.
    let key = try ES256PublicKey(pem: ecdsaPublicKey)
    
    // Add ECDSA with SHA-256 algorithm
    await keys.add(ecdsa: key)
  11. Configure EdDSA signing keys

    main

    EdDSA (specifically Ed25519) is an asymmetric algorithm. You can create keys using their coordinates and add either the public or private key to the collection.

    // Initialize an EdDSA key with public PEM
    let publicKey = try EdDSA.PublicKey(x: "...", curve: .ed25519)
    
    // Initialize an EdDSA key with private PEM
    let privateKey = try EdDSA.PrivateKey(x: "...", d: "...", curve: .ed25519)
    
    // Add public key to the key collection
    await keys.add(eddsa: publicKey)
    
    // Add private key to the key collection
    await keys.add(eddsa: privateKey)
  12. Configure HMAC signing keys

    main

    HMAC is a symmetric algorithm where the same key is used for both signing and verification. Use the add(hmac:digestAlgorithm:kid:) method to register an HMAC key.

    // Registers an HS256 (HMAC-SHA-256) signer with a key identifier.
    await keys.add(hmac: "secret", digestAlgorithm: .sha256, kid: "my-key")