From 652683721677a88bea448c164de13e17c52fee2d Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 16 Mar 2020 11:42:24 -0500 Subject: [PATCH 1/8] Stop tracking Package.resolved; it doesn't belong in a framework package --- .gitignore | 1 + Package.resolved | 160 ----------------------------------------------- 2 files changed, 1 insertion(+), 160 deletions(-) delete mode 100644 Package.resolved diff --git a/.gitignore b/.gitignore index 02c0875..7fb530a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.build /Packages /*.xcodeproj +Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index e745de6..0000000 --- a/Package.resolved +++ /dev/null @@ -1,160 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "async-http-client", - "repositoryURL": "https://github.com/swift-server/async-http-client.git", - "state": { - "branch": null, - "revision": "48e284d1ea6d0e8baac1af1c4ad8bd298670caf6", - "version": "1.0.1" - } - }, - { - "package": "async-kit", - "repositoryURL": "https://github.com/vapor/async-kit.git", - "state": { - "branch": null, - "revision": "8e9d41f31848809732661563b334c1bb1c93cf1a", - "version": "1.0.0-beta.2.2" - } - }, - { - "package": "console-kit", - "repositoryURL": "https://github.com/vapor/console-kit.git", - "state": { - "branch": null, - "revision": "54585747b6a5c435b7852c026028710fd7a94bec", - "version": "4.0.0-beta.2.1" - } - }, - { - "package": "jwt", - "repositoryURL": "https://github.com/vapor/jwt.git", - "state": { - "branch": null, - "revision": "9e121e0dc7fa169a60e20f263a09b4eab444180c", - "version": "4.0.0-beta.2" - } - }, - { - "package": "jwt-kit", - "repositoryURL": "https://github.com/vapor/jwt-kit.git", - "state": { - "branch": null, - "revision": "01c623f7dd82c3d4fd020ce87f19d858449989d3", - "version": "4.0.0-beta.2.2" - } - }, - { - "package": "multipart-kit", - "repositoryURL": "https://github.com/vapor/multipart-kit.git", - "state": { - "branch": null, - "revision": "b41a49b5756ac3fbf8f2b07228a7e75f9b60731a", - "version": "4.0.0-beta.2" - } - }, - { - "package": "open-crypto", - "repositoryURL": "https://github.com/vapor/open-crypto.git", - "state": { - "branch": null, - "revision": "90c49bc68ee6d992fa13cf84ca8fc54b97eaf4cc", - "version": "4.0.0-beta.2" - } - }, - { - "package": "routing-kit", - "repositoryURL": "https://github.com/vapor/routing-kit.git", - "state": { - "branch": null, - "revision": "6a8a1636ad26494b03f3c72d74a420fc3a44949c", - "version": "4.0.0-beta.3" - } - }, - { - "package": "swift-backtrace", - "repositoryURL": "https://github.com/ianpartridge/swift-backtrace.git", - "state": { - "branch": null, - "revision": "eaf2cef011c0c23d1701aa60b364def8015dc3c7", - "version": "1.1.1" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", - "version": "1.2.0" - } - }, - { - "package": "swift-metrics", - "repositoryURL": "https://github.com/apple/swift-metrics.git", - "state": { - "branch": null, - "revision": "3fefedaaef285830cc98ae80231140122076a7e0", - "version": "1.2.0" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "4409b57d4c0c40d41ac2b320fccf02e4d451e3db", - "version": "2.13.0" - } - }, - { - "package": "swift-nio-extras", - "repositoryURL": "https://github.com/apple/swift-nio-extras.git", - "state": { - "branch": null, - "revision": "b4dbfacff47fb8d0f9e0a422d8d37935a9f10570", - "version": "1.4.0" - } - }, - { - "package": "swift-nio-http2", - "repositoryURL": "https://github.com/apple/swift-nio-http2.git", - "state": { - "branch": null, - "revision": "c1bfb7ce3f201e41ff60ef38fa63e67e0eb66a24", - "version": "1.9.0" - } - }, - { - "package": "swift-nio-ssl", - "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", - "state": { - "branch": null, - "revision": "cf54f5c1db1c3740a6c7d662dc8569c150c3846c", - "version": "2.6.0" - } - }, - { - "package": "vapor", - "repositoryURL": "https://github.com/vapor/vapor.git", - "state": { - "branch": null, - "revision": "67165212d2f18b1f9a5df4eb27638e1f88ecbccf", - "version": "4.0.0-beta.3.10" - } - }, - { - "package": "websocket-kit", - "repositoryURL": "https://github.com/vapor/websocket-kit.git", - "state": { - "branch": null, - "revision": "766b4b0005a158550c345671c8e7cea42af104b6", - "version": "2.0.0-beta.2.3" - } - } - ] - }, - "version": 1 -} From 5660e1691fd8dc506faa3125b0383e51be214241 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 16 Mar 2020 11:42:50 -0500 Subject: [PATCH 2/8] Update manifest to Swift 5.2, require Vapor and JWT RCs, require XCTVapor in tests --- Package.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index 6847dab..a7e2b67 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ -// swift-tools-version:5.1 - +// swift-tools-version:5.2 import PackageDescription let package = Package( @@ -11,11 +10,17 @@ let package = Package( .library(name: "JWTMiddleware", targets: ["JWTMiddleware"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-beta.3"), - .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0-beta.2"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-rc"), + .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0-rc"), ], targets: [ - .target(name: "JWTMiddleware", dependencies: ["Vapor", "JWT"]), - .testTarget(name: "JWTMiddlewareTests", dependencies: ["JWTMiddleware"]) + .target(name: "JWTMiddleware", dependencies: [ + .product(name: "Vapor", package: "vapor"), + .product(name: "JWT", package: "jwt"), + ]), + .testTarget(name: "JWTMiddlewareTests", dependencies: [ + .byName(name: "JWTMiddleware"), + .product(name: "XCTVapor", package: "vapor"), + ]) ] ) From b3c330b0f2bafd09d9994e76d41fc6af67262065 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 16 Mar 2020 11:43:16 -0500 Subject: [PATCH 3/8] Ignore .swiftpm --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7fb530a..c0c6547 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /.build /Packages /*.xcodeproj +/.swiftpm Package.resolved From 10d0d9040d96cbcfa80322b30a92957ed337e842 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 16 Mar 2020 11:43:27 -0500 Subject: [PATCH 4/8] Remove obsolete single-format Payload struct --- Sources/JWTMiddleware/Payload.swift | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 Sources/JWTMiddleware/Payload.swift diff --git a/Sources/JWTMiddleware/Payload.swift b/Sources/JWTMiddleware/Payload.swift deleted file mode 100644 index e54f2a9..0000000 --- a/Sources/JWTMiddleware/Payload.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import Vapor -import JWT - -struct Payload: JWTPayload { - let firstname: String? - let lastname: String? - let email: String - let role: String - let id: Int - let status: Int = 0 - let exp: String - let iat: String - - init(id: Int, email: String, role: String) { - self.id = id - self.email = email - self.role = role - self.firstname = nil - self.lastname = nil - self.exp = String(Date().addingTimeInterval(60*60*24).timeIntervalSince1970) - self.iat = String(Date().timeIntervalSince1970) - } - - func verify(using signer: JWTSigner) throws { - let expiration = Date(timeIntervalSince1970: Double(self.exp)!) - try ExpirationClaim(value: expiration).verifyNotExpired() - } -} From b6c4a96b5dc4df583165550a3b90cbb6c73f1c82 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 16 Mar 2020 11:43:48 -0500 Subject: [PATCH 5/8] Make JWTMiddleware and its initializer public so it can be used. --- Sources/JWTMiddleware/JWTMiddleware.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/JWTMiddleware/JWTMiddleware.swift b/Sources/JWTMiddleware/JWTMiddleware.swift index 57aff4f..09d177d 100644 --- a/Sources/JWTMiddleware/JWTMiddleware.swift +++ b/Sources/JWTMiddleware/JWTMiddleware.swift @@ -1,10 +1,10 @@ import Vapor import JWT -final class JWTMiddleware: Middleware { - init() { } +public final class JWTMiddleware: Middleware { + public init() {} - func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { + public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { guard let token = request.headers.bearerAuthorization?.token.utf8 else { return request.eventLoop.makeFailedFuture(Abort(.unauthorized, reason: "Missing authorization bearer header")) From 49d8ad98e1361a3364de1fd60b23e20f1bec91ab Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 16 Mar 2020 11:44:08 -0500 Subject: [PATCH 6/8] Use Request.storage instead of the deprecated Request.userInfo --- Sources/JWTMiddleware/JWTMiddleware.swift | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Sources/JWTMiddleware/JWTMiddleware.swift b/Sources/JWTMiddleware/JWTMiddleware.swift index 09d177d..c6d44cc 100644 --- a/Sources/JWTMiddleware/JWTMiddleware.swift +++ b/Sources/JWTMiddleware/JWTMiddleware.swift @@ -24,19 +24,13 @@ public final class JWTMiddleware: Middleware { } -extension AnyHashable { - static let payload: String = "jwt_payload" -} - extension Request { - var loggedIn: Bool { - if (self.userInfo[.payload] as? JWTPayload) != nil { - return true - } - return false + private struct PayloadKey: StorageKey { + typealias Value = JWTPayload } + var payload: JWTPayload { - get { self.userInfo[.payload] as! JWTPayload } - set { self.userInfo[.payload] = newValue } + get { self.storage[PayloadKey.self]! } + set { self.storage[PayloadKey.self] = newValue } } } From 47242cddcf955e3e467f17a4b1f3336897d3a8d7 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 16 Mar 2020 11:45:46 -0500 Subject: [PATCH 7/8] Add unit tests to verify that the middleware actually verifies signed tokens. Generates a keyset on the fly (which requires importing JWTKit as testable to get at .base64URLEncodedBytes()). Would test EC keys if I knew how to translate what BoringSSL or CryptoKit outputs for them into the coordinate values needed for a JWK. --- .../JWTMiddlewareTests.swift | 84 ++++++++++++++++++- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/Tests/JWTMiddlewareTests/JWTMiddlewareTests.swift b/Tests/JWTMiddlewareTests/JWTMiddlewareTests.swift index e4cf95d..0385796 100644 --- a/Tests/JWTMiddlewareTests/JWTMiddlewareTests.swift +++ b/Tests/JWTMiddlewareTests/JWTMiddlewareTests.swift @@ -1,10 +1,86 @@ import XCTest +import JWT +import Vapor +import XCTVapor +import CNIOBoringSSL @testable import JWTMiddleware +@testable import JWTKit + +struct TestPayload: JWTPayload { + let id: UUID + let exp: TimeInterval + + init(id: UUID, exp: TimeInterval) { + self.id = id + self.exp = exp + } + + func verify(using signer: JWTSigner) throws { + _ = SubjectClaim(value: self.id.uuidString) // Nothing to verify here. + try ExpirationClaim(value: Date(timeIntervalSince1970: self.exp)).verifyNotExpired() + } +} final class JWTMiddlewareTests: XCTestCase { - func testExample() {} - static var allTests = [ - ("testExample", testExample), - ] + var tester: Application! + + override func setUpWithError() throws { + // CryptoKit only generates EC keys and I don't know how to turn the raw representation into JWKS. + var exp: BIGNUM = .init(); CNIOBoringSSL_BN_set_u64(&exp, 0x10001) + var rsa: RSA = .init(); CNIOBoringSSL_RSA_generate_key_ex(&rsa, 4096, &exp, nil) + + let dBytes: [UInt8] = .init(unsafeUninitializedCapacity: Int(CNIOBoringSSL_BN_num_bytes(rsa.d))) { $1 = CNIOBoringSSL_BN_bn2bin(rsa.d, $0.baseAddress!) } + let nBytes: [UInt8] = .init(unsafeUninitializedCapacity: Int(CNIOBoringSSL_BN_num_bytes(rsa.n))) { $1 = CNIOBoringSSL_BN_bn2bin(rsa.n, $0.baseAddress!) } + struct LocalJWKS: Codable { + struct LocalJWK: Codable { let kty, d, e, n, use, kid, alg: String } + let keys: [LocalJWK] + } + let keyset = LocalJWKS(keys: [.init(kty: "RSA", d: String(bytes: dBytes.base64URLEncodedBytes(), encoding: .utf8)!, e: "AQAB", n: String(bytes: nBytes.base64URLEncodedBytes(), encoding: .utf8)!, use: "sig", kid: "jwttest", alg: "RS256")]) + let json = try JSONEncoder().encode(keyset) + + tester = Application(.testing) + try tester.jwt.signers.use(jwksJSON: String(data: json, encoding: .utf8)!) + } + + override func tearDownWithError() throws { + tester?.shutdown() + } + + func testPayloadValidationUnexpired() throws { + let testPayload = TestPayload(id: UUID(), exp: Date(timeIntervalSinceNow: 10.0).timeIntervalSince1970) + + tester.middleware.use(JWTMiddleware()) + tester.get("hello") { _ in "world" } + + let token = try tester.jwt.signers.sign(testPayload, kid: "jwttest") + + _ = try XCTUnwrap(tester.testable(method: .inMemory).test(.GET, "/hello", headers: ["Authorization": "Bearer \(token)"]) { res in + XCTAssertEqual(res.body.string, "world") + }) + } + + func testPayloadValidationExpired() throws { + let testPayload = TestPayload(id: UUID(), exp: Date(timeIntervalSinceNow: -10.0).timeIntervalSince1970) + + tester.middleware.use(JWTMiddleware()) + tester.get("hello") { _ in "world" } + + let token = try tester.jwt.signers.sign(testPayload, kid: "jwttest") + + _ = try XCTUnwrap(tester.testable(method: .inMemory).test(.GET, "/hello", headers: ["Authorization": "Bearer \(token)"]) { res in + XCTAssertEqual(res.status, .unauthorized) + + struct JWTErrorResponse: Codable { + let error: Bool + let reason: String + } + + guard let content = try? XCTUnwrap(res.content.decode(JWTErrorResponse.self)) else { + return + } + XCTAssertEqual(content.error, true) + XCTAssertEqual(content.reason, "exp claim verification failed: expired") + }) + } } From 1154e0d42e236e6952e1f8c7e7d4712743319413 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 16 Mar 2020 11:48:15 -0500 Subject: [PATCH 8/8] Add Github workflow for CI --- .github/workflows/test.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7d36d02 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: test +on: +- pull_request +jobs: + #jwtmiddleware_macos: + # runs-on: macos-latest + # env: + # DEVELOPER_DIR: /Applications/Xcode_11.4_beta.app/Contents/Developer + # steps: + # - uses: actions/checkout@v2 + # - run: brew install vapor/tap/vapor-beta + # - run: xcrun swift test --enable-test-discovery --sanitize=thread + jwtmiddleware_xenial: + container: + image: vapor/swift:5.2-xenial + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: swift test --enable-test-discovery --sanitize=thread + jwtmiddleware_bionic: + container: + image: vapor/swift:5.2-bionic + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: swift test --enable-test-discovery --sanitize=thread +