Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Firebase Authentication #160

Merged
merged 2 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
168 changes: 168 additions & 0 deletions Sources/JWT/JWT+FirebaseAuth.swift
Original file line number Diff line number Diff line change
@@ -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<JWKS> {
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<JWKS>
var jwksEndpoint: URI
var applicationIdentifier: String? = nil
}

private let sendableBox: NIOLockedValueBox<SendableBox>

var jwks: EndpointCache<JWKS> {
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/[email protected]"
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
}
}
}
}
6 changes: 6 additions & 0 deletions Tests/JWTTests/JWTTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down