Skip to content
ecdsa.com

Verify an ES256 JWT · Swift

How to verify an ES256 JWT in Swift

To verify an ES256 JWT in Swift, use Vapor's jwt-kit: add an ES256PublicKey to a JWTKeyCollection and call verify with your claims type. The signature is checked with the key's own algorithm, and the claims are checked by the verify(using:) method you write on the payload — so expiry and issuer rules live in one visible place.

Tested with jwt-kit 5.7.1 (Swift 6.4.1 on Ubuntu 24.04, swift-crypto 4.5.2 (resolved by jwt-kit)).

Package.swift — jwt-kit (it brings swift-crypto and swift-certificates)
// swift-tools-version:6.2
import PackageDescription
let package = Package(
name: "howto",
platforms: [.macOS(.v15)],
dependencies: [
.package(url: "https://github.com/vapor/jwt-kit.git", exact: "5.7.1"), // Vapor's JWT library
],
targets: [
.executableTarget(name: "verify", dependencies: [.product(name: "JWTKit", package: "jwt-kit")]),
]
)
Sources/verify/Claims.swift — the payload type and its claim rules
import JWTKit
struct Claims: JWTPayload {
var sub: SubjectClaim
var iss: IssuerClaim
var exp: ExpirationClaim
// jwt-kit calls this after the signature check; every claim rule lives here
func verify(using algorithm: some JWTAlgorithm) async throws {
try exp.verifyNotExpired()
guard iss.value == "https://ecdsa.com" else {
throw JWTError.claimVerificationFailure(failedClaim: iss, reason: "unexpected issuer")
}
}
}
Sources/verify/main.swift — load the key, verify the token
import Foundation
import JWTKit
let keys = JWTKeyCollection()
await keys.add(ecdsa: try ES256PublicKey(pem: String(contentsOfFile: "pub.pem", encoding: .utf8)))
let token = try String(contentsOfFile: "token.txt", encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)
let claims = try await keys.verify(token, as: Claims.self) // ES256 signature, then Claims.verify
print("valid ES256 token for \(claims.sub.value), expires \(claims.exp.value)")

How it works

  1. ES256PublicKey(pem:) reads the SPKI public key; keys.add(ecdsa:) stores it as the default signer (pass kid: to store several keys for rotation).
  2. keys.verify(token, as: Claims.self) picks the signer by the token's kid (or the default), checks the ES256 signature over header.payload, decodes the payload and then calls Claims.verify(using:).
  3. ExpirationClaim.verifyNotExpired() rejects expired tokens; any other rule — issuer, audience, required scopes — is ordinary Swift code throwing JWTError.claimVerificationFailure.
  4. Failures arrive as JWTError with an errorType such as signatureVerificationFailed or claimVerificationFailure and a reason; the run triggers both.

Gotchas

  • jwt-kit verifies with the algorithm of the stored key, not with the one the token's header names: the token relabelled HS256 in the run fails as signatureVerificationFailed, not with an algorithm error. Expect that error kind for every header tampering.
  • Claims are only checked if your verify(using:) checks them. An empty implementation accepts expired tokens and tokens from any issuer.
  • keys.unverified(token, as:) and DefaultJWTParser().parse(...) decode without checking the signature — useful for logging a kid, never for authorization.
  • jwt-kit 5.7.1 requires swift-crypto below 5.0, so SwiftPM resolves it to 4.5.2 here. A package that also pins swift-crypto 5.x will not resolve until jwt-kit allows it — align the versions rather than forking.

Related recipes