Skip to content

Commit

Permalink
fix(realtime): prevent sending expired tokens (#618)
Browse files Browse the repository at this point in the history
* fix(realtime): prevent sending expired tokens

* fix test

* test

* remove jwt-kit

* update packages
  • Loading branch information
grdsdev authored Dec 4, 2024
1 parent 7e52aec commit 595277b
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 54 deletions.
2 changes: 1 addition & 1 deletion Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -861,7 +861,7 @@ public final class AuthClient: Sendable {
var hasExpired = true
var session: Session

let jwt = try decode(jwt: accessToken)
let jwt = JWT.decodePayload(accessToken)
if let exp = jwt?["exp"] as? TimeInterval {
expiresAt = Date(timeIntervalSince1970: exp)
hasExpired = expiresAt <= now
Expand Down
2 changes: 1 addition & 1 deletion Sources/Auth/AuthMFA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public struct AuthMFA: Sendable {
public func getAuthenticatorAssuranceLevel() async throws -> AuthMFAGetAuthenticatorAssuranceLevelResponse {
do {
let session = try await sessionManager.session()
let payload = try decode(jwt: session.accessToken)
let payload = JWT.decodePayload(session.accessToken)

var currentLevel: AuthenticatorAssuranceLevels?

Expand Down
30 changes: 0 additions & 30 deletions Sources/Auth/Internal/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,33 +39,3 @@ private func extractParams(from fragment: String) -> [URLQueryItem] {
: nil
}
}

func decode(jwt: String) throws -> [String: Any]? {
let parts = jwt.split(separator: ".")
guard parts.count == 3 else {
return nil
}

let payload = String(parts[1])
guard let data = base64URLDecode(payload) else {
return nil
}
let json = try JSONSerialization.jsonObject(with: data, options: [])
guard let decodedPayload = json as? [String: Any] else {
return nil
}
return decodedPayload
}

private func base64URLDecode(_ value: String) -> Data? {
var base64 = value.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let length = Double(base64.lengthOfBytes(using: .utf8))
let requiredLength = 4 * ceil(length / 4.0)
let paddingLength = requiredLength - length
if paddingLength > 0 {
let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
base64 = base64 + padding
}
return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
}
40 changes: 40 additions & 0 deletions Sources/Helpers/JWT.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// JWT.swift
// Supabase
//
// Created by Guilherme Souza on 28/11/24.
//

import Foundation

package enum JWT {
package static func decodePayload(_ jwt: String) -> [String: Any]? {
let parts = jwt.split(separator: ".")
guard parts.count == 3 else {
return nil
}

let payload = String(parts[1])
guard let data = base64URLDecode(payload) else {
return nil
}
let json = try? JSONSerialization.jsonObject(with: data, options: [])
guard let decodedPayload = json as? [String: Any] else {
return nil
}
return decodedPayload
}

private static func base64URLDecode(_ value: String) -> Data? {
var base64 = value.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let length = Double(base64.lengthOfBytes(using: .utf8))
let requiredLength = 4 * ceil(length / 4.0)
let paddingLength = requiredLength - length
if paddingLength > 0 {
let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
base64 = base64 + padding
}
return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
}
}
8 changes: 8 additions & 0 deletions Sources/Realtime/V2/RealtimeClientV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,14 @@ public final class RealtimeClientV2: Sendable {
/// Sets the JWT access token used for channel subscription authorization and Realtime RLS.
/// - Parameter token: A JWT string.
public func setAuth(_ token: String?) async {
if let token, let payload = JWT.decodePayload(token),
let exp = payload["exp"] as? TimeInterval, exp < Date().timeIntervalSince1970
{
options.logger?.warning(
"InvalidJWTToken: Invalid value for JWT claim \"exp\" with value \(exp)")
return
}

mutableState.withValue {
$0.accessToken = token
}
Expand Down
47 changes: 28 additions & 19 deletions Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/openid/AppAuth-iOS.git",
"state" : {
"revision" : "c89ed571ae140f8eb1142735e6e23d7bb8c34cb2",
"version" : "1.7.5"
"revision" : "2781038865a80e2c425a1da12cc1327bcd56501f",
"version" : "1.7.6"
}
},
{
Expand Down Expand Up @@ -45,40 +45,49 @@
"version" : "1.0.6"
}
},
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
"revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6",
"version" : "1.3.0"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "642e6aab8e03e5f992d9c83e38c5be98cfad5078",
"version" : "1.5.5"
"revision" : "bc92c4b27f9a84bfb498cdbfdf35d5a357e9161f",
"version" : "1.5.6"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "9bf03ff58ce34478e66aaee630e491823326fd06",
"version" : "1.1.3"
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
"version" : "1.1.4"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
"version" : "1.1.0"
"revision" : "163409ef7dae9d960b87f34b51587b6609a76c1f",
"version" : "1.3.0"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "9f95b4d033a4edd3814b48608db3f2ca90c7218b",
"version" : "3.7.0"
"revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779",
"version" : "3.10.0"
}
},
{
Expand All @@ -93,10 +102,10 @@
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types",
"location" : "https://github.com/apple/swift-http-types.git",
"state" : {
"revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd",
"version" : "1.3.0"
"revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3",
"version" : "1.3.1"
}
},
{
Expand All @@ -113,17 +122,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "6d932a79e7173b275b96c600c86c603cf84f153c",
"version" : "1.17.4"
"revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7",
"version" : "1.17.6"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax",
"state" : {
"revision" : "515f79b522918f83483068d99c68daeb5116342d",
"version" : "600.0.0-prerelease-2024-08-20"
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
},
{
Expand All @@ -140,8 +149,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "96beb108a57f24c8476ae1f309239270772b2940",
"version" : "1.2.5"
"revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1",
"version" : "1.4.3"
}
}
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import XCTest

@testable import Auth
@testable import Helpers

final class JWTTests: XCTestCase {
func testDecodeJWT() throws {
let token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6ImhpQGJpbmFyeXNjcmFwaW5nLmNvIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIn0.CGr5zNE5Yltlbn_3Ms2cjSLs_AW9RKM3lxh7cTQrg0w"
let jwt = try decode(jwt: token)
let jwt = JWT.decodePayload(token)
let exp = try XCTUnwrap(jwt?["exp"] as? TimeInterval)
XCTAssertEqual(exp, 1648640021)
XCTAssertEqual(exp, 1_648_640_021)
}
}
21 changes: 21 additions & 0 deletions Tests/RealtimeTests/RealtimeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,27 @@ final class RealtimeTests: XCTestCase {
}
}

func testSetAuth() async {
let validToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjY0MDkyMjExMjAwfQ.GfiEKLl36X8YWcatHg31jRbilovlGecfUKnOyXMSX9c"
await sut.setAuth(validToken)

XCTAssertEqual(sut.mutableState.accessToken, validToken)
}

func testSetAuthWithExpiredToken() async throws {
let expiredToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOi02NDA5MjIxMTIwMH0.tnbZRC8vEyK3zaxPxfOjNgvpnuum18dxYlXeHJ4r7u8"
await sut.setAuth(expiredToken)

XCTAssertNotEqual(sut.mutableState.accessToken, expiredToken)
}

func testSetAuthWithNonJWT() async throws {
let token = "sb-token"
await sut.setAuth(token)
}

private func connectSocketAndWait() async {
ws.mockConnect(.connected)
await sut.connect()
Expand Down

0 comments on commit 595277b

Please sign in to comment.