Skip to content

Commit 8ce7280

Browse files
authored
Add support for Firebase Authentication (#160)
* Support firebase auth * Update JWTKit dependency to version 5.1.0
1 parent 44dab3d commit 8ce7280

File tree

3 files changed

+175
-1
lines changed

3 files changed

+175
-1
lines changed

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ let package = Package(
1313
.library(name: "JWT", targets: ["JWT"])
1414
],
1515
dependencies: [
16-
.package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"),
16+
.package(url: "https://github.com/vapor/jwt-kit.git", from: "5.1.0"),
1717
.package(url: "https://github.com/vapor/vapor.git", from: "4.101.0"),
1818
],
1919
targets: [

Sources/JWT/JWT+FirebaseAuth.swift

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import NIOConcurrencyHelpers
2+
import Vapor
3+
4+
extension Request.JWT {
5+
public var firebaseAuth: FirebaseAuth {
6+
.init(_jwt: self)
7+
}
8+
9+
public struct FirebaseAuth: Sendable {
10+
public let _jwt: Request.JWT
11+
12+
public func verify(
13+
applicationIdentifier: String? = nil
14+
) async throws -> FirebaseAuthIdentityToken {
15+
guard let token = self._jwt._request.headers.bearerAuthorization?.token else {
16+
self._jwt._request.logger.error("Request is missing JWT bearer header.")
17+
throw Abort(.unauthorized)
18+
}
19+
return try await self.verify(token, applicationIdentifier: applicationIdentifier)
20+
}
21+
22+
public func verify(
23+
_ message: String,
24+
applicationIdentifier: String? = nil
25+
) async throws -> FirebaseAuthIdentityToken {
26+
try await self.verify([UInt8](message.utf8), applicationIdentifier: applicationIdentifier)
27+
}
28+
29+
public func verify(
30+
_ message: some DataProtocol & Sendable,
31+
applicationIdentifier: String? = nil
32+
) async throws -> FirebaseAuthIdentityToken {
33+
let keys = try await self._jwt._request.application.jwt.firebaseAuth.keys(on: self._jwt._request)
34+
let token = try await keys.verify(message, as: FirebaseAuthIdentityToken.self)
35+
if let applicationIdentifier = applicationIdentifier ?? self._jwt._request.application.jwt.firebaseAuth.applicationIdentifier {
36+
try token.audience.verifyIntendedAudience(includes: applicationIdentifier)
37+
guard token.audience.value.first == applicationIdentifier else {
38+
throw JWTError.claimVerificationFailure(
39+
failedClaim: token.audience,
40+
reason: "Audience claim does not match expected value"
41+
)
42+
}
43+
guard token.issuer.value == "https://securetoken.google.com/\(applicationIdentifier)" else {
44+
throw JWTError.claimVerificationFailure(
45+
failedClaim: token.issuer,
46+
reason: "Issuer claim does not match expected value"
47+
)
48+
}
49+
}
50+
return token
51+
}
52+
}
53+
}
54+
55+
extension Application.JWT {
56+
public var firebaseAuth: FirebaseAuth {
57+
.init(_jwt: self)
58+
}
59+
60+
public struct FirebaseAuth: Sendable {
61+
public let _jwt: Application.JWT
62+
63+
public func keys(on request: Request) async throws -> JWTKeyCollection {
64+
try await .init().add(jwks: jwks.get(on: request).get())
65+
}
66+
67+
public var jwks: EndpointCache<JWKS> {
68+
self.storage.jwks
69+
}
70+
71+
public var jwksEndpoint: URI {
72+
get {
73+
self.storage.jwksEndpoint
74+
}
75+
nonmutating set {
76+
self.storage.jwksEndpoint = newValue
77+
self.storage.jwks = .init(uri: newValue)
78+
}
79+
}
80+
81+
public var applicationIdentifier: String? {
82+
get {
83+
self.storage.applicationIdentifier
84+
}
85+
nonmutating set {
86+
self.storage.applicationIdentifier = newValue
87+
}
88+
}
89+
90+
private struct Key: StorageKey, LockKey {
91+
typealias Value = Storage
92+
}
93+
94+
private final class Storage: Sendable {
95+
private struct SendableBox: Sendable {
96+
var jwks: EndpointCache<JWKS>
97+
var jwksEndpoint: URI
98+
var applicationIdentifier: String? = nil
99+
}
100+
101+
private let sendableBox: NIOLockedValueBox<SendableBox>
102+
103+
var jwks: EndpointCache<JWKS> {
104+
get {
105+
self.sendableBox.withLockedValue { box in
106+
box.jwks
107+
}
108+
}
109+
set {
110+
self.sendableBox.withLockedValue { box in
111+
box.jwks = newValue
112+
}
113+
}
114+
}
115+
116+
var applicationIdentifier: String? {
117+
get {
118+
self.sendableBox.withLockedValue { box in
119+
box.applicationIdentifier
120+
}
121+
}
122+
set {
123+
self.sendableBox.withLockedValue { box in
124+
box.applicationIdentifier = newValue
125+
}
126+
}
127+
}
128+
129+
var jwksEndpoint: URI {
130+
get {
131+
self.sendableBox.withLockedValue { box in
132+
box.jwksEndpoint
133+
}
134+
}
135+
set {
136+
self.sendableBox.withLockedValue { box in
137+
box.jwksEndpoint = newValue
138+
}
139+
}
140+
}
141+
142+
init() {
143+
let jwksEndpoint: URI = "https://www.googleapis.com/service_accounts/v1/jwk/[email protected]"
144+
let box = SendableBox(
145+
jwks: .init(uri: jwksEndpoint),
146+
jwksEndpoint: jwksEndpoint
147+
)
148+
self.sendableBox = .init(box)
149+
}
150+
}
151+
152+
private var storage: Storage {
153+
if let existing = self._jwt._application.storage[Key.self] {
154+
return existing
155+
} else {
156+
let lock = self._jwt._application.locks.lock(for: Key.self)
157+
lock.lock()
158+
defer { lock.unlock() }
159+
if let existing = self._jwt._application.storage[Key.self] {
160+
return existing
161+
}
162+
let new = Storage()
163+
self._jwt._application.storage[Key.self] = new
164+
return new
165+
}
166+
}
167+
}
168+
}

Tests/JWTTests/JWTTests.swift

+6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ struct JWTTests {
5050
return .ok
5151
}
5252

53+
app.jwt.firebaseAuth.applicationIdentifier = "..."
54+
app.get("firebase") { req async throws -> HTTPStatus in
55+
_ = try await req.jwt.firebaseAuth.verify()
56+
return .ok
57+
}
58+
5359
// Fetch and verify JWT from incoming request.
5460
app.get("me") { req async throws -> HTTPStatus in
5561
try await req.jwt.verify(as: TestPayload.self)

0 commit comments

Comments
 (0)