diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 2ca227c1..d2c383f0 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -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 diff --git a/Sources/Auth/AuthMFA.swift b/Sources/Auth/AuthMFA.swift index 1c329e4d..fa5bb485 100644 --- a/Sources/Auth/AuthMFA.swift +++ b/Sources/Auth/AuthMFA.swift @@ -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? diff --git a/Sources/Auth/Internal/Helpers.swift b/Sources/Auth/Internal/Helpers.swift index 07d2a02a..de321a30 100644 --- a/Sources/Auth/Internal/Helpers.swift +++ b/Sources/Auth/Internal/Helpers.swift @@ -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) -} diff --git a/Sources/Helpers/JWT.swift b/Sources/Helpers/JWT.swift new file mode 100644 index 00000000..86dfb5d0 --- /dev/null +++ b/Sources/Helpers/JWT.swift @@ -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) + } +} diff --git a/Sources/Realtime/V2/RealtimeClientV2.swift b/Sources/Realtime/V2/RealtimeClientV2.swift index 5c072c4b..60976255 100644 --- a/Sources/Realtime/V2/RealtimeClientV2.swift +++ b/Sources/Realtime/V2/RealtimeClientV2.swift @@ -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 } diff --git a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved index 21ad5848..673d45b6 100644 --- a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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" } }, { @@ -45,13 +45,22 @@ "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" } }, { @@ -59,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", - "version" : "1.1.3" + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" } }, { @@ -68,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version" : "1.1.0" + "revision" : "163409ef7dae9d960b87f34b51587b6609a76c1f", + "version" : "1.3.0" } }, { @@ -77,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "9f95b4d033a4edd3814b48608db3f2ca90c7218b", - "version" : "3.7.0" + "revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779", + "version" : "3.10.0" } }, { @@ -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" } }, { @@ -113,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "6d932a79e7173b275b96c600c86c603cf84f153c", - "version" : "1.17.4" + "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7", + "version" : "1.17.6" } }, { @@ -122,8 +131,8 @@ "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" } }, { @@ -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" } } ], diff --git a/Tests/AuthTests/JWTTests.swift b/Tests/HelpersTests/JWTTests.swift similarity index 84% rename from Tests/AuthTests/JWTTests.swift rename to Tests/HelpersTests/JWTTests.swift index f505a05d..9e8161b9 100644 --- a/Tests/AuthTests/JWTTests.swift +++ b/Tests/HelpersTests/JWTTests.swift @@ -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) } } diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index 35a318cc..ea989a52 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -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()