diff --git a/Sources/JWT/JWT+Apple.swift b/Sources/JWT/JWT+Apple.swift index e1177ad..eb88e8f 100644 --- a/Sources/JWT/JWT+Apple.swift +++ b/Sources/JWT/JWT+Apple.swift @@ -49,13 +49,23 @@ public extension Application.JWT { public let _jwt: Application.JWT public func keys(on request: Request) async throws -> JWTKeyCollection { - try await JWTKeyCollection().add(jwks: jwks.get(on: request).get()) + try await .init().add(jwks: jwks.get(on: request).get()) } public var jwks: EndpointCache { self.storage.jwks } + public var jwksEndpoint: URI { + get { + self.storage.jwksEndpoint + } + nonmutating set { + self.storage.jwksEndpoint = newValue + self.storage.jwks = .init(uri: newValue) + } + } + public var applicationIdentifier: String? { get { self.storage.applicationIdentifier @@ -71,12 +81,39 @@ public extension Application.JWT { private final class Storage: Sendable { private struct SendableBox: Sendable { - var applicationIdentifier: String? + var jwks: EndpointCache + var jwksEndpoint: URI + var applicationIdentifier: String? = nil } - let jwks: EndpointCache private let sendableBox: NIOLockedValueBox + var jwks: EndpointCache { + get { + self.sendableBox.withLockedValue { box in + box.jwks + } + } + set { + self.sendableBox.withLockedValue { box in + box.jwks = newValue + } + } + } + + var jwksEndpoint: URI { + get { + self.sendableBox.withLockedValue { box in + box.jwksEndpoint + } + } + set { + self.sendableBox.withLockedValue { box in + box.jwksEndpoint = newValue + } + } + } + var applicationIdentifier: String? { get { self.sendableBox.withLockedValue { box in @@ -91,8 +128,8 @@ public extension Application.JWT { } init() { - self.jwks = .init(uri: "https://appleid.apple.com/auth/keys") - let box = SendableBox(applicationIdentifier: nil) + let jwksEndpoint: URI = "https://appleid.apple.com/auth/keys" + let box = SendableBox(jwks: .init(uri: jwksEndpoint), jwksEndpoint: jwksEndpoint) self.sendableBox = .init(box) } } diff --git a/Sources/JWT/JWT+Google.swift b/Sources/JWT/JWT+Google.swift index cf9cfe1..6fe86a5 100644 --- a/Sources/JWT/JWT+Google.swift +++ b/Sources/JWT/JWT+Google.swift @@ -60,13 +60,23 @@ public extension Application.JWT { public let _jwt: Application.JWT public func keys(on request: Request) async throws -> JWTKeyCollection { - try await JWTKeyCollection().add(jwks: jwks.get(on: request).get()) + try await .init().add(jwks: jwks.get(on: request).get()) } public var jwks: EndpointCache { self.storage.jwks } + public var jwksEndpoint: URI { + get { + self.storage.jwksEndpoint + } + nonmutating set { + self.storage.jwksEndpoint = newValue + self.storage.jwks = .init(uri: newValue) + } + } + public var applicationIdentifier: String? { get { self.storage.applicationIdentifier @@ -91,13 +101,27 @@ public extension Application.JWT { private final class Storage: Sendable { private struct SendableBox: Sendable { - var applicationIdentifier: String? - var gSuiteDomainName: String? + var jwks: EndpointCache + var jwksEndpoint: URI + var applicationIdentifier: String? = nil + var gSuiteDomainName: String? = nil } - let jwks: EndpointCache private let sendableBox: NIOLockedValueBox + var jwks: EndpointCache { + get { + self.sendableBox.withLockedValue { box in + box.jwks + } + } + set { + self.sendableBox.withLockedValue { box in + box.jwks = newValue + } + } + } + var applicationIdentifier: String? { get { self.sendableBox.withLockedValue { box in @@ -124,9 +148,25 @@ public extension Application.JWT { } } + var jwksEndpoint: URI { + get { + self.sendableBox.withLockedValue { box in + box.jwksEndpoint + } + } + set { + self.sendableBox.withLockedValue { box in + box.jwksEndpoint = newValue + } + } + } + init() { - self.jwks = .init(uri: "https://www.googleapis.com/oauth2/v3/certs") - let box = SendableBox(applicationIdentifier: nil, gSuiteDomainName: nil) + let jwksEndpoint: URI = "https://www.googleapis.com/oauth2/v3/certs" + let box = SendableBox( + jwks: .init(uri: jwksEndpoint), + jwksEndpoint: jwksEndpoint + ) self.sendableBox = .init(box) } } diff --git a/Sources/JWT/JWT+Microsoft.swift b/Sources/JWT/JWT+Microsoft.swift index 06dac33..3927a77 100644 --- a/Sources/JWT/JWT+Microsoft.swift +++ b/Sources/JWT/JWT+Microsoft.swift @@ -55,6 +55,16 @@ public extension Application.JWT { public var jwks: EndpointCache { self.storage.jwks } + + public var jwksEndpoint: URI { + get { + self.storage.jwksEndpoint + } + nonmutating set { + self.storage.jwksEndpoint = newValue + self.storage.jwks = .init(uri: newValue) + } + } public var applicationIdentifier: String? { get { @@ -71,11 +81,25 @@ public extension Application.JWT { private final class Storage: Sendable { private struct SendableBox: Sendable { - var applicationIdentifier: String? + var jwks: EndpointCache + var jwksEndpoint: URI + var applicationIdentifier: String? = nil } - let jwks: EndpointCache private let sendableBox: NIOLockedValueBox + + var jwks: EndpointCache { + get { + self.sendableBox.withLockedValue { box in + box.jwks + } + } + set { + self.sendableBox.withLockedValue { box in + box.jwks = newValue + } + } + } var applicationIdentifier: String? { get { @@ -89,10 +113,26 @@ public extension Application.JWT { } } } + + var jwksEndpoint: URI { + get { + self.sendableBox.withLockedValue { box in + box.jwksEndpoint + } + } + set { + self.sendableBox.withLockedValue { box in + box.jwksEndpoint = newValue + } + } + } init() { - self.jwks = .init(uri: "https://login.microsoftonline.com/common/discovery/keys") - let box = SendableBox(applicationIdentifier: nil) + let jwksEndpoint: URI = "https://login.microsoftonline.com/common/discovery/keys" + let box = SendableBox( + jwks: .init(uri: jwksEndpoint), + jwksEndpoint: jwksEndpoint + ) self.sendableBox = .init(box) } } diff --git a/Tests/JWTTests/JWTTests.swift b/Tests/JWTTests/JWTTests.swift index 66df1fa..ed562a9 100644 --- a/Tests/JWTTests/JWTTests.swift +++ b/Tests/JWTTests/JWTTests.swift @@ -4,16 +4,16 @@ import XCTVapor class JWTTests: XCTestCase { var app: Application! - + override func setUp() async throws { app = try await Application.make(.testing) XCTAssert(isLoggingConfigured) } - + override func tearDown() async throws { try await app.asyncShutdown() } - + func testDocs() async throws { // Add HMAC with SHA-256 signer. await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256) @@ -116,7 +116,7 @@ class JWTTests: XCTestCase { }, afterResponse: { res async in XCTAssertEqual(res.status, .ok) }) - + try await app.test(.POST, "login", beforeRequest: { req in print(req) }, afterResponse: { res async throws in @@ -274,7 +274,7 @@ class JWTTests: XCTestCase { try await app.test(.GET, "test", headers: headers) { res async in XCTAssertEqual(res.status, .unauthorized) } - + try await app.test(.GET, "test2", headers: headers) { res async in XCTAssertEqual(res.status, .unauthorized) } @@ -336,6 +336,34 @@ class JWTTests: XCTestCase { } } } + + func testMicrosoftEndpointSwitch() async throws { + await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256) + + let testUser = TestUser(name: "foo") + let token = try await app.jwt.keys.sign(testUser) + + app.jwt.microsoft.applicationIdentifier = "" + app.get("microsoft") { req async throws in + let token = try await req.jwt.microsoft.verify() + return token.name ?? "none" + } + + try await app.test(.GET, "microsoft", headers: ["Authorization": "Bearer \(token)"]) { res async in + XCTAssertEqual(res.status, .unauthorized) + } + + app.jwt.microsoft.jwksEndpoint = "https://login.microsoftonline.com/common/discovery/v2.0/keys" + try await app.test(.GET, "microsoft", headers: ["Authorization": "Bearer \(token)"]) { res async in + XCTAssertEqual(res.status, .unauthorized) + } + + // Use a non-existent endpoint to show that endpoint switching works + app.jwt.microsoft.jwksEndpoint = "https://login.microsoftonline.com/nonexistent/endpoint" + try await app.test(.GET, "microsoft", headers: ["Authorization": "Bearer \(token)"]) { res async in + XCTAssertEqual(res.status, .internalServerError) + } + } } extension ByteBuffer {