Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(realtime): prevent sending expired tokens #618

Merged
merged 5 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading