From 82baf78d61e77aa8032e521112e5fb184a112af3 Mon Sep 17 00:00:00 2001 From: Mark Murphy Date: Sat, 7 Dec 2019 15:39:44 +0000 Subject: [PATCH 1/2] Add HeaderProtocol to support flexible header definitions --- Sources/SwiftJWT/Header.swift | 26 ++++++---- Sources/SwiftJWT/JWT.swift | 14 +++--- Tests/SwiftJWTTests/TestJWT.swift | 80 +++++++++++++++---------------- 3 files changed, 64 insertions(+), 56 deletions(-) diff --git a/Sources/SwiftJWT/Header.swift b/Sources/SwiftJWT/Header.swift index 7f6860e..ea3404e 100644 --- a/Sources/SwiftJWT/Header.swift +++ b/Sources/SwiftJWT/Header.swift @@ -18,6 +18,20 @@ import Foundation // MARK: Header +/// Header protocol +public protocol HeaderProtocol: Codable { + /// Algorithm Header Parameter + var alg: String? { get set } +} +public extension HeaderProtocol { + func encode() throws -> String { + let jsonEncoder = JSONEncoder() + jsonEncoder.dateEncodingStrategy = .secondsSince1970 + let data = try jsonEncoder.encode(self) + return JWTEncoder.base64urlEncodedString(data: data) + } +} + /** A representation of a JSON Web Token header. https://tools.ietf.org/html/rfc7515#section-4.1 @@ -30,12 +44,12 @@ import Foundation let jwt = JWT(header: myHeader, claims: MyClaims(name: "Kitura")) ``` */ -public struct Header: Codable { - +public struct Header: HeaderProtocol { + /// Type Header Parameter public var typ: String? /// Algorithm Header Parameter - public internal(set) var alg: String? + public var alg: String? /// JSON Web Token Set URL Header Parameter public var jku : String? /// JSON Web Key Header Parameter @@ -93,10 +107,4 @@ public struct Header: Codable { self.crit = crit } - func encode() throws -> String { - let jsonEncoder = JSONEncoder() - jsonEncoder.dateEncodingStrategy = .secondsSince1970 - let data = try jsonEncoder.encode(self) - return JWTEncoder.base64urlEncodedString(data: data) - } } diff --git a/Sources/SwiftJWT/JWT.swift b/Sources/SwiftJWT/JWT.swift index fad09ff..563814d 100644 --- a/Sources/SwiftJWT/JWT.swift +++ b/Sources/SwiftJWT/JWT.swift @@ -27,16 +27,16 @@ import Foundation struct MyClaims: Claims { var name: String } - let jwt = JWT(claims: MyClaims(name: "Kitura")) + let jwt = JWT(header: Header(), claims: MyClaims(name: "Kitura")) let key = "".data(using: .utf8)! let signedJWT: String? = try? jwt.sign(using: .rs256(key: key, keyType: .privateKey)) ``` */ -public struct JWT: Codable { +public struct JWT: Codable { /// The JWT header. - public var header: Header + public var header: H /// The JWT claims public var claims: T @@ -46,11 +46,11 @@ public struct JWT: Codable { /// - Parameter header: A JSON Web Token header object. /// - Parameter claims: A JSON Web Token claims object. /// - Returns: A new instance of `JWT`. - public init(header: Header = Header(), claims: T) { + public init(header: H, claims: T) { self.header = header self.claims = claims } - + /// Initialize a `JWT` instance from a JWT String. /// The signature will be verified using the provided JWTVerifier. /// The time based standard JWT claims will be verified with `validateClaims()`. @@ -75,7 +75,7 @@ public struct JWT: Codable { } let jsonDecoder = JSONDecoder() jsonDecoder.dateDecodingStrategy = .secondsSince1970 - let header = try jsonDecoder.decode(Header.self, from: headerData) + let header = try jsonDecoder.decode(H.self, from: headerData) let claims = try jsonDecoder.decode(T.self, from: claimsData) self.header = header self.claims = claims @@ -114,7 +114,7 @@ public struct JWT: Codable { /// /// - Parameter leeway: The time in seconds that the JWT can be invalid but still accepted to account for clock differences. /// - Returns: A value of `ValidateClaimsResult`. - public func validateClaims(leeway: TimeInterval = 0) -> ValidateClaimsResult { + public func validateClaims(leeway: TimeInterval = 0) -> ValidateClaimsResult { if let expirationDate = claims.exp { if expirationDate + leeway < Date() { return .expired diff --git a/Tests/SwiftJWTTests/TestJWT.swift b/Tests/SwiftJWTTests/TestJWT.swift index ea7b8cd..72d1bc3 100644 --- a/Tests/SwiftJWTTests/TestJWT.swift +++ b/Tests/SwiftJWTTests/TestJWT.swift @@ -296,7 +296,7 @@ class TestJWT: XCTestCase { } func signAndVerify(signer: JWTSigner, verifier: JWTVerifier) throws { - var jwt = JWT(claims: TestClaims(name:"Kitura")) + var jwt = JWT(header: Header(), claims: TestClaims(name:"Kitura")) jwt.claims.name = "Kitura-JWT" XCTAssertEqual(jwt.claims.name, "Kitura-JWT") jwt.claims.iss = "issuer" @@ -305,14 +305,14 @@ class TestJWT: XCTestCase { jwt.claims.exp = Date(timeIntervalSince1970: 2485949565.58463) jwt.claims.nbf = Date(timeIntervalSince1970: 1485949565.58463) let signed = try jwt.sign(using: signer) - let ok = JWT.verify(signed, using: verifier) + let ok = JWT.verify(signed, using: verifier) XCTAssertTrue(ok, "Verification failed") - let decoded = try JWT(jwtString: signed) + let decoded = try JWT(jwtString: signed) check(jwt: decoded, algorithm: signer.name) XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") } - func check(jwt: JWT, algorithm: String) { + func check(jwt: JWT, algorithm: String) { XCTAssertEqual(jwt.header.alg, algorithm, "Wrong .alg in decoded") XCTAssertEqual(jwt.claims.exp, Date(timeIntervalSince1970: 2485949565.58463), "Wrong .exp in decoded") @@ -320,7 +320,7 @@ class TestJWT: XCTestCase { XCTAssertEqual(jwt.claims.nbf, Date(timeIntervalSince1970: 1485949565.58463), "Wrong .nbf in decoded") } - func checkMicroProfile(jwt: JWT, algorithm: String) { + func checkMicroProfile(jwt: JWT, algorithm: String) { XCTAssertEqual(jwt.header.alg, "RS256", "Wrong .alg in decoded. MicroProfile only supports RS256.") XCTAssertEqual(jwt.claims.iss, "https://server.example.com", "Wrong .iss in decoded") @@ -333,7 +333,7 @@ class TestJWT: XCTestCase { func testMicroProfile() { - var jwt = JWT(claims: MicroProfile(name: "MP-JWT")) + var jwt = JWT(header: Header(), claims: MicroProfile(name: "MP-JWT")) jwt.header.kid = "abc-1234567890" jwt.header.typ = "JWT" XCTAssertEqual(jwt.claims.name, "MP-JWT") @@ -346,10 +346,10 @@ class TestJWT: XCTestCase { // public key (MP-JWT needs to be signed) if let signed = try? jwt.sign(using: .rs256(privateKey: rsaPrivateKey)) { - let ok = JWT.verify(signed, using: .rs256(publicKey: rsaPublicKey)) + let ok = JWT.verify(signed, using: .rs256(publicKey: rsaPublicKey)) XCTAssertTrue(ok, "Verification failed") - if let decoded = try? JWT(jwtString: signed) { + if let decoded = try? JWT(jwtString: signed) { checkMicroProfile(jwt: decoded, algorithm: "RS256") XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") @@ -364,10 +364,10 @@ class TestJWT: XCTestCase { // certificate if let signed = try? jwt.sign(using: .rs256(privateKey: certPrivateKey)) { - let ok = JWT.verify(signed, using: .rs256(certificate: certificate)) + let ok = JWT.verify(signed, using: .rs256(certificate: certificate)) XCTAssertTrue(ok, "Verification failed") - if let decoded = try? JWT(jwtString: signed) { + if let decoded = try? JWT(jwtString: signed) { checkMicroProfile(jwt: decoded, algorithm: "RS256") XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") @@ -378,19 +378,19 @@ class TestJWT: XCTestCase { } } - // This test uses the rsaJWTEncoder to encode a JWT as a JWT String. + // This test uses the rsaJWTEncoder to encode a JWT as a JWT String. // It then decodes the resulting JWT String using the JWT init from String. // The test checks that the decoded JWT is the same as the JWT you started as well as the decoded rsaEncodedTestClaimJWT. func testJWTEncoder() { - var jwt = JWT(claims: TestClaims()) + var jwt = JWT(header: Header(), claims: TestClaims()) jwt.claims.sub = "1234567890" jwt.claims.name = "John Doe" jwt.claims.admin = true jwt.claims.iat = Date(timeIntervalSince1970: 1516239022) do { let jwtString = try rsaJWTEncoder.encodeToString(jwt) - let decodedJWTString = try JWT(jwtString: jwtString) - let decodedTestClaimJWT = try JWT(jwtString: rsaEncodedTestClaimJWT) + let decodedJWTString = try JWT(jwtString: jwtString) + let decodedTestClaimJWT = try JWT(jwtString: rsaEncodedTestClaimJWT) // Setting the alg field on the header since the decoded JWT will have had the alg header set in the signing process. jwt.header.alg = "RS256" XCTAssertEqual(jwt.claims, decodedJWTString.claims) @@ -402,16 +402,16 @@ class TestJWT: XCTestCase { } } - // This test uses the rsaJWTDecoder to decode the rsaEncodedTestClaimJWT as a JWT. + // This test uses the rsaJWTDecoder to decode the rsaEncodedTestClaimJWT as a JWT. // The test checks that the decoded JWT is the same as the JWT that was originally encoded. func testJWTDecoder() { - var jwt = JWT(claims: TestClaims()) + var jwt = JWT(header: Header(), claims: TestClaims()) jwt.claims.sub = "1234567890" jwt.claims.name = "John Doe" jwt.claims.admin = true jwt.claims.iat = Date(timeIntervalSince1970: 1516239022) do { - let decodedJWT = try rsaJWTDecoder.decode(JWT.self, fromString: rsaEncodedTestClaimJWT) + let decodedJWT = try rsaJWTDecoder.decode(JWT.self, fromString: rsaEncodedTestClaimJWT) jwt.header.alg = "RS256" XCTAssertEqual(decodedJWT.claims, jwt.claims) XCTAssertEqual(decodedJWT.header, jwt.header) @@ -420,15 +420,15 @@ class TestJWT: XCTestCase { } } - // This test encoded and then decoded a JWT and checks you get the original JWT back with only the alg header changed. + // This test encoded and then decoded a JWT and checks you get the original JWT back with only the alg header changed. func testJWTCoderCycle() { - var jwt = JWT(claims: TestClaims()) + var jwt = JWT(header: Header(), claims: TestClaims()) jwt.claims.sub = "1234567890" jwt.claims.name = "John Doe" jwt.claims.admin = true do { let jwtData = try rsaJWTEncoder.encode(jwt) - let decodedJWT = try rsaJWTDecoder.decode(JWT.self, from: jwtData) + let decodedJWT = try rsaJWTDecoder.decode(JWT.self, from: jwtData) jwt.header.alg = "RS256" XCTAssertEqual(decodedJWT.claims, jwt.claims) XCTAssertEqual(decodedJWT.header, jwt.header) @@ -437,18 +437,18 @@ class TestJWT: XCTestCase { } } - // This test uses the rsaJWTKidEncoder to encode a JWT as a JWT String using the kid header to select the JWTSigner. + // This test uses the rsaJWTKidEncoder to encode a JWT as a JWT String using the kid header to select the JWTSigner. // It then decodes the resulting JWT String using the JWT init from String. // The test checks that the decoded JWT is the same as the JWT you started as well as the decoded certificateEncodedTestClaimJWT. func testJWTEncoderKeyID() { - var jwt = JWT(claims: TestClaims()) + var jwt = JWT(header: Header(), claims: TestClaims()) jwt.header.kid = "0" jwt.claims.sub = "1234567890" jwt.claims.name = "John Doe" jwt.claims.admin = true do { let jwtString = try rsaJWTKidEncoder.encodeToString(jwt) - let decodedJWTString = try JWT(jwtString: jwtString) + let decodedJWTString = try JWT(jwtString: jwtString) jwt.header.alg = "RS256" XCTAssertEqual(jwt.claims, decodedJWTString.claims) XCTAssertEqual(jwt.header, decodedJWTString.header) @@ -457,16 +457,16 @@ class TestJWT: XCTestCase { } } - // This test uses the rsaJWTKidDecoder to decode the certificateEncodedTestClaimJWT as a JWT using the kid header to select the JWTVerifier. + // This test uses the rsaJWTKidDecoder to decode the certificateEncodedTestClaimJWT as a JWT using the kid header to select the JWTVerifier. // The test checks that the decoded JWT is the same as the JWT that was originally encoded. func testJWTDecoderKeyID() { - var jwt = JWT(claims: TestClaims()) + var jwt = JWT(header: Header(), claims: TestClaims()) jwt.header.kid = "1" jwt.claims.sub = "1234567890" jwt.claims.name = "John Doe" jwt.claims.admin = true do { - let decodedJWT = try rsaJWTKidDecoder.decode(JWT.self, fromString: certificateEncodedTestClaimJWT) + let decodedJWT = try rsaJWTKidDecoder.decode(JWT.self, fromString: certificateEncodedTestClaimJWT) jwt.header.alg = "RS256" XCTAssertEqual(decodedJWT.claims, jwt.claims) XCTAssertEqual(decodedJWT.header, jwt.header) @@ -475,17 +475,17 @@ class TestJWT: XCTestCase { } } - // This test encoded and then decoded a JWT and checks you get the original JWT back with only the alg header changed. + // This test encoded and then decoded a JWT and checks you get the original JWT back with only the alg header changed. // The kid header is used to select the rsa private and public keys for encoding/decoding. func testJWTCoderCycleKeyID() { - var jwt = JWT(claims: TestClaims()) + var jwt = JWT(header: Header(), claims: TestClaims()) jwt.header.kid = "1" jwt.claims.sub = "1234567890" jwt.claims.name = "John Doe" jwt.claims.admin = true do { let jwtData = try rsaJWTKidEncoder.encode(jwt) - let decodedJWT = try rsaJWTKidDecoder.decode(JWT.self, from: jwtData) + let decodedJWT = try rsaJWTKidDecoder.decode(JWT.self, from: jwtData) jwt.header.alg = "RS256" XCTAssertEqual(decodedJWT.claims, jwt.claims) XCTAssertEqual(decodedJWT.header, jwt.header) @@ -496,10 +496,10 @@ class TestJWT: XCTestCase { // From jwt.io func testJWT() { - let ok = JWT.verify(rsaEncodedTestClaimJWT, using: .rs256(publicKey: rsaPublicKey)) + let ok = JWT.verify(rsaEncodedTestClaimJWT, using: .rs256(publicKey: rsaPublicKey)) XCTAssertTrue(ok, "Verification failed") - if let decoded = try? JWT(jwtString: rsaEncodedTestClaimJWT) { + if let decoded = try? JWT(jwtString: rsaEncodedTestClaimJWT) { XCTAssertEqual(decoded.header.alg, "RS256", "Wrong .alg in decoded") XCTAssertEqual(decoded.header.typ, "JWT", "Wrong .typ in decoded") @@ -519,10 +519,10 @@ class TestJWT: XCTestCase { // From jwt.io func testJWTRSAPSS() { if #available(OSX 10.13, *) { - let ok = JWT.verify(rsaPSSEncodedTestClaimJWT, using: .ps256(publicKey: rsaPublicKey)) + let ok = JWT.verify(rsaPSSEncodedTestClaimJWT, using: .ps256(publicKey: rsaPublicKey)) XCTAssertTrue(ok, "Verification failed") - if let decoded = try? JWT(jwtString: rsaPSSEncodedTestClaimJWT) { + if let decoded = try? JWT(jwtString: rsaPSSEncodedTestClaimJWT) { XCTAssertEqual(decoded.header.alg, "PS256", "Wrong .alg in decoded") XCTAssertEqual(decoded.header.typ, "JWT", "Wrong .typ in decoded") @@ -544,10 +544,10 @@ class TestJWT: XCTestCase { guard let hmacData = "Super Secret Key".data(using: .utf8) else { return XCTFail("Failed to convert hmacKey to Data") } - let ok = JWT.verify(hmacEncodedTestClaimJWT, using: .hs256(key: hmacData)) + let ok = JWT.verify(hmacEncodedTestClaimJWT, using: .hs256(key: hmacData)) XCTAssertTrue(ok, "Verification failed") - if let decoded = try? JWT(jwtString: hmacEncodedTestClaimJWT) { + if let decoded = try? JWT(jwtString: hmacEncodedTestClaimJWT) { XCTAssertEqual(decoded.header.alg, "HS256", "Wrong .alg in decoded") XCTAssertEqual(decoded.header.typ, "JWT", "Wrong .typ in decoded") @@ -565,10 +565,10 @@ class TestJWT: XCTestCase { // Test using a JWT generated from jwt.io using es256 with `ecdsaPrivateKey` for interoperability. func testJWTUsingECDSA() { if #available(OSX 10.13, iOS 11, tvOS 11.0, *) { - let ok = JWT.verify(ecdsaEncodedTestClaimJWT, using: .es256(publicKey: ecdsaPublicKey)) + let ok = JWT.verify(ecdsaEncodedTestClaimJWT, using: .es256(publicKey: ecdsaPublicKey)) XCTAssertTrue(ok, "Verification failed") - if let decoded = try? JWT(jwtString: ecdsaEncodedTestClaimJWT) { + if let decoded = try? JWT(jwtString: ecdsaEncodedTestClaimJWT) { XCTAssertEqual(decoded.header.alg, "ES256", "Wrong .alg in decoded") XCTAssertEqual(decoded.header.typ, "JWT", "Wrong .typ in decoded") @@ -587,7 +587,7 @@ class TestJWT: XCTestCase { } func testValidateClaims() { - var jwt = JWT(claims: TestClaims(name:"Kitura")) + var jwt = JWT(header: Header(), claims: TestClaims(name:"Kitura")) jwt.claims.exp = Date() XCTAssertEqual(jwt.validateClaims(), .expired, "Validation failed") jwt.claims.exp = nil @@ -599,7 +599,7 @@ class TestJWT: XCTestCase { } func testValidateClaimsLeeway() { - var jwt = JWT(claims: TestClaims(name:"Kitura")) + var jwt = JWT(header: Header(), claims: TestClaims(name:"Kitura")) jwt.claims.exp = Date() XCTAssertEqual(jwt.validateClaims(leeway: 20), .success, "Validation failed") jwt.claims.exp = nil @@ -612,7 +612,7 @@ class TestJWT: XCTestCase { func testErrorPattenMatching() { do { - let _ = try JWT(jwtString: "InvalidString", verifier: .rs256(publicKey: rsaPublicKey)) + let _ = try JWT(jwtString: "InvalidString", verifier: .rs256(publicKey: rsaPublicKey)) } catch JWTError.invalidJWTString { // Caught correct error } catch { From 5ef48ce43bcfdbcf58668bff548f4979ede43e3d Mon Sep 17 00:00:00 2001 From: Mark Murphy Date: Mon, 20 Jan 2020 22:05:53 +0000 Subject: [PATCH 2/2] Ensure encode is customisation point. --- Sources/SwiftJWT/Header.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SwiftJWT/Header.swift b/Sources/SwiftJWT/Header.swift index ea3404e..420d78d 100644 --- a/Sources/SwiftJWT/Header.swift +++ b/Sources/SwiftJWT/Header.swift @@ -22,6 +22,7 @@ import Foundation public protocol HeaderProtocol: Codable { /// Algorithm Header Parameter var alg: String? { get set } + func encode() throws -> String } public extension HeaderProtocol { func encode() throws -> String {