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 @@
+
+
+
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(())
}
}