Skip to content

Commit b89ab16

Browse files
committed
JWT
1 parent f260732 commit b89ab16

File tree

7 files changed

+130
-125
lines changed

7 files changed

+130
-125
lines changed

Package.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ let package = Package(
2323
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
2424
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
2525
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
26+
.package(url: "https://github.com/vapor/jwt-kit.git", from: "4.13.5"),
2627
// Only used for DocC generation
2728
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"),
28-
// Only used for Testing
29-
.package(url: "https://github.com/vapor/jwt-kit.git", from: "4.13.4"),
3029
],
3130
targets: [
3231
.target(
@@ -41,6 +40,7 @@ let package = Package(
4140
.product(name: "DequeModule", package: "swift-collections"),
4241
.product(name: "OrderedCollections", package: "swift-collections"),
4342
.product(name: "Logging", package: "swift-log"),
43+
.product(name: "JWTKit", package: "jwt-kit"),
4444
"LKObjCHelpers",
4545
],
4646
exclude: [
@@ -57,14 +57,12 @@ let package = Package(
5757
name: "LiveKitTests",
5858
dependencies: [
5959
"LiveKit",
60-
.product(name: "JWTKit", package: "jwt-kit"),
6160
]
6261
),
6362
.testTarget(
6463
name: "LiveKitTestsObjC",
6564
dependencies: [
6665
"LiveKit",
67-
.product(name: "JWTKit", package: "jwt-kit"),
6866
]
6967
),
7068
],

Package@swift-6.0.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@ let package = Package(
2424
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
2525
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
2626
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
27+
.package(url: "https://github.com/vapor/jwt-kit.git", from: "4.13.5"),
2728
// Only used for DocC generation
2829
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"),
29-
// Only used for Testing
30-
.package(url: "https://github.com/vapor/jwt-kit.git", from: "4.13.4"),
3130
],
3231
targets: [
3332
.target(
@@ -42,6 +41,7 @@ let package = Package(
4241
.product(name: "DequeModule", package: "swift-collections"),
4342
.product(name: "OrderedCollections", package: "swift-collections"),
4443
.product(name: "Logging", package: "swift-log"),
44+
.product(name: "JWTKit", package: "jwt-kit"),
4545
"LKObjCHelpers",
4646
],
4747
exclude: [
@@ -58,14 +58,12 @@ let package = Package(
5858
name: "LiveKitTests",
5959
dependencies: [
6060
"LiveKit",
61-
.product(name: "JWTKit", package: "jwt-kit"),
6261
]
6362
),
6463
.testTarget(
6564
name: "LiveKitTestsObjC",
6665
dependencies: [
6766
"LiveKit",
68-
.product(name: "JWTKit", package: "jwt-kit"),
6967
]
7068
),
7169
],

Sources/LiveKit/Auth/JWT.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2025 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import JWTKit
18+
19+
public struct LiveKitJWTPayload: JWTPayload, Codable, Equatable {
20+
public struct VideoGrant: Codable, Equatable {
21+
/// Name of the room, must be set for admin or join permissions
22+
public let room: String?
23+
/// Permission to create a room
24+
public let roomCreate: Bool?
25+
/// Permission to join a room as a participant, room must be set
26+
public let roomJoin: Bool?
27+
/// Permission to list rooms
28+
public let roomList: Bool?
29+
/// Permission to start a recording
30+
public let roomRecord: Bool?
31+
/// Permission to control a specific room, room must be set
32+
public let roomAdmin: Bool?
33+
34+
/// Allow participant to publish. If neither canPublish or canSubscribe is set, both publish and subscribe are enabled
35+
public let canPublish: Bool?
36+
/// Allow participant to subscribe to other tracks
37+
public let canSubscribe: Bool?
38+
/// Allow participants to publish data, defaults to true if not set
39+
public let canPublishData: Bool?
40+
/// Allowed sources for publishing
41+
public let canPublishSources: [String]?
42+
/// Participant isn't visible to others
43+
public let hidden: Bool?
44+
/// Participant is recording the room, when set, allows room to indicate it's being recorded
45+
public let recorder: Bool?
46+
47+
public init(room: String? = nil,
48+
roomCreate: Bool? = nil,
49+
roomJoin: Bool? = nil,
50+
roomList: Bool? = nil,
51+
roomRecord: Bool? = nil,
52+
roomAdmin: Bool? = nil,
53+
canPublish: Bool? = nil,
54+
canSubscribe: Bool? = nil,
55+
canPublishData: Bool? = nil,
56+
canPublishSources: [String]? = nil,
57+
hidden: Bool? = nil,
58+
recorder: Bool? = nil)
59+
{
60+
self.room = room
61+
self.roomCreate = roomCreate
62+
self.roomJoin = roomJoin
63+
self.roomList = roomList
64+
self.roomRecord = roomRecord
65+
self.roomAdmin = roomAdmin
66+
self.canPublish = canPublish
67+
self.canSubscribe = canSubscribe
68+
self.canPublishData = canPublishData
69+
self.canPublishSources = canPublishSources
70+
self.hidden = hidden
71+
self.recorder = recorder
72+
}
73+
}
74+
75+
/// Expiration time claim
76+
public let exp: ExpirationClaim
77+
/// Issuer claim
78+
public let iss: IssuerClaim
79+
/// Not before claim
80+
public let nbf: NotBeforeClaim
81+
/// Subject claim
82+
public let sub: SubjectClaim
83+
84+
/// Participant name
85+
public let name: String?
86+
/// Participant metadata
87+
public let metadata: String?
88+
/// Video grants for the participant
89+
public let video: VideoGrant?
90+
91+
public func verify(using _: JWTSigner) throws {
92+
try nbf.verifyNotBefore()
93+
try exp.verifyNotExpired()
94+
}
95+
96+
static func fromUnverified(token: String) -> Self? {
97+
try? JWTSigners().unverified(token, as: Self.self)
98+
}
99+
}

Sources/LiveKit/Auth/TokenSource.swift

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -249,39 +249,23 @@ public extension Token.Response {
249249
/// - Parameter tolerance: Time tolerance in seconds for token expiration check (default: 60 seconds)
250250
/// - Returns: `true` if the token is valid and not expired, `false` otherwise
251251
func hasValidToken(withTolerance tolerance: TimeInterval = 60) -> Bool {
252-
let parts = participantToken.components(separatedBy: ".")
253-
guard parts.count == 3 else {
252+
guard let jwt = jwt() else {
254253
return false
255254
}
256255

257-
let payloadData = parts[1]
258-
259-
struct JWTPayload: Decodable {
260-
let nbf: Double
261-
let exp: Double
262-
}
263-
264-
guard let payloadJSON = payloadData.base64Decode(),
265-
let payload = try? JSONDecoder().decode(JWTPayload.self, from: payloadJSON)
266-
else {
256+
do {
257+
try jwt.nbf.verifyNotBefore()
258+
try jwt.exp.verifyNotExpired(currentDate: Date().addingTimeInterval(tolerance))
259+
} catch {
267260
return false
268261
}
269262

270-
let now = Date().timeIntervalSince1970
271-
return payload.nbf <= now && payload.exp > now - tolerance
263+
return true
272264
}
273-
}
274-
275-
private extension String {
276-
func base64Decode() -> Data? {
277-
var base64 = self
278-
base64 = base64.replacingOccurrences(of: "-", with: "+")
279-
base64 = base64.replacingOccurrences(of: "_", with: "/")
280-
281-
while base64.count % 4 != 0 {
282-
base64.append("=")
283-
}
284265

285-
return Data(base64Encoded: base64)
266+
/// Extracts the JWT payload from the participant token.
267+
/// - Returns: The JWT payload if found, nil otherwise
268+
func jwt() -> LiveKitJWTPayload? {
269+
LiveKitJWTPayload.fromUnverified(token: participantToken)
286270
}
287271
}

Tests/LiveKitTests/Auth/TokenSourceTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class TokenSourceTests: LKTestCase {
3737
identity: request.participantIdentity ?? "test-identity"
3838
)
3939
tokenGenerator.name = request.participantName ?? participantName
40-
tokenGenerator.videoGrant = VideoGrant(room: request.roomName ?? "test-room", roomJoin: true)
40+
tokenGenerator.videoGrant = LiveKitJWTPayload.VideoGrant(room: request.roomName ?? "test-room", roomJoin: true)
4141

4242
let token = try tokenGenerator.sign()
4343

@@ -76,7 +76,7 @@ class TokenSourceTests: LKTestCase {
7676
ttl: -60
7777
)
7878
tokenGenerator.name = request.participantName ?? "test-participant"
79-
tokenGenerator.videoGrant = VideoGrant(room: request.roomName ?? "test-room", roomJoin: true)
79+
tokenGenerator.videoGrant = LiveKitJWTPayload.VideoGrant(room: request.roomName ?? "test-room", roomJoin: true)
8080

8181
let token = try tokenGenerator.sign()
8282

Tests/LiveKitTests/Support/Room.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ extension LKTestCase {
7676
apiSecret: apiSecret,
7777
identity: identity)
7878

79-
tokenGenerator.videoGrant = VideoGrant(room: room,
80-
roomJoin: true,
81-
canPublish: canPublish,
82-
canSubscribe: canSubscribe,
83-
canPublishData: canPublishData,
84-
canPublishSources: canPublishSources.map(String.init))
79+
tokenGenerator.videoGrant = LiveKitJWTPayload.VideoGrant(room: room,
80+
roomJoin: true,
81+
canPublish: canPublish,
82+
canSubscribe: canSubscribe,
83+
canPublishData: canPublishData,
84+
canPublishSources: canPublishSources.map(String.init))
8585
return try tokenGenerator.sign()
8686
}
8787

Tests/LiveKitTests/Support/TokenGenerator.swift

Lines changed: 9 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -16,83 +16,9 @@
1616

1717
import Foundation
1818
import JWTKit
19-
20-
public struct VideoGrant: Codable, Equatable {
21-
/** name of the room, must be set for admin or join permissions */
22-
let room: String?
23-
/** permission to create a room */
24-
let roomCreate: Bool?
25-
/** permission to join a room as a participant, room must be set */
26-
let roomJoin: Bool?
27-
/** permission to list rooms */
28-
let roomList: Bool?
29-
/** permission to start a recording */
30-
let roomRecord: Bool?
31-
/** permission to control a specific room, room must be set */
32-
let roomAdmin: Bool?
33-
34-
/**
35-
* allow participant to publish. If neither canPublish or canSubscribe is set,
36-
* both publish and subscribe are enabled
37-
*/
38-
let canPublish: Bool?
39-
/** allow participant to subscribe to other tracks */
40-
let canSubscribe: Bool?
41-
/**
42-
* allow participants to publish data, defaults to true if not set
43-
*/
44-
let canPublishData: Bool?
45-
/** allowed sources for publishing */
46-
let canPublishSources: [String]? // String as returned in the JWT
47-
/** participant isn't visible to others */
48-
let hidden: Bool?
49-
/** participant is recording the room, when set, allows room to indicate it's being recorded */
50-
let recorder: Bool?
51-
52-
init(room: String? = nil,
53-
roomCreate: Bool? = nil,
54-
roomJoin: Bool? = nil,
55-
roomList: Bool? = nil,
56-
roomRecord: Bool? = nil,
57-
roomAdmin: Bool? = nil,
58-
canPublish: Bool? = nil,
59-
canSubscribe: Bool? = nil,
60-
canPublishData: Bool? = nil,
61-
canPublishSources: [String]? = nil,
62-
hidden: Bool? = nil,
63-
recorder: Bool? = nil)
64-
{
65-
self.room = room
66-
self.roomCreate = roomCreate
67-
self.roomJoin = roomJoin
68-
self.roomList = roomList
69-
self.roomRecord = roomRecord
70-
self.roomAdmin = roomAdmin
71-
self.canPublish = canPublish
72-
self.canSubscribe = canSubscribe
73-
self.canPublishData = canPublishData
74-
self.canPublishSources = canPublishSources
75-
self.hidden = hidden
76-
self.recorder = recorder
77-
}
78-
}
19+
@testable import LiveKit
7920

8021
public class TokenGenerator {
81-
private struct Payload: JWTPayload, Equatable {
82-
let exp: ExpirationClaim
83-
let iss: IssuerClaim
84-
let nbf: NotBeforeClaim
85-
let sub: SubjectClaim
86-
87-
let name: String?
88-
let metadata: String?
89-
let video: VideoGrant?
90-
91-
func verify(using _: JWTSigner) throws {
92-
fatalError("not implemented")
93-
}
94-
}
95-
9622
// 30 mins
9723
static let defaultTTL: TimeInterval = 30 * 60
9824

@@ -104,7 +30,7 @@ public class TokenGenerator {
10430
public var ttl: TimeInterval
10531
public var name: String?
10632
public var metadata: String?
107-
public var videoGrant: VideoGrant?
33+
public var videoGrant: LiveKitJWTPayload.VideoGrant?
10834

10935
// MARK: - Private
11036

@@ -127,13 +53,13 @@ public class TokenGenerator {
12753

12854
let n = Date().timeIntervalSince1970
12955

130-
let p = Payload(exp: .init(value: Date(timeIntervalSince1970: floor(n + ttl))),
131-
iss: .init(stringLiteral: apiKey),
132-
nbf: .init(value: Date(timeIntervalSince1970: floor(n))),
133-
sub: .init(stringLiteral: identity),
134-
name: name,
135-
metadata: metadata,
136-
video: videoGrant)
56+
let p = LiveKitJWTPayload(exp: .init(value: Date(timeIntervalSince1970: floor(n + ttl))),
57+
iss: .init(stringLiteral: apiKey),
58+
nbf: .init(value: Date(timeIntervalSince1970: floor(n))),
59+
sub: .init(stringLiteral: identity),
60+
name: name,
61+
metadata: metadata,
62+
video: videoGrant)
13763

13864
return try signers.sign(p)
13965
}

0 commit comments

Comments
 (0)