From 256c7252f79050500223198fd303aaeea9196485 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Wed, 28 Sep 2016 21:46:02 +0200 Subject: [PATCH 1/3] Payload and general swift improvements - merges PayloadBuilder and Payload - add type safe access to Payload properties - use guard syntax where applicable - use Swift error handling instead of returning optional InvalidTokens - rename Test classes - simplified Date verification error handling - consistent colon/space syntax --- JWTTests/JWTTests.swift | 118 ++++++++++++++------------------ Sources/Base64.swift | 5 +- Sources/Claims.swift | 65 ++++++++---------- Sources/Decode.swift | 82 +++++++++------------- Sources/JWT.swift | 148 ++++++++++++++++++++++++---------------- 5 files changed, 204 insertions(+), 214 deletions(-) diff --git a/JWTTests/JWTTests.swift b/JWTTests/JWTTests.swift index eddc6e5..3daba8d 100644 --- a/JWTTests/JWTTests.swift +++ b/JWTTests/JWTTests.swift @@ -2,84 +2,68 @@ import Foundation import XCTest import JWT -class JWTEncodeTests : XCTestCase { +class EncodeTests: XCTestCase { func testEncodingJWT() { - let payload = ["name": "Kyle"] as Payload - let jwt = JWT.encode(payload, algorithm: .hs256("secret".data(using: .utf8)!)) + let payload: Payload = ["name": "Kyle"] + let jwt = try! JWT.encode(payload, algorithm: .hs256("secret".data(using: .utf8)!)) let fixture = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiS3lsZSJ9.zxm7xcp1eZtZhp4t-nlw09ATQnnFKIiSN83uG8u6cAg" XCTAssertEqual(jwt, fixture) } - - func testEncodingWithBuilder() { - let algorithm = Algorithm.hs256("secret".data(using: .utf8)!) - let jwt = JWT.encode(algorithm) { builder in - builder.issuer = "fuller.li" - } - - assertSuccess(try JWT.decode(jwt, algorithm: algorithm)) { payload in - XCTAssertEqual(payload as NSDictionary, ["iss": "fuller.li"]) - } - } } -class JWTPayloadBuilder : XCTestCase { +class PayloadTests: XCTestCase { func testIssuer() { - JWT.encode(.none) { builder in - builder.issuer = "fuller.li" - XCTAssertEqual(builder.issuer, "fuller.li") - XCTAssertEqual(builder["iss"] as? String, "fuller.li") - } + var payload = Payload() + payload.issuer = "fuller.li" + XCTAssertEqual(payload.issuer, "fuller.li") } func testAudience() { - JWT.encode(.none) { builder in - builder.audience = "cocoapods" - XCTAssertEqual(builder.audience, "cocoapods") - XCTAssertEqual(builder["aud"] as? String, "cocoapods") - } + var payload = Payload() + payload.audience = "cocoapods" + XCTAssertEqual(payload.audience, "cocoapods") + } + + func testAudiences() { + var payload = Payload() + payload.audiences = ["cocoapods", "carthage"] + XCTAssertEqual(payload.audiences ?? [], ["cocoapods", "carthage"]) } func testExpiration() { - JWT.encode(.none) { builder in - let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970) - builder.expiration = date - XCTAssertEqual(builder.expiration, date) - XCTAssertEqual(builder["exp"] as? TimeInterval, date.timeIntervalSince1970) - } + var payload = Payload() + let date = Date() + payload.expiration = date + XCTAssertEqual(payload.expiration?.timeIntervalSince1970, date.timeIntervalSince1970) } func testNotBefore() { - JWT.encode(.none) { builder in - let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970) - builder.notBefore = date - XCTAssertEqual(builder.notBefore, date) - XCTAssertEqual(builder["nbf"] as? TimeInterval, date.timeIntervalSince1970) - } + var payload = Payload() + let date = Date() + payload.notBefore = date + XCTAssertEqual(payload.notBefore?.timeIntervalSince1970, date.timeIntervalSince1970) } func testIssuedAt() { - JWT.encode(.none) { builder in - let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970) - builder.issuedAt = date - XCTAssertEqual(builder.issuedAt, date) - XCTAssertEqual(builder["iat"] as? TimeInterval, date.timeIntervalSince1970) - } + var payload = Payload() + let date = Date() + payload.issuedAt = date + XCTAssertEqual(payload.issuedAt?.timeIntervalSince1970, date.timeIntervalSince1970) } func testCustomAttributes() { - JWT.encode(.none) { builder in - builder["user"] = "kyle" - XCTAssertEqual(builder["user"] as? String, "kyle") - } + var payload = Payload() + payload["user"] = "kyle" + XCTAssertEqual(payload["user"], "kyle") } } -class JWTDecodeTests : XCTestCase { +class DecodeTests: XCTestCase { func testDecodingValidJWT() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiS3lsZSJ9.zxm7xcp1eZtZhp4t-nlw09ATQnnFKIiSN83uG8u6cAg" assertSuccess(try JWT.decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) { payload in - XCTAssertEqual(payload as NSDictionary, ["name": "Kyle"]) + XCTAssertEqual(payload["name"], "Kyle") } } @@ -91,26 +75,26 @@ class JWTDecodeTests : XCTestCase { func testDisablingVerify() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" - assertSuccess(try decode(jwt, algorithm: .none, verify:false, issuer:"fuller.li")) + assertSuccess(try decode(jwt, algorithm: .none, verify: false, issuer: "fuller.li")) } // MARK: Issuer claim func testSuccessfulIssuerValidation() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.d7B7PAQcz1E6oNhrlxmHxHXHgg39_k7X7wWeahl8kSQ" - assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), issuer:"fuller.li")) { payload in - XCTAssertEqual(payload as NSDictionary, ["iss": "fuller.li"]) + assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), issuer: "fuller.li")) { payload in + XCTAssertEqual(payload.issuer, "fuller.li") } } func testIncorrectIssuerValidation() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmdWxsZXIubGkifQ.wOhJ9_6lx-3JGJPmJmtFCDI3kt7uMAMmhHIslti7ryI" - assertFailure(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), issuer:"querykit.org")) + assertFailure(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), issuer: "querykit.org")) } func testMissingIssuerValidation() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.2_8pWJfyPup0YwOXK7g9Dn0cF1E3pdn299t4hSeJy5w" - assertFailure(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), issuer:"fuller.li")) + assertFailure(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), issuer: "fuller.li")) } // MARK: Expiration claim @@ -129,7 +113,7 @@ class JWTDecodeTests : XCTestCase { // If this just started failing, hello 2024! let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjgxODg0OTF9.EW7k-8Mvnv0GpvOKJalFRLoCB3a3xGG3i7hAZZXNAz0" assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) { payload in - XCTAssertEqual(payload as NSDictionary, ["exp": 1728188491]) + XCTAssertEqual(payload.expiration?.timeIntervalSince1970, 1728188491) } } @@ -137,7 +121,7 @@ class JWTDecodeTests : XCTestCase { // If this just started failing, hello 2024! let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNzI4MTg4NDkxIn0.y4w7lNLrfRRPzuNUfM-ZvPkoOtrTU_d8ZVYasLdZGpk" assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) { payload in - XCTAssertEqual(payload as NSDictionary, ["exp": "1728188491"]) + XCTAssertEqual(payload.expiration?.timeIntervalSince1970, 1728188491) } } @@ -146,20 +130,20 @@ class JWTDecodeTests : XCTestCase { func testNotBeforeClaim() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0MjgxODk3MjB9.jFT0nXAJvEwyG6R7CMJlzNJb7FtZGv30QRZpYam5cvs" assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) { payload in - XCTAssertEqual(payload as NSDictionary, ["nbf": 1428189720]) + XCTAssertEqual(payload.notBefore?.timeIntervalSince1970, 1428189720) } } func testNotBeforeClaimString() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNDI4MTg5NzIwIn0.qZsj36irdmIAeXv6YazWDSFbpuxHtEh4Deof5YTpnVI" assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) { payload in - XCTAssertEqual(payload as NSDictionary, ["nbf": "1428189720"]) + XCTAssertEqual(payload.notBefore?.timeIntervalSince1970, 1428189720) } } func testInvalidNotBeforeClaim() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOlsxNDI4MTg5NzIwXX0.PUL1FQubzzJa4MNXe2D3d5t5cMaqFr3kYlzRUzly-C8" - assertDecodeError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)), error: "Not before claim (nbf) must be an integer") + assertDecodeError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)), error: "Incorrect date claim value for key: 'nbf'") } func testUnmetNotBeforeClaim() { @@ -173,14 +157,14 @@ class JWTDecodeTests : XCTestCase { func testIssuedAtClaimInThePast() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MjgxODk3MjB9.I_5qjRcCUZVQdABLwG82CSuu2relSdIyJOyvXWUAJh4" assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) { payload in - XCTAssertEqual(payload as NSDictionary, ["iat": 1428189720]) + XCTAssertEqual(payload.issuedAt?.timeIntervalSince1970, 1428189720) } } func testIssuedAtClaimInThePastString() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNDI4MTg5NzIwIn0.M8veWtsY52oBwi7LRKzvNnzhjK0QBS8Su1r0atlns2k" assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!))) { payload in - XCTAssertEqual(payload as NSDictionary, ["iat": "1428189720"]) + XCTAssertEqual(payload.issuedAt?.timeIntervalSince1970, 1428189720) } } @@ -193,7 +177,7 @@ class JWTDecodeTests : XCTestCase { func testInvalidIssuedAtClaim() { // If this just started failing, hello 2024! let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOlsxNzI4MTg4NDkxXX0.ND7QMWtLkXDXH38OaXM3SQgLo3Z5TNgF_pcfWHV_alQ" - assertDecodeError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)), error: "Issued at claim (iat) must be an integer") + assertDecodeError(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!)), error: "Incorrect date claim value for key: 'iat'") } // MARK: Audience claims @@ -201,14 +185,14 @@ class JWTDecodeTests : XCTestCase { func testAudiencesClaim() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWF4aW5lIiwia2F0aWUiXX0.-PKvdNLCClrWG7CvesHP6PB0-vxu-_IZcsYhJxBy5JM" assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), audience:"maxine")) { payload in - XCTAssertEqual(payload as NSDictionary, ["aud": ["maxine", "katie"]]) + XCTAssertEqual(payload.audiences ?? [], ["maxine", "katie"]) } } func testAudienceClaim() { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJreWxlIn0.dpgH4JOwueReaBoanLSxsGTc7AjKUvo7_M1sAfy_xVE" assertSuccess(try decode(jwt, algorithm: .hs256("secret".data(using: .utf8)!), audience:"kyle")) { payload in - XCTAssertEqual(payload as NSDictionary, ["aud": "kyle"]) + XCTAssertEqual(payload.audience, "kyle") } } @@ -227,7 +211,7 @@ class JWTDecodeTests : XCTestCase { func testNoneAlgorithm() { let jwt = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ0ZXN0IjoiaW5nIn0." assertSuccess(try decode(jwt, algorithm:.none)) { payload in - XCTAssertEqual(payload as NSDictionary, ["test": "ing"]) + XCTAssertEqual(payload["test"], "ing") } } @@ -244,14 +228,14 @@ class JWTDecodeTests : XCTestCase { func testHS384Algorithm() { let jwt = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.lddiriKLoo42qXduMhCTKZ5Lo3njXxOC92uXyvbLyYKzbq4CVVQOb3MpDwnI19u4" assertSuccess(try decode(jwt, algorithm: .hs384("secret".data(using: .utf8)!))) { payload in - XCTAssertEqual(payload as NSDictionary, ["some": "payload"]) + XCTAssertEqual(payload["some"], "payload") } } func testHS512Algorithm() { let jwt = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.WTzLzFO079PduJiFIyzrOah54YaM8qoxH9fLMQoQhKtw3_fMGjImIOokijDkXVbyfBqhMo2GCNu4w9v7UXvnpA" assertSuccess(try decode(jwt, algorithm: .hs512("secret".data(using: .utf8)!))) { payload in - XCTAssertEqual(payload as NSDictionary, ["some": "payload"]) + XCTAssertEqual(payload["some"], "payload") } } } diff --git a/Sources/Base64.swift b/Sources/Base64.swift index 32b18cf..f44f1e3 100644 --- a/Sources/Base64.swift +++ b/Sources/Base64.swift @@ -1,8 +1,7 @@ import Foundation - /// URI Safe base64 encode -func base64encode(_ input:Data) -> String { +func base64encode(_ input: Data) -> String { let data = input.base64EncodedData(options: NSData.Base64EncodingOptions(rawValue: 0)) let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as! String return string @@ -12,7 +11,7 @@ func base64encode(_ input:Data) -> String { } /// URI Safe base64 decode -func base64decode(_ input:String) -> Data? { +func base64decode(_ input: String) -> Data? { let rem = input.characters.count % 4 var ending = "" diff --git a/Sources/Claims.swift b/Sources/Claims.swift index 2b3f33d..9a817b7 100644 --- a/Sources/Claims.swift +++ b/Sources/Claims.swift @@ -1,53 +1,42 @@ import Foundation -func validateClaims(_ payload:Payload, audience:String?, issuer:String?) -> InvalidToken? { - return validateIssuer(payload, issuer: issuer) ?? validateAudience(payload, audience: audience) ?? - validateDate(payload, key: "exp", comparison: .orderedAscending, failure: .expiredSignature, decodeError: "Expiration time claim (exp) must be an integer") ?? - validateDate(payload, key: "nbf", comparison: .orderedDescending, failure: .immatureSignature, decodeError: "Not before claim (nbf) must be an integer") ?? - validateDate(payload, key: "iat", comparison: .orderedDescending, failure: .invalidIssuedAt, decodeError: "Issued at claim (iat) must be an integer") +func validateClaims(payload: Payload, audience: String?, issuer: String?) throws { + try validateIssuer(payload: payload, issuer: issuer) + try validateAudience(payload: payload, audience: audience) + try validateDate(payload: payload, key: "exp", comparison: .orderedAscending, failure: .expiredSignature) + try validateDate(payload: payload, key: "nbf", comparison: .orderedDescending, failure: .immatureSignature) + try validateDate(payload: payload, key: "iat", comparison: .orderedDescending, failure: .invalidIssuedAt) } -func validateAudience(_ payload:Payload, audience:String?) -> InvalidToken? { - if let audience = audience { - if let aud = payload["aud"] as? [String] { - if !aud.contains(audience) { - return .invalidAudience - } - } else if let aud = payload["aud"] as? String { - if aud != audience { - return .invalidAudience - } - } else { - return .decodeError("Invalid audience claim, must be a string or an array of strings") - } +func validateAudience(payload: Payload, audience: String?) throws { + guard let audience = audience else { + return } - return nil -} + guard let audiences = payload.audiences else { + throw InvalidToken.decodeError("Invalid audience claim, must be a string or an array of strings") + } -func validateIssuer(_ payload:Payload, issuer:String?) -> InvalidToken? { - if let issuer = issuer { - if let iss = payload["iss"] as? String { - if iss != issuer { - return .invalidIssuer - } - } else { - return .invalidIssuer - } + if !audiences.contains(audience) { + throw InvalidToken.invalidAudience } +} - return nil +func validateIssuer(payload: Payload, issuer: String?) throws { + if let issuer = issuer, issuer != payload.issuer { + throw InvalidToken.invalidIssuer + } } -func validateDate(_ payload:Payload, key:String, comparison:ComparisonResult, failure:InvalidToken, decodeError:String) -> InvalidToken? { - if let timestamp = payload[key] as? TimeInterval ?? (payload[key] as? NSString)?.doubleValue as TimeInterval? { - let date = Date(timeIntervalSince1970: timestamp) - if date.compare(Date()) == comparison { - return failure +func validateDate(payload: Payload, key: String, comparison: ComparisonResult, failure: InvalidToken) throws { + guard let validationDate: Date = payload[key] else { + if payload.store[key] != nil { + throw InvalidToken.decodeError("Incorrect date claim value for key: '\(key)'") } - } else if payload[key] != nil { - return .decodeError(decodeError) + return } - return nil + if validationDate.compare(Date()) == comparison { + throw failure + } } diff --git a/Sources/Decode.swift b/Sources/Decode.swift index 4ec9642..311fd72 100644 --- a/Sources/Decode.swift +++ b/Sources/Decode.swift @@ -1,8 +1,7 @@ import Foundation - /// Failure reasons from decoding a JWT -public enum InvalidToken : CustomStringConvertible, Error { +public enum InvalidToken: CustomStringConvertible, Error { /// Decoding the JWT itself failed case decodeError(String) @@ -45,39 +44,30 @@ public enum InvalidToken : CustomStringConvertible, Error { } } - /// Decode a JWT -public func decode(_ jwt:String, algorithms:[Algorithm], verify:Bool = true, audience:String? = nil, issuer:String? = nil) throws -> Payload { - switch load(jwt) { - case let .success(header, payload, signature, signatureInput): - if verify { - if let failure = validateClaims(payload, audience: audience, issuer: issuer) ?? verifySignature(algorithms, header: header, signingInput: signatureInput, signature: signature) { - throw failure - } - } +public func decode(_ jwt: String, algorithms: [Algorithm], verify: Bool = true, audience: String? = nil, issuer: String? = nil) throws -> Payload { - return payload - case .failure(let failure): - throw failure + let (header, payload, signature, signatureInput) = try load(jwt) + if verify { + try validateClaims(payload: payload, audience: audience, issuer: issuer) + try verifySignature(algorithms, header: header, signingInput: signatureInput, signature: signature) } + return payload } /// Decode a JWT -public func decode(_ jwt:String, algorithm:Algorithm, verify:Bool = true, audience:String? = nil, issuer:String? = nil) throws -> Payload { +public func decode(_ jwt: String, algorithm: Algorithm, verify: Bool = true, audience: String? = nil, issuer: String? = nil) throws -> Payload { return try decode(jwt, algorithms: [algorithm], verify: verify, audience: audience, issuer: issuer) } // MARK: Parsing a JWT -enum LoadResult { - case success(header:Payload, payload:Payload, signature:Data, signatureInput:String) - case failure(InvalidToken) -} +typealias LoadedToken = (header: Payload, payload: Payload, signature: Data, signatureInput: String) -func load(_ jwt:String) -> LoadResult { +func load(_ jwt: String) throws -> LoadedToken { let segments = jwt.components(separatedBy: ".") - if segments.count != 3 { - return .failure(.decodeError("Not enough segments")) + guard segments.count == 3 else { + throw InvalidToken.decodeError("Not enough segments") } let headerSegment = segments[0] @@ -85,47 +75,41 @@ func load(_ jwt:String) -> LoadResult { let signatureSegment = segments[2] let signatureInput = "\(headerSegment).\(payloadSegment)" - let headerData = base64decode(headerSegment) - if headerData == nil { - return .failure(.decodeError("Header is not correctly encoded as base64")) + guard let headerData = base64decode(headerSegment) else { + throw InvalidToken.decodeError("Header is not correctly encoded as base64") } - let header = (try? JSONSerialization.jsonObject(with: headerData!, options: JSONSerialization.ReadingOptions(rawValue: 0))) as? Payload - if header == nil { - return .failure(.decodeError("Invalid header")) + guard let header = try Payload(jsonData: headerData) else { + throw InvalidToken.decodeError("Invalid header") } - let payloadData = base64decode(payloadSegment) - if payloadData == nil { - return .failure(.decodeError("Payload is not correctly encoded as base64")) + guard let payloadData = base64decode(payloadSegment) else { + throw InvalidToken.decodeError("Payload is not correctly encoded as base64") } - let payload = (try? JSONSerialization.jsonObject(with: payloadData!, options: JSONSerialization.ReadingOptions(rawValue: 0))) as? Payload - if payload == nil { - return .failure(.decodeError("Invalid payload")) + guard let payload = try Payload(jsonData: payloadData) else { + throw InvalidToken.decodeError("Invalid payload") } - let signature = base64decode(signatureSegment) - if signature == nil { - return .failure(.decodeError("Signature is not correctly encoded as base64")) + guard let signature = base64decode(signatureSegment) else { + throw InvalidToken.decodeError("Signature is not correctly encoded as base64") } - return .success(header:header!, payload:payload!, signature:signature!, signatureInput:signatureInput) + return (header: header, payload: payload, signature: signature, signatureInput: signatureInput) } // MARK: Signature Verification -func verifySignature(_ algorithms:[Algorithm], header:Payload, signingInput:String, signature:Data) -> InvalidToken? { - if let alg = header["alg"] as? String { - let matchingAlgorithms = algorithms.filter { algorithm in algorithm.description == alg } - let results = matchingAlgorithms.map { algorithm in algorithm.verify(signingInput, signature: signature) } - let successes = results.filter { $0 } - if successes.count > 0 { - return nil - } - - return .invalidAlgorithm +func verifySignature(_ algorithms: [Algorithm], header: Payload, signingInput: String, signature: Data) throws { + guard let algorithmDescription: String = header["alg"] else { + throw InvalidToken.decodeError("Missing Algorithm") } - return .decodeError("Missing Algorithm") + let verifiedAlgorithmsMatchingDescription = algorithms + .filter { algorithm in algorithm.description == algorithmDescription } + .filter { algorithm in algorithm.verify(signingInput, signature: signature) } + + if verifiedAlgorithmsMatchingDescription.count == 0 { + throw InvalidToken.invalidAlgorithm + } } diff --git a/Sources/JWT.swift b/Sources/JWT.swift index 84f4809..9f7698d 100644 --- a/Sources/JWT.swift +++ b/Sources/JWT.swift @@ -1,10 +1,8 @@ import Foundation import CryptoSwift -public typealias Payload = [String: Any] - /// The supported Algorithms -public enum Algorithm : CustomStringConvertible { +public enum Algorithm: CustomStringConvertible { /// No Algorithm, i-e, insecure case none @@ -31,10 +29,10 @@ public enum Algorithm : CustomStringConvertible { } /// Sign a message using the algorithm - func sign(_ message:String) -> String { - func signHS(_ key: Data, variant:CryptoSwift.HMAC.Variant) -> String { + func sign(_ message: String) -> String { + func signHS(_ key: Data, variant: CryptoSwift.HMAC.Variant) -> String { let messageData = message.data(using: String.Encoding.utf8, allowLossyConversion: false)! - let mac = HMAC(key: key.bytes, variant:variant) + let mac = HMAC(key: key.bytes, variant: variant) let result: [UInt8] do { result = try mac.authenticate(messageData.bytes) @@ -60,106 +58,142 @@ public enum Algorithm : CustomStringConvertible { } /// Verify a signature for a message using the algorithm - func verify(_ message:String, signature:Data) -> Bool { + func verify(_ message: String, signature: Data) -> Bool { return sign(message) == base64encode(signature) } } -// MARK: Encoding - -/*** Encode a payload - - parameter payload: The payload to sign - - parameter algorithm: The algorithm to sign the payload with - - returns: The JSON web token as a String -*/ -public func encode(_ payload:Payload, algorithm:Algorithm) -> String { - func encodeJSON(_ payload:Payload) -> String? { - if let data = try? JSONSerialization.data(withJSONObject: payload, options: JSONSerialization.WritingOptions(rawValue: 0)) { - return base64encode(data) - } - - return nil +/// Encode a payload +/// +/// - parameter payload: The payload to sign +/// - parameter algorithm: The algorithm to sign the payload with +/// +/// - throws: when serialization fails +/// +/// - returns: The JSON web token as a String +public func encode(_ payload: Payload, algorithm: Algorithm) throws -> String { + func encodeJSON(_ payload: Payload) throws -> String { + let data = try JSONSerialization.data(withJSONObject: payload.store, options: JSONSerialization.WritingOptions(rawValue: 0)) + return base64encode(data) } - let header = encodeJSON(["typ": "JWT" as AnyObject, "alg": algorithm.description as AnyObject])! - let payload = encodeJSON(payload)! + let header = try encodeJSON(["typ": "JWT", "alg": algorithm.description]) + let payload = try encodeJSON(payload) let signingInput = "\(header).\(payload)" let signature = algorithm.sign(signingInput) return "\(signingInput).\(signature)" } -open class PayloadBuilder { - var payload = Payload() +public struct Payload: ExpressibleByDictionaryLiteral { + + typealias BackingStore = [String: Any] + + var store: BackingStore = [:] - open var issuer:String? { + public init(dictionaryLiteral elements: (String, Any)...) { + for (key, value) in elements { + store[key] = value + } + } + + init?(jsonData: Data) throws { + guard let store = try JSONSerialization.jsonObject(with: jsonData) as? BackingStore else { + return nil + } + self.store = store + } + + public var issuer: String? { get { - return payload["iss"] as? String + return self["iss"] } set { - payload["iss"] = newValue as AnyObject? + self["iss"] = newValue } } - open var audience:String? { + public var audience: String? { get { - return payload["aud"] as? String + return self["aud"] } set { - payload["aud"] = newValue as AnyObject? + self["aud"] = newValue } } - open var expiration:Date? { + public var audiences: [String]? { get { - if let expiration = payload["exp"] as? TimeInterval { - return Date(timeIntervalSince1970: expiration) - } + return self["aud"] ?? audience.map { [$0] } + } + set { + self["aud"] = newValue + } + } - return nil + public var expiration: Date? { + get { + return self["exp"] } set { - payload["exp"] = newValue?.timeIntervalSince1970 as AnyObject? + self["exp"] = newValue } } - open var notBefore:Date? { + public var notBefore: Date? { get { - if let notBefore = payload["nbf"] as? TimeInterval { - return Date(timeIntervalSince1970: notBefore) - } + return self["nbf"] + } + set { + self["nbf"] = newValue + } + } - return nil + public var issuedAt: Date? { + get { + return self["iat"] } set { - payload["nbf"] = newValue?.timeIntervalSince1970 as AnyObject? + self["iat"] = newValue } } - open var issuedAt:Date? { + public subscript(key: String) -> Any { get { - if let issuedAt = payload["iat"] as? TimeInterval { - return Date(timeIntervalSince1970: issuedAt) - } + return store[key] + } + set { + store[key] = newValue + } + } - return nil + public subscript(key: String) -> Date? { + get { + guard let timeInterval = store[key] as? TimeInterval ?? + (store[key] as? NSString)?.doubleValue else { + return nil + } + return Date.init(timeIntervalSince1970: timeInterval) } set { - payload["iat"] = newValue?.timeIntervalSince1970 as AnyObject? + store[key] = newValue?.timeIntervalSince1970 } } - open subscript(key: String) -> Any { + public subscript(key: String) -> String? { get { - return payload[key] + return store[key] as? String } set { - payload[key] = newValue + store[key] = newValue } } -} -public func encode(_ algorithm:Algorithm, closure:((PayloadBuilder) -> ())) -> String { - let builder = PayloadBuilder() - closure(builder) - return encode(builder.payload, algorithm: algorithm) + public subscript(key: String) -> [String]? { + get { + return store[key] as? [String] + } + set { + store[key] = newValue + } + } } From 0199fef0cd0b6d45df9640575d3d21821b55ad71 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Wed, 28 Sep 2016 21:54:27 +0200 Subject: [PATCH 2/3] Perform recommended project changes --- JWT.xcodeproj/project.pbxproj | 17 ++++++++++++++--- .../xcshareddata/xcschemes/JWT-OSX.xcscheme | 2 +- .../xcshareddata/xcschemes/JWT-iOS.xcscheme | 2 +- .../xcshareddata/xcschemes/JWT-tvOS.xcscheme | 2 +- .../xcshareddata/xcschemes/JWT-watchOS.xcscheme | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/JWT.xcodeproj/project.pbxproj b/JWT.xcodeproj/project.pbxproj index 86147ea..159ef93 100644 --- a/JWT.xcodeproj/project.pbxproj +++ b/JWT.xcodeproj/project.pbxproj @@ -357,7 +357,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0700; + LastUpgradeCheck = 0800; ORGANIZATIONNAME = Cocode; TargetAttributes = { 279D639B1AD07FFF0024E2BC = { @@ -532,8 +532,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; @@ -542,6 +544,7 @@ ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -579,8 +582,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; @@ -589,6 +594,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -599,6 +605,7 @@ MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 3.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -676,7 +683,7 @@ CD9B62211C7753D8005D4844 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -695,7 +702,7 @@ CD9B62221C7753D8005D4844 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -715,6 +722,7 @@ CD9B62331C7753EC005D4844 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -733,6 +741,7 @@ CD9B62341C7753EC005D4844 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -752,6 +761,7 @@ CD9B62451C7753FB005D4844 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -770,6 +780,7 @@ CD9B62461C7753FB005D4844 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; diff --git a/JWT.xcodeproj/xcshareddata/xcschemes/JWT-OSX.xcscheme b/JWT.xcodeproj/xcshareddata/xcschemes/JWT-OSX.xcscheme index 1d4884e..ff0db9d 100644 --- a/JWT.xcodeproj/xcshareddata/xcschemes/JWT-OSX.xcscheme +++ b/JWT.xcodeproj/xcshareddata/xcschemes/JWT-OSX.xcscheme @@ -1,6 +1,6 @@ Date: Thu, 29 Sep 2016 00:15:17 +0200 Subject: [PATCH 3/3] Remove subscript for Any type It took unexpected precedence over explicit types, eg when setting a value of type Date and it is unsafe. --- JWTTests/JWTTests.swift | 7 ++++++ Sources/JWT.swift | 53 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/JWTTests/JWTTests.swift b/JWTTests/JWTTests.swift index 3daba8d..2c5b64b 100644 --- a/JWTTests/JWTTests.swift +++ b/JWTTests/JWTTests.swift @@ -30,6 +30,13 @@ class PayloadTests: XCTestCase { XCTAssertEqual(payload.audiences ?? [], ["cocoapods", "carthage"]) } + func testCustomDate() { + let date = Date() + var payload = Payload() + payload["date"] = date + XCTAssertEqual(payload["date"]?.timeIntervalSince1970, date.timeIntervalSince1970) + } + func testExpiration() { var payload = Payload() let date = Date() diff --git a/Sources/JWT.swift b/Sources/JWT.swift index 9f7698d..9ebd7d3 100644 --- a/Sources/JWT.swift +++ b/Sources/JWT.swift @@ -73,7 +73,7 @@ public enum Algorithm: CustomStringConvertible { /// - returns: The JSON web token as a String public func encode(_ payload: Payload, algorithm: Algorithm) throws -> String { func encodeJSON(_ payload: Payload) throws -> String { - let data = try JSONSerialization.data(withJSONObject: payload.store, options: JSONSerialization.WritingOptions(rawValue: 0)) + let data = try JSONSerialization.data(withJSONObject: payload.store) return base64encode(data) } @@ -90,6 +90,7 @@ public struct Payload: ExpressibleByDictionaryLiteral { var store: BackingStore = [:] + // TODO: this is unsafe because it allows invalid JSON values public init(dictionaryLiteral elements: (String, Any)...) { for (key, value) in elements { store[key] = value @@ -157,9 +158,55 @@ public struct Payload: ExpressibleByDictionaryLiteral { } } - public subscript(key: String) -> Any { + subscript(key: String) -> Int? { get { - return store[key] + return store[key] as? Int + } + set { + store[key] = newValue + } + } + + subscript(key: String) -> [Int]? { + get { + return store[key] as? [Int] + } + set { + store[key] = newValue + } + } + + + subscript(key: String) -> UInt? { + get { + return store[key] as? UInt + } + set { + store[key] = newValue + } + } + + subscript(key: String) -> [UInt]? { + get { + return store[key] as? [UInt] + } + set { + store[key] = newValue + } + } + + subscript(key: String) -> Double? { + get { + return store[key] as? Double + } + set { + store[key] = newValue + } + } + + subscript(key: String) -> [Double]? { + get { + return store[key] as? [Double] } set { store[key] = newValue