From a1b86f1b86f2e180217b96ebe79cda67efdc4a5b Mon Sep 17 00:00:00 2001 From: Tom Knapen Date: Fri, 4 Aug 2023 23:05:10 +0200 Subject: [PATCH 1/6] replace BlueRSA, BlueECC and BlueCryptor with Apple's Swift Crypto this currently drops support for RSA --- Package.swift | 12 +-- Sources/SwiftJWT/BlueECDSA.swift | 110 ------------------- Sources/SwiftJWT/BlueHMAC.swift | 85 --------------- Sources/SwiftJWT/BlueRSA.swift | 128 ----------------------- Sources/SwiftJWT/JWTSigner.swift | 24 ++--- Sources/SwiftJWT/JWTVerifier.swift | 38 +++---- Sources/SwiftJWT/RSAKeyType.swift | 29 ----- Sources/SwiftJWT/SwiftCryptoECDSA.swift | 105 +++++++++++++++++++ Sources/SwiftJWT/SwiftCryptoHMAC.swift | 88 ++++++++++++++++ Sources/SwiftJWT/VerifierAlgorithm.swift | 3 +- Tests/SwiftJWTTests/TestJWT.swift | 38 +++---- 11 files changed, 250 insertions(+), 410 deletions(-) delete mode 100644 Sources/SwiftJWT/BlueECDSA.swift delete mode 100644 Sources/SwiftJWT/BlueHMAC.swift delete mode 100644 Sources/SwiftJWT/BlueRSA.swift delete mode 100644 Sources/SwiftJWT/RSAKeyType.swift create mode 100644 Sources/SwiftJWT/SwiftCryptoECDSA.swift create mode 100644 Sources/SwiftJWT/SwiftCryptoHMAC.swift diff --git a/Package.swift b/Package.swift index 01d3fc2..51beb5f 100644 --- a/Package.swift +++ b/Package.swift @@ -23,6 +23,10 @@ let targetDependencies: [Target.Dependency] let package = Package( name: "SwiftJWT", + platforms: [ + .iOS("14.0"), + .macOS("11.0"), + ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( @@ -31,9 +35,7 @@ let package = Package( ) ], dependencies: [ - .package(name: "CryptorRSA", url: "https://github.com/Kitura/BlueRSA.git", from: "1.0.200"), - .package(name: "Cryptor", url: "https://github.com/Kitura/BlueCryptor.git", from: "2.0.1"), - .package(name: "CryptorECC", url: "https://github.com/Kitura/BlueECC.git", from: "1.2.200"), + .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/Kitura/LoggerAPI.git", from: "2.0.0"), .package(url: "https://github.com/Kitura/KituraContracts.git", from: "2.0.1") ], @@ -41,9 +43,7 @@ let package = Package( .target(name: "SwiftJWT", dependencies: [ "LoggerAPI", "KituraContracts", - "CryptorRSA", - "Cryptor", - "CryptorECC", + .product(name: "Crypto", package: "swift-crypto") ]), .testTarget(name: "SwiftJWTTests", dependencies: ["SwiftJWT"]) ] diff --git a/Sources/SwiftJWT/BlueECDSA.swift b/Sources/SwiftJWT/BlueECDSA.swift deleted file mode 100644 index 4f7e0f4..0000000 --- a/Sources/SwiftJWT/BlueECDSA.swift +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright IBM Corporation 2019 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ - -import CryptorECC -import LoggerAPI -import Foundation - -// Class for ECDSA signing using BlueECC -@available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) -class BlueECSigner: SignerAlgorithm { - let name: String = "ECDSA" - - private let key: Data - private let curve: EllipticCurve - - // Initialize a signer using .utf8 encoded PEM private key. - init(key: Data, curve: EllipticCurve) { - self.key = key - self.curve = curve - } - - // Sign the header and claims to produce a signed JWT String - func sign(header: String, claims: String) throws -> String { - let unsignedJWT = header + "." + claims - guard let unsignedData = unsignedJWT.data(using: .utf8) else { - throw JWTError.invalidJWTString - } - let signature = try sign(unsignedData) - let signatureString = JWTEncoder.base64urlEncodedString(data: signature) - return header + "." + claims + "." + signatureString - } - - // send utf8 encoded `header.claims` to BlueECC for signing - private func sign(_ data: Data) throws -> Data { - guard let keyString = String(data: key, encoding: .utf8) else { - throw JWTError.invalidPrivateKey - } - let privateKey = try ECPrivateKey(key: keyString) - guard privateKey.curve == curve else { - throw JWTError.invalidPrivateKey - } - let signedData = try data.sign(with: privateKey) - return signedData.r + signedData.s - } -} - -// Class for ECDSA verifying using BlueECC -@available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) -class BlueECVerifier: VerifierAlgorithm { - - let name: String = "ECDSA" - - private let key: Data - private let curve: EllipticCurve - - // Initialize a verifier using .utf8 encoded PEM public key. - init(key: Data, curve: EllipticCurve) { - self.key = key - self.curve = curve - } - - // Verify a signed JWT String - func verify(jwt: String) -> Bool { - let components = jwt.components(separatedBy: ".") - if components.count == 3 { - guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), - let jwtData = (components[0] + "." + components[1]).data(using: .utf8) - else { - return false - } - return self.verify(signature: signature, for: jwtData) - } else { - return false - } - } - - // Send the base64URLencoded signature and `header.claims` to BlueECC for verification. - private func verify(signature: Data, for data: Data) -> Bool { - do { - guard let keyString = String(data: key, encoding: .utf8) else { - return false - } - let r = signature.subdata(in: 0 ..< signature.count/2) - let s = signature.subdata(in: signature.count/2 ..< signature.count) - let signature = try ECSignature(r: r, s: s) - let publicKey = try ECPublicKey(key: keyString) - guard publicKey.curve == curve else { - return false - } - return signature.verify(plaintext: data, using: publicKey) - } - catch { - Log.error("Verification failed: \(error)") - return false - } - } -} diff --git a/Sources/SwiftJWT/BlueHMAC.swift b/Sources/SwiftJWT/BlueHMAC.swift deleted file mode 100644 index f04a10c..0000000 --- a/Sources/SwiftJWT/BlueHMAC.swift +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright IBM Corporation 2017 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ - -import Cryptor -import LoggerAPI -import Foundation - -class BlueHMAC: SignerAlgorithm, VerifierAlgorithm { - let name: String = "HMAC" - - private let key: Data - private let algorithm: HMAC.Algorithm - - init(key: Data, algorithm: HMAC.Algorithm) { - self.key = key - self.algorithm = algorithm - } - - func sign(header: String, claims: String) throws -> String { - let unsignedJWT = header + "." + claims - guard let unsignedData = unsignedJWT.data(using: .utf8) else { - throw JWTError.invalidJWTString - } - let signature = try sign(unsignedData) - let signatureString = JWTEncoder.base64urlEncodedString(data: signature) - return header + "." + claims + "." + signatureString - } - - func sign(_ data: Data) throws -> Data { - guard #available(macOS 10.12, iOS 10.0, *) else { - Log.error("macOS 10.12.0 (Sierra) or higher or iOS 10.0 or higher is required by Cryptor") - throw JWTError.osVersionToLow - } - guard let hmac = HMAC(using: algorithm, key: key).update(data: data)?.final() else { - throw JWTError.invalidPrivateKey - } - #if swift(>=5.0) - return Data(hmac) - #else - return Data(bytes: hmac) - #endif - } - - - func verify(jwt: String) -> Bool { - let components = jwt.components(separatedBy: ".") - if components.count == 3 { - guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), - let jwtData = (components[0] + "." + components[1]).data(using: .utf8) - else { - return false - } - return self.verify(signature: signature, for: jwtData) - } else { - return false - } - } - - func verify(signature: Data, for data: Data) -> Bool { - guard #available(macOS 10.12, iOS 10.0, *) else { - return false - } - do { - let expectedHMAC = try sign(data) - return expectedHMAC == signature - } - catch { - Log.error("Verification failed: \(error)") - return false - } - } -} diff --git a/Sources/SwiftJWT/BlueRSA.swift b/Sources/SwiftJWT/BlueRSA.swift deleted file mode 100644 index fcf2d45..0000000 --- a/Sources/SwiftJWT/BlueRSA.swift +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright IBM Corporation 2017 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ - -import CryptorRSA -import LoggerAPI - -import Foundation - -class BlueRSA: SignerAlgorithm, VerifierAlgorithm { - let name: String = "RSA" - - private let key: Data - private let keyType: RSAKeyType - private let algorithm: Data.Algorithm - private let usePSS: Bool - - init(key: Data, keyType: RSAKeyType?=nil, algorithm: Data.Algorithm, usePSS: Bool = false) { - self.key = key - self.keyType = keyType ?? .publicKey - self.algorithm = algorithm - self.usePSS = usePSS - } - - func sign(header: String, claims: String) throws -> String { - let unsignedJWT = header + "." + claims - guard let unsignedData = unsignedJWT.data(using: .utf8) else { - throw JWTError.invalidJWTString - } - let signature = try sign(unsignedData) - let signatureString = JWTEncoder.base64urlEncodedString(data: signature) - return header + "." + claims + "." + signatureString - } - - func sign(_ data: Data) throws -> Data { - guard #available(macOS 10.12, iOS 10.3, tvOS 12.0, watchOS 3.3, *) else { - Log.error("macOS 10.12.0 (Sierra) or higher or iOS 10.0 or higher is required by CryptorRSA") - throw JWTError.osVersionToLow - } - // Convert PEM format to DER - let keyDer: Data - if let keyString = String(data: key, encoding: .utf8) { - let strippedKey = String(keyString.filter { !" \n\t\r".contains($0) }) - let pemComponents = strippedKey.components(separatedBy: "-----") - guard pemComponents.count >= 5 else { - throw JWTError.missingPEMHeaders - } - guard let der = Data(base64Encoded: pemComponents[2]) else { - throw JWTError.invalidPrivateKey - } - keyDer = der - } else { - keyDer = key - } - let privateKey = try CryptorRSA.createPrivateKey(with: keyDer) - let myPlaintext = CryptorRSA.createPlaintext(with: data) - guard let signedData = try myPlaintext.signed(with: privateKey, algorithm: algorithm, usePSS: usePSS) else { - throw JWTError.invalidPrivateKey - } - return signedData.data - } - - - func verify(jwt: String) -> Bool { - let components = jwt.components(separatedBy: ".") - if components.count == 3 { - guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), - let jwtData = (components[0] + "." + components[1]).data(using: .utf8) - else { - return false - } - return self.verify(signature: signature, for: jwtData) - } else { - return false - } - } - - func verify(signature: Data, for data: Data) -> Bool { - guard #available(macOS 10.12, iOS 10.3, tvOS 12.0, watchOS 3.3, *) else { - return false - } - do { - var publicKey: CryptorRSA.PublicKey - switch keyType { - case .privateKey: - return false - case .publicKey: - // Convert PEM format to DER - let keyDer: Data - if let keyString = String(data: key, encoding: .utf8) { - let strippedKey = String(keyString.filter { !" \n\t\r".contains($0) }) - let pemComponents = strippedKey.components(separatedBy: "-----") - guard pemComponents.count >= 5 else { - return false - } - guard let der = Data(base64Encoded: pemComponents[2]) else { - return false - } - keyDer = der - } else { - keyDer = key - } - publicKey = try CryptorRSA.createPublicKey(with: keyDer) - case .certificate: - publicKey = try CryptorRSA.createPublicKey(extractingFrom: key) - } - let myPlaintext = CryptorRSA.createPlaintext(with: data) - let signedData = CryptorRSA.createSigned(with: signature) - return try myPlaintext.verify(with: publicKey, signature: signedData, algorithm: algorithm, usePSS: usePSS) - } - catch { - Log.error("Verification failed: \(error)") - return false - } - } -} diff --git a/Sources/SwiftJWT/JWTSigner.swift b/Sources/SwiftJWT/JWTSigner.swift index 4af3bfd..b510995 100644 --- a/Sources/SwiftJWT/JWTSigner.swift +++ b/Sources/SwiftJWT/JWTSigner.swift @@ -68,77 +68,77 @@ public struct JWTSigner { /// Initialize a JWTSigner using the RSA 256 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. public static func rs256(privateKey: Data) -> JWTSigner { - return JWTSigner(name: "RS256", signerAlgorithm: BlueRSA(key: privateKey, keyType: .privateKey, algorithm: .sha256)) + preconditionFailure("not implemented") } /// Initialize a JWTSigner using the RSA 384 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. public static func rs384(privateKey: Data) -> JWTSigner { - return JWTSigner(name: "RS384", signerAlgorithm: BlueRSA(key: privateKey, keyType: .privateKey, algorithm: .sha384)) + preconditionFailure("not implemented") } /// Initialize a JWTSigner using the RSA 512 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. public static func rs512(privateKey: Data) -> JWTSigner { - return JWTSigner(name: "RS512", signerAlgorithm: BlueRSA(key: privateKey, keyType: .privateKey, algorithm: .sha512)) + preconditionFailure("not implemented") } /// Initialize a JWTSigner using the RSA-PSS 256 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. public static func ps256(privateKey: Data) -> JWTSigner { - return JWTSigner(name: "PS256", signerAlgorithm: BlueRSA(key: privateKey, keyType: .privateKey, algorithm: .sha256, usePSS: true)) + preconditionFailure("not implemented") } /// Initialize a JWTSigner using the RSA-PSS 384 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. public static func ps384(privateKey: Data) -> JWTSigner { - return JWTSigner(name: "PS384", signerAlgorithm: BlueRSA(key: privateKey, keyType: .privateKey, algorithm: .sha384, usePSS: true)) + preconditionFailure("not implemented") } /// Initialize a JWTSigner using the RSA-PSS 512 bits algorithm and the provided privateKey. /// This signer requires at least a 2048 bit RSA key. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. public static func ps512(privateKey: Data) -> JWTSigner { - return JWTSigner(name: "PS512", signerAlgorithm: BlueRSA(key: privateKey, keyType: .privateKey, algorithm: .sha512, usePSS: true)) + preconditionFailure("not implemented") } /// Initialize a JWTSigner using the HMAC 256 bits algorithm and the provided privateKey. /// - Parameter key: The HMAC symmetric password data. public static func hs256(key: Data) -> JWTSigner { - return JWTSigner(name: "HS256", signerAlgorithm: BlueHMAC(key: key, algorithm: .sha256)) + JWTSigner(name: "HS256", signerAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs256)) } /// Initialize a JWTSigner using the HMAC 384 bits algorithm and the provided privateKey. /// - Parameter key: The HMAC symmetric password data. public static func hs384(key: Data) -> JWTSigner { - return JWTSigner(name: "HS384", signerAlgorithm: BlueHMAC(key: key, algorithm: .sha384)) + JWTSigner(name: "HS384", signerAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs384)) } /// Initialize a JWTSigner using the HMAC 512 bits algorithm and the provided privateKey. /// - Parameter key: The HMAC symmetric password data. public static func hs512(key: Data) -> JWTSigner { - return JWTSigner(name: "HS512", signerAlgorithm: BlueHMAC(key: key, algorithm: .sha512)) + JWTSigner(name: "HS512", signerAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs512)) } /// Initialize a JWTSigner using the ECDSA SHA256 algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with either a "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" header. @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) public static func es256(privateKey: Data) -> JWTSigner { - return JWTSigner(name: "ES256", signerAlgorithm: BlueECSigner(key: privateKey, curve: .prime256v1)) + JWTSigner(name: "ES256", signerAlgorithm: SwiftCryptoECDSA(key: privateKey, algorithm: .es256)) } /// Initialize a JWTSigner using the ECDSA SHA384 algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with either a "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" header. @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) public static func es384(privateKey: Data) -> JWTSigner { - return JWTSigner(name: "ES384", signerAlgorithm: BlueECSigner(key: privateKey, curve: .secp384r1)) + JWTSigner(name: "ES384", signerAlgorithm: SwiftCryptoECDSA(key: privateKey, algorithm: .es384)) } /// Initialize a JWTSigner using the ECDSA SHA512 algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with either a "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" header. @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) public static func es512(privateKey: Data) -> JWTSigner { - return JWTSigner(name: "ES512", signerAlgorithm: BlueECSigner(key: privateKey, curve: .secp521r1)) + JWTSigner(name: "ES512", signerAlgorithm: SwiftCryptoECDSA(key: privateKey, algorithm: .es512)) } /// Initialize a JWTSigner that will not sign the JWT. This is equivelent to using the "none" alg header. diff --git a/Sources/SwiftJWT/JWTVerifier.swift b/Sources/SwiftJWT/JWTVerifier.swift index 35df293..e0951ea 100644 --- a/Sources/SwiftJWT/JWTVerifier.swift +++ b/Sources/SwiftJWT/JWTVerifier.swift @@ -43,108 +43,108 @@ import Foundation ``` */ public struct JWTVerifier { - let verifierAlgorithm: VerifierAlgorithm + private let verifierAlgorithm: VerifierAlgorithm - init(verifierAlgorithm: VerifierAlgorithm) { + internal init(verifierAlgorithm: VerifierAlgorithm) { self.verifierAlgorithm = verifierAlgorithm } - - func verify(jwt: String) -> Bool { + + internal func verify(jwt: String) -> Bool { return verifierAlgorithm.verify(jwt: jwt) } /// Initialize a JWTVerifier using the RSA 256 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. public static func rs256(publicKey: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueRSA(key: publicKey, keyType: .publicKey, algorithm: .sha256)) + preconditionFailure("not implemented") } /// Initialize a JWTVerifier using the RSA 384 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. public static func rs384(publicKey: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueRSA(key: publicKey, keyType: .publicKey, algorithm: .sha384)) + preconditionFailure("not implemented") } /// Initialize a JWTVerifier using the RSA 512 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. public static func rs512(publicKey: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueRSA(key: publicKey, keyType: .publicKey, algorithm: .sha512)) + preconditionFailure("not implemented") } /// Initialize a JWTVerifier using the RSA 256 bits algorithm and the provided certificate. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN CERTIFICATE" header. public static func rs256(certificate: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueRSA(key: certificate, keyType: .certificate, algorithm: .sha256)) + preconditionFailure("not implemented") } /// Initialize a JWTVerifier using the RSA 384 bits algorithm and the provided certificate. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN CERTIFICATE" header. public static func rs384(certificate: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueRSA(key: certificate, keyType: .certificate, algorithm: .sha384)) + preconditionFailure("not implemented") } /// Initialize a JWTVerifier using the RSA 512 bits algorithm and the provided certificate. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN CERTIFICATE" header. public static func rs512(certificate: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueRSA(key: certificate, keyType: .certificate, algorithm: .sha512)) + preconditionFailure("not implemented") } /// Initialize a JWTVerifier using the RSA-PSS 256 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. public static func ps256(publicKey: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueRSA(key: publicKey, keyType: .publicKey, algorithm: .sha256, usePSS: true)) + preconditionFailure("not implemented") } /// Initialize a JWTVerifier using the RSA-PSS 384 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. public static func ps384(publicKey: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueRSA(key: publicKey, keyType: .publicKey, algorithm: .sha384, usePSS: true)) + preconditionFailure("not implemented") } /// Initialize a JWTVerifier using the RSA-PSS 512 bits algorithm and the provided publicKey. /// This verifier requires at least a 2048 bit RSA key. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. public static func ps512(publicKey: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueRSA(key: publicKey, keyType: .publicKey, algorithm: .sha512, usePSS: true)) + preconditionFailure("not implemented") } /// Initialize a JWTSigner using the HMAC 256 bits algorithm and the provided privateKey. /// - Parameter key: The HMAC symmetric password data. public static func hs256(key: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueHMAC(key: key, algorithm: .sha256)) + JWTVerifier(verifierAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs256)) } /// Initialize a JWTSigner using the HMAC 384 bits algorithm and the provided privateKey. /// - Parameter key: The HMAC symmetric password data. public static func hs384(key: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueHMAC(key: key, algorithm: .sha384)) + JWTVerifier(verifierAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs384)) } /// Initialize a JWTSigner using the HMAC 512 bits algorithm and the provided privateKey. /// - Parameter key: The HMAC symmetric password data. public static func hs512(key: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueHMAC(key: key, algorithm: .sha512)) + JWTVerifier(verifierAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs512)) } /// Initialize a JWTVerifier using the ECDSA SHA 256 algorithm and the provided public key. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) public static func es256(publicKey: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueECVerifier(key: publicKey, curve: .prime256v1)) + JWTVerifier(verifierAlgorithm: SwiftCryptoECDSA(key: publicKey, algorithm: .es256)) } /// Initialize a JWTVerifier using the ECDSA SHA 384 algorithm and the provided public key. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) public static func es384(publicKey: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueECVerifier(key: publicKey, curve: .secp384r1)) + JWTVerifier(verifierAlgorithm: SwiftCryptoECDSA(key: publicKey, algorithm: .es384)) } /// Initialize a JWTVerifier using the ECDSA SHA 512 algorithm and the provided public key. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) public static func es512(publicKey: Data) -> JWTVerifier { - return JWTVerifier(verifierAlgorithm: BlueECVerifier(key: publicKey, curve: .secp521r1)) + JWTVerifier(verifierAlgorithm: SwiftCryptoECDSA(key: publicKey, algorithm: .es512)) } /// Initialize a JWTVerifier that will always return true when verifying the JWT. This is equivelent to using the "none" alg header. diff --git a/Sources/SwiftJWT/RSAKeyType.swift b/Sources/SwiftJWT/RSAKeyType.swift deleted file mode 100644 index 048e4cf..0000000 --- a/Sources/SwiftJWT/RSAKeyType.swift +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright IBM Corporation 2017 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ - -// MARK RSAKeyType - -/// The type of the key used in the RSA algorithm. -enum RSAKeyType { - /// The key is a certificate containing both the private and the public keys. - case certificate - - /// The key is an RSA public key. - case publicKey - - /// The key is an RSA private key. - case privateKey -} diff --git a/Sources/SwiftJWT/SwiftCryptoECDSA.swift b/Sources/SwiftJWT/SwiftCryptoECDSA.swift new file mode 100644 index 0000000..4f4a8f7 --- /dev/null +++ b/Sources/SwiftJWT/SwiftCryptoECDSA.swift @@ -0,0 +1,105 @@ +import Crypto +import Foundation +import LoggerAPI + +internal struct SwiftCryptoECDSA: VerifierAlgorithm, SignerAlgorithm { + private let key: Data + private let algorithm: Algorithm + + internal enum Algorithm { + case es256, es384, es512 + } + + internal init(key: Data, algorithm: Algorithm) { + self.key = key + self.algorithm = algorithm + } + + internal func verify(jwt: String) -> Bool { + verify(jwt) + } + + internal func sign(header: String, claims: String) throws -> String { + let unsignedJWT = header + "." + claims + let signature = try sign(data: Data(unsignedJWT.utf8)) + return header + "." + claims + "." + signature + } +} + +extension SwiftCryptoECDSA { + private func verify(_ jwt: String) -> Bool { + let components = jwt.components(separatedBy: ".") + if components.count == 3 { + guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), + let jwtData = (components[0] + "." + components[1]).data(using: .utf8) + else { + return false + } + return verify(signature: signature, for: jwtData) + } else { + return false + } + } + + private func verify(signature: Data, for data: Data) -> Bool { + guard #available(macOS 10.12, iOS 10.3, tvOS 12.0, watchOS 3.3, *) else { + return false + } + + guard let publicKey = String(data: key, encoding: .utf8) else { + return false + } + + do { + return try algorithm.verify(signature: signature, digest: data, publicKey: publicKey) + } catch { + Log.error("Verification failed: \(error)") + return false + } + } + + private func sign(data: Data) throws -> String { + guard let privateKey = String(data: key, encoding: .utf8) else { + throw JWTError.invalidPrivateKey + } + + let signature = try algorithm.signature(for: data, privateKey: privateKey) + return JWTEncoder.base64urlEncodedString(data: signature) + } +} + +private extension SwiftCryptoECDSA.Algorithm { + func verify(signature: Data, digest: Data, publicKey: String) throws -> Bool { + switch self { + case .es256: + let signature = try P256.Signing.ECDSASignature(rawRepresentation: signature) + let publicKey = try P256.Signing.PublicKey(pemRepresentation: publicKey) + return publicKey.isValidSignature(signature, for: digest) + case .es384: + let signature = try P384.Signing.ECDSASignature(rawRepresentation: signature) + let publicKey = try P384.Signing.PublicKey(pemRepresentation: publicKey) + return publicKey.isValidSignature(signature, for: digest) + case .es512: + let signature = try P521.Signing.ECDSASignature(rawRepresentation: signature) + let publicKey = try P521.Signing.PublicKey(pemRepresentation: publicKey) + return publicKey.isValidSignature(signature, for: digest) + } + } + + func signature(for digest: Data, privateKey: String) throws -> Data { + switch self { + case .es256: + let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKey) + let signedData = try privateKey.signature(for: digest) + return signedData.rawRepresentation + case .es384: + let privateKey = try P384.Signing.PrivateKey(pemRepresentation: privateKey) + let signedData = try privateKey.signature(for: digest) + return signedData.rawRepresentation + case .es512: + let privateKey = try P521.Signing.PrivateKey(pemRepresentation: privateKey) + let signedData = try privateKey.signature(for: digest) + return signedData.rawRepresentation + } + } +} diff --git a/Sources/SwiftJWT/SwiftCryptoHMAC.swift b/Sources/SwiftJWT/SwiftCryptoHMAC.swift new file mode 100644 index 0000000..b19510a --- /dev/null +++ b/Sources/SwiftJWT/SwiftCryptoHMAC.swift @@ -0,0 +1,88 @@ +import Crypto +import Foundation +import LoggerAPI + +internal struct SwiftCryptoHMAC: VerifierAlgorithm, SignerAlgorithm { + private let key: Data + private let algorithm: Algorithm + + internal enum Algorithm { + case hs256, hs384, hs512 + } + + internal init(key: Data, algorithm: Algorithm) { + self.key = key + self.algorithm = algorithm + } + + internal func verify(jwt: String) -> Bool { + verify(jwt) + } + + internal func sign(header: String, claims: String) throws -> String { + let unsignedJWT = header + "." + claims + let signature = try sign(data: Data(unsignedJWT.utf8)) + return header + "." + claims + "." + signature + } +} + +extension SwiftCryptoHMAC { + private func verify(_ jwt: String) -> Bool { + let components = jwt.components(separatedBy: ".") + if components.count == 3 { + guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), + let jwtData = (components[0] + "." + components[1]).data(using: .utf8) + else { + return false + } + return verify(signature: signature, for: jwtData) + } else { + return false + } + } + + private func verify(signature: Data, for data: Data) -> Bool { + do { + return try algorithm.verify(signature: signature, digest: data, key: key) + } catch { + Log.error("Verification failed: \(error)") + return false + } + } + + private func sign(data: Data) throws -> String { + let signedData = try algorithm.signature(for: data, key: key) + return JWTEncoder.base64urlEncodedString(data: signedData) + } +} + +private extension SwiftCryptoHMAC.Algorithm { + func verify(signature: Data, digest: Data, key: Data) throws -> Bool { + let key = SymmetricKey(data: key) + + switch self { + case .hs256: + return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) + case .hs384: + return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) + case .hs512: + return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) + } + } + + func signature(for digest: Data, key: Data) throws -> Data { + let key = SymmetricKey(data: key) + + switch self { + case .hs256: + let mac = HMAC.authenticationCode(for: digest, using: key) + return Data(mac) + case .hs384: + let mac = HMAC.authenticationCode(for: digest, using: key) + return Data(mac) + case .hs512: + let mac = HMAC.authenticationCode(for: digest, using: key) + return Data(mac) + } + } +} diff --git a/Sources/SwiftJWT/VerifierAlgorithm.swift b/Sources/SwiftJWT/VerifierAlgorithm.swift index e7edd1d..4eea45e 100644 --- a/Sources/SwiftJWT/VerifierAlgorithm.swift +++ b/Sources/SwiftJWT/VerifierAlgorithm.swift @@ -14,8 +14,7 @@ * limitations under the License. **/ -protocol VerifierAlgorithm { +internal protocol VerifierAlgorithm { /// A function to verify the signature of a JSON web token string is correct for the header and claims. func verify(jwt: String) -> Bool - } diff --git a/Tests/SwiftJWTTests/TestJWT.swift b/Tests/SwiftJWTTests/TestJWT.swift index 647727a..b3cc12c 100644 --- a/Tests/SwiftJWTTests/TestJWT.swift +++ b/Tests/SwiftJWTTests/TestJWT.swift @@ -112,35 +112,35 @@ class TestJWT: XCTestCase { static var allTests: [(String, (TestJWT) -> () throws -> Void)] { return [ ("testSignAndVerify", testSignAndVerify), - ("testSignAndVerifyRSA", testSignAndVerifyRSA), - ("testSignAndVerifyRSAPSS", testSignAndVerifyRSAPSS), - ("testSignAndVerifyCert", testSignAndVerifyCert), +// ("testSignAndVerifyRSA", testSignAndVerifyRSA), +// ("testSignAndVerifyRSAPSS", testSignAndVerifyRSAPSS), +// ("testSignAndVerifyCert", testSignAndVerifyCert), ("testSignAndVerifyHMAC", testSignAndVerifyHMAC), ("testSignAndVerifyECDSA", testSignAndVerifyECDSA), - ("testSignAndVerifyRSA384", testSignAndVerifyRSA384), - ("testSignAndVerifyRSAPSS384", testSignAndVerifyRSAPSS384), - ("testSignAndVerifyCert384", testSignAndVerifyCert384), +// ("testSignAndVerifyRSA384", testSignAndVerifyRSA384), +// ("testSignAndVerifyRSAPSS384", testSignAndVerifyRSAPSS384), +// ("testSignAndVerifyCert384", testSignAndVerifyCert384), ("testSignAndVerifyHMAC384", testSignAndVerifyHMAC384), ("testSignAndVerifyECDSA384", testSignAndVerifyECDSA384), - ("testSignAndVerifyRSA512", testSignAndVerifyRSA512), - ("testSignAndVerifyRSAPSS512", testSignAndVerifyRSAPSS512), - ("testSignAndVerifyCert512", testSignAndVerifyCert512), +// ("testSignAndVerifyRSA512", testSignAndVerifyRSA512), +// ("testSignAndVerifyRSAPSS512", testSignAndVerifyRSAPSS512), +// ("testSignAndVerifyCert512", testSignAndVerifyCert512), ("testSignAndVerifyHMAC512", testSignAndVerifyHMAC512), ("testSignAndVerifyECDSA512", testSignAndVerifyECDSA512), - ("testJWTEncoder", testJWTEncoder), - ("testJWTDecoder", testJWTDecoder), - ("testJWTCoderCycle", testJWTCoderCycle), - ("testJWTEncoderKeyID", testJWTEncoderKeyID), - ("testJWTDecoderKeyID", testJWTDecoderKeyID), - ("testJWTCoderCycleKeyID", testJWTCoderCycleKeyID), - ("testJWT", testJWT), - ("testJWTRSAPSS", testJWTRSAPSS), +// ("testJWTEncoder", testJWTEncoder), +// ("testJWTDecoder", testJWTDecoder), +// ("testJWTCoderCycle", testJWTCoderCycle), +// ("testJWTEncoderKeyID", testJWTEncoderKeyID), +// ("testJWTDecoderKeyID", testJWTDecoderKeyID), +// ("testJWTCoderCycleKeyID", testJWTCoderCycleKeyID), +// ("testJWT", testJWT), +// ("testJWTRSAPSS", testJWTRSAPSS), ("testJWTUsingHMAC", testJWTUsingHMAC), ("testJWTUsingECDSA", testJWTUsingECDSA), - ("testMicroProfile", testMicroProfile), +// ("testMicroProfile", testMicroProfile), ("testValidateClaims", testValidateClaims), ("testValidateClaimsLeeway", testValidateClaimsLeeway), - ("testErrorPattenMatching", testErrorPattenMatching), +// ("testErrorPattenMatching", testErrorPattenMatching), ("testTypeErasedErrorLocalizedDescription", testTypeErasedErrorLocalizedDescription), ] } From 30704043b30552a9d522fa0b2049403770b3f808 Mon Sep 17 00:00:00 2001 From: Tom Knapen Date: Fri, 4 Aug 2023 23:06:51 +0200 Subject: [PATCH 2/6] add and run SwiftFormat run using `swift package plugin --allow-writing-to-package-directory swiftformat` --- .swiftformat | 26 + Package.swift | 3 +- Sources/SwiftJWT/Claims.swift | 120 +- .../ClaimsExamples/ClaimsMicroProfile.swift | 125 +- .../ClaimsExamples/ClaimsOpenID.swift | 345 +++-- .../ClaimsExamples/ClaimsStandardJWT.swift | 171 ++- Sources/SwiftJWT/Data+Base64URLEncoded.swift | 52 +- Sources/SwiftJWT/Header.swift | 135 +- Sources/SwiftJWT/JWT.swift | 202 ++- Sources/SwiftJWT/JWTDecoder.swift | 436 +++--- Sources/SwiftJWT/JWTEncoder.swift | 344 +++-- Sources/SwiftJWT/JWTError.swift | 81 +- Sources/SwiftJWT/JWTSigner.swift | 186 ++- Sources/SwiftJWT/JWTVerifier.swift | 216 +-- Sources/SwiftJWT/NoneAlgorithm.swift | 19 +- Sources/SwiftJWT/SignerAlgorithm.swift | 4 +- Sources/SwiftJWT/SwiftCryptoECDSA.swift | 166 +-- Sources/SwiftJWT/SwiftCryptoHMAC.swift | 134 +- Sources/SwiftJWT/ValidateClaimsResult.swift | 57 +- Sources/SwiftJWT/VerifierAlgorithm.swift | 4 +- Tests/SwiftJWTTests/TestJWT.swift | 1172 ++++++++--------- 21 files changed, 1997 insertions(+), 2001 deletions(-) create mode 100644 .swiftformat diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..ff94402 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,26 @@ +--swiftversion 5.7 + +# format options +--commas inline +--patternlet inline +--nospaceoperators ...,..< +--stripunusedargs closure-only + +--binarygrouping none +--decimalgrouping 3,4 +--hexgrouping none +--octalgrouping none + +--ifdef no-indent +--indent tabs +--tabwidth 4 + +--wraparguments preserve +--wrapcollections before-first +--wrapparameters before-first + +# file options +--exclude Frameworks + +# rules +--disable emptyBraces,modifierOrder,wrapMultilineStatementBraces diff --git a/Package.swift b/Package.swift index 51beb5f..87be766 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/Kitura/LoggerAPI.git", from: "2.0.0"), - .package(url: "https://github.com/Kitura/KituraContracts.git", from: "2.0.1") + .package(url: "https://github.com/Kitura/KituraContracts.git", from: "2.0.1"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.4"), ], targets: [ .target(name: "SwiftJWT", dependencies: [ diff --git a/Sources/SwiftJWT/Claims.swift b/Sources/SwiftJWT/Claims.swift index a858fff..bbb7cdf 100644 --- a/Sources/SwiftJWT/Claims.swift +++ b/Sources/SwiftJWT/Claims.swift @@ -17,68 +17,68 @@ import Foundation // MARK: Claims + /** - A protocol for representing the claims on a JSON web token. - https://tools.ietf.org/html/rfc7519#section-4.1 -### Usage Example: ### -```swift -struct AdminClaims: Claims { - var sub: String - var isAdmin: Bool - var exp: Date? -} - let jwt = JWT(claims: AdminClaims(sub: "Kitura", isAdmin: true, exp: Date(timeIntervalSinceNow: 3600))) -``` -*/ + A protocol for representing the claims on a JSON web token. + https://tools.ietf.org/html/rfc7519#section-4.1 + ### Usage Example: ### + ```swift + struct AdminClaims: Claims { + var sub: String + var isAdmin: Bool + var exp: Date? + } + let jwt = JWT(claims: AdminClaims(sub: "Kitura", isAdmin: true, exp: Date(timeIntervalSinceNow: 3600))) + ``` + */ public protocol Claims: Codable { - - /** - The "exp" (expiration time) claim identifies the expiration time on - or after which the JWT MUST NOT be accepted for processing. The - processing of the "exp" claim requires that the current date/time - MUST be before the expiration date/time listed in the "exp" claim. - Implementers MAY provide for some small leeway, usually no more than - a few minutes, to account for clock skew. - */ - var exp: Date? { get } - - /** - The "nbf" (not before) claim identifies the time before which the JWT - MUST NOT be accepted for processing. The processing of the "nbf" - claim requires that the current date/time MUST be after or equal to - the not-before date/time listed in the "nbf" claim. Implementers MAY - provide for some small leeway, usually no more than a few minutes, to - account for clock skew. - */ - var nbf: Date? { get } - - /** - The "iat" (issued at) claim identifies the time at which the JWT was - issued. This claim can be used to determine the age of the JWT. - */ - var iat: Date? { get } - - /// Encode the Claim object as a Base64 String. - func encode() throws -> String + /** + The "exp" (expiration time) claim identifies the expiration time on + or after which the JWT MUST NOT be accepted for processing. The + processing of the "exp" claim requires that the current date/time + MUST be before the expiration date/time listed in the "exp" claim. + Implementers MAY provide for some small leeway, usually no more than + a few minutes, to account for clock skew. + */ + var exp: Date? { get } + + /** + The "nbf" (not before) claim identifies the time before which the JWT + MUST NOT be accepted for processing. The processing of the "nbf" + claim requires that the current date/time MUST be after or equal to + the not-before date/time listed in the "nbf" claim. Implementers MAY + provide for some small leeway, usually no more than a few minutes, to + account for clock skew. + */ + var nbf: Date? { get } + + /** + The "iat" (issued at) claim identifies the time at which the JWT was + issued. This claim can be used to determine the age of the JWT. + */ + var iat: Date? { get } + + /// Encode the Claim object as a Base64 String. + func encode() throws -> String } + public extension Claims { - - var exp: Date? { - return nil - } - - var nbf: Date? { - return nil - } - - var iat: Date? { - return nil - } - - func encode() throws -> String { - let jsonEncoder = JSONEncoder() - jsonEncoder.dateEncodingStrategy = .secondsSince1970 - let data = try jsonEncoder.encode(self) - return JWTEncoder.base64urlEncodedString(data: data) - } + var exp: Date? { + nil + } + + var nbf: Date? { + nil + } + + var iat: Date? { + nil + } + + 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/ClaimsExamples/ClaimsMicroProfile.swift b/Sources/SwiftJWT/ClaimsExamples/ClaimsMicroProfile.swift index c77fba7..e8c865b 100644 --- a/Sources/SwiftJWT/ClaimsExamples/ClaimsMicroProfile.swift +++ b/Sources/SwiftJWT/ClaimsExamples/ClaimsMicroProfile.swift @@ -16,70 +16,69 @@ import Foundation -// MARK ClaimsMicroProfile +// MARK: ClaimsMicroProfile /// A class representing the MicroProfile claims as listed in [MicroProfile specs](http://microprofile.io/project/eclipse/microprofile-jwt-auth/spec/src/main/asciidoc/interoperability.asciidoc). public class ClaimsMicroProfile: Claims { - - /// Initialize a `ClaimsMicroProfile` - public init( - iss: String, - sub: String, - exp: Date, - iat: Date, - jti: String, - upn: String, - groups: [String] - ) { - self.iss = iss - self.sub = sub - self.exp = exp - self.iat = iat - self.jti = jti - self.upn = upn - self.groups = groups - } - - /** - The MP-JWT issuer. [RFC7519, Section 4.1.1](https://tools.ietf.org/html/rfc7519#section-4.1.1) - */ - public var iss: String - - /** - Identifies the principal that is the subject of the JWT. - */ - public var sub: String - - /** - Identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. - */ - public var exp: Date - - /** - Identifies the time at which the JWT was issued. - */ - public var iat: Date - - /** - The "jti" (JWT ID) claim provides a unique identifier for the JWT. - The identifier value MUST be assigned in a manner that ensures that - there is a negligible probability that the same value will be - accidentally assigned to a different data object. - */ - public var jti: String - - /** - This MP-JWT custom claim is the user principal name in the java.security.Principal interface, and is the caller principal name in javax.security.enterprise.identitystore.IdentityStore. If this claim is missing, fallback to the "preferred_username", should be attempted, and if that claim is missing, fallback to the "sub" claim should be used. - */ - public var upn: String? - - /** - Shorthand name by which the End-User wishes to be referred to at the RP, such as janedoe or j.doe. This value MAY be any valid JSON string including special characters such as @, /, or whitespace. - */ - public var preferred_username: String? - - /** - This MP-JWT custom claim is the list of group names that have been assigned to the principal of the MP-JWT. This typically will required a mapping at the application container level to application deployment roles, but a one-to-one between group names and application role names is required to be performed in addition to any other mapping. - */ - public var groups: [String] + /// Initialize a `ClaimsMicroProfile` + public init( + iss: String, + sub: String, + exp: Date, + iat: Date, + jti: String, + upn: String, + groups: [String] + ) { + self.iss = iss + self.sub = sub + self.exp = exp + self.iat = iat + self.jti = jti + self.upn = upn + self.groups = groups + } + + /** + The MP-JWT issuer. [RFC7519, Section 4.1.1](https://tools.ietf.org/html/rfc7519#section-4.1.1) + */ + public var iss: String + + /** + Identifies the principal that is the subject of the JWT. + */ + public var sub: String + + /** + Identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. + */ + public var exp: Date + + /** + Identifies the time at which the JWT was issued. + */ + public var iat: Date + + /** + The "jti" (JWT ID) claim provides a unique identifier for the JWT. + The identifier value MUST be assigned in a manner that ensures that + there is a negligible probability that the same value will be + accidentally assigned to a different data object. + */ + public var jti: String + + /** + This MP-JWT custom claim is the user principal name in the java.security.Principal interface, and is the caller principal name in javax.security.enterprise.identitystore.IdentityStore. If this claim is missing, fallback to the "preferred_username", should be attempted, and if that claim is missing, fallback to the "sub" claim should be used. + */ + public var upn: String? + + /** + Shorthand name by which the End-User wishes to be referred to at the RP, such as janedoe or j.doe. This value MAY be any valid JSON string including special characters such as @, /, or whitespace. + */ + public var preferred_username: String? + + /** + This MP-JWT custom claim is the list of group names that have been assigned to the principal of the MP-JWT. This typically will required a mapping at the application container level to application deployment roles, but a one-to-one between group names and application role names is required to be performed in addition to any other mapping. + */ + public var groups: [String] } diff --git a/Sources/SwiftJWT/ClaimsExamples/ClaimsOpenID.swift b/Sources/SwiftJWT/ClaimsExamples/ClaimsOpenID.swift index 2da906e..803b98b 100644 --- a/Sources/SwiftJWT/ClaimsExamples/ClaimsOpenID.swift +++ b/Sources/SwiftJWT/ClaimsExamples/ClaimsOpenID.swift @@ -16,185 +16,182 @@ import Foundation -// MARK ClaimsOpenID +// MARK: ClaimsOpenID /// A class representing OpenID related claims as decsribed in [OpenID specs](http://openid.net/specs/openid-connect-core-1_0.html). public class ClaimsOpenID: Claims { - - /// Initalise the `ClaimsOpenID` - public init( - iss: String, - sub: String, - aud: [String], - exp: Date, - iat: Date, - auth_time: Date? = nil, - nonce: String? = nil, - acr: String? = nil, - amr: [String]? = nil, - azp: String? = nil, - name: String? = nil, - given_name: String? = nil, - family_name: String? = nil, - middle_name: String? = nil, - nickname: String? = nil, - preferred_username: String? = nil, - profile: String? = nil, - picture: String? = nil, - website: String? = nil, - email: String? = nil, - email_verified: Bool? = nil, - gender: String? = nil, - birthdate: String? = nil, - zoneinfo: String? = nil, - locale: String? = nil, - phone_number: String? = nil, - phone_number_verified: Bool? = nil, - address: AddressClaim? = nil, - updated_at: Date? = nil - ) { - self.iss = iss - self.sub = sub - self.aud = aud - self.exp = exp - self.iat = iat - self.auth_time = auth_time - self.nonce = nonce - self.acr = acr - self.amr = amr - self.azp = azp - self.name = name - self.given_name = given_name - self.family_name = family_name - self.middle_name = middle_name - self.nickname = nickname - self.preferred_username = preferred_username - self.profile = profile - self.picture = picture - self.website = website - self.email = email - self.email_verified = email_verified - self.gender = gender - self.birthdate = birthdate - self.zoneinfo = zoneinfo - self.locale = locale - self.phone_number = phone_number - self.phone_number_verified = phone_number_verified - self.address = address - self.updated_at = updated_at - } - - // MARK: ID Token - - /// Issuer Identifier for the Issuer of the response. The iss value is a case sensitive URL using the https scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components. - public var iss: String - - /// Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255 ASCII characters in length. The sub value is case sensitive. - public var sub: String - - /// Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. - public var aud: [String] - - /// Expiration time on or after which the ID Token MUST NOT be accepted for processing. The processing of this parameter requires that the current date/time MUST be before the expiration date/time listed in the value. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. - public var exp: Date - - /// Time at which the JWT was issued. - public var iat: Date - - /// Time when the End-User authentication occurred. - public var auth_time: Date? - - /// String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request. If present in the Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token with the Claim Value being the nonce value sent in the Authentication Request. Authorization Servers SHOULD perform no other processing on nonce values used. - public var nonce: String? - - /// Authentication Context Class Reference. String specifying an Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied. The value "0" indicates the End-User authentication did not meet the requirements of ISO/IEC 29115 level 1. Authentications with level 0 SHOULD NOT be used to authorize access to any resource of any monetary value. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. - public var acr: String? - - /// Authentication Methods References. JSON array of strings that are identifiers for authentication methods used in the authentication. For instance, values might indicate that both password and OTP authentication methods were used. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. - public var amr: [String]? - - /// Authorized party - the party to which the ID Token was issued. If present, it MUST contain the OAuth 2.0 Client ID of this party. This Claim is only needed when the ID Token has a single audience value and that audience is different than the authorized party. It MAY be included even when the authorized party is the same as the sole audience. - public var azp: String? - - // MARK: Standard Claims - - /// End-User's full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the End-User's locale and preferences. - public var name: String? - - /// Given name(s) or first name(s) of the End-User. Note that in some cultures, people can have multiple given names; all can be present, with the names being separated by space characters. - public var given_name: String? - - /// Surname(s) or last name(s) of the End-User. Note that in some cultures, people can have multiple family names or no family name; all can be present, with the names being separated by space characters. - public var family_name: String? - - /// Middle name(s) of the End-User. Note that in some cultures, people can have multiple middle names; all can be present, with the names being separated by space characters. Also note that in some cultures, middle names are not used. - public var middle_name: String? - - /// Casual name of the End-User that may or may not be the same as the given_name. For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. - public var nickname: String? - - /// Shorthand name by which the End-User wishes to be referred to at the RP, such as janedoe or j.doe. This value MAY be any valid JSON string including special characters such as @, /, or whitespace. - public var preferred_username: String? - - /// URL of the End-User's profile page. The contents of this Web page SHOULD be about the End-User. - public var profile: String? - - /// URL of the End-User's profile picture. This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), rather than to a Web page containing an image. Note that this URL SHOULD specifically reference a profile photo of the End-User suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User. - public var picture: String? - - /// URL of the End-User's Web page or blog. This Web page SHOULD contain information published by the End-User or an organization that the End-User is affiliated with. - public var website: String? - - /// End-User's preferred e-mail address. - public var email: String? - - /// True if the End-User's e-mail address has been verified; otherwise false. When this Claim Value is true, this means that the OP took affirmative steps to ensure that this e-mail address was controlled by the End-User at the time the verification was performed. The means by which an e-mail address is verified is context-specific, and dependent upon the trust framework or contractual agreements within which the parties are operating. - public var email_verified: Bool? - - /// End-User's gender. Values defined by this specification are female and male. Other values MAY be used when neither of the defined values are applicable. - public var gender: String? - - /// End-User's birthday, represented as an ISO 8601:2004 YYYY-MM-DD format. The year MAY be 0000, indicating that it is omitted. To represent only the year, YYYY format is allowed. - public var birthdate: String? - - /// String from zoneinfo time zone database representing the End-User's time zone. For example, Europe/Paris or America/Los_Angeles. - public var zoneinfo: String? - - /// End-User's locale, represented as a BCP47 language tag. This is typically an ISO 639-1 Alpha-2 language code in lowercase and an ISO 3166-1 Alpha-2 country code in uppercase, separated by a dash. For example, en-US or fr-CA. As a compatibility note, some implementations have used an underscore as the separator rather than a dash, for example, en_US; Relying Parties MAY choose to accept this locale syntax as well. - public var locale: String? - - /// End-User's preferred telephone number. E.164 is RECOMMENDED as the format of this Claim, for example, +1 (425) 555-1212 or +56 (2) 687 2400. - public var phone_number: String? - - /// True if the End-User's phone number has been verified; otherwise false. When this Claim Value is true, this means that the OP took affirmative steps to ensure that this phone number was controlled by the End-User at the time the verification was performed. The means by which a phone number is verified is context-specific, and dependent upon the trust framework or contractual agreements within which the parties are operating. When true, the phone_number Claim MUST be in E.164 format and any extensions MUST be represented in RFC 3966 format. - public var phone_number_verified: Bool? - - /// End-User's preferred postal address. - public var address: AddressClaim? - - /// Time the End-User's information was last updated. - public var updated_at: Date? + /// Initalise the `ClaimsOpenID` + public init( + iss: String, + sub: String, + aud: [String], + exp: Date, + iat: Date, + auth_time: Date? = nil, + nonce: String? = nil, + acr: String? = nil, + amr: [String]? = nil, + azp: String? = nil, + name: String? = nil, + given_name: String? = nil, + family_name: String? = nil, + middle_name: String? = nil, + nickname: String? = nil, + preferred_username: String? = nil, + profile: String? = nil, + picture: String? = nil, + website: String? = nil, + email: String? = nil, + email_verified: Bool? = nil, + gender: String? = nil, + birthdate: String? = nil, + zoneinfo: String? = nil, + locale: String? = nil, + phone_number: String? = nil, + phone_number_verified: Bool? = nil, + address: AddressClaim? = nil, + updated_at: Date? = nil + ) { + self.iss = iss + self.sub = sub + self.aud = aud + self.exp = exp + self.iat = iat + self.auth_time = auth_time + self.nonce = nonce + self.acr = acr + self.amr = amr + self.azp = azp + self.name = name + self.given_name = given_name + self.family_name = family_name + self.middle_name = middle_name + self.nickname = nickname + self.preferred_username = preferred_username + self.profile = profile + self.picture = picture + self.website = website + self.email = email + self.email_verified = email_verified + self.gender = gender + self.birthdate = birthdate + self.zoneinfo = zoneinfo + self.locale = locale + self.phone_number = phone_number + self.phone_number_verified = phone_number_verified + self.address = address + self.updated_at = updated_at + } + + // MARK: ID Token + + /// Issuer Identifier for the Issuer of the response. The iss value is a case sensitive URL using the https scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components. + public var iss: String + + /// Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255 ASCII characters in length. The sub value is case sensitive. + public var sub: String + + /// Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. + public var aud: [String] + + /// Expiration time on or after which the ID Token MUST NOT be accepted for processing. The processing of this parameter requires that the current date/time MUST be before the expiration date/time listed in the value. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. + public var exp: Date + + /// Time at which the JWT was issued. + public var iat: Date + + /// Time when the End-User authentication occurred. + public var auth_time: Date? + + /// String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request. If present in the Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token with the Claim Value being the nonce value sent in the Authentication Request. Authorization Servers SHOULD perform no other processing on nonce values used. + public var nonce: String? + + /// Authentication Context Class Reference. String specifying an Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied. The value "0" indicates the End-User authentication did not meet the requirements of ISO/IEC 29115 level 1. Authentications with level 0 SHOULD NOT be used to authorize access to any resource of any monetary value. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. + public var acr: String? + + /// Authentication Methods References. JSON array of strings that are identifiers for authentication methods used in the authentication. For instance, values might indicate that both password and OTP authentication methods were used. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. + public var amr: [String]? + + /// Authorized party - the party to which the ID Token was issued. If present, it MUST contain the OAuth 2.0 Client ID of this party. This Claim is only needed when the ID Token has a single audience value and that audience is different than the authorized party. It MAY be included even when the authorized party is the same as the sole audience. + public var azp: String? + + // MARK: Standard Claims + + /// End-User's full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the End-User's locale and preferences. + public var name: String? + + /// Given name(s) or first name(s) of the End-User. Note that in some cultures, people can have multiple given names; all can be present, with the names being separated by space characters. + public var given_name: String? + + /// Surname(s) or last name(s) of the End-User. Note that in some cultures, people can have multiple family names or no family name; all can be present, with the names being separated by space characters. + public var family_name: String? + + /// Middle name(s) of the End-User. Note that in some cultures, people can have multiple middle names; all can be present, with the names being separated by space characters. Also note that in some cultures, middle names are not used. + public var middle_name: String? + + /// Casual name of the End-User that may or may not be the same as the given_name. For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. + public var nickname: String? + + /// Shorthand name by which the End-User wishes to be referred to at the RP, such as janedoe or j.doe. This value MAY be any valid JSON string including special characters such as @, /, or whitespace. + public var preferred_username: String? + + /// URL of the End-User's profile page. The contents of this Web page SHOULD be about the End-User. + public var profile: String? + + /// URL of the End-User's profile picture. This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), rather than to a Web page containing an image. Note that this URL SHOULD specifically reference a profile photo of the End-User suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User. + public var picture: String? + + /// URL of the End-User's Web page or blog. This Web page SHOULD contain information published by the End-User or an organization that the End-User is affiliated with. + public var website: String? + + /// End-User's preferred e-mail address. + public var email: String? + + /// True if the End-User's e-mail address has been verified; otherwise false. When this Claim Value is true, this means that the OP took affirmative steps to ensure that this e-mail address was controlled by the End-User at the time the verification was performed. The means by which an e-mail address is verified is context-specific, and dependent upon the trust framework or contractual agreements within which the parties are operating. + public var email_verified: Bool? + + /// End-User's gender. Values defined by this specification are female and male. Other values MAY be used when neither of the defined values are applicable. + public var gender: String? + + /// End-User's birthday, represented as an ISO 8601:2004 YYYY-MM-DD format. The year MAY be 0000, indicating that it is omitted. To represent only the year, YYYY format is allowed. + public var birthdate: String? + + /// String from zoneinfo time zone database representing the End-User's time zone. For example, Europe/Paris or America/Los_Angeles. + public var zoneinfo: String? + + /// End-User's locale, represented as a BCP47 language tag. This is typically an ISO 639-1 Alpha-2 language code in lowercase and an ISO 3166-1 Alpha-2 country code in uppercase, separated by a dash. For example, en-US or fr-CA. As a compatibility note, some implementations have used an underscore as the separator rather than a dash, for example, en_US; Relying Parties MAY choose to accept this locale syntax as well. + public var locale: String? + + /// End-User's preferred telephone number. E.164 is RECOMMENDED as the format of this Claim, for example, +1 (425) 555-1212 or +56 (2) 687 2400. + public var phone_number: String? + + /// True if the End-User's phone number has been verified; otherwise false. When this Claim Value is true, this means that the OP took affirmative steps to ensure that this phone number was controlled by the End-User at the time the verification was performed. The means by which a phone number is verified is context-specific, and dependent upon the trust framework or contractual agreements within which the parties are operating. When true, the phone_number Claim MUST be in E.164 format and any extensions MUST be represented in RFC 3966 format. + public var phone_number_verified: Bool? + + /// End-User's preferred postal address. + public var address: AddressClaim? + + /// Time the End-User's information was last updated. + public var updated_at: Date? } /// Struct representing an AddressClaim as defined in the [OpenID specs](http://openid.net/specs/openid-connect-core-1_0.html). public struct AddressClaim: Codable { - - /// Full mailing address, formatted for display or use on a mailing label. This field MAY contain multiple lines, separated by newlines. Newlines can be represented either as a carriage return/line feed pair ("\r\n") or as a single line feed character ("\n"). - public var formatted: String? - - /// Full street address component, which MAY include house number, street name, Post Office Box, and multi-line extended street address information. This field MAY contain multiple lines, separated by newlines. Newlines can be represented either as a carriage return/line feed pair ("\r\n") or as a single line feed character ("\n"). - public var street_address: String? - - /// City or locality component. - public var locality: String? - - /// State, province, prefecture, or region component. - public var region: String? - - /// Zip code or postal code component. - public var postal_code: String? - - /// Country name component. - public var country: String? - + /// Full mailing address, formatted for display or use on a mailing label. This field MAY contain multiple lines, separated by newlines. Newlines can be represented either as a carriage return/line feed pair ("\r\n") or as a single line feed character ("\n"). + public var formatted: String? + + /// Full street address component, which MAY include house number, street name, Post Office Box, and multi-line extended street address information. This field MAY contain multiple lines, separated by newlines. Newlines can be represented either as a carriage return/line feed pair ("\r\n") or as a single line feed character ("\n"). + public var street_address: String? + + /// City or locality component. + public var locality: String? + + /// State, province, prefecture, or region component. + public var region: String? + + /// Zip code or postal code component. + public var postal_code: String? + + /// Country name component. + public var country: String? } diff --git a/Sources/SwiftJWT/ClaimsExamples/ClaimsStandardJWT.swift b/Sources/SwiftJWT/ClaimsExamples/ClaimsStandardJWT.swift index 96a1a8c..3ebc3ae 100644 --- a/Sources/SwiftJWT/ClaimsExamples/ClaimsStandardJWT.swift +++ b/Sources/SwiftJWT/ClaimsExamples/ClaimsStandardJWT.swift @@ -16,93 +16,92 @@ import Foundation -// MARK ClaimsStandardJWT +// MARK: ClaimsStandardJWT /// A class representing the Standard JWT claims as described in [RFC7519](https://tools.ietf.org/html/rfc7519#section-4.1). public class ClaimsStandardJWT: Claims { - - /// Initialize a `ClaimsStandardJWT` - public init( - iss: String? = nil, - sub: String? = nil, - aud: [String]? = nil, - exp: Date? = nil, - nbf: Date? = nil, - iat: Date? = nil, - jti: String? = nil - ) { - self.iss = iss - self.sub = sub - self.aud = aud - self.exp = exp - self.nbf = nbf - self.iat = iat - self.jti = jti - } - - /** - The "iss" (issuer) claim identifies the principal that issued the - JWT. The processing of this claim is generally application specific. - The "iss" value is a case-sensitive. - */ - public var iss: String? - - /** - The "sub" (subject) claim identifies the principal that is the - subject of the JWT. The claims in a JWT are normally statements - about the subject. The subject value MUST either be scoped to be - locally unique in the context of the issuer or be globally unique. - The processing of this claim is generally application specific. The - "sub" value is case-sensitive. - */ - public var sub: String? - - /** - The "aud" (audience) claim identifies the recipients that the JWT is - intended for. Each principal intended to process the JWT MUST - identify itself with a value in the audience claim. If the principal - processing the claim does not identify itself with a value in the - "aud" claim when this claim is present, then the JWT MUST be - rejected. The interpretation of audience values is generally application specific. - The "aud" value is case-sensitive. - */ - public var aud: [String]? - - /** - The "exp" (expiration time) claim identifies the expiration time on - or after which the JWT MUST NOT be accepted for processing. The - processing of the "exp" claim requires that the current date/time - MUST be before the expiration date/time listed in the "exp" claim. - Implementers MAY provide for some small leeway, usually no more than - a few minutes, to account for clock skew. - */ - public var exp: Date? - - /** - The "nbf" (not before) claim identifies the time before which the JWT - MUST NOT be accepted for processing. The processing of the "nbf" - claim requires that the current date/time MUST be after or equal to - the not-before date/time listed in the "nbf" claim. Implementers MAY - provide for some small leeway, usually no more than a few minutes, to - account for clock skew. - */ - public var nbf: Date? - - /** - The "iat" (issued at) claim identifies the time at which the JWT was - issued. This claim can be used to determine the age of the JWT. - */ - public var iat: Date? - - /** - The "jti" (JWT ID) claim provides a unique identifier for the JWT. - The identifier value MUST be assigned in a manner that ensures that - there is a negligible probability that the same value will be - accidentally assigned to a different data object; if the application - uses multiple issuers, collisions MUST be prevented among values - produced by different issuers as well. The "jti" claim can be used - to prevent the JWT from being replayed. The "jti" value is case- - sensitive - */ - public var jti: String? + /// Initialize a `ClaimsStandardJWT` + public init( + iss: String? = nil, + sub: String? = nil, + aud: [String]? = nil, + exp: Date? = nil, + nbf: Date? = nil, + iat: Date? = nil, + jti: String? = nil + ) { + self.iss = iss + self.sub = sub + self.aud = aud + self.exp = exp + self.nbf = nbf + self.iat = iat + self.jti = jti + } + + /** + The "iss" (issuer) claim identifies the principal that issued the + JWT. The processing of this claim is generally application specific. + The "iss" value is a case-sensitive. + */ + public var iss: String? + + /** + The "sub" (subject) claim identifies the principal that is the + subject of the JWT. The claims in a JWT are normally statements + about the subject. The subject value MUST either be scoped to be + locally unique in the context of the issuer or be globally unique. + The processing of this claim is generally application specific. The + "sub" value is case-sensitive. + */ + public var sub: String? + + /** + The "aud" (audience) claim identifies the recipients that the JWT is + intended for. Each principal intended to process the JWT MUST + identify itself with a value in the audience claim. If the principal + processing the claim does not identify itself with a value in the + "aud" claim when this claim is present, then the JWT MUST be + rejected. The interpretation of audience values is generally application specific. + The "aud" value is case-sensitive. + */ + public var aud: [String]? + + /** + The "exp" (expiration time) claim identifies the expiration time on + or after which the JWT MUST NOT be accepted for processing. The + processing of the "exp" claim requires that the current date/time + MUST be before the expiration date/time listed in the "exp" claim. + Implementers MAY provide for some small leeway, usually no more than + a few minutes, to account for clock skew. + */ + public var exp: Date? + + /** + The "nbf" (not before) claim identifies the time before which the JWT + MUST NOT be accepted for processing. The processing of the "nbf" + claim requires that the current date/time MUST be after or equal to + the not-before date/time listed in the "nbf" claim. Implementers MAY + provide for some small leeway, usually no more than a few minutes, to + account for clock skew. + */ + public var nbf: Date? + + /** + The "iat" (issued at) claim identifies the time at which the JWT was + issued. This claim can be used to determine the age of the JWT. + */ + public var iat: Date? + + /** + The "jti" (JWT ID) claim provides a unique identifier for the JWT. + The identifier value MUST be assigned in a manner that ensures that + there is a negligible probability that the same value will be + accidentally assigned to a different data object; if the application + uses multiple issuers, collisions MUST be prevented among values + produced by different issuers as well. The "jti" claim can be used + to prevent the JWT from being replayed. The "jti" value is case- + sensitive + */ + public var jti: String? } diff --git a/Sources/SwiftJWT/Data+Base64URLEncoded.swift b/Sources/SwiftJWT/Data+Base64URLEncoded.swift index d9c9204..1d4ae5d 100644 --- a/Sources/SwiftJWT/Data+Base64URLEncoded.swift +++ b/Sources/SwiftJWT/Data+Base64URLEncoded.swift @@ -17,34 +17,32 @@ import Foundation /// Convenience extension for encoding a `Data` as a base64url-encoded `String`. -extension JWTEncoder { - - /// Returns a `String` representation of this data, encoded in base64url format - /// as defined in RFC4648 (https://tools.ietf.org/html/rfc4648). - /// - /// This is the appropriate format for encoding the header and claims of a JWT. - public static func base64urlEncodedString(data: Data) -> String { - let result = data.base64EncodedString() - return result.replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "=", with: "") - } +public extension JWTEncoder { + /// Returns a `String` representation of this data, encoded in base64url format + /// as defined in RFC4648 (https://tools.ietf.org/html/rfc4648). + /// + /// This is the appropriate format for encoding the header and claims of a JWT. + static func base64urlEncodedString(data: Data) -> String { + let result = data.base64EncodedString() + return result.replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } } /// Convenience extension for decoding a `Data` from a base64url-encoded `String`. -extension JWTDecoder { - - /// Initializes a new `Data` from the base64url-encoded `String` provided. The - /// base64url encoding is defined in RFC4648 (https://tools.ietf.org/html/rfc4648). - /// - /// This is appropriate for reading the header or claims portion of a JWT string. - public static func data(base64urlEncoded: String) -> Data? { - let paddingLength = 4 - base64urlEncoded.count % 4 - let padding = (paddingLength < 4) ? String(repeating: "=", count: paddingLength) : "" - let base64EncodedString = base64urlEncoded - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - + padding - return Data(base64Encoded: base64EncodedString) - } +public extension JWTDecoder { + /// Initializes a new `Data` from the base64url-encoded `String` provided. The + /// base64url encoding is defined in RFC4648 (https://tools.ietf.org/html/rfc4648). + /// + /// This is appropriate for reading the header or claims portion of a JWT string. + static func data(base64urlEncoded: String) -> Data? { + let paddingLength = 4 - base64urlEncoded.count % 4 + let padding = (paddingLength < 4) ? String(repeating: "=", count: paddingLength) : "" + let base64EncodedString = base64urlEncoded + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + padding + return Data(base64Encoded: base64EncodedString) + } } diff --git a/Sources/SwiftJWT/Header.swift b/Sources/SwiftJWT/Header.swift index 7f6860e..92ac2a2 100644 --- a/Sources/SwiftJWT/Header.swift +++ b/Sources/SwiftJWT/Header.swift @@ -31,72 +31,71 @@ import Foundation ``` */ public struct Header: Codable { - - /// Type Header Parameter - public var typ: String? - /// Algorithm Header Parameter - public internal(set) var alg: String? - /// JSON Web Token Set URL Header Parameter - public var jku : String? - /// JSON Web Key Header Parameter - public var jwk: String? - /// Key ID Header Parameter - public var kid: String? - /// X.509 URL Header Parameter - public var x5u: String? - /// X.509 Certificate Chain Header Parameter - public var x5c: [String]? - /// X.509 Certificate SHA-1 Thumbprint Header Parameter - public var x5t: String? - /// X.509 Certificate SHA-256 Thumbprint Header Parameter - public var x5tS256: String? - /// Content Type Header Parameter - public var cty: String? - /// Critical Header Parameter - public var crit: [String]? - - /// Initialize a `Header` instance. - /// - /// - Parameter typ: The Type Header Parameter - /// - Parameter jku: The JSON Web Token Set URL Header Parameter - /// - Parameter jwk: The JSON Web Key Header Parameter - /// - Parameter kid: The Key ID Header Parameter - /// - Parameter x5u: The X.509 URL Header Parameter - /// - Parameter x5c: The X.509 Certificate Chain Header Parameter - /// - Parameter x5t: The X.509 Certificate SHA-1 Thumbprint Header Parameter - /// - Parameter x5tS256: X.509 Certificate SHA-256 Thumbprint Header Parameter - /// - Parameter cty: The Content Type Header Parameter - /// - Parameter crit: The Critical Header Parameter - /// - Returns: A new instance of `Header`. - public init( - typ: String? = "JWT", - jku: String? = nil, - jwk: String? = nil, - kid: String? = nil, - x5u: String? = nil, - x5c: [String]? = nil, - x5t: String? = nil, - x5tS256: String? = nil, - cty: String? = nil, - crit: [String]? = nil - ) { - self.typ = typ - self.alg = nil - self.jku = jku - self.jwk = jwk - self.kid = kid - self.x5u = x5u - self.x5c = x5c - self.x5t = x5t - self.x5tS256 = x5tS256 - self.cty = cty - self.crit = crit - } - - func encode() throws -> String { - let jsonEncoder = JSONEncoder() - jsonEncoder.dateEncodingStrategy = .secondsSince1970 - let data = try jsonEncoder.encode(self) - return JWTEncoder.base64urlEncodedString(data: data) - } + /// Type Header Parameter + public var typ: String? + /// Algorithm Header Parameter + public internal(set) var alg: String? + /// JSON Web Token Set URL Header Parameter + public var jku: String? + /// JSON Web Key Header Parameter + public var jwk: String? + /// Key ID Header Parameter + public var kid: String? + /// X.509 URL Header Parameter + public var x5u: String? + /// X.509 Certificate Chain Header Parameter + public var x5c: [String]? + /// X.509 Certificate SHA-1 Thumbprint Header Parameter + public var x5t: String? + /// X.509 Certificate SHA-256 Thumbprint Header Parameter + public var x5tS256: String? + /// Content Type Header Parameter + public var cty: String? + /// Critical Header Parameter + public var crit: [String]? + + /// Initialize a `Header` instance. + /// + /// - Parameter typ: The Type Header Parameter + /// - Parameter jku: The JSON Web Token Set URL Header Parameter + /// - Parameter jwk: The JSON Web Key Header Parameter + /// - Parameter kid: The Key ID Header Parameter + /// - Parameter x5u: The X.509 URL Header Parameter + /// - Parameter x5c: The X.509 Certificate Chain Header Parameter + /// - Parameter x5t: The X.509 Certificate SHA-1 Thumbprint Header Parameter + /// - Parameter x5tS256: X.509 Certificate SHA-256 Thumbprint Header Parameter + /// - Parameter cty: The Content Type Header Parameter + /// - Parameter crit: The Critical Header Parameter + /// - Returns: A new instance of `Header`. + public init( + typ: String? = "JWT", + jku: String? = nil, + jwk: String? = nil, + kid: String? = nil, + x5u: String? = nil, + x5c: [String]? = nil, + x5t: String? = nil, + x5tS256: String? = nil, + cty: String? = nil, + crit: [String]? = nil + ) { + self.typ = typ + alg = nil + self.jku = jku + self.jwk = jwk + self.kid = kid + self.x5u = x5u + self.x5c = x5c + self.x5t = x5t + self.x5tS256 = x5tS256 + self.cty = cty + 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..7fa498d 100644 --- a/Sources/SwiftJWT/JWT.swift +++ b/Sources/SwiftJWT/JWT.swift @@ -19,9 +19,9 @@ import Foundation // MARK: JWT /** - + A struct representing the `Header` and `Claims` of a JSON Web Token. - + ### Usage Example: ### ```swift struct MyClaims: Claims { @@ -34,106 +34,104 @@ import Foundation */ public struct JWT: Codable { - - /// The JWT header. - public var header: Header - - /// The JWT claims - public var claims: T - - /// Initialize a `JWT` instance from a `Header` and `Claims`. - /// - /// - 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) { - 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()`. - /// If the string is not a valid JWT, or the verification fails, the initializer returns nil. - /// - /// - Parameter jwt: A String with the encoded and signed JWT. - /// - Parameter verifier: The `JWTVerifier` used to verify the JWT. - /// - Returns: An instance of `JWT` if the decoding succeeds. - /// - Throws: `JWTError.invalidJWTString` if the provided String is not in the form mandated by the JWT specification. - /// - Throws: `JWTError.failedVerification` if the verifier fails to verify the jwtString. - /// - Throws: A DecodingError if the JSONDecoder throws an error while decoding the JWT. - public init(jwtString: String, verifier: JWTVerifier = .none ) throws { - let components = jwtString.components(separatedBy: ".") - guard components.count == 2 || components.count == 3, - let headerData = JWTDecoder.data(base64urlEncoded: components[0]), - let claimsData = JWTDecoder.data(base64urlEncoded: components[1]) - else { - throw JWTError.invalidJWTString - } - guard JWT.verify(jwtString, using: verifier) else { - throw JWTError.failedVerification - } - let jsonDecoder = JSONDecoder() - jsonDecoder.dateDecodingStrategy = .secondsSince1970 - let header = try jsonDecoder.decode(Header.self, from: headerData) - let claims = try jsonDecoder.decode(T.self, from: claimsData) - self.header = header - self.claims = claims - } - - /// Sign the JWT using the given algorithm and encode the header, claims and signature as a JWT String. - /// - /// - Note: This function will set header.alg field to the name of the signing algorithm. - /// - /// - Parameter using algorithm: The algorithm to sign with. - /// - Returns: A String with the encoded and signed JWT. - /// - Throws: An EncodingError if the JSONEncoder throws an error while encoding the JWT. - /// - Throws: `JWTError.osVersionToLow` if not using macOS 10.12.0 (Sierra) or iOS 10.0 or higher. - /// - Throws: A Signing error if the jwtSigner is unable to sign the JWT with the provided key. - public mutating func sign(using jwtSigner: JWTSigner) throws -> String { - var tempHeader = header - tempHeader.alg = jwtSigner.name - let headerString = try tempHeader.encode() - let claimsString = try claims.encode() - header.alg = tempHeader.alg - return try jwtSigner.sign(header: headerString, claims: claimsString) - } + /// The JWT header. + public var header: Header - /// Verify the signature of the encoded JWT using the given algorithm. - /// - /// - Parameter jwt: A String with the encoded and signed JWT. - /// - Parameter using algorithm: The algorithm to verify with. - /// - Returns: A Bool indicating whether the verification was successful. - public static func verify(_ jwt: String, using jwtVerifier: JWTVerifier) -> Bool { - return jwtVerifier.verify(jwt: jwt) - } + /// The JWT claims + public var claims: T - /// Validate the time based standard JWT claims. - /// This function checks that the "exp" (expiration time) is in the future - /// and the "iat" (issued at) and "nbf" (not before) headers are in the past, - /// - /// - 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 { - if let expirationDate = claims.exp { - if expirationDate + leeway < Date() { - return .expired - } - } - - if let notBeforeDate = claims.nbf { - if notBeforeDate > Date() + leeway { - return .notBefore - } - } - - if let issuedAtDate = claims.iat { - if issuedAtDate > Date() + leeway { - return .issuedAt - } - } - - return .success - } -} + /// Initialize a `JWT` instance from a `Header` and `Claims`. + /// + /// - 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) { + 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()`. + /// If the string is not a valid JWT, or the verification fails, the initializer returns nil. + /// + /// - Parameter jwt: A String with the encoded and signed JWT. + /// - Parameter verifier: The `JWTVerifier` used to verify the JWT. + /// - Returns: An instance of `JWT` if the decoding succeeds. + /// - Throws: `JWTError.invalidJWTString` if the provided String is not in the form mandated by the JWT specification. + /// - Throws: `JWTError.failedVerification` if the verifier fails to verify the jwtString. + /// - Throws: A DecodingError if the JSONDecoder throws an error while decoding the JWT. + public init(jwtString: String, verifier: JWTVerifier = .none) throws { + let components = jwtString.components(separatedBy: ".") + guard components.count == 2 || components.count == 3, + let headerData = JWTDecoder.data(base64urlEncoded: components[0]), + let claimsData = JWTDecoder.data(base64urlEncoded: components[1]) + else { + throw JWTError.invalidJWTString + } + guard JWT.verify(jwtString, using: verifier) else { + throw JWTError.failedVerification + } + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .secondsSince1970 + let header = try jsonDecoder.decode(Header.self, from: headerData) + let claims = try jsonDecoder.decode(T.self, from: claimsData) + self.header = header + self.claims = claims + } + + /// Sign the JWT using the given algorithm and encode the header, claims and signature as a JWT String. + /// + /// - Note: This function will set header.alg field to the name of the signing algorithm. + /// + /// - Parameter using algorithm: The algorithm to sign with. + /// - Returns: A String with the encoded and signed JWT. + /// - Throws: An EncodingError if the JSONEncoder throws an error while encoding the JWT. + /// - Throws: `JWTError.osVersionToLow` if not using macOS 10.12.0 (Sierra) or iOS 10.0 or higher. + /// - Throws: A Signing error if the jwtSigner is unable to sign the JWT with the provided key. + public mutating func sign(using jwtSigner: JWTSigner) throws -> String { + var tempHeader = header + tempHeader.alg = jwtSigner.name + let headerString = try tempHeader.encode() + let claimsString = try claims.encode() + header.alg = tempHeader.alg + return try jwtSigner.sign(header: headerString, claims: claimsString) + } + /// Verify the signature of the encoded JWT using the given algorithm. + /// + /// - Parameter jwt: A String with the encoded and signed JWT. + /// - Parameter using algorithm: The algorithm to verify with. + /// - Returns: A Bool indicating whether the verification was successful. + public static func verify(_ jwt: String, using jwtVerifier: JWTVerifier) -> Bool { + jwtVerifier.verify(jwt: jwt) + } + + /// Validate the time based standard JWT claims. + /// This function checks that the "exp" (expiration time) is in the future + /// and the "iat" (issued at) and "nbf" (not before) headers are in the past, + /// + /// - 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 { + if let expirationDate = claims.exp { + if expirationDate + leeway < Date() { + return .expired + } + } + + if let notBeforeDate = claims.nbf { + if notBeforeDate > Date() + leeway { + return .notBefore + } + } + + if let issuedAtDate = claims.iat { + if issuedAtDate > Date() + leeway { + return .issuedAt + } + } + + return .success + } +} diff --git a/Sources/SwiftJWT/JWTDecoder.swift b/Sources/SwiftJWT/JWTDecoder.swift index d855204..f2e26a0 100644 --- a/Sources/SwiftJWT/JWTDecoder.swift +++ b/Sources/SwiftJWT/JWTDecoder.swift @@ -37,89 +37,88 @@ import KituraContracts ``` */ public class JWTDecoder: BodyDecoder { - - let keyIDToVerifier: (String) -> JWTVerifier? - let jwtVerifier: JWTVerifier? - - // MARK: Initializers - - /// Initialize a `JWTDecoder` instance with a single `JWTVerifier`. - /// - /// - Parameter JWTVerifier: The `JWTVerifier` that will be used to verify the signiture of the JWT. - /// - Returns: A new instance of `JWTDecoder`. - public init(jwtVerifier: JWTVerifier) { - self.keyIDToVerifier = {_ in return jwtVerifier } - self.jwtVerifier = jwtVerifier - } - - /// Initialize a `JWTDecoder` instance with a function to generate the `JWTVerifier` from the JWT `kid` header. - /// - /// - Parameter keyIDToVerifier: The function that will generate the `JWTVerifier` using the "kid" header. - /// - Returns: A new instance of `JWTDecoder`. - public init(keyIDToVerifier: @escaping (String) -> JWTVerifier?) { - self.keyIDToVerifier = keyIDToVerifier - self.jwtVerifier = nil - } - - // MARK: Decode - - /// Decode a `JWT` instance from a JWT String. - /// - /// - Parameter type: The JWT type the String will be decoded as. - /// - Parameter fromString: The JWT String that will be decoded. - /// - Returns: A `JWT` instance of the provided type. - /// - throws: `JWTError.invalidJWTString` if the provided String is not in the form mandated by the JWT specification. - /// - throws: `JWTError.invalidKeyID` if the KeyID `kid` header fails to generate a jwtVerifier. - /// - throws: `JWTError.failedVerification` if the `JWTVerifier` fails to verify the decoded String. - /// - throws: `DecodingError` if the decoder fails to decode the String as the provided type. - public func decode(_ type: T.Type, fromString: String) throws -> T { - // Seperate the JWT into the headers and claims. - let components = fromString.components(separatedBy: ".") - guard components.count > 1, - let headerData = JWTDecoder.data(base64urlEncoded: components[0]), - let claimsData = JWTDecoder.data(base64urlEncoded: components[1]) - else { - throw JWTError.invalidJWTString - } - - // Decode the JWT headers and claims data into a _JWTDecoder. - let decoder = _JWTDecoder(header: headerData, claims: claimsData) - let jwt = try decoder.decode(type) - - let _jwtVerifier: JWTVerifier - // Verify the JWT String using the JWTDecoder constant jwtVerifier. - if let jwtVerifier = jwtVerifier { - _jwtVerifier = jwtVerifier - } else { - // The JWTVerifier is generated using the kid Header that was read inside the _JWTDecoder - // and then used to verify the JWT. - guard let keyID = decoder.keyID, let jwtVerifier = keyIDToVerifier(keyID) else { - throw JWTError.invalidKeyID - } - _jwtVerifier = jwtVerifier - } - guard _jwtVerifier.verify(jwt: fromString) else { - throw JWTError.failedVerification - } - return jwt - } - - /// Decode a `JWT` instance from a utf8 encoded JWT String. - /// - /// - Parameter type: The JWT type the Data will be decoded as. - /// - Parameter data: The utf8 encoded JWT String that will be decoded. - /// - Returns: A `JWT` instance of the provided type. - /// - throws: `JWTError.invalidUTF8Data` if the provided Data can't be decoded to a String. - /// - throws: `JWTError.invalidJWTString` if the provided String is not in the form mandated by the JWT specification. - /// - throws: `JWTError.invalidKeyID` if the KeyID `kid` header fails to generate a `JWTVerifier`. - /// - throws: `JWTError.failedVerification` if the `JWTVerifier` fails to verify the decoded String. - /// - throws: `DecodingError` if the decoder fails to decode the String as the provided type. - public func decode(_ type: T.Type, from data: Data) throws -> T { - guard let jwtString = String(data: data, encoding: .utf8) else { - throw JWTError.invalidUTF8Data - } - return try decode(type, fromString: jwtString) - } + let keyIDToVerifier: (String) -> JWTVerifier? + let jwtVerifier: JWTVerifier? + + // MARK: Initializers + + /// Initialize a `JWTDecoder` instance with a single `JWTVerifier`. + /// + /// - Parameter JWTVerifier: The `JWTVerifier` that will be used to verify the signiture of the JWT. + /// - Returns: A new instance of `JWTDecoder`. + public init(jwtVerifier: JWTVerifier) { + keyIDToVerifier = { _ in jwtVerifier } + self.jwtVerifier = jwtVerifier + } + + /// Initialize a `JWTDecoder` instance with a function to generate the `JWTVerifier` from the JWT `kid` header. + /// + /// - Parameter keyIDToVerifier: The function that will generate the `JWTVerifier` using the "kid" header. + /// - Returns: A new instance of `JWTDecoder`. + public init(keyIDToVerifier: @escaping (String) -> JWTVerifier?) { + self.keyIDToVerifier = keyIDToVerifier + jwtVerifier = nil + } + + // MARK: Decode + + /// Decode a `JWT` instance from a JWT String. + /// + /// - Parameter type: The JWT type the String will be decoded as. + /// - Parameter fromString: The JWT String that will be decoded. + /// - Returns: A `JWT` instance of the provided type. + /// - throws: `JWTError.invalidJWTString` if the provided String is not in the form mandated by the JWT specification. + /// - throws: `JWTError.invalidKeyID` if the KeyID `kid` header fails to generate a jwtVerifier. + /// - throws: `JWTError.failedVerification` if the `JWTVerifier` fails to verify the decoded String. + /// - throws: `DecodingError` if the decoder fails to decode the String as the provided type. + public func decode(_ type: T.Type, fromString: String) throws -> T { + // Seperate the JWT into the headers and claims. + let components = fromString.components(separatedBy: ".") + guard components.count > 1, + let headerData = JWTDecoder.data(base64urlEncoded: components[0]), + let claimsData = JWTDecoder.data(base64urlEncoded: components[1]) + else { + throw JWTError.invalidJWTString + } + + // Decode the JWT headers and claims data into a _JWTDecoder. + let decoder = _JWTDecoder(header: headerData, claims: claimsData) + let jwt = try decoder.decode(type) + + let _jwtVerifier: JWTVerifier + // Verify the JWT String using the JWTDecoder constant jwtVerifier. + if let jwtVerifier { + _jwtVerifier = jwtVerifier + } else { + // The JWTVerifier is generated using the kid Header that was read inside the _JWTDecoder + // and then used to verify the JWT. + guard let keyID = decoder.keyID, let jwtVerifier = keyIDToVerifier(keyID) else { + throw JWTError.invalidKeyID + } + _jwtVerifier = jwtVerifier + } + guard _jwtVerifier.verify(jwt: fromString) else { + throw JWTError.failedVerification + } + return jwt + } + + /// Decode a `JWT` instance from a utf8 encoded JWT String. + /// + /// - Parameter type: The JWT type the Data will be decoded as. + /// - Parameter data: The utf8 encoded JWT String that will be decoded. + /// - Returns: A `JWT` instance of the provided type. + /// - throws: `JWTError.invalidUTF8Data` if the provided Data can't be decoded to a String. + /// - throws: `JWTError.invalidJWTString` if the provided String is not in the form mandated by the JWT specification. + /// - throws: `JWTError.invalidKeyID` if the KeyID `kid` header fails to generate a `JWTVerifier`. + /// - throws: `JWTError.failedVerification` if the `JWTVerifier` fails to verify the decoded String. + /// - throws: `DecodingError` if the decoder fails to decode the String as the provided type. + public func decode(_ type: T.Type, from data: Data) throws -> T { + guard let jwtString = String(data: data, encoding: .utf8) else { + throw JWTError.invalidUTF8Data + } + return try decode(type, fromString: jwtString) + } } /* @@ -139,148 +138,145 @@ public class JWTDecoder: BodyDecoder { ``` Where decoder is a _JWTDecoder instance, and MyClaims is the user defined object conforming to Claims. */ -fileprivate class _JWTDecoder: Decoder { - - init(header: Data, claims: Data) { - self.header = header - self.claims = claims - } - - var header: Data - - var claims: Data - - var keyID: String? - - var codingPath: [CodingKey] = [] - - var userInfo: [CodingUserInfoKey : Any] = [:] - - // Call the Codable Types init from decoder function. - public func decode(_ type: T.Type) throws -> T { - return try type.init(from: self) - } - - // JWT should only be a Keyed container - func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { - let container = _JWTKeyedDecodingContainer(decoder: self, header: header, claims: claims) - return KeyedDecodingContainer(container) - } - - // This function should not be called when decoding a JWT - func unkeyedContainer() throws -> UnkeyedDecodingContainer { - return UnkeyedContainer(decoder: self) - } - - // This function should not be called when decoding a JWT - func singleValueContainer() throws -> SingleValueDecodingContainer { - return UnkeyedContainer(decoder: self) - } +private class _JWTDecoder: Decoder { + init(header: Data, claims: Data) { + self.header = header + self.claims = claims + } + + var header: Data + + var claims: Data + + var keyID: String? + + var codingPath: [CodingKey] = [] + + var userInfo: [CodingUserInfoKey: Any] = [:] + + // Call the Codable Types init from decoder function. + public func decode(_ type: T.Type) throws -> T { + try type.init(from: self) + } + + // JWT should only be a Keyed container + func container(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { + let container = _JWTKeyedDecodingContainer(decoder: self, header: header, claims: claims) + return KeyedDecodingContainer(container) + } + + // This function should not be called when decoding a JWT + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + UnkeyedContainer(decoder: self) + } + + // This function should not be called when decoding a JWT + func singleValueContainer() throws -> SingleValueDecodingContainer { + UnkeyedContainer(decoder: self) + } } private struct _JWTKeyedDecodingContainer: KeyedDecodingContainerProtocol { - - // A reference to the Decoder the container is inside - let decoder: _JWTDecoder - - var header: Data - - var claims: Data - - var codingPath: [CodingKey] - - public var allKeys: [Key] - { - #if swift(>=4.1) - return ["header", "claims"].compactMap { Key(stringValue: $0) } - #else - return ["header", "claims"].flatMap { Key(stringValue: $0) } - #endif - } - - fileprivate init(decoder: _JWTDecoder, header: Data, claims: Data) { - self.decoder = decoder - self.header = header - self.claims = claims - self.codingPath = decoder.codingPath - } - - public func contains(_ key: Key) -> Bool { - return key.stringValue == "header" || key.stringValue == "claims" - } - - // The JWT Class should only have to decode Decodable types - // Those types will be a `Header` object and a generic `Claims` object. - func decode(_ type: T.Type, forKey key: Key) throws -> T { - decoder.codingPath.append(key) - let jsonDecoder = JSONDecoder() - jsonDecoder.dateDecodingStrategy = .secondsSince1970 - if key.stringValue == "header" { - let header = try jsonDecoder.decode(Header.self, from: self.header) - decoder.keyID = header.kid - guard let decodedHeader = header as? T else { - throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Type of header key was not a JWT Header")) - } - return decodedHeader - } else - if key.stringValue == "claims" { - return try jsonDecoder.decode(type, from: claims) - } else { - throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "value not found for provided key")) - } - } - -// No functions beyond this point should be called when decoding JWT, However the functions are required by KeyedDecodingContainerProtocol. - func decodeNil(forKey key: Key) throws -> Bool { - throw DecodingError.typeMismatch(Key.self, DecodingError.Context(codingPath: codingPath, debugDescription: "JWTDecoder can only Decode JWT tokens")) - } - - func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { - return try decoder.container(keyedBy: type) - } - - func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { - return try decoder.unkeyedContainer() - } - - func superDecoder() throws -> Decoder { - return decoder - } - - func superDecoder(forKey key: Key) throws -> Decoder { - return decoder - } + // A reference to the Decoder the container is inside + let decoder: _JWTDecoder + + var header: Data + + var claims: Data + + var codingPath: [CodingKey] + + public var allKeys: [Key] { + #if swift(>=4.1) + return ["header", "claims"].compactMap { Key(stringValue: $0) } + #else + return ["header", "claims"].flatMap { Key(stringValue: $0) } + #endif + } + + fileprivate init(decoder: _JWTDecoder, header: Data, claims: Data) { + self.decoder = decoder + self.header = header + self.claims = claims + codingPath = decoder.codingPath + } + + public func contains(_ key: Key) -> Bool { + key.stringValue == "header" || key.stringValue == "claims" + } + + // The JWT Class should only have to decode Decodable types + // Those types will be a `Header` object and a generic `Claims` object. + func decode(_ type: T.Type, forKey key: Key) throws -> T { + decoder.codingPath.append(key) + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .secondsSince1970 + if key.stringValue == "header" { + let header = try jsonDecoder.decode(Header.self, from: header) + decoder.keyID = header.kid + guard let decodedHeader = header as? T else { + throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Type of header key was not a JWT Header")) + } + return decodedHeader + } else + if key.stringValue == "claims" { + return try jsonDecoder.decode(type, from: claims) + } else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "value not found for provided key")) + } + } + + // No functions beyond this point should be called when decoding JWT, However the functions are required by KeyedDecodingContainerProtocol. + func decodeNil(forKey _: Key) throws -> Bool { + throw DecodingError.typeMismatch(Key.self, DecodingError.Context(codingPath: codingPath, debugDescription: "JWTDecoder can only Decode JWT tokens")) + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey _: Key) throws -> KeyedDecodingContainer where NestedKey: CodingKey { + try decoder.container(keyedBy: type) + } + + func nestedUnkeyedContainer(forKey _: Key) throws -> UnkeyedDecodingContainer { + try decoder.unkeyedContainer() + } + + func superDecoder() throws -> Decoder { + decoder + } + + func superDecoder(forKey _: Key) throws -> Decoder { + decoder + } } // When decoding a JWT you should not have an UnkeyedContainer private struct UnkeyedContainer: UnkeyedDecodingContainer, SingleValueDecodingContainer { - var decoder: _JWTDecoder - - var codingPath: [CodingKey] { return [] } - - var count: Int? { return nil } - - var currentIndex: Int { return 0 } - - var isAtEnd: Bool { return false } - - func decode(_ type: T.Type) throws -> T { - throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "JWTDecoder can only Decode JWT tokens")) - } - - func decodeNil() -> Bool { - return true - } - - func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { - return try decoder.container(keyedBy: type) - } - - func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { - return self - } - - func superDecoder() throws -> Decoder { - return decoder - } + var decoder: _JWTDecoder + + var codingPath: [CodingKey] { [] } + + var count: Int? { nil } + + var currentIndex: Int { 0 } + + var isAtEnd: Bool { false } + + func decode(_ type: T.Type) throws -> T { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "JWTDecoder can only Decode JWT tokens")) + } + + func decodeNil() -> Bool { + true + } + + func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey: CodingKey { + try decoder.container(keyedBy: type) + } + + func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + self + } + + func superDecoder() throws -> Decoder { + decoder + } } diff --git a/Sources/SwiftJWT/JWTEncoder.swift b/Sources/SwiftJWT/JWTEncoder.swift index 2ae6813..df173fe 100644 --- a/Sources/SwiftJWT/JWTEncoder.swift +++ b/Sources/SwiftJWT/JWTEncoder.swift @@ -21,7 +21,7 @@ import KituraContracts /** A thread safe encoder that signs the JWT header and claims using the provided algorithm and encodes a `JWT` instance as either Data or a JWT String. - + ### Usage Example: ### ```swift struct MyClaims: Claims { @@ -38,63 +38,62 @@ import KituraContracts ``` */ public class JWTEncoder: BodyEncoder { - - let keyIDToSigner: (String) -> JWTSigner? - let jwtSigner: JWTSigner? - - // MARK: Initializers - - /// Initialize a `JWTEncoder` instance with a single `JWTSigner`. - /// - /// - Parameter jwtSigner: The `JWTSigner` that will be used to sign the JWT. - /// - Returns: A new instance of `JWTEncoder`. - public init(jwtSigner: JWTSigner) { - self.keyIDToSigner = {_ in return jwtSigner } - self.jwtSigner = jwtSigner - } - - /// Initialize a `JWTEncoder` instance with a function to generate the `JWTSigner` from the JWT `kid` header. - /// - /// - Parameter keyIDToSigner: The function to generate the `JWTSigner` from the JWT `kid` header. - /// - Returns: A new instance of `JWTEncoder`. - public init(keyIDToSigner: @escaping (String) -> JWTSigner?) { - self.keyIDToSigner = keyIDToSigner - self.jwtSigner = nil - } - - // MARK: Encode - - /// Encode a `JWT` instance into a UTF8 encoded JWT String. - /// - /// - Parameter value: The JWT instance to be encoded as Data. - /// - Returns: The UTF8 encoded JWT String. - /// - throws: `JWTError.invalidUTF8Data` if the provided Data can't be decoded to a String. - /// - throws: `JWTError.invalidKeyID` if the KeyID `kid` header fails to generate a jwtSigner. - /// - throws: `EncodingError` if the encoder fails to encode the object as Data. - public func encode(_ value: T) throws -> Data { - guard let jwt = try self.encodeToString(value).data(using: .utf8) else { - throw JWTError.invalidUTF8Data - } - return jwt - } - - /// Encode a `JWT` instance as a JWT String. - /// - /// - Parameter value: The JWT instance to be encoded as a JWT String. - /// - Returns: A JWT String. - /// - throws: `JWTError.invalidKeyID` if the KeyID `kid` header fails to generate a jwtSigner. - /// - throws: `EncodingError` if the encoder fails to encode the object as Data. - public func encodeToString(_ value: T) throws -> String { - let encoder = _JWTEncoder(jwtSigner: jwtSigner, keyIDToSigner: keyIDToSigner) - try value.encode(to: encoder) - guard let header = encoder.header, - let claims = encoder.claims, - let jwtSigner = encoder.jwtSigner - else { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Failed to sign JWT Header and Claims")) - } - return try jwtSigner.sign(header: header, claims: claims) - } + let keyIDToSigner: (String) -> JWTSigner? + let jwtSigner: JWTSigner? + + // MARK: Initializers + + /// Initialize a `JWTEncoder` instance with a single `JWTSigner`. + /// + /// - Parameter jwtSigner: The `JWTSigner` that will be used to sign the JWT. + /// - Returns: A new instance of `JWTEncoder`. + public init(jwtSigner: JWTSigner) { + keyIDToSigner = { _ in jwtSigner } + self.jwtSigner = jwtSigner + } + + /// Initialize a `JWTEncoder` instance with a function to generate the `JWTSigner` from the JWT `kid` header. + /// + /// - Parameter keyIDToSigner: The function to generate the `JWTSigner` from the JWT `kid` header. + /// - Returns: A new instance of `JWTEncoder`. + public init(keyIDToSigner: @escaping (String) -> JWTSigner?) { + self.keyIDToSigner = keyIDToSigner + jwtSigner = nil + } + + // MARK: Encode + + /// Encode a `JWT` instance into a UTF8 encoded JWT String. + /// + /// - Parameter value: The JWT instance to be encoded as Data. + /// - Returns: The UTF8 encoded JWT String. + /// - throws: `JWTError.invalidUTF8Data` if the provided Data can't be decoded to a String. + /// - throws: `JWTError.invalidKeyID` if the KeyID `kid` header fails to generate a jwtSigner. + /// - throws: `EncodingError` if the encoder fails to encode the object as Data. + public func encode(_ value: some Encodable) throws -> Data { + guard let jwt = try encodeToString(value).data(using: .utf8) else { + throw JWTError.invalidUTF8Data + } + return jwt + } + + /// Encode a `JWT` instance as a JWT String. + /// + /// - Parameter value: The JWT instance to be encoded as a JWT String. + /// - Returns: A JWT String. + /// - throws: `JWTError.invalidKeyID` if the KeyID `kid` header fails to generate a jwtSigner. + /// - throws: `EncodingError` if the encoder fails to encode the object as Data. + public func encodeToString(_ value: some Encodable) throws -> String { + let encoder = _JWTEncoder(jwtSigner: jwtSigner, keyIDToSigner: keyIDToSigner) + try value.encode(to: encoder) + guard let header = encoder.header, + let claims = encoder.claims, + let jwtSigner = encoder.jwtSigner + else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Failed to sign JWT Header and Claims")) + } + return try jwtSigner.sign(header: header, claims: claims) + } } /* @@ -115,120 +114,117 @@ public class JWTEncoder: BodyEncoder { ``` Where encoder is a _JWTEncoder instance, and MyClaims is the user defined object conforming to Claims. */ -fileprivate class _JWTEncoder: Encoder { - - init(jwtSigner: JWTSigner?, keyIDToSigner: @escaping (String) -> JWTSigner?) { - self.jwtSigner = jwtSigner - self.keyIDToSigner = keyIDToSigner - } - - var claims: String? - - var header: String? - - var jwtSigner: JWTSigner? - - let keyIDToSigner: (String) -> JWTSigner? - - var codingPath: [CodingKey] = [] - - var userInfo: [CodingUserInfoKey : Any] = [:] - - // We will be provided a keyed container representing the JWT instance - func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { - let container = _JWTKeyedEncodingContainer(encoder: self, codingPath: self.codingPath) - return KeyedEncodingContainer(container) - } - - private struct _JWTKeyedEncodingContainer: KeyedEncodingContainerProtocol { - - /// A reference to the encoder we're writing to. - let encoder: _JWTEncoder - - var codingPath: [CodingKey] - - // Set the Encoder header and claims Strings using the container - mutating func encode(_ value: T, forKey key: Key) throws { - self.codingPath.append(key) - let fieldName = key.stringValue - let jsonEncoder = JSONEncoder() - jsonEncoder.dateEncodingStrategy = .secondsSince1970 - if fieldName == "header" { - guard var _header = value as? Header else { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Failed to encode into header CodingKey")) - } - // Set the jwtSigner while you have acces to the keyID - if encoder.jwtSigner == nil { - guard let keyID = _header.kid, let keyIDJWTSigner = encoder.keyIDToSigner(keyID) else { - throw JWTError.invalidKeyID - } - encoder.jwtSigner = keyIDJWTSigner - } - _header.alg = encoder.jwtSigner?.name - let data = try jsonEncoder.encode(_header) - encoder.header = JWTEncoder.base64urlEncodedString(data: data) - } else if fieldName == "claims" { - let data = try jsonEncoder.encode(value) - encoder.claims = JWTEncoder.base64urlEncodedString(data: data) - } - } - - // No functions beyond this point should be called for encoding a JWT token - mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { - return encoder.container(keyedBy: keyType) - } - - mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { - return encoder.unkeyedContainer() - } - - mutating func superEncoder() -> Encoder { - return encoder - } - - mutating func superEncoder(forKey key: Key) -> Encoder { - return encoder - } - - // Throw if trying to encode something other than a JWT token - mutating func encodeNil(forKey key: Key) throws { - throw EncodingError.invalidValue(key, EncodingError.Context(codingPath: codingPath, debugDescription: "JWTEncoder can only encode JWT tokens")) - } - - } - - func unkeyedContainer() -> UnkeyedEncodingContainer { - return UnkeyedContainer(encoder: self) - } - - func singleValueContainer() -> SingleValueEncodingContainer { - return UnkeyedContainer(encoder: self) - } - - // This Decoder should not be used to decode UnkeyedContainer - private struct UnkeyedContainer: UnkeyedEncodingContainer, SingleValueEncodingContainer { - var encoder: _JWTEncoder - - var codingPath: [CodingKey] { return [] } - - var count: Int { return 0 } - - func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { - return encoder.container(keyedBy: keyType) - } - - func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - return self - } - - func superEncoder() -> Encoder { - return encoder - } - - func encodeNil() throws {} - - func encode(_ value: T) throws where T : Encodable { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "JWTEncoder can only encode JWT tokens")) - } - } +private class _JWTEncoder: Encoder { + init(jwtSigner: JWTSigner?, keyIDToSigner: @escaping (String) -> JWTSigner?) { + self.jwtSigner = jwtSigner + self.keyIDToSigner = keyIDToSigner + } + + var claims: String? + + var header: String? + + var jwtSigner: JWTSigner? + + let keyIDToSigner: (String) -> JWTSigner? + + var codingPath: [CodingKey] = [] + + var userInfo: [CodingUserInfoKey: Any] = [:] + + // We will be provided a keyed container representing the JWT instance + func container(keyedBy _: Key.Type) -> KeyedEncodingContainer { + let container = _JWTKeyedEncodingContainer(encoder: self, codingPath: codingPath) + return KeyedEncodingContainer(container) + } + + private struct _JWTKeyedEncodingContainer: KeyedEncodingContainerProtocol { + /// A reference to the encoder we're writing to. + let encoder: _JWTEncoder + + var codingPath: [CodingKey] + + // Set the Encoder header and claims Strings using the container + mutating func encode(_ value: some Encodable, forKey key: Key) throws { + codingPath.append(key) + let fieldName = key.stringValue + let jsonEncoder = JSONEncoder() + jsonEncoder.dateEncodingStrategy = .secondsSince1970 + if fieldName == "header" { + guard var _header = value as? Header else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Failed to encode into header CodingKey")) + } + // Set the jwtSigner while you have acces to the keyID + if encoder.jwtSigner == nil { + guard let keyID = _header.kid, let keyIDJWTSigner = encoder.keyIDToSigner(keyID) else { + throw JWTError.invalidKeyID + } + encoder.jwtSigner = keyIDJWTSigner + } + _header.alg = encoder.jwtSigner?.name + let data = try jsonEncoder.encode(_header) + encoder.header = JWTEncoder.base64urlEncodedString(data: data) + } else if fieldName == "claims" { + let data = try jsonEncoder.encode(value) + encoder.claims = JWTEncoder.base64urlEncodedString(data: data) + } + } + + // No functions beyond this point should be called for encoding a JWT token + mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey _: Key) -> KeyedEncodingContainer where NestedKey: CodingKey { + encoder.container(keyedBy: keyType) + } + + mutating func nestedUnkeyedContainer(forKey _: Key) -> UnkeyedEncodingContainer { + encoder.unkeyedContainer() + } + + mutating func superEncoder() -> Encoder { + encoder + } + + mutating func superEncoder(forKey _: Key) -> Encoder { + encoder + } + + // Throw if trying to encode something other than a JWT token + mutating func encodeNil(forKey key: Key) throws { + throw EncodingError.invalidValue(key, EncodingError.Context(codingPath: codingPath, debugDescription: "JWTEncoder can only encode JWT tokens")) + } + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + UnkeyedContainer(encoder: self) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + UnkeyedContainer(encoder: self) + } + + // This Decoder should not be used to decode UnkeyedContainer + private struct UnkeyedContainer: UnkeyedEncodingContainer, SingleValueEncodingContainer { + var encoder: _JWTEncoder + + var codingPath: [CodingKey] { [] } + + var count: Int { 0 } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey: CodingKey { + encoder.container(keyedBy: keyType) + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + self + } + + func superEncoder() -> Encoder { + encoder + } + + func encodeNil() throws {} + + func encode(_ value: some Encodable) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "JWTEncoder can only encode JWT tokens")) + } + } } diff --git a/Sources/SwiftJWT/JWTError.swift b/Sources/SwiftJWT/JWTError.swift index 5ad1877..d3101c6 100644 --- a/Sources/SwiftJWT/JWTError.swift +++ b/Sources/SwiftJWT/JWTError.swift @@ -20,47 +20,46 @@ import Foundation /// A struct representing the different errors that can be thrown by SwiftJWT public struct JWTError: LocalizedError, Equatable { + /// A human readable description of the error. + public let errorDescription: String? - /// A human readable description of the error. - public let errorDescription: String? - - private let internalError: InternalError - - private enum InternalError { - case invalidJWTString, failedVerification, osVersionToLow, invalidPrivateKey, invalidData, invalidKeyID, missingPEMHeaders - } - - /// Error when an invalid JWT String is provided - public static let invalidJWTString = JWTError(errorDescription: "Input was not a valid JWT String", internalError: .invalidJWTString) - - /// Error when the JWT signiture fails verification. - public static let failedVerification = JWTError(errorDescription: "JWT verifier failed to verify the JWT String signiture", internalError: .failedVerification) - - /// Error when using RSA encryption with an OS version that is too low. - public static let osVersionToLow = JWTError(errorDescription: "macOS 10.12.0 (Sierra) or higher or iOS 10.0 or higher is required by CryptorRSA", internalError: .osVersionToLow) - - /// Error when an invalid private key is provided for RSA encryption. - public static let invalidPrivateKey = JWTError(errorDescription: "Provided private key could not be used to sign JWT", internalError: .invalidPrivateKey) - - /// Error when the provided Data cannot be decoded to a String - public static let invalidUTF8Data = JWTError(errorDescription: "Could not decode Data from UTF8 to String", internalError: .invalidData) - - /// Error when the KeyID field `kid` in the JWT header fails to generate a JWTSigner or JWTVerifier - public static let invalidKeyID = JWTError(errorDescription: "The JWT KeyID `kid` header failed to generate a JWTSigner/JWTVerifier", internalError: .invalidKeyID) - - /// Error when a PEM string is provided without the expected PEM headers/footers. (e.g. -----BEGIN PRIVATE KEY-----) - public static let missingPEMHeaders = JWTError(errorDescription: "The provided key did not have the expected PEM headers/footers", internalError: .missingPEMHeaders) - - /// Function to check if JWTErrors are equal. Required for equatable protocol. - public static func == (lhs: JWTError, rhs: JWTError) -> Bool { - return lhs.internalError == rhs.internalError - } + private let internalError: InternalError - /// Function to enable pattern matching against generic Errors. - public static func ~= (lhs: JWTError, rhs: Error) -> Bool { - guard let rhs = rhs as? JWTError else { - return false - } - return lhs == rhs - } + private enum InternalError { + case invalidJWTString, failedVerification, osVersionToLow, invalidPrivateKey, invalidData, invalidKeyID, missingPEMHeaders + } + + /// Error when an invalid JWT String is provided + public static let invalidJWTString = JWTError(errorDescription: "Input was not a valid JWT String", internalError: .invalidJWTString) + + /// Error when the JWT signiture fails verification. + public static let failedVerification = JWTError(errorDescription: "JWT verifier failed to verify the JWT String signiture", internalError: .failedVerification) + + /// Error when using RSA encryption with an OS version that is too low. + public static let osVersionToLow = JWTError(errorDescription: "macOS 10.12.0 (Sierra) or higher or iOS 10.0 or higher is required by CryptorRSA", internalError: .osVersionToLow) + + /// Error when an invalid private key is provided for RSA encryption. + public static let invalidPrivateKey = JWTError(errorDescription: "Provided private key could not be used to sign JWT", internalError: .invalidPrivateKey) + + /// Error when the provided Data cannot be decoded to a String + public static let invalidUTF8Data = JWTError(errorDescription: "Could not decode Data from UTF8 to String", internalError: .invalidData) + + /// Error when the KeyID field `kid` in the JWT header fails to generate a JWTSigner or JWTVerifier + public static let invalidKeyID = JWTError(errorDescription: "The JWT KeyID `kid` header failed to generate a JWTSigner/JWTVerifier", internalError: .invalidKeyID) + + /// Error when a PEM string is provided without the expected PEM headers/footers. (e.g. -----BEGIN PRIVATE KEY-----) + public static let missingPEMHeaders = JWTError(errorDescription: "The provided key did not have the expected PEM headers/footers", internalError: .missingPEMHeaders) + + /// Function to check if JWTErrors are equal. Required for equatable protocol. + public static func == (lhs: JWTError, rhs: JWTError) -> Bool { + lhs.internalError == rhs.internalError + } + + /// Function to enable pattern matching against generic Errors. + public static func ~= (lhs: JWTError, rhs: Error) -> Bool { + guard let rhs = rhs as? JWTError else { + return false + } + return lhs == rhs + } } diff --git a/Sources/SwiftJWT/JWTSigner.swift b/Sources/SwiftJWT/JWTSigner.swift index b510995..b7a2947 100644 --- a/Sources/SwiftJWT/JWTSigner.swift +++ b/Sources/SwiftJWT/JWTSigner.swift @@ -50,98 +50,96 @@ import Foundation ``` */ public struct JWTSigner { - - /// The name of the algorithm that will be set in the "alg" header - let name: String - - let signerAlgorithm: SignerAlgorithm - - init(name: String, signerAlgorithm: SignerAlgorithm) { - self.name = name - self.signerAlgorithm = signerAlgorithm - } - - func sign(header: String, claims: String) throws -> String { - return try signerAlgorithm.sign(header: header, claims: claims) - } - - /// Initialize a JWTSigner using the RSA 256 bits algorithm and the provided privateKey. - /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func rs256(privateKey: Data) -> JWTSigner { - preconditionFailure("not implemented") - } - - /// Initialize a JWTSigner using the RSA 384 bits algorithm and the provided privateKey. - /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func rs384(privateKey: Data) -> JWTSigner { - preconditionFailure("not implemented") - } - - /// Initialize a JWTSigner using the RSA 512 bits algorithm and the provided privateKey. - /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func rs512(privateKey: Data) -> JWTSigner { - preconditionFailure("not implemented") - } - - /// Initialize a JWTSigner using the RSA-PSS 256 bits algorithm and the provided privateKey. - /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func ps256(privateKey: Data) -> JWTSigner { - preconditionFailure("not implemented") - } - - /// Initialize a JWTSigner using the RSA-PSS 384 bits algorithm and the provided privateKey. - /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func ps384(privateKey: Data) -> JWTSigner { - preconditionFailure("not implemented") - } - - /// Initialize a JWTSigner using the RSA-PSS 512 bits algorithm and the provided privateKey. - /// This signer requires at least a 2048 bit RSA key. - /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func ps512(privateKey: Data) -> JWTSigner { - preconditionFailure("not implemented") - } - - /// Initialize a JWTSigner using the HMAC 256 bits algorithm and the provided privateKey. - /// - Parameter key: The HMAC symmetric password data. - public static func hs256(key: Data) -> JWTSigner { - JWTSigner(name: "HS256", signerAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs256)) - } - - /// Initialize a JWTSigner using the HMAC 384 bits algorithm and the provided privateKey. - /// - Parameter key: The HMAC symmetric password data. - public static func hs384(key: Data) -> JWTSigner { - JWTSigner(name: "HS384", signerAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs384)) - } - - /// Initialize a JWTSigner using the HMAC 512 bits algorithm and the provided privateKey. - /// - Parameter key: The HMAC symmetric password data. - public static func hs512(key: Data) -> JWTSigner { - JWTSigner(name: "HS512", signerAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs512)) - } - - /// Initialize a JWTSigner using the ECDSA SHA256 algorithm and the provided privateKey. - /// - Parameter privateKey: The UTF8 encoded PEM private key, with either a "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" header. - @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) - public static func es256(privateKey: Data) -> JWTSigner { - JWTSigner(name: "ES256", signerAlgorithm: SwiftCryptoECDSA(key: privateKey, algorithm: .es256)) - } - - /// Initialize a JWTSigner using the ECDSA SHA384 algorithm and the provided privateKey. - /// - Parameter privateKey: The UTF8 encoded PEM private key, with either a "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" header. - @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) - public static func es384(privateKey: Data) -> JWTSigner { - JWTSigner(name: "ES384", signerAlgorithm: SwiftCryptoECDSA(key: privateKey, algorithm: .es384)) - } - - /// Initialize a JWTSigner using the ECDSA SHA512 algorithm and the provided privateKey. - /// - Parameter privateKey: The UTF8 encoded PEM private key, with either a "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" header. - @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) - public static func es512(privateKey: Data) -> JWTSigner { - JWTSigner(name: "ES512", signerAlgorithm: SwiftCryptoECDSA(key: privateKey, algorithm: .es512)) - } - - /// Initialize a JWTSigner that will not sign the JWT. This is equivelent to using the "none" alg header. - public static let none = JWTSigner(name: "none", signerAlgorithm: NoneAlgorithm()) -} + /// The name of the algorithm that will be set in the "alg" header + let name: String + + let signerAlgorithm: SignerAlgorithm + + init(name: String, signerAlgorithm: SignerAlgorithm) { + self.name = name + self.signerAlgorithm = signerAlgorithm + } + + func sign(header: String, claims: String) throws -> String { + try signerAlgorithm.sign(header: header, claims: claims) + } + + /// Initialize a JWTSigner using the RSA 256 bits algorithm and the provided privateKey. + /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. + public static func rs256(privateKey _: Data) -> JWTSigner { + preconditionFailure("not implemented") + } + + /// Initialize a JWTSigner using the RSA 384 bits algorithm and the provided privateKey. + /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. + public static func rs384(privateKey _: Data) -> JWTSigner { + preconditionFailure("not implemented") + } + + /// Initialize a JWTSigner using the RSA 512 bits algorithm and the provided privateKey. + /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. + public static func rs512(privateKey _: Data) -> JWTSigner { + preconditionFailure("not implemented") + } + + /// Initialize a JWTSigner using the RSA-PSS 256 bits algorithm and the provided privateKey. + /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. + public static func ps256(privateKey _: Data) -> JWTSigner { + preconditionFailure("not implemented") + } + + /// Initialize a JWTSigner using the RSA-PSS 384 bits algorithm and the provided privateKey. + /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. + public static func ps384(privateKey _: Data) -> JWTSigner { + preconditionFailure("not implemented") + } + /// Initialize a JWTSigner using the RSA-PSS 512 bits algorithm and the provided privateKey. + /// This signer requires at least a 2048 bit RSA key. + /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. + public static func ps512(privateKey _: Data) -> JWTSigner { + preconditionFailure("not implemented") + } + + /// Initialize a JWTSigner using the HMAC 256 bits algorithm and the provided privateKey. + /// - Parameter key: The HMAC symmetric password data. + public static func hs256(key: Data) -> JWTSigner { + JWTSigner(name: "HS256", signerAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs256)) + } + + /// Initialize a JWTSigner using the HMAC 384 bits algorithm and the provided privateKey. + /// - Parameter key: The HMAC symmetric password data. + public static func hs384(key: Data) -> JWTSigner { + JWTSigner(name: "HS384", signerAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs384)) + } + + /// Initialize a JWTSigner using the HMAC 512 bits algorithm and the provided privateKey. + /// - Parameter key: The HMAC symmetric password data. + public static func hs512(key: Data) -> JWTSigner { + JWTSigner(name: "HS512", signerAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs512)) + } + + /// Initialize a JWTSigner using the ECDSA SHA256 algorithm and the provided privateKey. + /// - Parameter privateKey: The UTF8 encoded PEM private key, with either a "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" header. + @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) + public static func es256(privateKey: Data) -> JWTSigner { + JWTSigner(name: "ES256", signerAlgorithm: SwiftCryptoECDSA(key: privateKey, algorithm: .es256)) + } + + /// Initialize a JWTSigner using the ECDSA SHA384 algorithm and the provided privateKey. + /// - Parameter privateKey: The UTF8 encoded PEM private key, with either a "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" header. + @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) + public static func es384(privateKey: Data) -> JWTSigner { + JWTSigner(name: "ES384", signerAlgorithm: SwiftCryptoECDSA(key: privateKey, algorithm: .es384)) + } + + /// Initialize a JWTSigner using the ECDSA SHA512 algorithm and the provided privateKey. + /// - Parameter privateKey: The UTF8 encoded PEM private key, with either a "BEGIN EC PRIVATE KEY" or "BEGIN PRIVATE KEY" header. + @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) + public static func es512(privateKey: Data) -> JWTSigner { + JWTSigner(name: "ES512", signerAlgorithm: SwiftCryptoECDSA(key: privateKey, algorithm: .es512)) + } + + /// Initialize a JWTSigner that will not sign the JWT. This is equivelent to using the "none" alg header. + public static let none = JWTSigner(name: "none", signerAlgorithm: NoneAlgorithm()) +} diff --git a/Sources/SwiftJWT/JWTVerifier.swift b/Sources/SwiftJWT/JWTVerifier.swift index e0951ea..904ce78 100644 --- a/Sources/SwiftJWT/JWTVerifier.swift +++ b/Sources/SwiftJWT/JWTVerifier.swift @@ -19,7 +19,7 @@ import Foundation // MARK: JWTVerifier /** - + A struct that will be used to verify the signature of a JWT is valid for the provided `Header` and `Claims`. For RSA and ECDSA, the provided key should be a .utf8 encoded PEM String. ### Usage Example: ### @@ -42,111 +42,111 @@ import Foundation let verified: Bool = jwt.verify(signedJWT, using: jwtVerifier) ``` */ -public struct JWTVerifier { - private let verifierAlgorithm: VerifierAlgorithm - - internal init(verifierAlgorithm: VerifierAlgorithm) { - self.verifierAlgorithm = verifierAlgorithm - } - - internal func verify(jwt: String) -> Bool { - return verifierAlgorithm.verify(jwt: jwt) - } - - /// Initialize a JWTVerifier using the RSA 256 bits algorithm and the provided publicKey. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func rs256(publicKey: Data) -> JWTVerifier { - preconditionFailure("not implemented") - } - - /// Initialize a JWTVerifier using the RSA 384 bits algorithm and the provided publicKey. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func rs384(publicKey: Data) -> JWTVerifier { - preconditionFailure("not implemented") - } - - /// Initialize a JWTVerifier using the RSA 512 bits algorithm and the provided publicKey. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func rs512(publicKey: Data) -> JWTVerifier { - preconditionFailure("not implemented") - } - - /// Initialize a JWTVerifier using the RSA 256 bits algorithm and the provided certificate. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN CERTIFICATE" header. - public static func rs256(certificate: Data) -> JWTVerifier { - preconditionFailure("not implemented") - } - - /// Initialize a JWTVerifier using the RSA 384 bits algorithm and the provided certificate. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN CERTIFICATE" header. - public static func rs384(certificate: Data) -> JWTVerifier { - preconditionFailure("not implemented") - } - - /// Initialize a JWTVerifier using the RSA 512 bits algorithm and the provided certificate. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN CERTIFICATE" header. - public static func rs512(certificate: Data) -> JWTVerifier { - preconditionFailure("not implemented") - } - - /// Initialize a JWTVerifier using the RSA-PSS 256 bits algorithm and the provided publicKey. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func ps256(publicKey: Data) -> JWTVerifier { - preconditionFailure("not implemented") - } - - /// Initialize a JWTVerifier using the RSA-PSS 384 bits algorithm and the provided publicKey. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func ps384(publicKey: Data) -> JWTVerifier { - preconditionFailure("not implemented") - } - - /// Initialize a JWTVerifier using the RSA-PSS 512 bits algorithm and the provided publicKey. - /// This verifier requires at least a 2048 bit RSA key. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func ps512(publicKey: Data) -> JWTVerifier { - preconditionFailure("not implemented") - } - - /// Initialize a JWTSigner using the HMAC 256 bits algorithm and the provided privateKey. - /// - Parameter key: The HMAC symmetric password data. - public static func hs256(key: Data) -> JWTVerifier { - JWTVerifier(verifierAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs256)) - } - - /// Initialize a JWTSigner using the HMAC 384 bits algorithm and the provided privateKey. - /// - Parameter key: The HMAC symmetric password data. - public static func hs384(key: Data) -> JWTVerifier { - JWTVerifier(verifierAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs384)) - } - - /// Initialize a JWTSigner using the HMAC 512 bits algorithm and the provided privateKey. - /// - Parameter key: The HMAC symmetric password data. - public static func hs512(key: Data) -> JWTVerifier { - JWTVerifier(verifierAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs512)) - } - - /// Initialize a JWTVerifier using the ECDSA SHA 256 algorithm and the provided public key. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) - public static func es256(publicKey: Data) -> JWTVerifier { - JWTVerifier(verifierAlgorithm: SwiftCryptoECDSA(key: publicKey, algorithm: .es256)) - } - - /// Initialize a JWTVerifier using the ECDSA SHA 384 algorithm and the provided public key. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) - public static func es384(publicKey: Data) -> JWTVerifier { - JWTVerifier(verifierAlgorithm: SwiftCryptoECDSA(key: publicKey, algorithm: .es384)) - } - - /// Initialize a JWTVerifier using the ECDSA SHA 512 algorithm and the provided public key. - /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) - public static func es512(publicKey: Data) -> JWTVerifier { - JWTVerifier(verifierAlgorithm: SwiftCryptoECDSA(key: publicKey, algorithm: .es512)) - } - - /// Initialize a JWTVerifier that will always return true when verifying the JWT. This is equivelent to using the "none" alg header. - public static let none = JWTVerifier(verifierAlgorithm: NoneAlgorithm()) +public struct JWTVerifier { + private let verifierAlgorithm: VerifierAlgorithm + + internal init(verifierAlgorithm: VerifierAlgorithm) { + self.verifierAlgorithm = verifierAlgorithm + } + + internal func verify(jwt: String) -> Bool { + verifierAlgorithm.verify(jwt: jwt) + } + + /// Initialize a JWTVerifier using the RSA 256 bits algorithm and the provided publicKey. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. + public static func rs256(publicKey _: Data) -> JWTVerifier { + preconditionFailure("not implemented") + } + + /// Initialize a JWTVerifier using the RSA 384 bits algorithm and the provided publicKey. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. + public static func rs384(publicKey _: Data) -> JWTVerifier { + preconditionFailure("not implemented") + } + + /// Initialize a JWTVerifier using the RSA 512 bits algorithm and the provided publicKey. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. + public static func rs512(publicKey _: Data) -> JWTVerifier { + preconditionFailure("not implemented") + } + + /// Initialize a JWTVerifier using the RSA 256 bits algorithm and the provided certificate. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN CERTIFICATE" header. + public static func rs256(certificate _: Data) -> JWTVerifier { + preconditionFailure("not implemented") + } + + /// Initialize a JWTVerifier using the RSA 384 bits algorithm and the provided certificate. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN CERTIFICATE" header. + public static func rs384(certificate _: Data) -> JWTVerifier { + preconditionFailure("not implemented") + } + + /// Initialize a JWTVerifier using the RSA 512 bits algorithm and the provided certificate. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN CERTIFICATE" header. + public static func rs512(certificate _: Data) -> JWTVerifier { + preconditionFailure("not implemented") + } + + /// Initialize a JWTVerifier using the RSA-PSS 256 bits algorithm and the provided publicKey. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. + public static func ps256(publicKey _: Data) -> JWTVerifier { + preconditionFailure("not implemented") + } + + /// Initialize a JWTVerifier using the RSA-PSS 384 bits algorithm and the provided publicKey. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. + public static func ps384(publicKey _: Data) -> JWTVerifier { + preconditionFailure("not implemented") + } + + /// Initialize a JWTVerifier using the RSA-PSS 512 bits algorithm and the provided publicKey. + /// This verifier requires at least a 2048 bit RSA key. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. + public static func ps512(publicKey _: Data) -> JWTVerifier { + preconditionFailure("not implemented") + } + + /// Initialize a JWTSigner using the HMAC 256 bits algorithm and the provided privateKey. + /// - Parameter key: The HMAC symmetric password data. + public static func hs256(key: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs256)) + } + + /// Initialize a JWTSigner using the HMAC 384 bits algorithm and the provided privateKey. + /// - Parameter key: The HMAC symmetric password data. + public static func hs384(key: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs384)) + } + + /// Initialize a JWTSigner using the HMAC 512 bits algorithm and the provided privateKey. + /// - Parameter key: The HMAC symmetric password data. + public static func hs512(key: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoHMAC(key: key, algorithm: .hs512)) + } + + /// Initialize a JWTVerifier using the ECDSA SHA 256 algorithm and the provided public key. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. + @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) + public static func es256(publicKey: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoECDSA(key: publicKey, algorithm: .es256)) + } + + /// Initialize a JWTVerifier using the ECDSA SHA 384 algorithm and the provided public key. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. + @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) + public static func es384(publicKey: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoECDSA(key: publicKey, algorithm: .es384)) + } + + /// Initialize a JWTVerifier using the ECDSA SHA 512 algorithm and the provided public key. + /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. + @available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) + public static func es512(publicKey: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoECDSA(key: publicKey, algorithm: .es512)) + } + + /// Initialize a JWTVerifier that will always return true when verifying the JWT. This is equivelent to using the "none" alg header. + public static let none = JWTVerifier(verifierAlgorithm: NoneAlgorithm()) } diff --git a/Sources/SwiftJWT/NoneAlgorithm.swift b/Sources/SwiftJWT/NoneAlgorithm.swift index c809280..82e4113 100644 --- a/Sources/SwiftJWT/NoneAlgorithm.swift +++ b/Sources/SwiftJWT/NoneAlgorithm.swift @@ -19,14 +19,13 @@ import Foundation /// An EncryptionAlgorithm representing an alg of "none" in a JWT. /// Using this algorithm means the header and claims will not be signed or verified. struct NoneAlgorithm: VerifierAlgorithm, SignerAlgorithm { - - let name: String = "none" - - func sign(header: String, claims: String) -> String { - return header + "." + claims - } - - func verify(jwt: String) -> Bool { - return true - } + let name: String = "none" + + func sign(header: String, claims: String) -> String { + header + "." + claims + } + + func verify(jwt _: String) -> Bool { + true + } } diff --git a/Sources/SwiftJWT/SignerAlgorithm.swift b/Sources/SwiftJWT/SignerAlgorithm.swift index 67cfa4a..42fbbaf 100644 --- a/Sources/SwiftJWT/SignerAlgorithm.swift +++ b/Sources/SwiftJWT/SignerAlgorithm.swift @@ -15,6 +15,6 @@ **/ protocol SignerAlgorithm { - /// A function to sign the header and claims of a JSON web token and return a signed JWT string. - func sign(header: String, claims: String) throws -> String + /// A function to sign the header and claims of a JSON web token and return a signed JWT string. + func sign(header: String, claims: String) throws -> String } diff --git a/Sources/SwiftJWT/SwiftCryptoECDSA.swift b/Sources/SwiftJWT/SwiftCryptoECDSA.swift index 4f4a8f7..694da34 100644 --- a/Sources/SwiftJWT/SwiftCryptoECDSA.swift +++ b/Sources/SwiftJWT/SwiftCryptoECDSA.swift @@ -3,103 +3,103 @@ import Foundation import LoggerAPI internal struct SwiftCryptoECDSA: VerifierAlgorithm, SignerAlgorithm { - private let key: Data - private let algorithm: Algorithm + private let key: Data + private let algorithm: Algorithm - internal enum Algorithm { - case es256, es384, es512 - } + internal enum Algorithm { + case es256, es384, es512 + } - internal init(key: Data, algorithm: Algorithm) { - self.key = key - self.algorithm = algorithm - } + internal init(key: Data, algorithm: Algorithm) { + self.key = key + self.algorithm = algorithm + } - internal func verify(jwt: String) -> Bool { - verify(jwt) - } + internal func verify(jwt: String) -> Bool { + verify(jwt) + } - internal func sign(header: String, claims: String) throws -> String { - let unsignedJWT = header + "." + claims - let signature = try sign(data: Data(unsignedJWT.utf8)) - return header + "." + claims + "." + signature - } + internal func sign(header: String, claims: String) throws -> String { + let unsignedJWT = header + "." + claims + let signature = try sign(data: Data(unsignedJWT.utf8)) + return header + "." + claims + "." + signature + } } extension SwiftCryptoECDSA { - private func verify(_ jwt: String) -> Bool { - let components = jwt.components(separatedBy: ".") - if components.count == 3 { - guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), - let jwtData = (components[0] + "." + components[1]).data(using: .utf8) - else { - return false - } - return verify(signature: signature, for: jwtData) - } else { - return false - } - } + private func verify(_ jwt: String) -> Bool { + let components = jwt.components(separatedBy: ".") + if components.count == 3 { + guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), + let jwtData = (components[0] + "." + components[1]).data(using: .utf8) + else { + return false + } + return verify(signature: signature, for: jwtData) + } else { + return false + } + } - private func verify(signature: Data, for data: Data) -> Bool { - guard #available(macOS 10.12, iOS 10.3, tvOS 12.0, watchOS 3.3, *) else { - return false - } + private func verify(signature: Data, for data: Data) -> Bool { + guard #available(macOS 10.12, iOS 10.3, tvOS 12.0, watchOS 3.3, *) else { + return false + } - guard let publicKey = String(data: key, encoding: .utf8) else { - return false - } + guard let publicKey = String(data: key, encoding: .utf8) else { + return false + } - do { - return try algorithm.verify(signature: signature, digest: data, publicKey: publicKey) - } catch { - Log.error("Verification failed: \(error)") - return false - } - } + do { + return try algorithm.verify(signature: signature, digest: data, publicKey: publicKey) + } catch { + Log.error("Verification failed: \(error)") + return false + } + } - private func sign(data: Data) throws -> String { - guard let privateKey = String(data: key, encoding: .utf8) else { - throw JWTError.invalidPrivateKey - } + private func sign(data: Data) throws -> String { + guard let privateKey = String(data: key, encoding: .utf8) else { + throw JWTError.invalidPrivateKey + } - let signature = try algorithm.signature(for: data, privateKey: privateKey) - return JWTEncoder.base64urlEncodedString(data: signature) - } + let signature = try algorithm.signature(for: data, privateKey: privateKey) + return JWTEncoder.base64urlEncodedString(data: signature) + } } private extension SwiftCryptoECDSA.Algorithm { - func verify(signature: Data, digest: Data, publicKey: String) throws -> Bool { - switch self { - case .es256: - let signature = try P256.Signing.ECDSASignature(rawRepresentation: signature) - let publicKey = try P256.Signing.PublicKey(pemRepresentation: publicKey) - return publicKey.isValidSignature(signature, for: digest) - case .es384: - let signature = try P384.Signing.ECDSASignature(rawRepresentation: signature) - let publicKey = try P384.Signing.PublicKey(pemRepresentation: publicKey) - return publicKey.isValidSignature(signature, for: digest) - case .es512: - let signature = try P521.Signing.ECDSASignature(rawRepresentation: signature) - let publicKey = try P521.Signing.PublicKey(pemRepresentation: publicKey) - return publicKey.isValidSignature(signature, for: digest) - } - } + func verify(signature: Data, digest: Data, publicKey: String) throws -> Bool { + switch self { + case .es256: + let signature = try P256.Signing.ECDSASignature(rawRepresentation: signature) + let publicKey = try P256.Signing.PublicKey(pemRepresentation: publicKey) + return publicKey.isValidSignature(signature, for: digest) + case .es384: + let signature = try P384.Signing.ECDSASignature(rawRepresentation: signature) + let publicKey = try P384.Signing.PublicKey(pemRepresentation: publicKey) + return publicKey.isValidSignature(signature, for: digest) + case .es512: + let signature = try P521.Signing.ECDSASignature(rawRepresentation: signature) + let publicKey = try P521.Signing.PublicKey(pemRepresentation: publicKey) + return publicKey.isValidSignature(signature, for: digest) + } + } - func signature(for digest: Data, privateKey: String) throws -> Data { - switch self { - case .es256: - let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKey) - let signedData = try privateKey.signature(for: digest) - return signedData.rawRepresentation - case .es384: - let privateKey = try P384.Signing.PrivateKey(pemRepresentation: privateKey) - let signedData = try privateKey.signature(for: digest) - return signedData.rawRepresentation - case .es512: - let privateKey = try P521.Signing.PrivateKey(pemRepresentation: privateKey) - let signedData = try privateKey.signature(for: digest) - return signedData.rawRepresentation - } - } + func signature(for digest: Data, privateKey: String) throws -> Data { + switch self { + case .es256: + let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKey) + let signedData = try privateKey.signature(for: digest) + return signedData.rawRepresentation + case .es384: + let privateKey = try P384.Signing.PrivateKey(pemRepresentation: privateKey) + let signedData = try privateKey.signature(for: digest) + return signedData.rawRepresentation + case .es512: + let privateKey = try P521.Signing.PrivateKey(pemRepresentation: privateKey) + let signedData = try privateKey.signature(for: digest) + return signedData.rawRepresentation + } + } } diff --git a/Sources/SwiftJWT/SwiftCryptoHMAC.swift b/Sources/SwiftJWT/SwiftCryptoHMAC.swift index b19510a..145ded8 100644 --- a/Sources/SwiftJWT/SwiftCryptoHMAC.swift +++ b/Sources/SwiftJWT/SwiftCryptoHMAC.swift @@ -3,86 +3,86 @@ import Foundation import LoggerAPI internal struct SwiftCryptoHMAC: VerifierAlgorithm, SignerAlgorithm { - private let key: Data - private let algorithm: Algorithm + private let key: Data + private let algorithm: Algorithm - internal enum Algorithm { - case hs256, hs384, hs512 - } + internal enum Algorithm { + case hs256, hs384, hs512 + } - internal init(key: Data, algorithm: Algorithm) { - self.key = key - self.algorithm = algorithm - } + internal init(key: Data, algorithm: Algorithm) { + self.key = key + self.algorithm = algorithm + } - internal func verify(jwt: String) -> Bool { - verify(jwt) - } + internal func verify(jwt: String) -> Bool { + verify(jwt) + } - internal func sign(header: String, claims: String) throws -> String { - let unsignedJWT = header + "." + claims - let signature = try sign(data: Data(unsignedJWT.utf8)) - return header + "." + claims + "." + signature - } + internal func sign(header: String, claims: String) throws -> String { + let unsignedJWT = header + "." + claims + let signature = try sign(data: Data(unsignedJWT.utf8)) + return header + "." + claims + "." + signature + } } extension SwiftCryptoHMAC { - private func verify(_ jwt: String) -> Bool { - let components = jwt.components(separatedBy: ".") - if components.count == 3 { - guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), - let jwtData = (components[0] + "." + components[1]).data(using: .utf8) - else { - return false - } - return verify(signature: signature, for: jwtData) - } else { - return false - } - } + private func verify(_ jwt: String) -> Bool { + let components = jwt.components(separatedBy: ".") + if components.count == 3 { + guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), + let jwtData = (components[0] + "." + components[1]).data(using: .utf8) + else { + return false + } + return verify(signature: signature, for: jwtData) + } else { + return false + } + } - private func verify(signature: Data, for data: Data) -> Bool { - do { - return try algorithm.verify(signature: signature, digest: data, key: key) - } catch { - Log.error("Verification failed: \(error)") - return false - } - } + private func verify(signature: Data, for data: Data) -> Bool { + do { + return try algorithm.verify(signature: signature, digest: data, key: key) + } catch { + Log.error("Verification failed: \(error)") + return false + } + } - private func sign(data: Data) throws -> String { - let signedData = try algorithm.signature(for: data, key: key) - return JWTEncoder.base64urlEncodedString(data: signedData) - } + private func sign(data: Data) throws -> String { + let signedData = try algorithm.signature(for: data, key: key) + return JWTEncoder.base64urlEncodedString(data: signedData) + } } private extension SwiftCryptoHMAC.Algorithm { - func verify(signature: Data, digest: Data, key: Data) throws -> Bool { - let key = SymmetricKey(data: key) + func verify(signature: Data, digest: Data, key: Data) throws -> Bool { + let key = SymmetricKey(data: key) - switch self { - case .hs256: - return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) - case .hs384: - return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) - case .hs512: - return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) - } - } + switch self { + case .hs256: + return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) + case .hs384: + return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) + case .hs512: + return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) + } + } - func signature(for digest: Data, key: Data) throws -> Data { - let key = SymmetricKey(data: key) + func signature(for digest: Data, key: Data) throws -> Data { + let key = SymmetricKey(data: key) - switch self { - case .hs256: - let mac = HMAC.authenticationCode(for: digest, using: key) - return Data(mac) - case .hs384: - let mac = HMAC.authenticationCode(for: digest, using: key) - return Data(mac) - case .hs512: - let mac = HMAC.authenticationCode(for: digest, using: key) - return Data(mac) - } - } + switch self { + case .hs256: + let mac = HMAC.authenticationCode(for: digest, using: key) + return Data(mac) + case .hs384: + let mac = HMAC.authenticationCode(for: digest, using: key) + return Data(mac) + case .hs512: + let mac = HMAC.authenticationCode(for: digest, using: key) + return Data(mac) + } + } } diff --git a/Sources/SwiftJWT/ValidateClaimsResult.swift b/Sources/SwiftJWT/ValidateClaimsResult.swift index 91e9d54..2d09f7a 100644 --- a/Sources/SwiftJWT/ValidateClaimsResult.swift +++ b/Sources/SwiftJWT/ValidateClaimsResult.swift @@ -20,33 +20,32 @@ /// In case of successful validation, .success is returned, all other cases list various /// problems that may occur during claims validation and indicate that the validation failed. public struct ValidateClaimsResult: CustomStringConvertible, Equatable { - - /// The human readable description of the ValidateClaimsResult - public let description: String - - /// Successful validation. - public static let success = ValidateClaimsResult(description: "Success") - - /// Invalid Expiration claim. - public static let invalidExpiration = ValidateClaimsResult(description: "Invalid Expiration claim") - - /// Expired token: expiration time claim is in the past. - public static let expired = ValidateClaimsResult(description: "Expired token") - - /// Invalid Not Before claim. - public static let invalidNotBefore = ValidateClaimsResult(description: "Invalid Not Before claim") - - /// Not Before claim is in the future. - public static let notBefore = ValidateClaimsResult(description: "Token is not valid yet, Not Before claim is greater than the current time") - - /// Invalid Issued At claim. - public static let invalidIssuedAt = ValidateClaimsResult(description: "Invalid Issued At claim") - - /// Issued At claim is in the future. - public static let issuedAt = ValidateClaimsResult(description: "Issued At claim is greater than the current time") - - /// Check if two ValidateClaimsResults are equal. Required for the Equatable protocol - public static func == (lhs: ValidateClaimsResult, rhs: ValidateClaimsResult) -> Bool { - return lhs.description == rhs.description - } + /// The human readable description of the ValidateClaimsResult + public let description: String + + /// Successful validation. + public static let success = ValidateClaimsResult(description: "Success") + + /// Invalid Expiration claim. + public static let invalidExpiration = ValidateClaimsResult(description: "Invalid Expiration claim") + + /// Expired token: expiration time claim is in the past. + public static let expired = ValidateClaimsResult(description: "Expired token") + + /// Invalid Not Before claim. + public static let invalidNotBefore = ValidateClaimsResult(description: "Invalid Not Before claim") + + /// Not Before claim is in the future. + public static let notBefore = ValidateClaimsResult(description: "Token is not valid yet, Not Before claim is greater than the current time") + + /// Invalid Issued At claim. + public static let invalidIssuedAt = ValidateClaimsResult(description: "Invalid Issued At claim") + + /// Issued At claim is in the future. + public static let issuedAt = ValidateClaimsResult(description: "Issued At claim is greater than the current time") + + /// Check if two ValidateClaimsResults are equal. Required for the Equatable protocol + public static func == (lhs: ValidateClaimsResult, rhs: ValidateClaimsResult) -> Bool { + lhs.description == rhs.description + } } diff --git a/Sources/SwiftJWT/VerifierAlgorithm.swift b/Sources/SwiftJWT/VerifierAlgorithm.swift index 4eea45e..2dc09bc 100644 --- a/Sources/SwiftJWT/VerifierAlgorithm.swift +++ b/Sources/SwiftJWT/VerifierAlgorithm.swift @@ -15,6 +15,6 @@ **/ internal protocol VerifierAlgorithm { - /// A function to verify the signature of a JSON web token string is correct for the header and claims. - func verify(jwt: String) -> Bool + /// A function to verify the signature of a JSON web token string is correct for the header and claims. + func verify(jwt: String) -> Bool } diff --git a/Tests/SwiftJWTTests/TestJWT.swift b/Tests/SwiftJWTTests/TestJWT.swift index b3cc12c..95a0aa9 100644 --- a/Tests/SwiftJWTTests/TestJWT.swift +++ b/Tests/SwiftJWTTests/TestJWT.swift @@ -1,12 +1,12 @@ /** Copyright IBM Corporation 2017 - + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,8 +14,8 @@ limitations under the License. */ -import XCTest import Foundation +import XCTest @testable import SwiftJWT @@ -29,8 +29,8 @@ let ec384PrivateKey = read(fileName: "ec384_private_key") let ec384PublicKey = read(fileName: "ec384_public_key") let ec512PrivateKey = read(fileName: "ec512_private_key") let ec512PublicKey = read(fileName: "ec512_public_key") -let rsaJWTEncoder = JWTEncoder(jwtSigner: .rs256(privateKey: rsaPrivateKey)) -let rsaJWTDecoder = JWTDecoder(jwtVerifier: .rs256(publicKey: rsaPublicKey)) +let ecdsaJWTEncoder = JWTEncoder(jwtSigner: .es512(privateKey: ec512PrivateKey)) +let ecdsaJWTDecoder = JWTDecoder(jwtVerifier: .es512(publicKey: ec512PublicKey)) let certPrivateKey = read(fileName: "cert_private_key") let certificate = read(fileName: "certificate") let rsaEncodedTestClaimJWT = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.pOeiYYHuxBu27llpKrLfHX-tt0Cr41m3hn7d1_CPl7dRMksQRJC5U7AM2CkF8uyObwAKg88orK6eHlOQ0x2C4gDoG7WmgszpthOB6ZUTUPj_FNsn3z4fM8sFx3wON7jtRRSuULH13f-RjLoIFhY_VuqVhla3ybjnfbwjcsd8EqDumdFN6La5D0KugCgvuH51JaEjdHfwXkxkRsynmhv3jCpvRbUbforfEnDjyAImez2hd0Pnb3Vtqr-21z1vFWqqRiz_K-qSiO5NTaO1VbLg7SOYBB9hMAD-_6R2ZZh0JvFP7hycCftRIxTSDd5r0I9sQh9iqurVq03_h0ZjS9BwJQ" @@ -42,602 +42,594 @@ let hmacEncodedTestClaimJWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi let ecdsaEncodedTestClaimJWT = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.z1nUPt7mJk5EZBJKrRiCRLSum1B5E5ucaMeuMqxcvnw3a5FnKC-XsR6rvBVdUPRVzWF6L9CHQuSBlDy579SqQA" let jwtSigners: [String: JWTSigner] = ["0": .rs256(privateKey: rsaPrivateKey), "1": .rs256(privateKey: certPrivateKey)] let jwtVerifiers: [String: JWTVerifier] = ["0": .rs256(publicKey: rsaPublicKey), "1": .rs256(certificate: certificate)] -let rsaJWTKidEncoder = JWTEncoder(keyIDToSigner: { kid in return jwtSigners[kid]}) -let rsaJWTKidDecoder = JWTDecoder(keyIDToVerifier: { kid in return jwtVerifiers[kid]}) +let rsaJWTKidEncoder = JWTEncoder(keyIDToSigner: { kid in jwtSigners[kid] }) +let rsaJWTKidDecoder = JWTDecoder(keyIDToVerifier: { kid in jwtVerifiers[kid] }) struct TestClaims: Claims, Equatable { - var name: String? - var admin: Bool? - var iss: String? - var sub: String? - var aud: [String]? - var exp: Date? - var nbf: Date? - var iat: Date? - var jti: String? - init(name: String? = nil) { - self.name = name - } - - static func == (lhs: TestClaims, rhs: TestClaims) -> Bool { - return lhs.name == rhs.name && - lhs.admin == rhs.admin && - lhs.iss == rhs.iss && - lhs.sub == rhs.sub && - lhs.aud ?? [""] == rhs.aud ?? [""] && - lhs.exp == rhs.exp && - lhs.nbf == rhs.nbf && - lhs.iat == rhs.iat && - lhs.jti == rhs.jti - } + var name: String? + var admin: Bool? + var iss: String? + var sub: String? + var aud: [String]? + var exp: Date? + var nbf: Date? + var iat: Date? + var jti: String? + init(name: String? = nil) { + self.name = name + } + + static func == (lhs: TestClaims, rhs: TestClaims) -> Bool { + lhs.name == rhs.name && + lhs.admin == rhs.admin && + lhs.iss == rhs.iss && + lhs.sub == rhs.sub && + lhs.aud ?? [""] == rhs.aud ?? [""] && + lhs.exp == rhs.exp && + lhs.nbf == rhs.nbf && + lhs.iat == rhs.iat && + lhs.jti == rhs.jti + } } extension Header: Equatable { - - /// Function to check if two headers are equal. Required to conform to the equatable protocol. - public static func == (lhs: Header, rhs: Header) -> Bool { - return lhs.alg == rhs.alg && - lhs.crit ?? [] == rhs.crit ?? [] && - lhs.cty == rhs.cty && - lhs.jku == rhs.jku && - lhs.jwk == rhs.jwk && - lhs.kid == rhs.kid && - lhs.typ == rhs.typ && - lhs.x5c ?? [] == rhs.x5c ?? [] && - lhs.x5t == rhs.x5t && - lhs.x5tS256 == rhs.x5tS256 && - lhs.x5u == rhs.x5u - } + /// Function to check if two headers are equal. Required to conform to the equatable protocol. + public static func == (lhs: Header, rhs: Header) -> Bool { + lhs.alg == rhs.alg && + lhs.crit ?? [] == rhs.crit ?? [] && + lhs.cty == rhs.cty && + lhs.jku == rhs.jku && + lhs.jwk == rhs.jwk && + lhs.kid == rhs.kid && + lhs.typ == rhs.typ && + lhs.x5c ?? [] == rhs.x5c ?? [] && + lhs.x5t == rhs.x5t && + lhs.x5tS256 == rhs.x5tS256 && + lhs.x5u == rhs.x5u + } } struct MicroProfile: Claims { - var name: String? - var groups: [String]? - var upn: String? - var admin: Bool? - var iss: String? - var sub: String? - var aud: [String]? - var exp: Date? - var nbf: Date? - var iat: Date? - var jti: String? - init(name: String) { - self.name = name - } + var name: String? + var groups: [String]? + var upn: String? + var admin: Bool? + var iss: String? + var sub: String? + var aud: [String]? + var exp: Date? + var nbf: Date? + var iat: Date? + var jti: String? + init(name: String) { + self.name = name + } } + @available(macOS 10.12, iOS 10.0, *) class TestJWT: XCTestCase { - - static var allTests: [(String, (TestJWT) -> () throws -> Void)] { - return [ - ("testSignAndVerify", testSignAndVerify), -// ("testSignAndVerifyRSA", testSignAndVerifyRSA), -// ("testSignAndVerifyRSAPSS", testSignAndVerifyRSAPSS), -// ("testSignAndVerifyCert", testSignAndVerifyCert), - ("testSignAndVerifyHMAC", testSignAndVerifyHMAC), - ("testSignAndVerifyECDSA", testSignAndVerifyECDSA), -// ("testSignAndVerifyRSA384", testSignAndVerifyRSA384), -// ("testSignAndVerifyRSAPSS384", testSignAndVerifyRSAPSS384), -// ("testSignAndVerifyCert384", testSignAndVerifyCert384), - ("testSignAndVerifyHMAC384", testSignAndVerifyHMAC384), - ("testSignAndVerifyECDSA384", testSignAndVerifyECDSA384), -// ("testSignAndVerifyRSA512", testSignAndVerifyRSA512), -// ("testSignAndVerifyRSAPSS512", testSignAndVerifyRSAPSS512), -// ("testSignAndVerifyCert512", testSignAndVerifyCert512), - ("testSignAndVerifyHMAC512", testSignAndVerifyHMAC512), - ("testSignAndVerifyECDSA512", testSignAndVerifyECDSA512), -// ("testJWTEncoder", testJWTEncoder), -// ("testJWTDecoder", testJWTDecoder), -// ("testJWTCoderCycle", testJWTCoderCycle), -// ("testJWTEncoderKeyID", testJWTEncoderKeyID), -// ("testJWTDecoderKeyID", testJWTDecoderKeyID), -// ("testJWTCoderCycleKeyID", testJWTCoderCycleKeyID), -// ("testJWT", testJWT), -// ("testJWTRSAPSS", testJWTRSAPSS), - ("testJWTUsingHMAC", testJWTUsingHMAC), - ("testJWTUsingECDSA", testJWTUsingECDSA), -// ("testMicroProfile", testMicroProfile), - ("testValidateClaims", testValidateClaims), - ("testValidateClaimsLeeway", testValidateClaimsLeeway), -// ("testErrorPattenMatching", testErrorPattenMatching), - ("testTypeErasedErrorLocalizedDescription", testTypeErasedErrorLocalizedDescription), - ] - } - - func testSignAndVerify() { - do { - try signAndVerify(signer: .none, verifier: .none) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyRSA() { - do { - try signAndVerify(signer: .rs256(privateKey: rsaPrivateKey), verifier: .rs256(publicKey: rsaPublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyRSADERKey() { - do { - try signAndVerify(signer: .rs256(privateKey: rsaDERPrivateKey), verifier: .rs256(publicKey: rsaDERPublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyRSAPSS() { - if #available(OSX 10.13, *) { - do { - try signAndVerify(signer: .ps256(privateKey: rsaPrivateKey), verifier: .ps256(publicKey: rsaPublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - } - - func testSignAndVerifyCert() { - do { - try signAndVerify(signer: .rs256(privateKey: certPrivateKey), verifier: .rs256(certificate: certificate)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyHMAC() { - do { - let hmacData = "Super Secret Key".data(using: .utf8)! - try signAndVerify(signer: .hs256(key: hmacData), verifier: .hs256(key: hmacData)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyECDSA() { - if #available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) { - do { - try signAndVerify(signer: .es256(privateKey: ecdsaPrivateKey), verifier: .es256(publicKey: ecdsaPublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - } - - func testSignAndVerifyRSA384() { - do { - try signAndVerify(signer: .rs384(privateKey: rsaPrivateKey), verifier: .rs384(publicKey: rsaPublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyRSAPSS384() { - if #available(OSX 10.13, *) { - do { - try signAndVerify(signer: .ps384(privateKey: rsaPrivateKey), verifier: .ps384(publicKey: rsaPublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - } - - func testSignAndVerifyCert384() { - do { - try signAndVerify(signer: .rs384(privateKey: certPrivateKey), verifier: .rs384(certificate: certificate)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyHMAC384() { - do { - let hmacData = "Super Secret Key".data(using: .utf8)! - try signAndVerify(signer: .hs384(key: hmacData), verifier: .hs384(key: hmacData)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyECDSA384() { - if #available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) { - do { - try signAndVerify(signer: .es384(privateKey: ec384PrivateKey), verifier: .es384(publicKey: ec384PublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - } - - func testSignAndVerifyRSA512() { - do { - try signAndVerify(signer: .rs512(privateKey: rsaPrivateKey), verifier: .rs512(publicKey: rsaPublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyRSAPSS512() { - if #available(OSX 10.13, iOS 11, *) { - do { - try signAndVerify(signer: .ps512(privateKey: rsaPrivateKey), verifier: .ps512(publicKey: rsaPublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - } - - func testSignAndVerifyCert512() { - do { - try signAndVerify(signer: .rs512(privateKey: certPrivateKey), verifier: .rs512(certificate: certificate)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyHMAC512() { - do { - let hmacData = "Super Secret Key".data(using: .utf8)! - try signAndVerify(signer: .hs512(key: hmacData), verifier: .hs512(key: hmacData)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - - func testSignAndVerifyECDSA512() { - if #available(OSX 10.13, iOS 11, tvOS 11.0, *) { - do { - try signAndVerify(signer: .es512(privateKey: ec512PrivateKey), verifier: .es512(publicKey: ec512PublicKey)) - } catch { - XCTFail("testSignAndVerify failed: \(error)") - } - } - } - - func signAndVerify(signer: JWTSigner, verifier: JWTVerifier) throws { - var jwt = JWT(claims: TestClaims(name:"Kitura")) - jwt.claims.name = "Kitura-JWT" - XCTAssertEqual(jwt.claims.name, "Kitura-JWT") - jwt.claims.iss = "issuer" - jwt.claims.aud = ["clientID"] - jwt.claims.iat = Date(timeIntervalSince1970: 1485949565.58463) - 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) - XCTAssertTrue(ok, "Verification failed") - let decoded = try JWT(jwtString: signed) - check(jwt: decoded, algorithm: signer.name) - XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - } - - 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") - 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, 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") - 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.aud ?? [""], ["clientID"], "Wrong .aud in decoded") - XCTAssertEqual(jwt.claims.groups ?? [""], ["red-group", "green-group", "admin-group", "admin"], "Wrong .groups in decoded") - - } - - - func testMicroProfile() { - var jwt = JWT(claims: MicroProfile(name: "MP-JWT")) - jwt.header.kid = "abc-1234567890" - jwt.header.typ = "JWT" - XCTAssertEqual(jwt.claims.name, "MP-JWT") - jwt.claims.iss = "https://server.example.com" - jwt.claims.aud = ["clientID"] - jwt.claims.iat = Date(timeIntervalSince1970: 1485949565.58463) - jwt.claims.exp = Date(timeIntervalSince1970: 2485949565.58463) - jwt.claims.upn = "jdoe@server.example.com" - jwt.claims.groups = ["red-group", "green-group", "admin-group", "admin"] - - // 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)) - XCTAssertTrue(ok, "Verification failed") - - if let decoded = try? JWT(jwtString: signed) { - checkMicroProfile(jwt: decoded, algorithm: "RS256") - - XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - } - else { - XCTFail("Failed to decode") - } - } - else { - XCTFail("Failed to sign") - } - - // certificate - if let signed = try? jwt.sign(using: .rs256(privateKey: certPrivateKey)) { - let ok = JWT.verify(signed, using: .rs256(certificate: certificate)) - XCTAssertTrue(ok, "Verification failed") - - if let decoded = try? JWT(jwtString: signed) { - checkMicroProfile(jwt: decoded, algorithm: "RS256") - - XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - } - else { - XCTFail("Failed to decode") - } - } - } - - // 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()) - 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) - // 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) - XCTAssertEqual(jwt.header, decodedJWTString.header) - XCTAssertEqual(jwt.claims, decodedTestClaimJWT.claims) - XCTAssertEqual(jwt.header, decodedTestClaimJWT.header) - } catch { - XCTFail("Failed to encode JTW: \(error)") - } - } - - // 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()) - 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) - jwt.header.alg = "RS256" - XCTAssertEqual(decodedJWT.claims, jwt.claims) - XCTAssertEqual(decodedJWT.header, jwt.header) - } catch { - XCTFail("Failed to encode JTW: \(error)") - } - } - - // 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()) - 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) - jwt.header.alg = "RS256" - XCTAssertEqual(decodedJWT.claims, jwt.claims) - XCTAssertEqual(decodedJWT.header, jwt.header) - } catch { - XCTFail("Failed to encode JTW: \(error)") - } - } - - // 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()) - 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) - jwt.header.alg = "RS256" - XCTAssertEqual(jwt.claims, decodedJWTString.claims) - XCTAssertEqual(jwt.header, decodedJWTString.header) - } catch { - XCTFail("Failed to encode JTW: \(error)") - } - } - - // 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()) - 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) - jwt.header.alg = "RS256" - XCTAssertEqual(decodedJWT.claims, jwt.claims) - XCTAssertEqual(decodedJWT.header, jwt.header) - } catch { - XCTFail("Failed to encode JTW: \(error)") - } - } - - // 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()) - 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) - jwt.header.alg = "RS256" - XCTAssertEqual(decodedJWT.claims, jwt.claims) - XCTAssertEqual(decodedJWT.header, jwt.header) - } catch { - XCTFail("Failed to encode JTW: \(error)") - } - } - - // From jwt.io - func testJWT() { - let ok = JWT.verify(rsaEncodedTestClaimJWT, using: .rs256(publicKey: rsaPublicKey)) - XCTAssertTrue(ok, "Verification failed") - - 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") - - XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") - XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") - XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") - XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1516239022), "Wrong .iat in decoded") - - - XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - } - else { - XCTFail("Failed to decode") - } - } - - // From jwt.io - func testJWTRSAPSS() { - if #available(OSX 10.13, *) { - let ok = JWT.verify(rsaPSSEncodedTestClaimJWT, using: .ps256(publicKey: rsaPublicKey)) - XCTAssertTrue(ok, "Verification failed") - - 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") - - XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") - XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") - XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") - XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1516239022), "Wrong .iat in decoded") - - - XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - } - else { - XCTFail("Failed to decode") - } - } - } - - func testJWTUsingHMAC() { - 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)) - XCTAssertTrue(ok, "Verification failed") - - 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") - - XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") - XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") - XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") - - XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - } - else { - XCTFail("Failed to decode") - } - } - - // 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)) - XCTAssertTrue(ok, "Verification failed") - - 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") - - XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") - XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") - XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") - XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1516239022), "Wrong .iat in decoded") - - - XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - } - else { - XCTFail("Failed to decode") - } - } - } - - func testValidateClaims() { - var jwt = JWT(claims: TestClaims(name:"Kitura")) - jwt.claims.exp = Date() - XCTAssertEqual(jwt.validateClaims(), .expired, "Validation failed") - jwt.claims.exp = nil - jwt.claims.iat = Date(timeIntervalSinceNow: 10) - XCTAssertEqual(jwt.validateClaims(), .issuedAt, "Validation failed") - jwt.claims.iat = nil - jwt.claims.nbf = Date(timeIntervalSinceNow: 10) - XCTAssertEqual(jwt.validateClaims(), .notBefore, "Validation failed") - } - - func testValidateClaimsLeeway() { - var jwt = JWT(claims: TestClaims(name:"Kitura")) - jwt.claims.exp = Date() - XCTAssertEqual(jwt.validateClaims(leeway: 20), .success, "Validation failed") - jwt.claims.exp = nil - jwt.claims.iat = Date(timeIntervalSinceNow: 10) - XCTAssertEqual(jwt.validateClaims(leeway: 20), .success, "Validation failed") - jwt.claims.iat = nil - jwt.claims.nbf = Date(timeIntervalSinceNow: 10) - XCTAssertEqual(jwt.validateClaims(leeway: 20), .success, "Validation failed") - } - - func testErrorPattenMatching() { - do { - let _ = try JWT(jwtString: "InvalidString", verifier: .rs256(publicKey: rsaPublicKey)) - } catch JWTError.invalidJWTString { - // Caught correct error - } catch { - XCTFail("Incorrect error thrown: \(error)") - } - } - - func testTypeErasedErrorLocalizedDescription() { - let error = JWTError.invalidJWTString - XCTAssertEqual((error as Error).localizedDescription, error.errorDescription) - } + static var allTests: [(String, (TestJWT) -> () throws -> Void)] { + [ + ("testSignAndVerify", testSignAndVerify), + // ("testSignAndVerifyRSA", testSignAndVerifyRSA), + // ("testSignAndVerifyRSAPSS", testSignAndVerifyRSAPSS), + // ("testSignAndVerifyCert", testSignAndVerifyCert), + ("testSignAndVerifyHMAC", testSignAndVerifyHMAC), + ("testSignAndVerifyECDSA", testSignAndVerifyECDSA), + // ("testSignAndVerifyRSA384", testSignAndVerifyRSA384), + // ("testSignAndVerifyRSAPSS384", testSignAndVerifyRSAPSS384), + // ("testSignAndVerifyCert384", testSignAndVerifyCert384), + ("testSignAndVerifyHMAC384", testSignAndVerifyHMAC384), + ("testSignAndVerifyECDSA384", testSignAndVerifyECDSA384), + // ("testSignAndVerifyRSA512", testSignAndVerifyRSA512), + // ("testSignAndVerifyRSAPSS512", testSignAndVerifyRSAPSS512), + // ("testSignAndVerifyCert512", testSignAndVerifyCert512), + ("testSignAndVerifyHMAC512", testSignAndVerifyHMAC512), + ("testSignAndVerifyECDSA512", testSignAndVerifyECDSA512), + // ("testJWTEncoder", testJWTEncoder), + // ("testJWTDecoder", testJWTDecoder), + // ("testJWTCoderCycle", testJWTCoderCycle), + // ("testJWTEncoderKeyID", testJWTEncoderKeyID), + // ("testJWTDecoderKeyID", testJWTDecoderKeyID), + // ("testJWTCoderCycleKeyID", testJWTCoderCycleKeyID), + // ("testJWT", testJWT), + // ("testJWTRSAPSS", testJWTRSAPSS), + ("testJWTUsingHMAC", testJWTUsingHMAC), + ("testJWTUsingECDSA", testJWTUsingECDSA), + // ("testMicroProfile", testMicroProfile), + ("testValidateClaims", testValidateClaims), + ("testValidateClaimsLeeway", testValidateClaimsLeeway), + // ("testErrorPattenMatching", testErrorPattenMatching), + ("testTypeErasedErrorLocalizedDescription", testTypeErasedErrorLocalizedDescription) + ] + } + + func testSignAndVerify() { + do { + try signAndVerify(signer: .none, verifier: .none) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + + // func testSignAndVerifyRSA() { + // do { + // try signAndVerify(signer: .rs256(privateKey: rsaPrivateKey), verifier: .rs256(publicKey: rsaPublicKey)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } + + // func testSignAndVerifyRSADERKey() { + // do { + // try signAndVerify(signer: .rs256(privateKey: rsaDERPrivateKey), verifier: .rs256(publicKey: rsaDERPublicKey)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } + + // func testSignAndVerifyRSAPSS() { + // if #available(OSX 10.13, *) { + // do { + // try signAndVerify(signer: .ps256(privateKey: rsaPrivateKey), verifier: .ps256(publicKey: rsaPublicKey)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } + // } + + // func testSignAndVerifyCert() { + // do { + // try signAndVerify(signer: .rs256(privateKey: certPrivateKey), verifier: .rs256(certificate: certificate)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } + + func testSignAndVerifyHMAC() { + do { + let hmacData = "Super Secret Key".data(using: .utf8)! + try signAndVerify(signer: .hs256(key: hmacData), verifier: .hs256(key: hmacData)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + + func testSignAndVerifyECDSA() { + if #available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) { + do { + try signAndVerify(signer: .es256(privateKey: ecdsaPrivateKey), verifier: .es256(publicKey: ecdsaPublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + } + + // func testSignAndVerifyRSA384() { + // do { + // try signAndVerify(signer: .rs384(privateKey: rsaPrivateKey), verifier: .rs384(publicKey: rsaPublicKey)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } +// + // func testSignAndVerifyRSAPSS384() { + // if #available(OSX 10.13, *) { + // do { + // try signAndVerify(signer: .ps384(privateKey: rsaPrivateKey), verifier: .ps384(publicKey: rsaPublicKey)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } + // } + + // func testSignAndVerifyCert384() { + // do { + // try signAndVerify(signer: .rs384(privateKey: certPrivateKey), verifier: .rs384(certificate: certificate)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } + + func testSignAndVerifyHMAC384() { + do { + let hmacData = "Super Secret Key".data(using: .utf8)! + try signAndVerify(signer: .hs384(key: hmacData), verifier: .hs384(key: hmacData)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + + func testSignAndVerifyECDSA384() { + if #available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) { + do { + try signAndVerify(signer: .es384(privateKey: ec384PrivateKey), verifier: .es384(publicKey: ec384PublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + } + + // func testSignAndVerifyRSA512() { + // do { + // try signAndVerify(signer: .rs512(privateKey: rsaPrivateKey), verifier: .rs512(publicKey: rsaPublicKey)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } + + // func testSignAndVerifyRSAPSS512() { + // if #available(OSX 10.13, iOS 11, *) { + // do { + // try signAndVerify(signer: .ps512(privateKey: rsaPrivateKey), verifier: .ps512(publicKey: rsaPublicKey)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } + // } + + // func testSignAndVerifyCert512() { + // do { + // try signAndVerify(signer: .rs512(privateKey: certPrivateKey), verifier: .rs512(certificate: certificate)) + // } catch { + // XCTFail("testSignAndVerify failed: \(error)") + // } + // } + + func testSignAndVerifyHMAC512() { + do { + let hmacData = "Super Secret Key".data(using: .utf8)! + try signAndVerify(signer: .hs512(key: hmacData), verifier: .hs512(key: hmacData)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + + func testSignAndVerifyECDSA512() { + if #available(OSX 10.13, iOS 11, tvOS 11.0, *) { + do { + try signAndVerify(signer: .es512(privateKey: ec512PrivateKey), verifier: .es512(publicKey: ec512PublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + } + + func signAndVerify(signer: JWTSigner, verifier: JWTVerifier) throws { + var jwt = JWT(claims: TestClaims(name: "Kitura")) + jwt.claims.name = "Kitura-JWT" + XCTAssertEqual(jwt.claims.name, "Kitura-JWT") + jwt.claims.iss = "issuer" + jwt.claims.aud = ["clientID"] + jwt.claims.iat = Date(timeIntervalSince1970: 1_485_949_565.58463) + jwt.claims.exp = Date(timeIntervalSince1970: 2_485_949_565.58463) + jwt.claims.nbf = Date(timeIntervalSince1970: 1_485_949_565.58463) + let signed = try jwt.sign(using: signer) + let ok = JWT.verify(signed, using: verifier) + XCTAssertTrue(ok, "Verification failed") + let decoded = try JWT(jwtString: signed) + check(jwt: decoded, algorithm: signer.name) + XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") + } + + func check(jwt: JWT, algorithm: String) { + XCTAssertEqual(jwt.header.alg, algorithm, "Wrong .alg in decoded") + XCTAssertEqual(jwt.claims.exp, Date(timeIntervalSince1970: 2_485_949_565.58463), "Wrong .exp in decoded") + XCTAssertEqual(jwt.claims.iat, Date(timeIntervalSince1970: 1_485_949_565.58463), "Wrong .iat in decoded") + XCTAssertEqual(jwt.claims.nbf, Date(timeIntervalSince1970: 1_485_949_565.58463), "Wrong .nbf in decoded") + } + + 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") + XCTAssertEqual(jwt.claims.exp, Date(timeIntervalSince1970: 2_485_949_565.58463), "Wrong .exp in decoded") + XCTAssertEqual(jwt.claims.iat, Date(timeIntervalSince1970: 1_485_949_565.58463), "Wrong .iat in decoded") + XCTAssertEqual(jwt.claims.aud ?? [""], ["clientID"], "Wrong .aud in decoded") + XCTAssertEqual(jwt.claims.groups ?? [""], ["red-group", "green-group", "admin-group", "admin"], "Wrong .groups in decoded") + } + + // func testMicroProfile() { + // var jwt = JWT(claims: MicroProfile(name: "MP-JWT")) + // jwt.header.kid = "abc-1234567890" + // jwt.header.typ = "JWT" + // XCTAssertEqual(jwt.claims.name, "MP-JWT") + // jwt.claims.iss = "https://server.example.com" + // jwt.claims.aud = ["clientID"] + // jwt.claims.iat = Date(timeIntervalSince1970: 1485949565.58463) + // jwt.claims.exp = Date(timeIntervalSince1970: 2485949565.58463) + // jwt.claims.upn = "jdoe@server.example.com" + // jwt.claims.groups = ["red-group", "green-group", "admin-group", "admin"] +// + // // 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)) + // XCTAssertTrue(ok, "Verification failed") +// + // if let decoded = try? JWT(jwtString: signed) { + // checkMicroProfile(jwt: decoded, algorithm: "RS256") +// + // XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") + // } + // else { + // XCTFail("Failed to decode") + // } + // } + // else { + // XCTFail("Failed to sign") + // } +// + // // certificate + // if let signed = try? jwt.sign(using: .rs256(privateKey: certPrivateKey)) { + // let ok = JWT.verify(signed, using: .rs256(certificate: certificate)) + // XCTAssertTrue(ok, "Verification failed") +// + // if let decoded = try? JWT(jwtString: signed) { + // checkMicroProfile(jwt: decoded, algorithm: "RS256") +// + // XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") + // } + // else { + // XCTFail("Failed to decode") + // } + // } + // } + + // // 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()) + // jwt.claims.sub = "1234567890" + // jwt.claims.name = "John Doe" + // jwt.claims.admin = true + // jwt.claims.iat = Date(timeIntervalSince1970: 1_516_239_022) + // do { + // let jwtString = try ecdsaJWTEncoder.encodeToString(jwt) + // let decodedJWTString = try JWT(jwtString: jwtString) + // let decodedTestClaimJWT = try JWT(jwtString: ecdsaEncodedTestClaimJWT) + // // 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 = "ES256" + // XCTAssertEqual(jwt.claims, decodedJWTString.claims) + //// XCTAssertEqual(jwt.header, decodedJWTString.header) + // XCTAssertEqual(jwt.claims, decodedTestClaimJWT.claims) + // XCTAssertEqual(jwt.header, decodedTestClaimJWT.header) + // } catch { + // XCTFail("Failed to encode JTW: \(error)") + // } + // } + + // // 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()) + // jwt.claims.sub = "1234567890" + // jwt.claims.name = "John Doe" + // jwt.claims.admin = true + // jwt.claims.iat = Date(timeIntervalSince1970: 1_516_239_022) + // do { + // let decodedJWT = try ecdsaJWTDecoder.decode(JWT.self, fromString: ecdsaEncodedTestClaimJWT) + // jwt.header.alg = "ES256" + // XCTAssertEqual(decodedJWT.claims, jwt.claims) + // XCTAssertEqual(decodedJWT.header, jwt.header) + // } catch { + // XCTFail("Failed to encode JTW: \(error)") + // } + // } + + // // 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()) + // 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) + // jwt.header.alg = "RS256" + // XCTAssertEqual(decodedJWT.claims, jwt.claims) + // XCTAssertEqual(decodedJWT.header, jwt.header) + // } catch { + // XCTFail("Failed to encode JTW: \(error)") + // } + // } + + // // 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()) + // 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) + // jwt.header.alg = "RS256" + // XCTAssertEqual(jwt.claims, decodedJWTString.claims) + // XCTAssertEqual(jwt.header, decodedJWTString.header) + // } catch { + // XCTFail("Failed to encode JTW: \(error)") + // } + // } + + // // 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()) + // 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) + // jwt.header.alg = "RS256" + // XCTAssertEqual(decodedJWT.claims, jwt.claims) + // XCTAssertEqual(decodedJWT.header, jwt.header) + // } catch { + // XCTFail("Failed to encode JTW: \(error)") + // } + // } + + // // 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()) + // 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) + // jwt.header.alg = "RS256" + // XCTAssertEqual(decodedJWT.claims, jwt.claims) + // XCTAssertEqual(decodedJWT.header, jwt.header) + // } catch { + // XCTFail("Failed to encode JTW: \(error)") + // } + // } + + // // From jwt.io + // func testJWT() { + // let ok = JWT.verify(rsaEncodedTestClaimJWT, using: .rs256(publicKey: rsaPublicKey)) + // XCTAssertTrue(ok, "Verification failed") +// + // 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") +// + // XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") + // XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") + // XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") + // XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1516239022), "Wrong .iat in decoded") +// +// + // XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") + // } + // else { + // XCTFail("Failed to decode") + // } + // } + + // // From jwt.io + // func testJWTRSAPSS() { + // if #available(OSX 10.13, *) { + // let ok = JWT.verify(rsaPSSEncodedTestClaimJWT, using: .ps256(publicKey: rsaPublicKey)) + // XCTAssertTrue(ok, "Verification failed") +// + // 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") +// + // XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") + // XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") + // XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") + // XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1516239022), "Wrong .iat in decoded") +// +// + // XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") + // } + // else { + // XCTFail("Failed to decode") + // } + // } + // } + + func testJWTUsingHMAC() { + 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)) + XCTAssertTrue(ok, "Verification failed") + + 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") + + XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") + XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") + XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") + + XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") + } else { + XCTFail("Failed to decode") + } + } + + // 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)) + XCTAssertTrue(ok, "Verification failed") + + 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") + + XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") + XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") + XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") + XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1_516_239_022), "Wrong .iat in decoded") + + XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") + } else { + XCTFail("Failed to decode") + } + } + } + + func testValidateClaims() { + var jwt = JWT(claims: TestClaims(name: "Kitura")) + jwt.claims.exp = Date() + XCTAssertEqual(jwt.validateClaims(), .expired, "Validation failed") + jwt.claims.exp = nil + jwt.claims.iat = Date(timeIntervalSinceNow: 10) + XCTAssertEqual(jwt.validateClaims(), .issuedAt, "Validation failed") + jwt.claims.iat = nil + jwt.claims.nbf = Date(timeIntervalSinceNow: 10) + XCTAssertEqual(jwt.validateClaims(), .notBefore, "Validation failed") + } + + func testValidateClaimsLeeway() { + var jwt = JWT(claims: TestClaims(name: "Kitura")) + jwt.claims.exp = Date() + XCTAssertEqual(jwt.validateClaims(leeway: 20), .success, "Validation failed") + jwt.claims.exp = nil + jwt.claims.iat = Date(timeIntervalSinceNow: 10) + XCTAssertEqual(jwt.validateClaims(leeway: 20), .success, "Validation failed") + jwt.claims.iat = nil + jwt.claims.nbf = Date(timeIntervalSinceNow: 10) + XCTAssertEqual(jwt.validateClaims(leeway: 20), .success, "Validation failed") + } + + // func testErrorPattenMatching() { + // do { + // let _ = try JWT(jwtString: "InvalidString", verifier: .rs256(publicKey: rsaPublicKey)) + // } catch JWTError.invalidJWTString { + // // Caught correct error + // } catch { + // XCTFail("Incorrect error thrown: \(error)") + // } + // } + + func testTypeErasedErrorLocalizedDescription() { + let error = JWTError.invalidJWTString + XCTAssertEqual((error as Error).localizedDescription, error.errorDescription) + } } func read(fileName: String) -> Data { - do { - var pathToTests = #file - if pathToTests.hasSuffix("TestJWT.swift") { - pathToTests = pathToTests.replacingOccurrences(of: "TestJWT.swift", with: "") - } - let fileData = try Data(contentsOf: URL(fileURLWithPath: "\(pathToTests)\(fileName)")) - XCTAssertNotNil(fileData, "Failed to read in the \(fileName) file") - return fileData - } catch { - XCTFail("Error in \(fileName).") - exit(1) - } + do { + var pathToTests = #file + if pathToTests.hasSuffix("TestJWT.swift") { + pathToTests = pathToTests.replacingOccurrences(of: "TestJWT.swift", with: "") + } + let fileData = try Data(contentsOf: URL(fileURLWithPath: "\(pathToTests)\(fileName)")) + XCTAssertNotNil(fileData, "Failed to read in the \(fileName) file") + return fileData + } catch { + XCTFail("Error in \(fileName).") + exit(1) + } } From 7d429549348a0f4a853e40950a8cb48bb4ea3de3 Mon Sep 17 00:00:00 2001 From: Tom Knapen Date: Sun, 6 Aug 2023 16:06:19 +0200 Subject: [PATCH 3/6] re-enable some tests - add testSignAndVerifyECDSA256 - rewrite testJWTEncoder, testJWTDecoder and testJWTCoderCycle to use ECDSA instead of RSA - re-enable testJWTEncoder, testJWTDecoder and testJWTCoderCycle --- Tests/SwiftJWTTests/TestJWT.swift | 192 +++++++++--------- .../{ecdsa_private_key => ec256_private_key} | 0 .../{ecdsa_public_key => ec256_public_key} | 0 3 files changed, 100 insertions(+), 92 deletions(-) rename Tests/SwiftJWTTests/{ecdsa_private_key => ec256_private_key} (100%) rename Tests/SwiftJWTTests/{ecdsa_public_key => ec256_public_key} (100%) diff --git a/Tests/SwiftJWTTests/TestJWT.swift b/Tests/SwiftJWTTests/TestJWT.swift index 95a0aa9..0f3b1e7 100644 --- a/Tests/SwiftJWTTests/TestJWT.swift +++ b/Tests/SwiftJWTTests/TestJWT.swift @@ -23,14 +23,14 @@ let rsaPrivateKey = read(fileName: "rsa_private_key") let rsaPublicKey = read(fileName: "rsa_public_key") let rsaDERPrivateKey = read(fileName: "privateRSA.der") let rsaDERPublicKey = read(fileName: "publicRSA.der") -let ecdsaPrivateKey = read(fileName: "ecdsa_private_key") -let ecdsaPublicKey = read(fileName: "ecdsa_public_key") +let ec256PrivateKey = read(fileName: "ec256_private_key") +let ec256PublicKey = read(fileName: "ec256_public_key") let ec384PrivateKey = read(fileName: "ec384_private_key") let ec384PublicKey = read(fileName: "ec384_public_key") let ec512PrivateKey = read(fileName: "ec512_private_key") let ec512PublicKey = read(fileName: "ec512_public_key") -let ecdsaJWTEncoder = JWTEncoder(jwtSigner: .es512(privateKey: ec512PrivateKey)) -let ecdsaJWTDecoder = JWTDecoder(jwtVerifier: .es512(publicKey: ec512PublicKey)) +let ecdsaJWTEncoder = JWTEncoder(jwtSigner: .es256(privateKey: ec256PrivateKey)) +let ecdsaJWTDecoder = JWTDecoder(jwtVerifier: .es256(publicKey: ec256PublicKey)) let certPrivateKey = read(fileName: "cert_private_key") let certificate = read(fileName: "certificate") let rsaEncodedTestClaimJWT = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.pOeiYYHuxBu27llpKrLfHX-tt0Cr41m3hn7d1_CPl7dRMksQRJC5U7AM2CkF8uyObwAKg88orK6eHlOQ0x2C4gDoG7WmgszpthOB6ZUTUPj_FNsn3z4fM8sFx3wON7jtRRSuULH13f-RjLoIFhY_VuqVhla3ybjnfbwjcsd8EqDumdFN6La5D0KugCgvuH51JaEjdHfwXkxkRsynmhv3jCpvRbUbforfEnDjyAImez2hd0Pnb3Vtqr-21z1vFWqqRiz_K-qSiO5NTaO1VbLg7SOYBB9hMAD-_6R2ZZh0JvFP7hycCftRIxTSDd5r0I9sQh9iqurVq03_h0ZjS9BwJQ" @@ -38,7 +38,7 @@ let rsaPSSEncodedTestClaimJWT = "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOi let certificateEncodedTestClaimJWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwic3ViIjoiMTIzNDU2Nzg5MCJ9.CpnzQLuWGfH5Kba36vg0ZZKBnzwlrIgapFVfBfk_nea-eej84ktHZANqIeolskZopRJ4DQ3oaLtHWEg16-ZsujxmkOdiAIbk0-C4QLOVFLZH78WLZAqkyNLS8rFuK9hloLNwz1j6VVUd1f0SOT-wIRzL0_0VRYqQd1bVcCj7wc7BmXENlOfHY7KGHS-6JX-EClT1DygDSoCmdvBExBf3vx0lwMIbP4ryKkyhOoU13ZfSUt1gpP9nZAfzqfRTPxZc_f7neiAlMlF6SzsedsskRCNegW8cg5e_NuVmZZkj0_bnswXFDMmIaxiPdtOEWkmyEOca-EHSwbO5PgCgXOIrgg" // A `TestClaims` encoded using HMAC with "Super Secret Key" from "www.jwt.io" let hmacEncodedTestClaimJWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwic3ViIjoiMTIzNDU2Nzg5MCJ9.8kIE0ZCq1Vw7aW1kACpgJLcgY2DpTXgO6P5T3cdCuTs" -// A `TestClaims` encoded using es256 with `ecdsaPrivateKey` +// A `TestClaims` encoded using es256 with `ec256PrivateKey` let ecdsaEncodedTestClaimJWT = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.z1nUPt7mJk5EZBJKrRiCRLSum1B5E5ucaMeuMqxcvnw3a5FnKC-XsR6rvBVdUPRVzWF6L9CHQuSBlDy579SqQA" let jwtSigners: [String: JWTSigner] = ["0": .rs256(privateKey: rsaPrivateKey), "1": .rs256(privateKey: certPrivateKey)] let jwtVerifiers: [String: JWTVerifier] = ["0": .rs256(publicKey: rsaPublicKey), "1": .rs256(certificate: certificate)] @@ -114,24 +114,25 @@ class TestJWT: XCTestCase { // ("testSignAndVerifyRSA", testSignAndVerifyRSA), // ("testSignAndVerifyRSAPSS", testSignAndVerifyRSAPSS), // ("testSignAndVerifyCert", testSignAndVerifyCert), - ("testSignAndVerifyHMAC", testSignAndVerifyHMAC), - ("testSignAndVerifyECDSA", testSignAndVerifyECDSA), // ("testSignAndVerifyRSA384", testSignAndVerifyRSA384), // ("testSignAndVerifyRSAPSS384", testSignAndVerifyRSAPSS384), // ("testSignAndVerifyCert384", testSignAndVerifyCert384), + ("testSignAndVerifyHMAC", testSignAndVerifyHMAC), ("testSignAndVerifyHMAC384", testSignAndVerifyHMAC384), + ("testSignAndVerifyHMAC512", testSignAndVerifyHMAC512), + ("testSignAndVerifyECDSA", testSignAndVerifyECDSA), + ("testSignAndVerifyECDSA256", testSignAndVerifyECDSA256), ("testSignAndVerifyECDSA384", testSignAndVerifyECDSA384), + ("testSignAndVerifyECDSA512", testSignAndVerifyECDSA512), // ("testSignAndVerifyRSA512", testSignAndVerifyRSA512), // ("testSignAndVerifyRSAPSS512", testSignAndVerifyRSAPSS512), // ("testSignAndVerifyCert512", testSignAndVerifyCert512), - ("testSignAndVerifyHMAC512", testSignAndVerifyHMAC512), - ("testSignAndVerifyECDSA512", testSignAndVerifyECDSA512), - // ("testJWTEncoder", testJWTEncoder), - // ("testJWTDecoder", testJWTDecoder), - // ("testJWTCoderCycle", testJWTCoderCycle), - // ("testJWTEncoderKeyID", testJWTEncoderKeyID), - // ("testJWTDecoderKeyID", testJWTDecoderKeyID), - // ("testJWTCoderCycleKeyID", testJWTCoderCycleKeyID), + ("testJWTEncoder", testJWTEncoder), + ("testJWTDecoder", testJWTDecoder), + ("testJWTCoderCycle", testJWTCoderCycle), +// ("testJWTEncoderKeyID", testJWTEncoderKeyID), +// ("testJWTDecoderKeyID", testJWTDecoderKeyID), +// ("testJWTCoderCycleKeyID", testJWTCoderCycleKeyID), // ("testJWT", testJWT), // ("testJWTRSAPSS", testJWTRSAPSS), ("testJWTUsingHMAC", testJWTUsingHMAC), @@ -198,7 +199,7 @@ class TestJWT: XCTestCase { func testSignAndVerifyECDSA() { if #available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) { do { - try signAndVerify(signer: .es256(privateKey: ecdsaPrivateKey), verifier: .es256(publicKey: ecdsaPublicKey)) + try signAndVerify(signer: .es256(privateKey: ec256PrivateKey), verifier: .es256(publicKey: ec256PublicKey)) } catch { XCTFail("testSignAndVerify failed: \(error)") } @@ -240,6 +241,12 @@ class TestJWT: XCTestCase { } } + func testSignAndVerifyECDSA256() { + if #available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) { + XCTAssertNoThrow(try signAndVerify(signer: .es256(privateKey: ec256PrivateKey), verifier: .es256(publicKey: ec256PublicKey))) + } + } + func testSignAndVerifyECDSA384() { if #available(OSX 10.13, iOS 11, tvOS 11.0, watchOS 4.0, *) { do { @@ -374,84 +381,85 @@ class TestJWT: XCTestCase { // } // } - // // 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()) - // jwt.claims.sub = "1234567890" - // jwt.claims.name = "John Doe" - // jwt.claims.admin = true - // jwt.claims.iat = Date(timeIntervalSince1970: 1_516_239_022) - // do { - // let jwtString = try ecdsaJWTEncoder.encodeToString(jwt) - // let decodedJWTString = try JWT(jwtString: jwtString) - // let decodedTestClaimJWT = try JWT(jwtString: ecdsaEncodedTestClaimJWT) - // // 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 = "ES256" - // XCTAssertEqual(jwt.claims, decodedJWTString.claims) - //// XCTAssertEqual(jwt.header, decodedJWTString.header) - // XCTAssertEqual(jwt.claims, decodedTestClaimJWT.claims) - // XCTAssertEqual(jwt.header, decodedTestClaimJWT.header) - // } catch { - // XCTFail("Failed to encode JTW: \(error)") - // } - // } + // 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()) + jwt.claims.sub = "1234567890" + jwt.claims.name = "John Doe" + jwt.claims.admin = true + jwt.claims.admin = true + jwt.claims.iat = Date(timeIntervalSince1970: 1_516_239_022) + do { + let jwtString = try ecdsaJWTEncoder.encodeToString(jwt) + let decodedJWTString = try JWT(jwtString: jwtString) + let decodedTestClaimJWT = try JWT(jwtString: ecdsaEncodedTestClaimJWT) + // 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 = "ES256" + XCTAssertEqual(jwt.claims, decodedJWTString.claims) + // XCTAssertEqual(jwt.header, decodedJWTString.header) + XCTAssertEqual(jwt.claims, decodedTestClaimJWT.claims) + XCTAssertEqual(jwt.header, decodedTestClaimJWT.header) + } catch { + XCTFail("Failed to encode JTW: \(error)") + } + } - // // 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()) - // jwt.claims.sub = "1234567890" - // jwt.claims.name = "John Doe" - // jwt.claims.admin = true - // jwt.claims.iat = Date(timeIntervalSince1970: 1_516_239_022) - // do { - // let decodedJWT = try ecdsaJWTDecoder.decode(JWT.self, fromString: ecdsaEncodedTestClaimJWT) - // jwt.header.alg = "ES256" - // XCTAssertEqual(decodedJWT.claims, jwt.claims) - // XCTAssertEqual(decodedJWT.header, jwt.header) - // } catch { - // XCTFail("Failed to encode JTW: \(error)") - // } - // } + // 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()) + jwt.claims.sub = "1234567890" + jwt.claims.name = "John Doe" + jwt.claims.admin = true + jwt.claims.iat = Date(timeIntervalSince1970: 1_516_239_022) + do { + let decodedJWT = try ecdsaJWTDecoder.decode(JWT.self, fromString: ecdsaEncodedTestClaimJWT) + jwt.header.alg = "ES256" + XCTAssertEqual(decodedJWT.claims, jwt.claims) + XCTAssertEqual(decodedJWT.header, jwt.header) + } catch { + XCTFail("Failed to encode JTW: \(error)") + } + } - // // 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()) - // 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) - // jwt.header.alg = "RS256" - // XCTAssertEqual(decodedJWT.claims, jwt.claims) - // XCTAssertEqual(decodedJWT.header, jwt.header) - // } catch { - // XCTFail("Failed to encode JTW: \(error)") - // } - // } + // 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()) + jwt.claims.sub = "1234567890" + jwt.claims.name = "John Doe" + jwt.claims.admin = true + do { + let jwtData = try ecdsaJWTEncoder.encode(jwt) + let decodedJWT = try ecdsaJWTDecoder.decode(JWT.self, from: jwtData) + jwt.header.alg = "ES256" + XCTAssertEqual(decodedJWT.claims, jwt.claims) + XCTAssertEqual(decodedJWT.header, jwt.header) + } catch { + XCTFail("Failed to encode JTW: \(error)") + } + } - // // 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()) - // 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) - // jwt.header.alg = "RS256" - // XCTAssertEqual(jwt.claims, decodedJWTString.claims) - // XCTAssertEqual(jwt.header, decodedJWTString.header) - // } catch { - // XCTFail("Failed to encode JTW: \(error)") - // } - // } +// // 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()) +// 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) +// jwt.header.alg = "RS256" +// XCTAssertEqual(jwt.claims, decodedJWTString.claims) +// XCTAssertEqual(jwt.header, decodedJWTString.header) +// } catch { +// XCTFail("Failed to encode JTW: \(error)") +// } +// } // // 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. @@ -560,7 +568,7 @@ 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: ec256PublicKey)) XCTAssertTrue(ok, "Verification failed") if let decoded = try? JWT(jwtString: ecdsaEncodedTestClaimJWT) { diff --git a/Tests/SwiftJWTTests/ecdsa_private_key b/Tests/SwiftJWTTests/ec256_private_key similarity index 100% rename from Tests/SwiftJWTTests/ecdsa_private_key rename to Tests/SwiftJWTTests/ec256_private_key diff --git a/Tests/SwiftJWTTests/ecdsa_public_key b/Tests/SwiftJWTTests/ec256_public_key similarity index 100% rename from Tests/SwiftJWTTests/ecdsa_public_key rename to Tests/SwiftJWTTests/ec256_public_key From d8b75e83ee495bf7589a394761e917b920a30f9c Mon Sep 17 00:00:00 2001 From: Tom Knapen Date: Sun, 6 Aug 2023 16:08:46 +0200 Subject: [PATCH 4/6] rename incorrectly named `digest` parameter to `data` --- Sources/SwiftJWT/SwiftCryptoECDSA.swift | 18 +++++++++--------- Sources/SwiftJWT/SwiftCryptoHMAC.swift | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/SwiftJWT/SwiftCryptoECDSA.swift b/Sources/SwiftJWT/SwiftCryptoECDSA.swift index 694da34..236c513 100644 --- a/Sources/SwiftJWT/SwiftCryptoECDSA.swift +++ b/Sources/SwiftJWT/SwiftCryptoECDSA.swift @@ -51,7 +51,7 @@ extension SwiftCryptoECDSA { } do { - return try algorithm.verify(signature: signature, digest: data, publicKey: publicKey) + return try algorithm.verify(signature: signature, data: data, publicKey: publicKey) } catch { Log.error("Verification failed: \(error)") return false @@ -69,36 +69,36 @@ extension SwiftCryptoECDSA { } private extension SwiftCryptoECDSA.Algorithm { - func verify(signature: Data, digest: Data, publicKey: String) throws -> Bool { + func verify(signature: Data, data: Data, publicKey: String) throws -> Bool { switch self { case .es256: let signature = try P256.Signing.ECDSASignature(rawRepresentation: signature) let publicKey = try P256.Signing.PublicKey(pemRepresentation: publicKey) - return publicKey.isValidSignature(signature, for: digest) + return publicKey.isValidSignature(signature, for: data) case .es384: let signature = try P384.Signing.ECDSASignature(rawRepresentation: signature) let publicKey = try P384.Signing.PublicKey(pemRepresentation: publicKey) - return publicKey.isValidSignature(signature, for: digest) + return publicKey.isValidSignature(signature, for: data) case .es512: let signature = try P521.Signing.ECDSASignature(rawRepresentation: signature) let publicKey = try P521.Signing.PublicKey(pemRepresentation: publicKey) - return publicKey.isValidSignature(signature, for: digest) + return publicKey.isValidSignature(signature, for: data) } } - func signature(for digest: Data, privateKey: String) throws -> Data { + func signature(for data: Data, privateKey: String) throws -> Data { switch self { case .es256: let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKey) - let signedData = try privateKey.signature(for: digest) + let signedData = try privateKey.signature(for: data) return signedData.rawRepresentation case .es384: let privateKey = try P384.Signing.PrivateKey(pemRepresentation: privateKey) - let signedData = try privateKey.signature(for: digest) + let signedData = try privateKey.signature(for: data) return signedData.rawRepresentation case .es512: let privateKey = try P521.Signing.PrivateKey(pemRepresentation: privateKey) - let signedData = try privateKey.signature(for: digest) + let signedData = try privateKey.signature(for: data) return signedData.rawRepresentation } } diff --git a/Sources/SwiftJWT/SwiftCryptoHMAC.swift b/Sources/SwiftJWT/SwiftCryptoHMAC.swift index 145ded8..63d366e 100644 --- a/Sources/SwiftJWT/SwiftCryptoHMAC.swift +++ b/Sources/SwiftJWT/SwiftCryptoHMAC.swift @@ -43,7 +43,7 @@ extension SwiftCryptoHMAC { private func verify(signature: Data, for data: Data) -> Bool { do { - return try algorithm.verify(signature: signature, digest: data, key: key) + return try algorithm.verify(signature: signature, data: data, key: key) } catch { Log.error("Verification failed: \(error)") return false @@ -57,31 +57,31 @@ extension SwiftCryptoHMAC { } private extension SwiftCryptoHMAC.Algorithm { - func verify(signature: Data, digest: Data, key: Data) throws -> Bool { + func verify(signature: Data, data: Data, key: Data) throws -> Bool { let key = SymmetricKey(data: key) switch self { case .hs256: - return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) + return HMAC.isValidAuthenticationCode(signature, authenticating: data, using: key) case .hs384: - return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) + return HMAC.isValidAuthenticationCode(signature, authenticating: data, using: key) case .hs512: - return HMAC.isValidAuthenticationCode(signature, authenticating: digest, using: key) + return HMAC.isValidAuthenticationCode(signature, authenticating: data, using: key) } } - func signature(for digest: Data, key: Data) throws -> Data { + func signature(for data: Data, key: Data) throws -> Data { let key = SymmetricKey(data: key) switch self { case .hs256: - let mac = HMAC.authenticationCode(for: digest, using: key) + let mac = HMAC.authenticationCode(for: data, using: key) return Data(mac) case .hs384: - let mac = HMAC.authenticationCode(for: digest, using: key) + let mac = HMAC.authenticationCode(for: data, using: key) return Data(mac) case .hs512: - let mac = HMAC.authenticationCode(for: digest, using: key) + let mac = HMAC.authenticationCode(for: data, using: key) return Data(mac) } } From 99430e9f5575a45ff79247846333fa6d175a0df7 Mon Sep 17 00:00:00 2001 From: Tom Knapen Date: Sun, 3 Mar 2024 14:21:42 +0100 Subject: [PATCH 5/6] add RSA support - RSA with either public key or private key is supported - RSA with certificate is not supported --- Package.swift | 56 ++- Sources/SwiftJWT/JWTSigner.swift | 24 +- Sources/SwiftJWT/JWTVerifier.swift | 24 +- Sources/SwiftJWT/SwiftCryptoRSA.swift | 142 ++++++++ Tests/SwiftJWTTests/TestJWT.swift | 491 +++++++++++++------------- 5 files changed, 434 insertions(+), 303 deletions(-) create mode 100644 Sources/SwiftJWT/SwiftCryptoRSA.swift diff --git a/Package.swift b/Package.swift index 87be766..530f687 100644 --- a/Package.swift +++ b/Package.swift @@ -18,36 +18,32 @@ import PackageDescription - -let targetDependencies: [Target.Dependency] - let package = Package( - name: "SwiftJWT", - platforms: [ - .iOS("14.0"), - .macOS("11.0"), - ], - products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. - .library( - name: "SwiftJWT", - targets: ["SwiftJWT"] - ) - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMajor(from: "2.0.0")), - .package(url: "https://github.com/Kitura/LoggerAPI.git", from: "2.0.0"), - .package(url: "https://github.com/Kitura/KituraContracts.git", from: "2.0.1"), - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.4"), - ], - targets: [ - .target(name: "SwiftJWT", dependencies: [ - "LoggerAPI", - "KituraContracts", - .product(name: "Crypto", package: "swift-crypto") - ]), - .testTarget(name: "SwiftJWTTests", dependencies: ["SwiftJWT"]) + name: "SwiftJWT", + platforms: [ + .iOS("14.0"), + .macOS("11.0") + ], + products: [ + // Products define the executables and libraries produced by a package, and make them visible to other packages. + .library( + name: "SwiftJWT", + targets: ["SwiftJWT"] + ) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-crypto.git", from: "2.6.0"), + .package(url: "https://github.com/Kitura/LoggerAPI.git", from: "2.0.0"), + .package(url: "https://github.com/Kitura/KituraContracts.git", from: "2.0.1"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.4") + ], + targets: [ + .target(name: "SwiftJWT", dependencies: [ + "LoggerAPI", + "KituraContracts", + .product(name: "Crypto", package: "swift-crypto"), + .product(name: "_CryptoExtras", package: "swift-crypto") + ]), + .testTarget(name: "SwiftJWTTests", dependencies: ["SwiftJWT"]) ] ) - - diff --git a/Sources/SwiftJWT/JWTSigner.swift b/Sources/SwiftJWT/JWTSigner.swift index b7a2947..9e0b9f6 100644 --- a/Sources/SwiftJWT/JWTSigner.swift +++ b/Sources/SwiftJWT/JWTSigner.swift @@ -66,39 +66,39 @@ public struct JWTSigner { /// Initialize a JWTSigner using the RSA 256 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func rs256(privateKey _: Data) -> JWTSigner { - preconditionFailure("not implemented") + public static func rs256(privateKey: Data) -> JWTSigner { + JWTSigner(name: "RS256", signerAlgorithm: SwiftCryptoRSA(privateKey: privateKey, algorithm: .rs256)) } /// Initialize a JWTSigner using the RSA 384 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func rs384(privateKey _: Data) -> JWTSigner { - preconditionFailure("not implemented") + public static func rs384(privateKey: Data) -> JWTSigner { + JWTSigner(name: "RS384", signerAlgorithm: SwiftCryptoRSA(privateKey: privateKey, algorithm: .rs384)) } /// Initialize a JWTSigner using the RSA 512 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func rs512(privateKey _: Data) -> JWTSigner { - preconditionFailure("not implemented") + public static func rs512(privateKey: Data) -> JWTSigner { + JWTSigner(name: "RS512", signerAlgorithm: SwiftCryptoRSA(privateKey: privateKey, algorithm: .rs512)) } /// Initialize a JWTSigner using the RSA-PSS 256 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func ps256(privateKey _: Data) -> JWTSigner { - preconditionFailure("not implemented") + public static func ps256(privateKey: Data) -> JWTSigner { + JWTSigner(name: "PS256", signerAlgorithm: SwiftCryptoRSA(privateKey: privateKey, algorithm: .ps256)) } /// Initialize a JWTSigner using the RSA-PSS 384 bits algorithm and the provided privateKey. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func ps384(privateKey _: Data) -> JWTSigner { - preconditionFailure("not implemented") + public static func ps384(privateKey: Data) -> JWTSigner { + JWTSigner(name: "PS384", signerAlgorithm: SwiftCryptoRSA(privateKey: privateKey, algorithm: .ps384)) } /// Initialize a JWTSigner using the RSA-PSS 512 bits algorithm and the provided privateKey. /// This signer requires at least a 2048 bit RSA key. /// - Parameter privateKey: The UTF8 encoded PEM private key, with a "BEGIN RSA PRIVATE KEY" header. - public static func ps512(privateKey _: Data) -> JWTSigner { - preconditionFailure("not implemented") + public static func ps512(privateKey: Data) -> JWTSigner { + JWTSigner(name: "PS512", signerAlgorithm: SwiftCryptoRSA(privateKey: privateKey, algorithm: .ps512)) } /// Initialize a JWTSigner using the HMAC 256 bits algorithm and the provided privateKey. diff --git a/Sources/SwiftJWT/JWTVerifier.swift b/Sources/SwiftJWT/JWTVerifier.swift index 904ce78..f123057 100644 --- a/Sources/SwiftJWT/JWTVerifier.swift +++ b/Sources/SwiftJWT/JWTVerifier.swift @@ -55,20 +55,20 @@ public struct JWTVerifier { /// Initialize a JWTVerifier using the RSA 256 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func rs256(publicKey _: Data) -> JWTVerifier { - preconditionFailure("not implemented") + public static func rs256(publicKey: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoRSA(publicKey: publicKey, algorithm: .rs256)) } /// Initialize a JWTVerifier using the RSA 384 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func rs384(publicKey _: Data) -> JWTVerifier { - preconditionFailure("not implemented") + public static func rs384(publicKey: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoRSA(publicKey: publicKey, algorithm: .rs384)) } /// Initialize a JWTVerifier using the RSA 512 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func rs512(publicKey _: Data) -> JWTVerifier { - preconditionFailure("not implemented") + public static func rs512(publicKey: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoRSA(publicKey: publicKey, algorithm: .rs512)) } /// Initialize a JWTVerifier using the RSA 256 bits algorithm and the provided certificate. @@ -91,21 +91,21 @@ public struct JWTVerifier { /// Initialize a JWTVerifier using the RSA-PSS 256 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func ps256(publicKey _: Data) -> JWTVerifier { - preconditionFailure("not implemented") + public static func ps256(publicKey: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoRSA(publicKey: publicKey, algorithm: .ps256)) } /// Initialize a JWTVerifier using the RSA-PSS 384 bits algorithm and the provided publicKey. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func ps384(publicKey _: Data) -> JWTVerifier { - preconditionFailure("not implemented") + public static func ps384(publicKey: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoRSA(publicKey: publicKey, algorithm: .ps384)) } /// Initialize a JWTVerifier using the RSA-PSS 512 bits algorithm and the provided publicKey. /// This verifier requires at least a 2048 bit RSA key. /// - Parameter publicKey: The UTF8 encoded PEM public key, with a "BEGIN PUBLIC KEY" header. - public static func ps512(publicKey _: Data) -> JWTVerifier { - preconditionFailure("not implemented") + public static func ps512(publicKey: Data) -> JWTVerifier { + JWTVerifier(verifierAlgorithm: SwiftCryptoRSA(publicKey: publicKey, algorithm: .ps512)) } /// Initialize a JWTSigner using the HMAC 256 bits algorithm and the provided privateKey. diff --git a/Sources/SwiftJWT/SwiftCryptoRSA.swift b/Sources/SwiftJWT/SwiftCryptoRSA.swift new file mode 100644 index 0000000..74537d0 --- /dev/null +++ b/Sources/SwiftJWT/SwiftCryptoRSA.swift @@ -0,0 +1,142 @@ +import _CryptoExtras +import Crypto +import Foundation +import LoggerAPI + +struct SwiftCryptoRSA: VerifierAlgorithm, SignerAlgorithm { + private let key: Key + private let algorithm: Algorithm + + fileprivate enum Key { + case privateKey(Data) + case publicKey(Data) + } + + enum Algorithm { + case rs256, rs384, rs512 + case ps256, ps384, ps512 + } + + init(publicKey: Data, algorithm: Algorithm) { + key = .publicKey(publicKey) + self.algorithm = algorithm + } + + init(privateKey: Data, algorithm: Algorithm) { + key = .privateKey(privateKey) + self.algorithm = algorithm + } + + func verify(jwt: String) -> Bool { + verify(jwt) + } + + func sign(header: String, claims: String) throws -> String { + let unsignedJWT = header + "." + claims + let signature = try sign(data: Data(unsignedJWT.utf8)) + return header + "." + claims + "." + signature + } +} + +extension SwiftCryptoRSA { + private func verify(_ jwt: String) -> Bool { + let components = jwt.components(separatedBy: ".") + if components.count == 3 { + guard let signature = JWTDecoder.data(base64urlEncoded: components[2]), + let jwtData = (components[0] + "." + components[1]).data(using: .utf8) + else { + return false + } + return verify(signature: signature, for: jwtData) + } else { + return false + } + } + + private func verify(signature: Data, for data: Data) -> Bool { + do { + return try algorithm.verify(signature: signature, data: data, key: key) + } catch { + Log.error("Verification failed: \(error)") + return false + } + } + + private func sign(data: Data) throws -> String { + let signedData = try algorithm.signature(for: data, key: key) + return JWTEncoder.base64urlEncodedString(data: signedData) + } +} + +private extension SwiftCryptoRSA.Algorithm { + func verify(signature: Data, data: Data, key: SwiftCryptoRSA.Key) throws -> Bool { + let publicKey = try publicKey(from: key) + let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) + return publicKey.isValidSignature(signature, for: data, padding: padding) + } + + func signature(for data: Data, key: SwiftCryptoRSA.Key) throws -> Data { + let privateKey = try privateKey(from: key) + let signature = try privateKey.signature(for: data, padding: padding) + return signature.rawRepresentation + } +} + +private extension SwiftCryptoRSA.Algorithm { + var padding: _RSA.Signing.Padding { + switch self { + case .rs256, .rs384, .rs512: + return .insecurePKCS1v1_5 + case .ps256, .ps384, .ps512: + return .PSS + } + } + + func publicKey(from key: SwiftCryptoRSA.Key) throws -> _RSA.Signing.PublicKey { + switch key { + case .privateKey(let data): + let der = (try? pem2der(data)) ?? data + return try _RSA.Signing.PrivateKey(derRepresentation: der).publicKey + case .publicKey(let data): + let der = (try? pem2der(data)) ?? data + return try .init(derRepresentation: der) + } + } + + func privateKey(from key: SwiftCryptoRSA.Key) throws -> _RSA.Signing.PrivateKey { + switch key { + case .privateKey(let data): + let der = (try? pem2der(data)) ?? data + return try .init(derRepresentation: der) + case .publicKey(let data): + let der = (try? pem2der(data)) ?? data + return try .init(derRepresentation: der) + } + } + + private var keySize: Int { + switch self { + case .rs256, .ps256: + return 256 + case .rs384, .ps384: + return 384 + case .rs512, .ps512: + return 512 + } + } +} + +private func pem2der(_ pem: Data) throws -> Data { + guard let pem = String(data: pem, encoding: .utf8) else { + throw JWTError.invalidUTF8Data + } + let strippedKey = String(pem.filter { !" \n\t\r".contains($0) }) + let pemComponents = strippedKey.components(separatedBy: "-----") + guard pemComponents.count >= 5 else { + throw JWTError.missingPEMHeaders + } + guard let der = Data(base64Encoded: pemComponents[2]) else { + throw JWTError.invalidPrivateKey + } + return der +} diff --git a/Tests/SwiftJWTTests/TestJWT.swift b/Tests/SwiftJWTTests/TestJWT.swift index 0f3b1e7..69d18a0 100644 --- a/Tests/SwiftJWTTests/TestJWT.swift +++ b/Tests/SwiftJWTTests/TestJWT.swift @@ -111,12 +111,12 @@ class TestJWT: XCTestCase { static var allTests: [(String, (TestJWT) -> () throws -> Void)] { [ ("testSignAndVerify", testSignAndVerify), - // ("testSignAndVerifyRSA", testSignAndVerifyRSA), - // ("testSignAndVerifyRSAPSS", testSignAndVerifyRSAPSS), - // ("testSignAndVerifyCert", testSignAndVerifyCert), - // ("testSignAndVerifyRSA384", testSignAndVerifyRSA384), - // ("testSignAndVerifyRSAPSS384", testSignAndVerifyRSAPSS384), - // ("testSignAndVerifyCert384", testSignAndVerifyCert384), + ("testSignAndVerifyRSA", testSignAndVerifyRSA), + ("testSignAndVerifyRSAPSS", testSignAndVerifyRSAPSS), +// ("testSignAndVerifyCert", testSignAndVerifyCert), + ("testSignAndVerifyRSA384", testSignAndVerifyRSA384), + ("testSignAndVerifyRSAPSS384", testSignAndVerifyRSAPSS384), +// ("testSignAndVerifyCert384", testSignAndVerifyCert384), ("testSignAndVerifyHMAC", testSignAndVerifyHMAC), ("testSignAndVerifyHMAC384", testSignAndVerifyHMAC384), ("testSignAndVerifyHMAC512", testSignAndVerifyHMAC512), @@ -124,23 +124,23 @@ class TestJWT: XCTestCase { ("testSignAndVerifyECDSA256", testSignAndVerifyECDSA256), ("testSignAndVerifyECDSA384", testSignAndVerifyECDSA384), ("testSignAndVerifyECDSA512", testSignAndVerifyECDSA512), - // ("testSignAndVerifyRSA512", testSignAndVerifyRSA512), - // ("testSignAndVerifyRSAPSS512", testSignAndVerifyRSAPSS512), - // ("testSignAndVerifyCert512", testSignAndVerifyCert512), + ("testSignAndVerifyRSA512", testSignAndVerifyRSA512), + ("testSignAndVerifyRSAPSS512", testSignAndVerifyRSAPSS512), +// ("testSignAndVerifyCert512", testSignAndVerifyCert512), ("testJWTEncoder", testJWTEncoder), ("testJWTDecoder", testJWTDecoder), ("testJWTCoderCycle", testJWTCoderCycle), -// ("testJWTEncoderKeyID", testJWTEncoderKeyID), -// ("testJWTDecoderKeyID", testJWTDecoderKeyID), -// ("testJWTCoderCycleKeyID", testJWTCoderCycleKeyID), - // ("testJWT", testJWT), - // ("testJWTRSAPSS", testJWTRSAPSS), + ("testJWTEncoderKeyID", testJWTEncoderKeyID), +// ("testJWTDecoderKeyID", testJWTDecoderKeyID), +// ("testJWTCoderCycleKeyID", testJWTCoderCycleKeyID), + ("testJWT", testJWT), + ("testJWTRSAPSS", testJWTRSAPSS), ("testJWTUsingHMAC", testJWTUsingHMAC), ("testJWTUsingECDSA", testJWTUsingECDSA), - // ("testMicroProfile", testMicroProfile), +// ("testMicroProfile", testMicroProfile), ("testValidateClaims", testValidateClaims), ("testValidateClaimsLeeway", testValidateClaimsLeeway), - // ("testErrorPattenMatching", testErrorPattenMatching), + ("testErrorPattenMatching", testErrorPattenMatching), ("testTypeErasedErrorLocalizedDescription", testTypeErasedErrorLocalizedDescription) ] } @@ -153,39 +153,39 @@ class TestJWT: XCTestCase { } } - // func testSignAndVerifyRSA() { - // do { - // try signAndVerify(signer: .rs256(privateKey: rsaPrivateKey), verifier: .rs256(publicKey: rsaPublicKey)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } - - // func testSignAndVerifyRSADERKey() { - // do { - // try signAndVerify(signer: .rs256(privateKey: rsaDERPrivateKey), verifier: .rs256(publicKey: rsaDERPublicKey)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } - - // func testSignAndVerifyRSAPSS() { - // if #available(OSX 10.13, *) { - // do { - // try signAndVerify(signer: .ps256(privateKey: rsaPrivateKey), verifier: .ps256(publicKey: rsaPublicKey)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } - // } - - // func testSignAndVerifyCert() { - // do { - // try signAndVerify(signer: .rs256(privateKey: certPrivateKey), verifier: .rs256(certificate: certificate)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } + func testSignAndVerifyRSA() { + do { + try signAndVerify(signer: .rs256(privateKey: rsaPrivateKey), verifier: .rs256(publicKey: rsaPublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + + func testSignAndVerifyRSADERKey() { + do { + try signAndVerify(signer: .rs256(privateKey: rsaDERPrivateKey), verifier: .rs256(publicKey: rsaDERPublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + + func testSignAndVerifyRSAPSS() { + if #available(OSX 10.13, *) { + do { + try signAndVerify(signer: .ps256(privateKey: rsaPrivateKey), verifier: .ps256(publicKey: rsaPublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + } + +// func testSignAndVerifyCert() { +// do { +// try signAndVerify(signer: .rs256(privateKey: certPrivateKey), verifier: .rs256(certificate: certificate)) +// } catch { +// XCTFail("testSignAndVerify failed: \(error)") +// } +// } func testSignAndVerifyHMAC() { do { @@ -206,31 +206,31 @@ class TestJWT: XCTestCase { } } - // func testSignAndVerifyRSA384() { - // do { - // try signAndVerify(signer: .rs384(privateKey: rsaPrivateKey), verifier: .rs384(publicKey: rsaPublicKey)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } -// - // func testSignAndVerifyRSAPSS384() { - // if #available(OSX 10.13, *) { - // do { - // try signAndVerify(signer: .ps384(privateKey: rsaPrivateKey), verifier: .ps384(publicKey: rsaPublicKey)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } - // } - - // func testSignAndVerifyCert384() { - // do { - // try signAndVerify(signer: .rs384(privateKey: certPrivateKey), verifier: .rs384(certificate: certificate)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } + func testSignAndVerifyRSA384() { + do { + try signAndVerify(signer: .rs384(privateKey: rsaPrivateKey), verifier: .rs384(publicKey: rsaPublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + + func testSignAndVerifyRSAPSS384() { + if #available(OSX 10.13, *) { + do { + try signAndVerify(signer: .ps384(privateKey: rsaPrivateKey), verifier: .ps384(publicKey: rsaPublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + } + +// func testSignAndVerifyCert384() { +// do { +// try signAndVerify(signer: .rs384(privateKey: certPrivateKey), verifier: .rs384(certificate: certificate)) +// } catch { +// XCTFail("testSignAndVerify failed: \(error)") +// } +// } func testSignAndVerifyHMAC384() { do { @@ -257,31 +257,31 @@ class TestJWT: XCTestCase { } } - // func testSignAndVerifyRSA512() { - // do { - // try signAndVerify(signer: .rs512(privateKey: rsaPrivateKey), verifier: .rs512(publicKey: rsaPublicKey)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } - - // func testSignAndVerifyRSAPSS512() { - // if #available(OSX 10.13, iOS 11, *) { - // do { - // try signAndVerify(signer: .ps512(privateKey: rsaPrivateKey), verifier: .ps512(publicKey: rsaPublicKey)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } - // } - - // func testSignAndVerifyCert512() { - // do { - // try signAndVerify(signer: .rs512(privateKey: certPrivateKey), verifier: .rs512(certificate: certificate)) - // } catch { - // XCTFail("testSignAndVerify failed: \(error)") - // } - // } + func testSignAndVerifyRSA512() { + do { + try signAndVerify(signer: .rs512(privateKey: rsaPrivateKey), verifier: .rs512(publicKey: rsaPublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + + func testSignAndVerifyRSAPSS512() { + if #available(OSX 10.13, iOS 11, *) { + do { + try signAndVerify(signer: .ps512(privateKey: rsaPrivateKey), verifier: .ps512(publicKey: rsaPublicKey)) + } catch { + XCTFail("testSignAndVerify failed: \(error)") + } + } + } + +// func testSignAndVerifyCert512() { +// do { +// try signAndVerify(signer: .rs512(privateKey: certPrivateKey), verifier: .rs512(certificate: certificate)) +// } catch { +// XCTFail("testSignAndVerify failed: \(error)") +// } +// } func testSignAndVerifyHMAC512() { do { @@ -335,51 +335,48 @@ class TestJWT: XCTestCase { XCTAssertEqual(jwt.claims.groups ?? [""], ["red-group", "green-group", "admin-group", "admin"], "Wrong .groups in decoded") } - // func testMicroProfile() { - // var jwt = JWT(claims: MicroProfile(name: "MP-JWT")) - // jwt.header.kid = "abc-1234567890" - // jwt.header.typ = "JWT" - // XCTAssertEqual(jwt.claims.name, "MP-JWT") - // jwt.claims.iss = "https://server.example.com" - // jwt.claims.aud = ["clientID"] - // jwt.claims.iat = Date(timeIntervalSince1970: 1485949565.58463) - // jwt.claims.exp = Date(timeIntervalSince1970: 2485949565.58463) - // jwt.claims.upn = "jdoe@server.example.com" - // jwt.claims.groups = ["red-group", "green-group", "admin-group", "admin"] +// func testMicroProfile() { +// var jwt = JWT(claims: MicroProfile(name: "MP-JWT")) +// jwt.header.kid = "abc-1234567890" +// jwt.header.typ = "JWT" +// XCTAssertEqual(jwt.claims.name, "MP-JWT") +// jwt.claims.iss = "https://server.example.com" +// jwt.claims.aud = ["clientID"] +// jwt.claims.iat = Date(timeIntervalSince1970: 1_485_949_565.58463) +// jwt.claims.exp = Date(timeIntervalSince1970: 2_485_949_565.58463) +// jwt.claims.upn = "jdoe@server.example.com" +// jwt.claims.groups = ["red-group", "green-group", "admin-group", "admin"] // - // // 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)) - // XCTAssertTrue(ok, "Verification failed") +// // 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)) +// XCTAssertTrue(ok, "Verification failed") // - // if let decoded = try? JWT(jwtString: signed) { - // checkMicroProfile(jwt: decoded, algorithm: "RS256") +// if let decoded = try? JWT(jwtString: signed) { +// checkMicroProfile(jwt: decoded, algorithm: "RS256") // - // XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - // } - // else { - // XCTFail("Failed to decode") - // } - // } - // else { - // XCTFail("Failed to sign") - // } +// XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") +// } else { +// XCTFail("Failed to decode") +// } +// } else { +// XCTFail("Failed to sign") +// } // - // // certificate - // if let signed = try? jwt.sign(using: .rs256(privateKey: certPrivateKey)) { - // let ok = JWT.verify(signed, using: .rs256(certificate: certificate)) - // XCTAssertTrue(ok, "Verification failed") +// // certificate +// if let signed = try? jwt.sign(using: .rs256(privateKey: certPrivateKey)) { +// let ok = JWT.verify(signed, using: .rs256(certificate: certificate)) +// XCTAssertTrue(ok, "Verification failed") // - // if let decoded = try? JWT(jwtString: signed) { - // checkMicroProfile(jwt: decoded, algorithm: "RS256") +// if let decoded = try? JWT(jwtString: signed) { +// checkMicroProfile(jwt: decoded, algorithm: "RS256") // - // XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - // } - // else { - // XCTFail("Failed to decode") - // } - // } - // } +// XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") +// } else { +// XCTFail("Failed to decode") +// } +// } +// } // 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. @@ -441,108 +438,104 @@ class TestJWT: XCTestCase { } } -// // 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()) -// 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) -// jwt.header.alg = "RS256" -// XCTAssertEqual(jwt.claims, decodedJWTString.claims) -// XCTAssertEqual(jwt.header, decodedJWTString.header) -// } catch { -// XCTFail("Failed to encode JTW: \(error)") -// } -// } - - // // 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()) - // 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) - // jwt.header.alg = "RS256" - // XCTAssertEqual(decodedJWT.claims, jwt.claims) - // XCTAssertEqual(decodedJWT.header, jwt.header) - // } catch { - // XCTFail("Failed to encode JTW: \(error)") - // } - // } - - // // 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()) - // 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) - // jwt.header.alg = "RS256" - // XCTAssertEqual(decodedJWT.claims, jwt.claims) - // XCTAssertEqual(decodedJWT.header, jwt.header) - // } catch { - // XCTFail("Failed to encode JTW: \(error)") - // } - // } - - // // From jwt.io - // func testJWT() { - // let ok = JWT.verify(rsaEncodedTestClaimJWT, using: .rs256(publicKey: rsaPublicKey)) - // XCTAssertTrue(ok, "Verification failed") -// - // 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") -// - // XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") - // XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") - // XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") - // XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1516239022), "Wrong .iat in decoded") -// -// - // XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - // } - // else { - // XCTFail("Failed to decode") - // } - // } - - // // From jwt.io - // func testJWTRSAPSS() { - // if #available(OSX 10.13, *) { - // let ok = JWT.verify(rsaPSSEncodedTestClaimJWT, using: .ps256(publicKey: rsaPublicKey)) - // XCTAssertTrue(ok, "Verification failed") -// - // 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") -// - // XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") - // XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") - // XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") - // XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1516239022), "Wrong .iat in decoded") -// -// - // XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") - // } - // else { - // XCTFail("Failed to decode") - // } - // } - // } + // 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()) + 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) + jwt.header.alg = "RS256" + XCTAssertEqual(jwt.claims, decodedJWTString.claims) + XCTAssertEqual(jwt.header, decodedJWTString.header) + } catch { + XCTFail("Failed to encode JTW: \(error)") + } + } + +// // 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()) +// 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) +// jwt.header.alg = "RS256" +// XCTAssertEqual(decodedJWT.claims, jwt.claims) +// XCTAssertEqual(decodedJWT.header, jwt.header) +// } catch { +// XCTFail("Failed to encode JTW: \(error)") +// } +// } + +// // 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()) +// 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) +// jwt.header.alg = "RS256" +// XCTAssertEqual(decodedJWT.claims, jwt.claims) +// XCTAssertEqual(decodedJWT.header, jwt.header) +// } catch { +// XCTFail("Failed to encode JTW: \(error)") +// } +// } + + // From jwt.io + func testJWT() { + let ok = JWT.verify(rsaEncodedTestClaimJWT, using: .rs256(publicKey: rsaPublicKey)) + XCTAssertTrue(ok, "Verification failed") + + 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") + + XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") + XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") + XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") + XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1_516_239_022), "Wrong .iat in decoded") + + XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") + } else { + XCTFail("Failed to decode") + } + } + + // From jwt.io + func testJWTRSAPSS() { + if #available(OSX 10.13, *) { + let ok = JWT.verify(rsaPSSEncodedTestClaimJWT, using: .ps256(publicKey: rsaPublicKey)) + XCTAssertTrue(ok, "Verification failed") + + 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") + + XCTAssertEqual(decoded.claims.sub, "1234567890", "Wrong .sub in decoded") + XCTAssertEqual(decoded.claims.name, "John Doe", "Wrong .name in decoded") + XCTAssertEqual(decoded.claims.admin, true, "Wrong .admin in decoded") + XCTAssertEqual(decoded.claims.iat, Date(timeIntervalSince1970: 1_516_239_022), "Wrong .iat in decoded") + + XCTAssertEqual(decoded.validateClaims(), .success, "Validation failed") + } else { + XCTFail("Failed to decode") + } + } + } func testJWTUsingHMAC() { guard let hmacData = "Super Secret Key".data(using: .utf8) else { @@ -611,15 +604,15 @@ class TestJWT: XCTestCase { XCTAssertEqual(jwt.validateClaims(leeway: 20), .success, "Validation failed") } - // func testErrorPattenMatching() { - // do { - // let _ = try JWT(jwtString: "InvalidString", verifier: .rs256(publicKey: rsaPublicKey)) - // } catch JWTError.invalidJWTString { - // // Caught correct error - // } catch { - // XCTFail("Incorrect error thrown: \(error)") - // } - // } + func testErrorPattenMatching() { + do { + let _ = try JWT(jwtString: "InvalidString", verifier: .rs256(publicKey: rsaPublicKey)) + } catch JWTError.invalidJWTString { + // Caught correct error + } catch { + XCTFail("Incorrect error thrown: \(error)") + } + } func testTypeErasedErrorLocalizedDescription() { let error = JWTError.invalidJWTString From 0a45253df726b0a655a038be2bac5159d0b92051 Mon Sep 17 00:00:00 2001 From: Tom Knapen Date: Sun, 3 Mar 2024 14:21:49 +0100 Subject: [PATCH 6/6] swiftformat --- Sources/SwiftJWT/JWTVerifier.swift | 4 ++-- Sources/SwiftJWT/SwiftCryptoECDSA.swift | 10 +++++----- Sources/SwiftJWT/SwiftCryptoHMAC.swift | 10 +++++----- Sources/SwiftJWT/VerifierAlgorithm.swift | 2 +- Tests/LinuxMain.swift | 5 ++--- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Sources/SwiftJWT/JWTVerifier.swift b/Sources/SwiftJWT/JWTVerifier.swift index f123057..d237656 100644 --- a/Sources/SwiftJWT/JWTVerifier.swift +++ b/Sources/SwiftJWT/JWTVerifier.swift @@ -45,11 +45,11 @@ import Foundation public struct JWTVerifier { private let verifierAlgorithm: VerifierAlgorithm - internal init(verifierAlgorithm: VerifierAlgorithm) { + init(verifierAlgorithm: VerifierAlgorithm) { self.verifierAlgorithm = verifierAlgorithm } - internal func verify(jwt: String) -> Bool { + func verify(jwt: String) -> Bool { verifierAlgorithm.verify(jwt: jwt) } diff --git a/Sources/SwiftJWT/SwiftCryptoECDSA.swift b/Sources/SwiftJWT/SwiftCryptoECDSA.swift index 236c513..aab99f6 100644 --- a/Sources/SwiftJWT/SwiftCryptoECDSA.swift +++ b/Sources/SwiftJWT/SwiftCryptoECDSA.swift @@ -2,24 +2,24 @@ import Crypto import Foundation import LoggerAPI -internal struct SwiftCryptoECDSA: VerifierAlgorithm, SignerAlgorithm { +struct SwiftCryptoECDSA: VerifierAlgorithm, SignerAlgorithm { private let key: Data private let algorithm: Algorithm - internal enum Algorithm { + enum Algorithm { case es256, es384, es512 } - internal init(key: Data, algorithm: Algorithm) { + init(key: Data, algorithm: Algorithm) { self.key = key self.algorithm = algorithm } - internal func verify(jwt: String) -> Bool { + func verify(jwt: String) -> Bool { verify(jwt) } - internal func sign(header: String, claims: String) throws -> String { + func sign(header: String, claims: String) throws -> String { let unsignedJWT = header + "." + claims let signature = try sign(data: Data(unsignedJWT.utf8)) return header + "." + claims + "." + signature diff --git a/Sources/SwiftJWT/SwiftCryptoHMAC.swift b/Sources/SwiftJWT/SwiftCryptoHMAC.swift index 63d366e..399362e 100644 --- a/Sources/SwiftJWT/SwiftCryptoHMAC.swift +++ b/Sources/SwiftJWT/SwiftCryptoHMAC.swift @@ -2,24 +2,24 @@ import Crypto import Foundation import LoggerAPI -internal struct SwiftCryptoHMAC: VerifierAlgorithm, SignerAlgorithm { +struct SwiftCryptoHMAC: VerifierAlgorithm, SignerAlgorithm { private let key: Data private let algorithm: Algorithm - internal enum Algorithm { + enum Algorithm { case hs256, hs384, hs512 } - internal init(key: Data, algorithm: Algorithm) { + init(key: Data, algorithm: Algorithm) { self.key = key self.algorithm = algorithm } - internal func verify(jwt: String) -> Bool { + func verify(jwt: String) -> Bool { verify(jwt) } - internal func sign(header: String, claims: String) throws -> String { + func sign(header: String, claims: String) throws -> String { let unsignedJWT = header + "." + claims let signature = try sign(data: Data(unsignedJWT.utf8)) return header + "." + claims + "." + signature diff --git a/Sources/SwiftJWT/VerifierAlgorithm.swift b/Sources/SwiftJWT/VerifierAlgorithm.swift index 2dc09bc..2d6d8b3 100644 --- a/Sources/SwiftJWT/VerifierAlgorithm.swift +++ b/Sources/SwiftJWT/VerifierAlgorithm.swift @@ -14,7 +14,7 @@ * limitations under the License. **/ -internal protocol VerifierAlgorithm { +protocol VerifierAlgorithm { /// A function to verify the signature of a JSON web token string is correct for the header and claims. func verify(jwt: String) -> Bool } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 30de8b4..d9b9b85 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -18,7 +18,6 @@ import XCTest @testable import SwiftJWTTests - XCTMain([ - testCase(TestJWT.allTests), - ]) + testCase(TestJWT.allTests) +])