Skip to content

Commit

Permalink
Add support for Firebase Authentication (#160)
Browse files Browse the repository at this point in the history
* Support firebase auth

* Update JWTKit dependency to version 5.1.0
  • Loading branch information
petrpavlik authored Oct 21, 2024
1 parent 44dab3d commit 8ce7280
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 1 deletion.
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/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
}
}
}
}
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

0 comments on commit 8ce7280

Please sign in to comment.