diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c99cc74..c07b281 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,8 @@ -* @0xTim @gwynne +* @ptoffy +/.github/CONTRIBUTING.md @ptoffy @0xTim @gwynne +/.github/workflows/*.yml @ptoffy @0xTim @gwynne +/.github/workflows/test.yml @ptoffy @gwynne +/.spi.yml @ptoffy @0xTim @gwynne +/.gitignore @ptoffy @0xTim @gwynne +/LICENSE @ptoffy @0xTim @gwynne +/README.md @ptoffy @0xTim @gwynne diff --git a/Package.swift b/Package.swift index 55d2c69..ebb1be1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,29 +1,37 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.9 import PackageDescription let package = Package( name: "jwt", platforms: [ - .macOS(.v10_15), - .iOS(.v13), - .tvOS(.v13), - .watchOS(.v6) + .macOS(.v13), + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9), ], products: [ .library(name: "JWT", targets: ["JWT"]), ], dependencies: [ - .package(url: "https://github.com/vapor/jwt-kit.git", from: "4.0.0"), - .package(url: "https://github.com/vapor/vapor.git", from: "4.50.0"), + .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0-beta.1"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.92.0"), ], targets: [ - .target(name: "JWT", dependencies: [ - .product(name: "JWTKit", package: "jwt-kit"), - .product(name: "Vapor", package: "vapor"), - ]), - .testTarget(name: "JWTTests", dependencies: [ - .target(name: "JWT"), - .product(name: "XCTVapor", package: "vapor"), - ]), + .target( + name: "JWT", + dependencies: [ + .product(name: "JWTKit", package: "jwt-kit"), + .product(name: "Vapor", package: "vapor"), + ], + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] + ), + .testTarget( + name: "JWTTests", + dependencies: [ + .target(name: "JWT"), + .product(name: "XCTVapor", package: "vapor"), + ], + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] + ), ] ) diff --git a/README.md b/README.md index dc6be73..78cb45a 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,14 @@ Continuous Integration + + Swift 5.9+ +


Support for JWT (JSON Web Tokens) in Vapor. -Supported versions: - -|Version|Swift|SPM| -|---|---|---| -|4.0|5.4+|`from: "4.0.0"`| - **Original author** - Siemen Sikkema, [@siemensikkema](http://github.com/siemensikkema) diff --git a/Sources/JWT/Application+JWT.swift b/Sources/JWT/Application+JWT.swift index db37935..45e16ea 100644 --- a/Sources/JWT/Application+JWT.swift +++ b/Sources/JWT/Application+JWT.swift @@ -1,16 +1,36 @@ -import Vapor import JWTKit +import Vapor +import NIOConcurrencyHelpers -extension Application { - public var jwt: JWT { +public extension Application { + var jwt: JWT { .init(_application: self) } - public struct JWT { - private final class Storage { - var signers: JWTSigners + struct JWT: Sendable { + private final class Storage: Sendable { + private struct SendableBox: Sendable { + var keys: JWTKeyCollection + } + + private let sendableBox: NIOLockedValueBox + + var keys: JWTKeyCollection { + get { + self.sendableBox.withLockedValue { box in + box.keys + } + } + set { + self.sendableBox.withLockedValue { box in + box.keys = newValue + } + } + } + init() { - self.signers = .init() + let box = SendableBox(keys: .init()) + self.sendableBox = .init(box) } } @@ -20,9 +40,9 @@ extension Application { public let _application: Application - public var signers: JWTSigners { - get { self.storage.signers } - set { self.storage.signers = newValue } + public var keys: JWTKeyCollection { + get { self.storage.keys } + set { self.storage.keys = newValue } } private var storage: Storage { diff --git a/Sources/JWT/AsyncJWTAuthenticator.swift b/Sources/JWT/AsyncJWTAuthenticator.swift deleted file mode 100644 index ef03440..0000000 --- a/Sources/JWT/AsyncJWTAuthenticator.swift +++ /dev/null @@ -1,38 +0,0 @@ -#if compiler(>=5.5) && canImport(_Concurrency) -import NIOCore -import Vapor - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -extension JWTPayload where Self: Authenticatable { - public static func asyncAuthenticator() -> AsyncAuthenticator { - AsyncJWTPayloadAuthenticator() - } -} - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -private struct AsyncJWTPayloadAuthenticator: AsyncJWTAuthenticator - where Payload: JWTPayload & Authenticatable -{ - func authenticate(jwt: Payload, for request: Request) async throws { - request.auth.login(jwt) - } -} - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -public protocol AsyncJWTAuthenticator: AsyncBearerAuthenticator { - associatedtype Payload: JWTPayload - func authenticate(jwt: Payload, for request: Request) async throws -} - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -extension AsyncJWTAuthenticator { - public func authenticate(bearer: BearerAuthorization, for request: Request) async throws { - try await self.authenticate( - jwt: request.jwt.verify(bearer.token), - for: request - ) - } -} - - -#endif diff --git a/Sources/JWT/JWT+Apple.swift b/Sources/JWT/JWT+Apple.swift index cf28c21..e1177ad 100644 --- a/Sources/JWT/JWT+Apple.swift +++ b/Sources/JWT/JWT+Apple.swift @@ -1,55 +1,55 @@ +import NIOConcurrencyHelpers import Vapor -extension Request.JWT { - public var apple: Apple { +public extension Request.JWT { + var apple: Apple { .init(_jwt: self) } - public struct Apple { + struct Apple: Sendable { public let _jwt: Request.JWT - public func verify(applicationIdentifier: String? = nil) -> EventLoopFuture { + public func verify( + applicationIdentifier: String? = nil + ) async throws -> AppleIdentityToken { guard let token = self._jwt._request.headers.bearerAuthorization?.token else { self._jwt._request.logger.error("Request is missing JWT bearer header.") - return self._jwt._request.eventLoop.makeFailedFuture(Abort(.unauthorized)) + throw Abort(.unauthorized) } - return self.verify(token, applicationIdentifier: applicationIdentifier) + return try await self.verify(token, applicationIdentifier: applicationIdentifier) } - public func verify(_ message: String, applicationIdentifier: String? = nil) -> EventLoopFuture { - self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier) + public func verify( + _ message: String, + applicationIdentifier: String? = nil + ) async throws -> AppleIdentityToken { + try await self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier) } - public func verify(_ message: Message, applicationIdentifier: String? = nil) -> EventLoopFuture - where Message: DataProtocol - { - self._jwt._request.application.jwt.apple.signers( - on: self._jwt._request - ).flatMapThrowing { signers in - let token = try signers.verify(message, as: AppleIdentityToken.self) - if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.apple.applicationIdentifier { - try token.audience.verifyIntendedAudience(includes: applicationIdentifier) - } - return token + public func verify( + _ message: some DataProtocol & Sendable, + applicationIdentifier: String? = nil + ) async throws -> AppleIdentityToken { + let keys = try await self._jwt._request.application.jwt.apple.keys(on: self._jwt._request) + let token = try await keys.verify(message, as: AppleIdentityToken.self) + if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.apple.applicationIdentifier { + try token.audience.verifyIntendedAudience(includes: applicationIdentifier) } + return token } } } -extension Application.JWT { - public var apple: Apple { +public extension Application.JWT { + var apple: Apple { .init(_jwt: self) } - public struct Apple { + struct Apple: Sendable { public let _jwt: Application.JWT - public func signers(on request: Request) -> EventLoopFuture { - self.jwks.get(on: request).flatMapThrowing { - let signers = JWTSigners() - try signers.use(jwks: $0) - return signers - } + public func keys(on request: Request) async throws -> JWTKeyCollection { + try await JWTKeyCollection().add(jwks: jwks.get(on: request).get()) } public var jwks: EndpointCache { @@ -69,12 +69,31 @@ extension Application.JWT { typealias Value = Storage } - private final class Storage { + private final class Storage: Sendable { + private struct SendableBox: Sendable { + var applicationIdentifier: String? + } + let jwks: EndpointCache - var applicationIdentifier: String? + private let sendableBox: NIOLockedValueBox + + var applicationIdentifier: String? { + get { + self.sendableBox.withLockedValue { box in + box.applicationIdentifier + } + } + set { + self.sendableBox.withLockedValue { box in + box.applicationIdentifier = newValue + } + } + } + init() { self.jwks = .init(uri: "https://appleid.apple.com/auth/keys") - self.applicationIdentifier = nil + let box = SendableBox(applicationIdentifier: nil) + self.sendableBox = .init(box) } } diff --git a/Sources/JWT/JWT+Concurrency.swift b/Sources/JWT/JWT+Concurrency.swift deleted file mode 100644 index f7c5a42..0000000 --- a/Sources/JWT/JWT+Concurrency.swift +++ /dev/null @@ -1,141 +0,0 @@ -#if compiler(>=5.5) && canImport(_Concurrency) -import NIOCore -import Vapor - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -extension Request.JWT.Apple { - public func verify(applicationIdentifier: String? = nil) async throws -> AppleIdentityToken { - guard let token = self._jwt._request.headers.bearerAuthorization?.token else { - self._jwt._request.logger.error("Request is missing JWT bearer header.") - throw Abort(.unauthorized) - } - return try await self.verify(token, applicationIdentifier: applicationIdentifier) - } - - public func verify(_ message: String, applicationIdentifier: String? = nil) async throws -> AppleIdentityToken { - try await self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier) - } - - public func verify(_ message: Message, applicationIdentifier: String? = nil) async throws -> AppleIdentityToken - where Message: DataProtocol - { - let signers = try await self._jwt._request.application.jwt.apple.signers(on: self._jwt._request) - let token = try signers.verify(message, as: AppleIdentityToken.self) - if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.apple.applicationIdentifier { - try token.audience.verifyIntendedAudience(includes: applicationIdentifier) - } - return token - } -} - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -extension Application.JWT.Apple { - public func signers(on request: Request) async throws -> JWTSigners { - let jwks = try await self.jwks.get(on: request).get() - let signers = JWTSigners() - try signers.use(jwks: jwks) - return signers - } -} - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -extension Request.JWT.Google { - public func verify( - applicationIdentifier: String? = nil, - gSuiteDomainName: String? = nil - ) async throws -> GoogleIdentityToken { - guard let token = self._jwt._request.headers.bearerAuthorization?.token else { - self._jwt._request.logger.error("Request is missing JWT bearer header.") - throw Abort(.unauthorized) - } - return try await self.verify( - token, - applicationIdentifier: applicationIdentifier, - gSuiteDomainName: gSuiteDomainName - ) - } - - public func verify( - _ message: String, - applicationIdentifier: String? = nil, - gSuiteDomainName: String? = nil - ) async throws -> GoogleIdentityToken { - try await self.verify( - [UInt8](message.utf8), - applicationIdentifier: applicationIdentifier, - gSuiteDomainName: gSuiteDomainName - ) - } - - public func verify( - _ message: Message, - applicationIdentifier: String? = nil, - gSuiteDomainName: String? = nil - ) async throws -> GoogleIdentityToken - where Message: DataProtocol - { - let signers = try await self._jwt._request.application.jwt.google.signers(on: self._jwt._request) - let token = try signers.verify(message, as: GoogleIdentityToken.self) - if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.google.applicationIdentifier { - try token.audience.verifyIntendedAudience(includes: applicationIdentifier) - } - - if let gSuiteDomainName = gSuiteDomainName ?? self._jwt._request.application.jwt.google.gSuiteDomainName { - guard let hd = token.hostedDomain, hd.value == gSuiteDomainName else { - throw JWTError.claimVerificationFailure( - name: "hostedDomain", - reason: "Hosted domain claim does not match gSuite domain name" - ) - } - } - return token - } -} - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -extension Application.JWT.Google { - public func signers(on request: Request) async throws -> JWTSigners { - let jwks = try await self.jwks.get(on: request).get() - let signers = JWTSigners() - try signers.use(jwks: jwks) - return signers - } -} - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -extension Request.JWT.Microsoft { - public func verify(applicationIdentifier: String? = nil) async throws -> MicrosoftIdentityToken { - guard let token = self._jwt._request.headers.bearerAuthorization?.token else { - self._jwt._request.logger.error("Request is missing JWT bearer header.") - throw Abort(.unauthorized) - } - return try await self.verify(token, applicationIdentifier: applicationIdentifier) - } - - public func verify(_ message: String, applicationIdentifier: String? = nil) async throws -> MicrosoftIdentityToken { - try await self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier) - } - - public func verify(_ message: Message, applicationIdentifier: String? = nil) async throws -> MicrosoftIdentityToken - where Message: DataProtocol - { - let signers = try await self._jwt._request.application.jwt.microsoft.signers(on: self._jwt._request) - let token = try signers.verify(message, as: MicrosoftIdentityToken.self) - if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.microsoft.applicationIdentifier { - try token.audience.verifyIntendedAudience(includes: applicationIdentifier) - } - return token - } -} - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -extension Application.JWT.Microsoft { - public func signers(on request: Request) async throws -> JWTSigners { - let jwks = try await self.jwks.get(on: request).get() - let signers = JWTSigners() - try signers.use(jwks: jwks) - return signers - } -} - -#endif diff --git a/Sources/JWT/JWT+Google.swift b/Sources/JWT/JWT+Google.swift index ae3a723..cf9cfe1 100644 --- a/Sources/JWT/JWT+Google.swift +++ b/Sources/JWT/JWT+Google.swift @@ -1,84 +1,66 @@ +import NIOConcurrencyHelpers import Vapor -extension Request.JWT { - public var google: Google { +public extension Request.JWT { + var google: Google { .init(_jwt: self) } - public struct Google { + struct Google: Sendable { public let _jwt: Request.JWT public func verify( applicationIdentifier: String? = nil, gSuiteDomainName: String? = nil - ) -> EventLoopFuture { + ) async throws -> GoogleIdentityToken { guard let token = self._jwt._request.headers.bearerAuthorization?.token else { self._jwt._request.logger.error("Request is missing JWT bearer header.") - return self._jwt._request.eventLoop.makeFailedFuture(Abort(.unauthorized)) + throw Abort(.unauthorized) } - return self.verify( - token, - applicationIdentifier: applicationIdentifier, - gSuiteDomainName: gSuiteDomainName - ) + return try await self.verify(token, applicationIdentifier: applicationIdentifier, gSuiteDomainName: gSuiteDomainName) } public func verify( _ message: String, applicationIdentifier: String? = nil, gSuiteDomainName: String? = nil - ) -> EventLoopFuture { - self.verify( - [UInt8](message.utf8), - applicationIdentifier: applicationIdentifier, - gSuiteDomainName: gSuiteDomainName - ) + ) async throws -> GoogleIdentityToken { + try await self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier, gSuiteDomainName: gSuiteDomainName) } - public func verify( - _ message: Message, + public func verify( + _ message: some DataProtocol & Sendable, applicationIdentifier: String? = nil, gSuiteDomainName: String? = nil - ) -> EventLoopFuture - where Message: DataProtocol - { - self._jwt._request.application.jwt.google.signers( - on: self._jwt._request - ).flatMapThrowing { signers in - let token = try signers.verify(message, as: GoogleIdentityToken.self) - if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.google.applicationIdentifier { - try token.audience.verifyIntendedAudience(includes: applicationIdentifier) - } - - if let gSuiteDomainName = gSuiteDomainName ?? self._jwt._request.application.jwt.google.gSuiteDomainName { - guard let hd = token.hostedDomain, hd.value == gSuiteDomainName else { - throw JWTError.claimVerificationFailure( - name: "hostedDomain", - reason: "Hosted domain claim does not match gSuite domain name" - ) - } + ) async throws -> GoogleIdentityToken { + let keys = try await self._jwt._request.application.jwt.google.keys(on: self._jwt._request) + let token = try await keys.verify(message, as: GoogleIdentityToken.self) + if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.google.applicationIdentifier { + try token.audience.verifyIntendedAudience(includes: applicationIdentifier) + } + if let gSuiteDomainName = gSuiteDomainName ?? self._jwt._request.application.jwt.google.gSuiteDomainName { + guard let hd = token.hostedDomain, hd.value == gSuiteDomainName else { + throw JWTError.claimVerificationFailure( + failedClaim: token.hostedDomain, + reason: "Hosted domain claim does not match gSuite domain name" + ) } - return token } + return token } - } } -extension Application.JWT { - public var google: Google { +public extension Application.JWT { + var google: Google { .init(_jwt: self) } - public struct Google { + struct Google: Sendable { public let _jwt: Application.JWT - public func signers(on request: Request) -> EventLoopFuture { - self.jwks.get(on: request).flatMapThrowing { - let signers = JWTSigners() - try signers.use(jwks: $0) - return signers - } + public func keys(on request: Request) async throws -> JWTKeyCollection { + try await JWTKeyCollection().add(jwks: jwks.get(on: request).get()) } public var jwks: EndpointCache { @@ -107,14 +89,45 @@ extension Application.JWT { typealias Value = Storage } - private final class Storage { + private final class Storage: Sendable { + private struct SendableBox: Sendable { + var applicationIdentifier: String? + var gSuiteDomainName: String? + } + let jwks: EndpointCache - var applicationIdentifier: String? - var gSuiteDomainName: String? + private let sendableBox: NIOLockedValueBox + + var applicationIdentifier: String? { + get { + self.sendableBox.withLockedValue { box in + box.applicationIdentifier + } + } + set { + self.sendableBox.withLockedValue { box in + box.applicationIdentifier = newValue + } + } + } + + var gSuiteDomainName: String? { + get { + self.sendableBox.withLockedValue { box in + box.gSuiteDomainName + } + } + set { + self.sendableBox.withLockedValue { box in + box.gSuiteDomainName = newValue + } + } + } + init() { self.jwks = .init(uri: "https://www.googleapis.com/oauth2/v3/certs") - self.applicationIdentifier = nil - self.gSuiteDomainName = nil + let box = SendableBox(applicationIdentifier: nil, gSuiteDomainName: nil) + self.sendableBox = .init(box) } } diff --git a/Sources/JWT/JWT+Microsoft.swift b/Sources/JWT/JWT+Microsoft.swift index 549e518..06dac33 100644 --- a/Sources/JWT/JWT+Microsoft.swift +++ b/Sources/JWT/JWT+Microsoft.swift @@ -1,55 +1,55 @@ +import NIOConcurrencyHelpers import Vapor -extension Request.JWT { - public var microsoft: Microsoft { +public extension Request.JWT { + var microsoft: Microsoft { .init(_jwt: self) } - public struct Microsoft { + struct Microsoft { public let _jwt: Request.JWT - public func verify(applicationIdentifier: String? = nil) -> EventLoopFuture { + public func verify( + applicationIdentifier: String? = nil + ) async throws -> MicrosoftIdentityToken { guard let token = self._jwt._request.headers.bearerAuthorization?.token else { self._jwt._request.logger.error("Request is missing JWT bearer header.") - return self._jwt._request.eventLoop.makeFailedFuture(Abort(.unauthorized)) + throw Abort(.unauthorized) } - return self.verify(token, applicationIdentifier: applicationIdentifier) + return try await self.verify(token, applicationIdentifier: applicationIdentifier) } - public func verify(_ message: String, applicationIdentifier: String? = nil) -> EventLoopFuture { - self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier) + public func verify( + _ message: String, + applicationIdentifier: String? = nil + ) async throws -> MicrosoftIdentityToken { + try await self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier) } - public func verify(_ message: Message, applicationIdentifier: String? = nil) -> EventLoopFuture - where Message: DataProtocol - { - self._jwt._request.application.jwt.microsoft.signers( - on: self._jwt._request - ).flatMapThrowing { signers in - let token = try signers.verify(message, as: MicrosoftIdentityToken.self) - if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.microsoft.applicationIdentifier { - try token.audience.verifyIntendedAudience(includes: applicationIdentifier) - } - return token + public func verify( + _ message: some DataProtocol & Sendable, + applicationIdentifier: String? = nil + ) async throws -> MicrosoftIdentityToken { + let keys = try await self._jwt._request.application.jwt.microsoft.keys(on: self._jwt._request) + let token = try await keys.verify(message, as: MicrosoftIdentityToken.self) + if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.microsoft.applicationIdentifier { + try token.audience.verifyIntendedAudience(includes: applicationIdentifier) } + return token } } } -extension Application.JWT { - public var microsoft: Microsoft { +public extension Application.JWT { + var microsoft: Microsoft { .init(_jwt: self) } - public struct Microsoft { + struct Microsoft { public let _jwt: Application.JWT - public func signers(on request: Request) -> EventLoopFuture { - self.jwks.get(on: request).flatMapThrowing { - let signers = JWTSigners() - try signers.use(jwks: $0) - return signers - } + public func keys(on request: Request) async throws -> JWTKeyCollection { + try await JWTKeyCollection().add(jwks: jwks.get(on: request).get()) } public var jwks: EndpointCache { @@ -69,12 +69,31 @@ extension Application.JWT { typealias Value = Storage } - private final class Storage { + private final class Storage: Sendable { + private struct SendableBox: Sendable { + var applicationIdentifier: String? + } + let jwks: EndpointCache - var applicationIdentifier: String? + private let sendableBox: NIOLockedValueBox + + var applicationIdentifier: String? { + get { + self.sendableBox.withLockedValue { box in + box.applicationIdentifier + } + } + set { + self.sendableBox.withLockedValue { box in + box.applicationIdentifier = newValue + } + } + } + init() { self.jwks = .init(uri: "https://login.microsoftonline.com/common/discovery/keys") - self.applicationIdentifier = nil + let box = SendableBox(applicationIdentifier: nil) + self.sendableBox = .init(box) } } diff --git a/Sources/JWT/JWTAuthenticator.swift b/Sources/JWT/JWTAuthenticator.swift index 91bf951..9f5bc78 100644 --- a/Sources/JWT/JWTAuthenticator.swift +++ b/Sources/JWT/JWTAuthenticator.swift @@ -1,7 +1,7 @@ import Vapor -extension JWTPayload where Self: Authenticatable { - public static func authenticator() -> Authenticator { +public extension JWTPayload where Self: Authenticatable { + static func authenticator() -> AsyncAuthenticator { JWTPayloadAuthenticator() } } @@ -9,26 +9,18 @@ extension JWTPayload where Self: Authenticatable { private struct JWTPayloadAuthenticator: JWTAuthenticator where Payload: JWTPayload & Authenticatable { - func authenticate(jwt: Payload, for request: Request) -> EventLoopFuture { + func authenticate(jwt: Payload, for request: Request) async throws { request.auth.login(jwt) - return request.eventLoop.makeSucceededFuture(()) } } -public protocol JWTAuthenticator: BearerAuthenticator { +public protocol JWTAuthenticator: AsyncBearerAuthenticator { associatedtype Payload: JWTPayload - func authenticate(jwt: Payload, for request: Request) -> EventLoopFuture + func authenticate(jwt: Payload, for request: Request) async throws } -extension JWTAuthenticator { - public func authenticate(bearer: BearerAuthorization, for request: Request) -> EventLoopFuture { - do { - return try self.authenticate( - jwt: request.jwt.verify(bearer.token), - for: request - ) - } catch { - return request.eventLoop.makeFailedFuture(error) - } +public extension JWTAuthenticator { + func authenticate(bearer: BearerAuthorization, for request: Request) async throws { + try await self.authenticate(jwt: request.jwt.verify(bearer.token), for: request) } } diff --git a/Sources/JWT/Request+JWT.swift b/Sources/JWT/Request+JWT.swift index 964dfb9..300b943 100644 --- a/Sources/JWT/Request+JWT.swift +++ b/Sources/JWT/Request+JWT.swift @@ -1,43 +1,43 @@ -import Vapor import JWTKit +import Vapor -extension Request { - public var jwt: JWT { +public extension Request { + var jwt: JWT { .init(_request: self) } - public struct JWT { + struct JWT: Sendable { public let _request: Request - + @discardableResult - public func verify(as payload: Payload.Type = Payload.self) throws -> Payload + public func verify(as _: Payload.Type = Payload.self) async throws -> Payload where Payload: JWTPayload { guard let token = self._request.headers.bearerAuthorization?.token else { self._request.logger.error("Request is missing JWT bearer header") throw Abort(.unauthorized) } - return try self.verify(token, as: Payload.self) + return try await self.verify(token, as: Payload.self) } - + @discardableResult - public func verify(_ message: String, as payload: Payload.Type = Payload.self) throws -> Payload + public func verify(_ message: String, as _: Payload.Type = Payload.self) async throws -> Payload where Payload: JWTPayload { - try self.verify([UInt8](message.utf8), as: Payload.self) + try await self.verify([UInt8](message.utf8), as: Payload.self) } - + @discardableResult - public func verify(_ message: Message, as payload: Payload.Type = Payload.self) throws -> Payload - where Message: DataProtocol, Payload: JWTPayload + public func verify(_ message: some DataProtocol & Sendable, as _: Payload.Type = Payload.self) async throws -> Payload + where Payload: JWTPayload { - try self._request.application.jwt.signers.verify(message, as: Payload.self) + try await self._request.application.jwt.keys.verify(message, as: Payload.self) } - public func sign(_ jwt: Payload, kid: JWKIdentifier? = nil) throws -> String + public func sign(_ jwt: Payload, header: JWTHeader = .init()) async throws -> String where Payload: JWTPayload { - try self._request.application.jwt.signers.sign(jwt, kid: kid) + return try await self._request.application.jwt.keys.sign(jwt, header: header) } } } diff --git a/Tests/JWTTests/JWTTests.swift b/Tests/JWTTests/JWTTests.swift index ca9b502..55ccd6e 100644 --- a/Tests/JWTTests/JWTTests.swift +++ b/Tests/JWTTests/JWTTests.swift @@ -3,40 +3,37 @@ import JWTKit import XCTVapor class JWTTests: XCTestCase { - func testDocs() throws { + func testDocs() async throws { // creates a new application for testing let app = Application(.testing) defer { app.shutdown() } // Add HMAC with SHA-256 signer. - app.jwt.signers.use(.hs256(key: "secret")) + await app.jwt.keys.addHS256(key: "secret") - app.jwt.signers.use(.hs256(key: "foo"), kid: "a") - app.jwt.signers.use(.hs256(key: "bar"), kid: "b") + await app.jwt.keys.addHS256(key: "foo", kid: "a") + await app.jwt.keys.addHS256(key: "bar", kid: "b") app.jwt.apple.applicationIdentifier = "..." - app.get("apple") { req -> EventLoopFuture in - req.jwt.apple.verify().map { token in - print(token) // AppleIdentityToken - return .ok - } + app.get("apple") { req async throws -> HTTPStatus in + let token = try await req.jwt.apple.verify() + print(token) // AppleIdentityToken + return .ok } app.jwt.google.applicationIdentifier = "..." app.jwt.google.gSuiteDomainName = "..." - app.get("google") { req -> EventLoopFuture in - req.jwt.google.verify().map { token in - print(token) // GoogleIdentityToken - return .ok - } + app.get("google") { req async throws -> HTTPStatus in + let token = try await req.jwt.google.verify() + print(token) // GoogleIdentityToken + return .ok } app.jwt.microsoft.applicationIdentifier = "..." - app.get("microsoft") { req -> EventLoopFuture in - req.jwt.microsoft.verify().map { token in - print(token) // MicrosoftIdentityToken - return .ok - } + app.get("microsoft") { req async throws -> HTTPStatus in + let token = try await req.jwt.microsoft.verify() + print(token) // MicrosoftIdentityToken + return .ok } // JWT payload structure. @@ -65,20 +62,20 @@ class JWTTests: XCTestCase { // signature verification here. // Since we have an ExpirationClaim, we will // call its verify method. - func verify(using signer: JWTSigner) throws { + func verify(using _: JWTAlgorithm) async throws { try self.expiration.verifyNotExpired() } } // Fetch and verify JWT from incoming request. - app.get("me") { req -> HTTPStatus in - let payload = try req.jwt.verify(as: TestPayload.self) + app.get("me") { req async throws -> HTTPStatus in + let payload = try await req.jwt.verify(as: TestPayload.self) print(payload) return .ok } // Generate and return a new JWT. - app.post("login") { req -> [String: String] in + app.post("login") { req async throws -> [String: String] in // Create a new instance of our JWTPayload let payload = TestPayload( subject: "vapor", @@ -86,8 +83,8 @@ class JWTTests: XCTestCase { isAdmin: true ) // Return the signed JWT - return try [ - "token": req.jwt.sign(payload, kid: "a") + return try await [ + "token": req.jwt.sign(payload, header: ["kid": "a"]), ] } @@ -121,24 +118,24 @@ class JWTTests: XCTestCase { } // manual authentication using req.jwt.verify - func testManual() throws { + func testManual() async throws { // creates a new application for testing let app = Application(.testing) defer { app.shutdown() } // configures an es512 signer using random key - try app.jwt.signers.use(.es512(key: .generate())) + await app.jwt.keys.addES512(key: ES512PrivateKey()) // jwt creation using req.jwt.sign - app.post("login") { req -> LoginResponse in + app.post("login") { req async throws -> LoginResponse in let credentials = try req.content.decode(LoginCredentials.self) - return try LoginResponse( + return try await LoginResponse( token: req.jwt.sign(TestUser(name: credentials.name)) ) } - app.get("me") { req -> String in - try req.jwt.verify(as: TestUser.self).name + app.get("me") { req async throws -> String in + try await req.jwt.verify(as: TestUser.self).name } // stores the token created during login @@ -168,7 +165,8 @@ class JWTTests: XCTestCase { } // create a token from a different signer - let fakeToken = try JWTSigner.es256(key: .generate()).sign(TestUser(name: "bob")) + let fakeToken = try await JWTKeyCollection() + .addES512(key: ES512PrivateKey()).sign(TestUser(name: "bob")) try app.testable().test( .GET, "me", headers: ["authorization": "Bearer \(fakeToken)"] ) { res in @@ -177,18 +175,18 @@ class JWTTests: XCTestCase { } // test middleware-based authentication using req.auth.require - func testMiddleware() throws { + func testMiddleware() async throws { // creates a new application for testing let app = Application(.testing) defer { app.shutdown() } // configures an es512 signer using random key - try app.jwt.signers.use(.es512(key: .generate())) + await app.jwt.keys.addES512(key: ES512PrivateKey()) // jwt creation using req.jwt.sign - app.post("login") { req -> LoginResponse in + app.post("login") { req async throws -> LoginResponse in let credentials = try req.content.decode(LoginCredentials.self) - return try LoginResponse( + return try await LoginResponse( token: req.jwt.sign(TestUser(name: credentials.name)) ) } @@ -235,7 +233,7 @@ class JWTTests: XCTestCase { // token from same signer but for a different user // this tests that the guard middleware catches the failure to auth before it reaches the route handler - let wrongNameToken = try app.jwt.signers.sign(TestUser(name: "bob")) + let wrongNameToken = try await app.jwt.keys.sign(TestUser(name: "bob")) try app.testable().test( .GET, "me", headers: ["authorization": "Bearer \(wrongNameToken)"] ) { res in @@ -243,7 +241,7 @@ class JWTTests: XCTestCase { } // create a token from a different signer - let fakeToken = try JWTSigner.es256(key: .generate()).sign(TestUser(name: "bob")) + let fakeToken = try await JWTKeyCollection().addES512(key: ES512PrivateKey()).sign(TestUser(name: "bob")) try app.testable().test( .GET, "me", headers: ["authorization": "Bearer \(fakeToken)"] ) { res in @@ -252,26 +250,23 @@ class JWTTests: XCTestCase { } /* - If this test expires you might need to regenerate the JWT. Use https://github.com/0xTim/vapor-jwt-test-siwa and run the project on a real device - Try signing in with Apple and it will print a new JWT to use. - Note that it takes a day for the JWT to expire before the test passes - */ - func testApple() throws { + If this test expires you might need to regenerate the JWT. Use https://github.com/0xTim/vapor-jwt-test-siwa and run the project on a real device + Try signing in with Apple and it will print a new JWT to use. + Note that it takes a day for the JWT to expire before the test passes + */ + func testApple() async throws { // creates a new application for testing let app = Application(.testing) defer { app.shutdown() } app.jwt.apple.applicationIdentifier = "dev.timc.siwa-demo.TILiOS" - app.get("test") { req in - req.jwt.apple.verify().map { - $0.email ?? "none" - } + app.get("test") { req async throws in + try await req.jwt.apple.verify().email ?? "none" } - app.get("test2") { req in - req.jwt.apple.verify(applicationIdentifier: "dev.timc.siwa-demo.TILiOS").map { - $0.email ?? "none" - } + + app.get("test2") { req async throws in + try await req.jwt.apple.verify(applicationIdentifier: "dev.timc.siwa-demo.TILiOS").email ?? "none" } var headers = HTTPHeaders() @@ -281,20 +276,18 @@ class JWTTests: XCTestCase { try app.test(.GET, "test", headers: headers) { res in XCTAssertEqual(res.status, .unauthorized) - XCTAssertContains(res.body.string, "expired") }.test(.GET, "test2", headers: headers) { res in XCTAssertEqual(res.status, .unauthorized) - XCTAssertContains(res.body.string, "expired") } } // https://github.com/vapor/jwt-kit/issues/26 - func testSignFailureSegfault() throws { + func testSignFailureSegfault() async throws { struct UserPayload: JWTPayload { var id: UUID var userName: String - func verify(using signer: JWTSigner) throws { } + func verify(using _: JWTAlgorithm) throws {} } // creates a new application for testing @@ -330,17 +323,17 @@ class JWTTests: XCTestCase { waNSUrQp9XZJLA9SgN+N2JwuDi0bxsr0saaLdmWn3S3L6rsg5Cja -----END RSA PRIVATE KEY----- """ - - try app.jwt.signers.use(.rs512(key: .private(pem: [UInt8](privateKeyString.utf8)))) - app.get { req -> String in + try await app.jwt.keys.addRS256(key: Insecure.RSA.PrivateKey(pem: [UInt8](privateKeyString.utf8))) + + app.get { req async throws -> String in let authorizationPayload = UserPayload(id: UUID(), userName: "John Smith") - let accessToken = try req.jwt.sign(authorizationPayload) + let accessToken = try await req.jwt.sign(authorizationPayload) return accessToken } - for _ in 0..<1_000 { - try app.test(.GET, "/") { res in + for _ in 0 ..< 1000 { + try app.test(.GET, "/") { res in XCTAssertEqual(res.status, .ok) } } @@ -366,7 +359,6 @@ let isLoggingConfigured: Bool = { return true }() - struct LoginResponse: Content { var token: String } @@ -378,7 +370,7 @@ struct LoginCredentials: Content { struct TestUser: Content, Authenticatable, JWTPayload { var name: String - func verify(using signer: JWTSigner) throws { + func verify(using _: JWTAlgorithm) throws { // nothing to verify } } @@ -386,11 +378,10 @@ struct TestUser: Content, Authenticatable, JWTPayload { struct UserAuthenticator: JWTAuthenticator { typealias Payload = TestUser - func authenticate(jwt: TestUser, for request: Request) -> EventLoopFuture { + func authenticate(jwt: TestUser, for request: Request) async throws { if jwt.name == "foo" { // Requiring this specific username makes the test for the guard middleware in testMiddleware() valid. request.auth.login(jwt) } - return request.eventLoop.makeSucceededFuture(()) } }