From 8ce7280a77ca45711f1b08bc1abfac7a2bb9c97b Mon Sep 17 00:00:00 2001 From: Petr Pavlik Date: Mon, 21 Oct 2024 16:20:40 +0200 Subject: [PATCH] Add support for Firebase Authentication (#160) * Support firebase auth * Update JWTKit dependency to version 5.1.0 --- Package.swift | 2 +- Sources/JWT/JWT+FirebaseAuth.swift | 168 +++++++++++++++++++++++++++++ Tests/JWTTests/JWTTests.swift | 6 ++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 Sources/JWT/JWT+FirebaseAuth.swift diff --git a/Package.swift b/Package.swift index 15b8791..edb2830 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( .library(name: "JWT", targets: ["JWT"]) ], dependencies: [ - .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"), + .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.1.0"), .package(url: "https://github.com/vapor/vapor.git", from: "4.101.0"), ], targets: [ diff --git a/Sources/JWT/JWT+FirebaseAuth.swift b/Sources/JWT/JWT+FirebaseAuth.swift new file mode 100644 index 0000000..71c0969 --- /dev/null +++ b/Sources/JWT/JWT+FirebaseAuth.swift @@ -0,0 +1,168 @@ +import NIOConcurrencyHelpers +import Vapor + +extension Request.JWT { + public var firebaseAuth: FirebaseAuth { + .init(_jwt: self) + } + + public struct FirebaseAuth: Sendable { + public let _jwt: Request.JWT + + public func verify( + applicationIdentifier: String? = nil + ) async throws -> FirebaseAuthIdentityToken { + 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 -> FirebaseAuthIdentityToken { + try await self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier) + } + + public func verify( + _ message: some DataProtocol & Sendable, + applicationIdentifier: String? = nil + ) async throws -> FirebaseAuthIdentityToken { + let keys = try await self._jwt._request.application.jwt.firebaseAuth.keys(on: self._jwt._request) + let token = try await keys.verify(message, as: FirebaseAuthIdentityToken.self) + if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.firebaseAuth.applicationIdentifier { + try token.audience.verifyIntendedAudience(includes: applicationIdentifier) + guard token.audience.value.first == applicationIdentifier else { + throw JWTError.claimVerificationFailure( + failedClaim: token.audience, + reason: "Audience claim does not match expected value" + ) + } + guard token.issuer.value == "https://securetoken.google.com/\(applicationIdentifier)" else { + throw JWTError.claimVerificationFailure( + failedClaim: token.issuer, + reason: "Issuer claim does not match expected value" + ) + } + } + return token + } + } +} + +extension Application.JWT { + public var firebaseAuth: FirebaseAuth { + .init(_jwt: self) + } + + public struct FirebaseAuth: Sendable { + public let _jwt: Application.JWT + + public func keys(on request: Request) async throws -> JWTKeyCollection { + 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 + } + nonmutating set { + self.storage.applicationIdentifier = newValue + } + } + + private struct Key: StorageKey, LockKey { + typealias Value = Storage + } + + private final class Storage: Sendable { + private struct SendableBox: Sendable { + var jwks: EndpointCache + var jwksEndpoint: URI + var applicationIdentifier: String? = nil + } + + 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 + box.applicationIdentifier + } + } + set { + self.sendableBox.withLockedValue { box in + box.applicationIdentifier = newValue + } + } + } + + var jwksEndpoint: URI { + get { + self.sendableBox.withLockedValue { box in + box.jwksEndpoint + } + } + set { + self.sendableBox.withLockedValue { box in + box.jwksEndpoint = newValue + } + } + } + + init() { + let jwksEndpoint: URI = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com" + let box = SendableBox( + jwks: .init(uri: jwksEndpoint), + jwksEndpoint: jwksEndpoint + ) + self.sendableBox = .init(box) + } + } + + private var storage: Storage { + if let existing = self._jwt._application.storage[Key.self] { + return existing + } else { + let lock = self._jwt._application.locks.lock(for: Key.self) + lock.lock() + defer { lock.unlock() } + if let existing = self._jwt._application.storage[Key.self] { + return existing + } + let new = Storage() + self._jwt._application.storage[Key.self] = new + return new + } + } + } +} diff --git a/Tests/JWTTests/JWTTests.swift b/Tests/JWTTests/JWTTests.swift index 2fa813d..ed2e981 100644 --- a/Tests/JWTTests/JWTTests.swift +++ b/Tests/JWTTests/JWTTests.swift @@ -50,6 +50,12 @@ struct JWTTests { return .ok } + app.jwt.firebaseAuth.applicationIdentifier = "..." + app.get("firebase") { req async throws -> HTTPStatus in + _ = try await req.jwt.firebaseAuth.verify() + return .ok + } + // Fetch and verify JWT from incoming request. app.get("me") { req async throws -> HTTPStatus in try await req.jwt.verify(as: TestPayload.self)