// swift-tools-version:6.0import PackageDescription let package = Package( name: "jwks-demo", platforms: [.macOS(.v13)], dependencies: [ .package(url: "https://github.com/vapor/jwt-kit.git", exact: "5.7.1"), ], targets: [ .executableTarget(name: "jwks-demo", dependencies: [.product(name: "JWTKit", package: "jwt-kit")]), ])import Foundation#if canImport(FoundationNetworking)import FoundationNetworking // URLSession on Linux#endifimport JWTKit struct AccessToken: JWTPayload { var iss: IssuerClaim var sub: SubjectClaim var aud: AudienceClaim var exp: ExpirationClaim // JWTKit checks the signature; the claims are checked here func verify(using algorithm: some JWTAlgorithm) throws { try exp.verifyNotExpired() try aud.verifyIntendedAudience(includes: "api://orders") guard iss.value == "https://issuer.example.com" else { throw JWTError.claimVerificationFailure(failedClaim: iss, reason: "unexpected issuer \(iss.value)") } }} // Keys from the JWKS, selected by the token's kid (fetch once, cache, refetch on rotation).// Each key verifies with its own "alg" from the JWKS, whatever the token header says.let jwksURL = URL(string: "https://issuer.example.com/.well-known/jwks.json")!let (jwksData, _) = try await URLSession.shared.data(from: jwksURL)let keys = try await JWTKeyCollection().add(jwksJSON: String(decoding: jwksData, as: UTF8.self))await keys.clearDefault() // otherwise a missing or unknown kid falls back to the first key of the set func verifyAccessToken(_ token: String) async throws -> AccessToken { try await keys.verify(token, as: AccessToken.self)}for line in try String(contentsOfFile: "tokens.txt", encoding: .utf8).split(separator: "\n") { let parts = line.split(separator: " ").map(String.init) do { let claims = try await verifyAccessToken(parts[1]) print("\(parts[0]): accepted, sub=\(claims.sub.value)") } catch { print("\(parts[0]): rejected: \(error)") }}How it works
URLSession.shared.data(from:)downloads the JWKS; on Linux,URLSessioncomes fromFoundationNetworking.add(jwksJSON:)registers every key under itskid; each JWK verifies with its ownalg.verify(_:as:)checks the signature, decodes the payload, and calls itsverify(using:), whereexp,audandissare checked.- Failures are
JWTErrorvalues:claimVerificationFailurewith the failing claim,signatureVerificationFailed, ornoKeyProvidedfor an unknownkid.
Gotchas
- The first key added becomes the default, and a token with an unknown or missing
kidis checked against it. WithoutclearDefault(), the token signed by an unpublished key fails assignatureVerificationFailed— misleading, and with a single-key JWKS it meanskidis not checked at all. - Claims are checked only in
verify(using:): JWTKit does nothing withaudorissunless the payload type does it. - The key collection here is loaded once. A server must reload it on a schedule and when a
kidis unknown, with a rate limit. import FoundationNetworkingis needed on Linux and must not be imported on Apple platforms; the#if canImportguard keeps one source for both.