Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HeaderProtocol to support flexible header definitions #84

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions Sources/SwiftJWT/Header.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ import Foundation

// MARK: Header

/// Header protocol
public protocol HeaderProtocol: Codable {
/// Algorithm Header Parameter
var alg: String? { get set }
func encode() throws -> String
}
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
Expand All @@ -30,12 +45,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
Expand Down Expand Up @@ -93,10 +108,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)
}
}
14 changes: 7 additions & 7 deletions Sources/SwiftJWT/JWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<PrivateKey>".data(using: .utf8)!
let signedJWT: String? = try? jwt.sign(using: .rs256(key: key, keyType: .privateKey))
```
*/

public struct JWT<T: Claims>: Codable {
public struct JWT<H: HeaderProtocol, T: Claims>: Codable {

/// The JWT header.
public var header: Header
public var header: H

/// The JWT claims
public var claims: T
Expand All @@ -46,11 +46,11 @@ public struct JWT<T: Claims>: 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a breaking change. Is it possible to retain the default value for header so that it is non-breaking? (ie. the tests should not have to change, unless you are adding one to test the new facility).

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()`.
Expand All @@ -75,7 +75,7 @@ public struct JWT<T: Claims>: 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
Expand Down Expand Up @@ -114,7 +114,7 @@ public struct JWT<T: Claims>: 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
Expand Down
80 changes: 40 additions & 40 deletions Tests/SwiftJWTTests/TestJWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -305,22 +305,22 @@ 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<TestClaims>.verify(signed, using: verifier)
let ok = JWT<Header, TestClaims>.verify(signed, using: verifier)
XCTAssertTrue(ok, "Verification failed")
let decoded = try JWT<TestClaims>(jwtString: signed)
let decoded = try JWT<Header, TestClaims>(jwtString: signed)
check(jwt: decoded, algorithm: signer.name)
XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed")
}

func check(jwt: JWT<TestClaims>, algorithm: String) {
func check(jwt: JWT<Header, TestClaims>, algorithm: String) {

XCTAssertEqual(jwt.header.alg, algorithm, "Wrong .alg in decoded")
XCTAssertEqual(jwt.claims.exp, Date(timeIntervalSince1970: 2485949565.58463), "Wrong .exp in decoded")
XCTAssertEqual(jwt.claims.iat, Date(timeIntervalSince1970: 1485949565.58463), "Wrong .iat in decoded")
XCTAssertEqual(jwt.claims.nbf, Date(timeIntervalSince1970: 1485949565.58463), "Wrong .nbf in decoded")
}

func checkMicroProfile(jwt: JWT<MicroProfile>, algorithm: String) {
func checkMicroProfile(jwt: JWT<Header, MicroProfile>, 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")
Expand All @@ -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")
Expand All @@ -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<MicroProfile>.verify(signed, using: .rs256(publicKey: rsaPublicKey))
let ok = JWT<Header, MicroProfile>.verify(signed, using: .rs256(publicKey: rsaPublicKey))
XCTAssertTrue(ok, "Verification failed")

if let decoded = try? JWT<MicroProfile>(jwtString: signed) {
if let decoded = try? JWT<Header, MicroProfile>(jwtString: signed) {
checkMicroProfile(jwt: decoded, algorithm: "RS256")

XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed")
Expand All @@ -364,10 +364,10 @@ class TestJWT: XCTestCase {

// certificate
if let signed = try? jwt.sign(using: .rs256(privateKey: certPrivateKey)) {
let ok = JWT<MicroProfile>.verify(signed, using: .rs256(certificate: certificate))
let ok = JWT<Header, MicroProfile>.verify(signed, using: .rs256(certificate: certificate))
XCTAssertTrue(ok, "Verification failed")

if let decoded = try? JWT<MicroProfile>(jwtString: signed) {
if let decoded = try? JWT<Header, MicroProfile>(jwtString: signed) {
checkMicroProfile(jwt: decoded, algorithm: "RS256")

XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed")
Expand All @@ -378,19 +378,19 @@ class TestJWT: XCTestCase {
}
}

// This test uses the rsaJWTEncoder to encode a JWT<TestClaims> as a JWT String.
// This test uses the rsaJWTEncoder to encode a JWT<Header, TestClaims> 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<TestClaims>(jwtString: jwtString)
let decodedTestClaimJWT = try JWT<TestClaims>(jwtString: rsaEncodedTestClaimJWT)
let decodedJWTString = try JWT<Header, TestClaims>(jwtString: jwtString)
let decodedTestClaimJWT = try JWT<Header, TestClaims>(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)
Expand All @@ -402,16 +402,16 @@ class TestJWT: XCTestCase {
}
}

// This test uses the rsaJWTDecoder to decode the rsaEncodedTestClaimJWT as a JWT<TestClaims>.
// This test uses the rsaJWTDecoder to decode the rsaEncodedTestClaimJWT as a JWT<Header, TestClaims>.
// 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<TestClaims>.self, fromString: rsaEncodedTestClaimJWT)
let decodedJWT = try rsaJWTDecoder.decode(JWT<Header, TestClaims>.self, fromString: rsaEncodedTestClaimJWT)
jwt.header.alg = "RS256"
XCTAssertEqual(decodedJWT.claims, jwt.claims)
XCTAssertEqual(decodedJWT.header, jwt.header)
Expand All @@ -420,15 +420,15 @@ class TestJWT: XCTestCase {
}
}

// This test encoded and then decoded a JWT<TestClaims> and checks you get the original JWT back with only the alg header changed.
// This test encoded and then decoded a JWT<Header, TestClaims> 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<TestClaims>.self, from: jwtData)
let decodedJWT = try rsaJWTDecoder.decode(JWT<Header, TestClaims>.self, from: jwtData)
jwt.header.alg = "RS256"
XCTAssertEqual(decodedJWT.claims, jwt.claims)
XCTAssertEqual(decodedJWT.header, jwt.header)
Expand All @@ -437,18 +437,18 @@ class TestJWT: XCTestCase {
}
}

// This test uses the rsaJWTKidEncoder to encode a JWT<TestClaims> as a JWT String using the kid header to select the JWTSigner.
// This test uses the rsaJWTKidEncoder to encode a JWT<Header, TestClaims> 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<TestClaims>(jwtString: jwtString)
let decodedJWTString = try JWT<Header, TestClaims>(jwtString: jwtString)
jwt.header.alg = "RS256"
XCTAssertEqual(jwt.claims, decodedJWTString.claims)
XCTAssertEqual(jwt.header, decodedJWTString.header)
Expand All @@ -457,16 +457,16 @@ class TestJWT: XCTestCase {
}
}

// This test uses the rsaJWTKidDecoder to decode the certificateEncodedTestClaimJWT as a JWT<TestClaims> using the kid header to select the JWTVerifier.
// This test uses the rsaJWTKidDecoder to decode the certificateEncodedTestClaimJWT as a JWT<Header, TestClaims> 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<TestClaims>.self, fromString: certificateEncodedTestClaimJWT)
let decodedJWT = try rsaJWTKidDecoder.decode(JWT<Header, TestClaims>.self, fromString: certificateEncodedTestClaimJWT)
jwt.header.alg = "RS256"
XCTAssertEqual(decodedJWT.claims, jwt.claims)
XCTAssertEqual(decodedJWT.header, jwt.header)
Expand All @@ -475,17 +475,17 @@ class TestJWT: XCTestCase {
}
}

// This test encoded and then decoded a JWT<TestClaims> and checks you get the original JWT back with only the alg header changed.
// This test encoded and then decoded a JWT<Header, TestClaims> 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<TestClaims>.self, from: jwtData)
let decodedJWT = try rsaJWTKidDecoder.decode(JWT<Header, TestClaims>.self, from: jwtData)
jwt.header.alg = "RS256"
XCTAssertEqual(decodedJWT.claims, jwt.claims)
XCTAssertEqual(decodedJWT.header, jwt.header)
Expand All @@ -496,10 +496,10 @@ class TestJWT: XCTestCase {

// From jwt.io
func testJWT() {
let ok = JWT<TestClaims>.verify(rsaEncodedTestClaimJWT, using: .rs256(publicKey: rsaPublicKey))
let ok = JWT<Header, TestClaims>.verify(rsaEncodedTestClaimJWT, using: .rs256(publicKey: rsaPublicKey))
XCTAssertTrue(ok, "Verification failed")

if let decoded = try? JWT<TestClaims>(jwtString: rsaEncodedTestClaimJWT) {
if let decoded = try? JWT<Header, TestClaims>(jwtString: rsaEncodedTestClaimJWT) {
XCTAssertEqual(decoded.header.alg, "RS256", "Wrong .alg in decoded")
XCTAssertEqual(decoded.header.typ, "JWT", "Wrong .typ in decoded")

Expand All @@ -519,10 +519,10 @@ class TestJWT: XCTestCase {
// From jwt.io
func testJWTRSAPSS() {
if #available(OSX 10.13, *) {
let ok = JWT<TestClaims>.verify(rsaPSSEncodedTestClaimJWT, using: .ps256(publicKey: rsaPublicKey))
let ok = JWT<Header, TestClaims>.verify(rsaPSSEncodedTestClaimJWT, using: .ps256(publicKey: rsaPublicKey))
XCTAssertTrue(ok, "Verification failed")

if let decoded = try? JWT<TestClaims>(jwtString: rsaPSSEncodedTestClaimJWT) {
if let decoded = try? JWT<Header, TestClaims>(jwtString: rsaPSSEncodedTestClaimJWT) {
XCTAssertEqual(decoded.header.alg, "PS256", "Wrong .alg in decoded")
XCTAssertEqual(decoded.header.typ, "JWT", "Wrong .typ in decoded")

Expand All @@ -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<TestClaims>.verify(hmacEncodedTestClaimJWT, using: .hs256(key: hmacData))
let ok = JWT<Header, TestClaims>.verify(hmacEncodedTestClaimJWT, using: .hs256(key: hmacData))
XCTAssertTrue(ok, "Verification failed")

if let decoded = try? JWT<TestClaims>(jwtString: hmacEncodedTestClaimJWT) {
if let decoded = try? JWT<Header, TestClaims>(jwtString: hmacEncodedTestClaimJWT) {
XCTAssertEqual(decoded.header.alg, "HS256", "Wrong .alg in decoded")
XCTAssertEqual(decoded.header.typ, "JWT", "Wrong .typ in decoded")

Expand All @@ -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<TestClaims>.verify(ecdsaEncodedTestClaimJWT, using: .es256(publicKey: ecdsaPublicKey))
let ok = JWT<Header, TestClaims>.verify(ecdsaEncodedTestClaimJWT, using: .es256(publicKey: ecdsaPublicKey))
XCTAssertTrue(ok, "Verification failed")

if let decoded = try? JWT<TestClaims>(jwtString: ecdsaEncodedTestClaimJWT) {
if let decoded = try? JWT<Header, TestClaims>(jwtString: ecdsaEncodedTestClaimJWT) {
XCTAssertEqual(decoded.header.alg, "ES256", "Wrong .alg in decoded")
XCTAssertEqual(decoded.header.typ, "JWT", "Wrong .typ in decoded")

Expand All @@ -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
Expand All @@ -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
Expand All @@ -612,7 +612,7 @@ class TestJWT: XCTestCase {

func testErrorPattenMatching() {
do {
let _ = try JWT<TestClaims>(jwtString: "InvalidString", verifier: .rs256(publicKey: rsaPublicKey))
let _ = try JWT<Header, TestClaims>(jwtString: "InvalidString", verifier: .rs256(publicKey: rsaPublicKey))
} catch JWTError.invalidJWTString {
// Caught correct error
} catch {
Expand Down