// swift-tools-version:6.2import 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")]), ]) 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") } }} import Foundationimport 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.verifyprint("valid ES256 token for \(claims.sub.value), expires \(claims.exp.value)") How it works
ES256PublicKey(pem:)reads the SPKI public key;keys.add(ecdsa:)stores it as the default signer (passkid:to store several keys for rotation).keys.verify(token, as: Claims.self)picks the signer by the token'skid(or the default), checks the ES256 signature overheader.payload, decodes the payload and then callsClaims.verify(using:).ExpirationClaim.verifyNotExpired()rejects expired tokens; any other rule — issuer, audience, required scopes — is ordinary Swift code throwingJWTError.claimVerificationFailure.- Failures arrive as
JWTErrorwith anerrorTypesuch assignatureVerificationFailedorclaimVerificationFailureand areason; 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
HS256in the run fails assignatureVerificationFailed, 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:)andDefaultJWTParser().parse(...)decode without checking the signature — useful for logging akid, 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.