// swift-tools-version:6.0import PackageDescription let package = Package( name: "es256-demo", platforms: [.macOS(.v13)], dependencies: [ .package(url: "https://github.com/vapor/jwt-kit.git", exact: "5.7.1"), ], targets: [ .executableTarget(name: "es256-demo", dependencies: [.product(name: "JWTKit", package: "jwt-kit")]), ])import Foundationimport JWTKit struct AccessToken: JWTPayload { var iss: IssuerClaim var sub: SubjectClaim var aud: AudienceClaim var iat: IssuedAtClaim var exp: ExpirationClaim func verify(using algorithm: some JWTAlgorithm) throws { try exp.verifyNotExpired() }} let key = try ES256PrivateKey(pem: String(contentsOfFile: "key.pem", encoding: .utf8))let keys = await JWTKeyCollection().add(ecdsa: key, kid: "2026-10") // Whole seconds: JSONEncoder would otherwise write fractional timestampslet now = Date(timeIntervalSince1970: Date().timeIntervalSince1970.rounded(.down))let payload = AccessToken( iss: "https://issuer.example.com", sub: "user-42", aud: "api://orders", iat: IssuedAtClaim(value: now), exp: ExpirationClaim(value: now.addingTimeInterval(15 * 60)))print(try await keys.sign(payload, kid: "2026-10")) // the kid is not added unless passed hereHow it works
ES256PrivateKey(pem:)reads PKCS#8 andEC PRIVATE KEYPEM.JWTKeyCollectionis an actor, soadd(ecdsa:kid:)andsignare awaited.- The payload is a
JWTPayloadstruct with JWTKit's claim types (IssuerClaim,AudienceClaim,ExpirationClaim, …);verify(using:)is what JWTKit runs when it verifies a token. sign(payload, kid:)writesalg,typandkidand returns the compact token.
Gotchas
- Registering the key under a kid does not put the kid in tokens:
sign(payload)withoutkid:writes onlyalgandtyp. Verifiers that select keys bykidthen cannot find the key. - Dates are encoded with fractional seconds:
Date()becomes"iat":1792499789.2461329. RFC 7519 allows it, but every other library on these pages writes whole seconds and integer-based checks downstream break — round the date as above. - Slashes come out escaped:
"iss":"https:\/\/issuer.example.com". That is valid JSON and verifiers decode it, butgrepor string comparisons over the raw payload do not seehttps://. - jwt-kit 5.7 needs Swift 6.2 or later (its manifest declares
swift-tools-version:6.2). Tutorials for jwt-kit 4 useJWTSignersandsigners.use(.es256(key:)), which jwt-kit 5 replaced withJWTKeyCollection.