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 + diff --git a/.gitignore b/.gitignore index 02c0875..c0c6547 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /.build /Packages /*.xcodeproj +/.swiftpm +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 -} 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"), + ]) ] ) diff --git a/Sources/JWTMiddleware/JWTMiddleware.swift b/Sources/JWTMiddleware/JWTMiddleware.swift index 57aff4f..c6d44cc 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")) @@ -24,19 +24,13 @@ 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 } } } 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() - } -} 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") + }) + } }