Skip to content

Commit 595277b

Browse files
authored
fix(realtime): prevent sending expired tokens (#618)
* fix(realtime): prevent sending expired tokens * fix test * test * remove jwt-kit * update packages
1 parent 7e52aec commit 595277b

File tree

8 files changed

+102
-54
lines changed

8 files changed

+102
-54
lines changed

Sources/Auth/AuthClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -861,7 +861,7 @@ public final class AuthClient: Sendable {
861861
var hasExpired = true
862862
var session: Session
863863

864-
let jwt = try decode(jwt: accessToken)
864+
let jwt = JWT.decodePayload(accessToken)
865865
if let exp = jwt?["exp"] as? TimeInterval {
866866
expiresAt = Date(timeIntervalSince1970: exp)
867867
hasExpired = expiresAt <= now

Sources/Auth/AuthMFA.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public struct AuthMFA: Sendable {
125125
public func getAuthenticatorAssuranceLevel() async throws -> AuthMFAGetAuthenticatorAssuranceLevelResponse {
126126
do {
127127
let session = try await sessionManager.session()
128-
let payload = try decode(jwt: session.accessToken)
128+
let payload = JWT.decodePayload(session.accessToken)
129129

130130
var currentLevel: AuthenticatorAssuranceLevels?
131131

Sources/Auth/Internal/Helpers.swift

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -39,33 +39,3 @@ private func extractParams(from fragment: String) -> [URLQueryItem] {
3939
: nil
4040
}
4141
}
42-
43-
func decode(jwt: String) throws -> [String: Any]? {
44-
let parts = jwt.split(separator: ".")
45-
guard parts.count == 3 else {
46-
return nil
47-
}
48-
49-
let payload = String(parts[1])
50-
guard let data = base64URLDecode(payload) else {
51-
return nil
52-
}
53-
let json = try JSONSerialization.jsonObject(with: data, options: [])
54-
guard let decodedPayload = json as? [String: Any] else {
55-
return nil
56-
}
57-
return decodedPayload
58-
}
59-
60-
private func base64URLDecode(_ value: String) -> Data? {
61-
var base64 = value.replacingOccurrences(of: "-", with: "+")
62-
.replacingOccurrences(of: "_", with: "/")
63-
let length = Double(base64.lengthOfBytes(using: .utf8))
64-
let requiredLength = 4 * ceil(length / 4.0)
65-
let paddingLength = requiredLength - length
66-
if paddingLength > 0 {
67-
let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
68-
base64 = base64 + padding
69-
}
70-
return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
71-
}

Sources/Helpers/JWT.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// JWT.swift
3+
// Supabase
4+
//
5+
// Created by Guilherme Souza on 28/11/24.
6+
//
7+
8+
import Foundation
9+
10+
package enum JWT {
11+
package static func decodePayload(_ jwt: String) -> [String: Any]? {
12+
let parts = jwt.split(separator: ".")
13+
guard parts.count == 3 else {
14+
return nil
15+
}
16+
17+
let payload = String(parts[1])
18+
guard let data = base64URLDecode(payload) else {
19+
return nil
20+
}
21+
let json = try? JSONSerialization.jsonObject(with: data, options: [])
22+
guard let decodedPayload = json as? [String: Any] else {
23+
return nil
24+
}
25+
return decodedPayload
26+
}
27+
28+
private static func base64URLDecode(_ value: String) -> Data? {
29+
var base64 = value.replacingOccurrences(of: "-", with: "+")
30+
.replacingOccurrences(of: "_", with: "/")
31+
let length = Double(base64.lengthOfBytes(using: .utf8))
32+
let requiredLength = 4 * ceil(length / 4.0)
33+
let paddingLength = requiredLength - length
34+
if paddingLength > 0 {
35+
let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
36+
base64 = base64 + padding
37+
}
38+
return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
39+
}
40+
}

Sources/Realtime/V2/RealtimeClientV2.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ public final class RealtimeClientV2: Sendable {
363363
/// Sets the JWT access token used for channel subscription authorization and Realtime RLS.
364364
/// - Parameter token: A JWT string.
365365
public func setAuth(_ token: String?) async {
366+
if let token, let payload = JWT.decodePayload(token),
367+
let exp = payload["exp"] as? TimeInterval, exp < Date().timeIntervalSince1970
368+
{
369+
options.logger?.warning(
370+
"InvalidJWTToken: Invalid value for JWT claim \"exp\" with value \(exp)")
371+
return
372+
}
373+
366374
mutableState.withValue {
367375
$0.accessToken = token
368376
}

Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 28 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import XCTest
22

3-
@testable import Auth
3+
@testable import Helpers
44

55
final class JWTTests: XCTestCase {
66
func testDecodeJWT() throws {
77
let token =
88
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6ImhpQGJpbmFyeXNjcmFwaW5nLmNvIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIn0.CGr5zNE5Yltlbn_3Ms2cjSLs_AW9RKM3lxh7cTQrg0w"
9-
let jwt = try decode(jwt: token)
9+
let jwt = JWT.decodePayload(token)
1010
let exp = try XCTUnwrap(jwt?["exp"] as? TimeInterval)
11-
XCTAssertEqual(exp, 1648640021)
11+
XCTAssertEqual(exp, 1_648_640_021)
1212
}
1313
}

Tests/RealtimeTests/RealtimeTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,27 @@ final class RealtimeTests: XCTestCase {
342342
}
343343
}
344344

345+
func testSetAuth() async {
346+
let validToken =
347+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjY0MDkyMjExMjAwfQ.GfiEKLl36X8YWcatHg31jRbilovlGecfUKnOyXMSX9c"
348+
await sut.setAuth(validToken)
349+
350+
XCTAssertEqual(sut.mutableState.accessToken, validToken)
351+
}
352+
353+
func testSetAuthWithExpiredToken() async throws {
354+
let expiredToken =
355+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOi02NDA5MjIxMTIwMH0.tnbZRC8vEyK3zaxPxfOjNgvpnuum18dxYlXeHJ4r7u8"
356+
await sut.setAuth(expiredToken)
357+
358+
XCTAssertNotEqual(sut.mutableState.accessToken, expiredToken)
359+
}
360+
361+
func testSetAuthWithNonJWT() async throws {
362+
let token = "sb-token"
363+
await sut.setAuth(token)
364+
}
365+
345366
private func connectSocketAndWait() async {
346367
ws.mockConnect(.connected)
347368
await sut.connect()

0 commit comments

Comments
 (0)