diff --git a/Sources/Auth/AuthClientConfiguration.swift b/Sources/Auth/AuthClientConfiguration.swift index cf1ebe53..4b724a0a 100644 --- a/Sources/Auth/AuthClientConfiguration.swift +++ b/Sources/Auth/AuthClientConfiguration.swift @@ -24,6 +24,9 @@ extension AuthClient { public var headers: [String: String] public let flowType: AuthFlowType public let redirectToURL: URL? + + /// Optional key name used for storing tokens in local storage. + public var storageKey: String? public let localStorage: any AuthLocalStorage public let logger: (any SupabaseLogger)? public let encoder: JSONEncoder @@ -40,6 +43,7 @@ extension AuthClient { /// - headers: Custom headers to be included in requests. /// - flowType: The authentication flow type. /// - redirectToURL: Default URL to be used for redirect on the flows that requires it. + /// - storageKey: Optional key name used for storing tokens in local storage. /// - localStorage: The storage mechanism for local data. /// - logger: The logger to use. /// - encoder: The JSON encoder to use for encoding requests. @@ -51,6 +55,7 @@ extension AuthClient { headers: [String: String] = [:], flowType: AuthFlowType = Configuration.defaultFlowType, redirectToURL: URL? = nil, + storageKey: String? = nil, localStorage: any AuthLocalStorage, logger: (any SupabaseLogger)? = nil, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, @@ -64,6 +69,7 @@ extension AuthClient { self.headers = headers self.flowType = flowType self.redirectToURL = redirectToURL + self.storageKey = storageKey self.localStorage = localStorage self.logger = logger self.encoder = encoder @@ -80,6 +86,7 @@ extension AuthClient { /// - headers: Custom headers to be included in requests. /// - flowType: The authentication flow type.. /// - redirectToURL: Default URL to be used for redirect on the flows that requires it. + /// - storageKey: Optional key name used for storing tokens in local storage. /// - localStorage: The storage mechanism for local data.. /// - logger: The logger to use. /// - encoder: The JSON encoder to use for encoding requests. @@ -91,6 +98,7 @@ extension AuthClient { headers: [String: String] = [:], flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, redirectToURL: URL? = nil, + storageKey: String? = nil, localStorage: any AuthLocalStorage, logger: (any SupabaseLogger)? = nil, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, @@ -104,6 +112,7 @@ extension AuthClient { headers: headers, flowType: flowType, redirectToURL: redirectToURL, + storageKey: storageKey, localStorage: localStorage, logger: logger, encoder: encoder, diff --git a/Sources/Auth/Defaults.swift b/Sources/Auth/Defaults.swift index d974f5f9..0675ee45 100644 --- a/Sources/Auth/Defaults.swift +++ b/Sources/Auth/Defaults.swift @@ -53,4 +53,6 @@ extension AuthClient.Configuration { /// The default value when initializing a ``AuthClient`` instance. public static let defaultAutoRefreshToken: Bool = true + + static let defaultStorageKey = "supabase.auth.token" } diff --git a/Sources/Auth/Internal/SessionStorage.swift b/Sources/Auth/Internal/SessionStorage.swift index 827f50aa..d96a2641 100644 --- a/Sources/Auth/Internal/SessionStorage.swift +++ b/Sources/Auth/Internal/SessionStorage.swift @@ -20,20 +20,37 @@ struct StoredSession: Codable { } extension AuthLocalStorage { + var key: String { + Current.configuration.storageKey ?? AuthClient.Configuration.defaultStorageKey + } + + var oldKey: String { "supabase.session" } + func getSession() throws -> Session? { - try retrieve(key: "supabase.session").flatMap { + var storedData = try? retrieve(key: oldKey) + + if let storedData { + // migrate to new key. + try store(key: key, value: storedData) + try? remove(key: oldKey) + } else { + storedData = try retrieve(key: key) + } + + return try storedData.flatMap { try AuthClient.Configuration.jsonDecoder.decode(StoredSession.self, from: $0).session } } func storeSession(_ session: Session) throws { try store( - key: "supabase.session", + key: key, value: AuthClient.Configuration.jsonEncoder.encode(StoredSession(session: session)) ) } func deleteSession() throws { - try remove(key: "supabase.session") + try remove(key: key) + try? remove(key: oldKey) } } diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index 02dca914..a77218fa 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -144,11 +144,15 @@ public final class SupabaseClient: Sendable { ]) .merged(with: HTTPHeaders(options.global.headers)) + // default storage key uses the supabase project ref as a namespace + let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token" + auth = AuthClient( url: supabaseURL.appendingPathComponent("/auth/v1"), headers: defaultHeaders.dictionary, flowType: options.auth.flowType, redirectToURL: options.auth.redirectToURL, + storageKey: options.auth.storageKey ?? defaultStorageKey, localStorage: options.auth.storage, logger: options.global.logger, encoder: options.auth.encoder, diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 188dc38d..7c354d77 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -44,6 +44,9 @@ public struct SupabaseClientOptions: Sendable { /// Default URL to be used for redirect on the flows that requires it. public let redirectToURL: URL? + /// Optional key name used for storing tokens in local storage. + public let storageKey: String? + /// OAuth flow to use - defaults to PKCE flow. PKCE is recommended for mobile and server-side /// applications. public let flowType: AuthFlowType @@ -60,6 +63,7 @@ public struct SupabaseClientOptions: Sendable { public init( storage: any AuthLocalStorage, redirectToURL: URL? = nil, + storageKey: String? = nil, flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, @@ -67,6 +71,7 @@ public struct SupabaseClientOptions: Sendable { ) { self.storage = storage self.redirectToURL = redirectToURL + self.storageKey = storageKey self.flowType = flowType self.encoder = encoder self.decoder = decoder @@ -145,6 +150,7 @@ extension SupabaseClientOptions.AuthOptions { #if !os(Linux) public init( redirectToURL: URL? = nil, + storageKey: String? = nil, flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, @@ -153,6 +159,7 @@ extension SupabaseClientOptions.AuthOptions { self.init( storage: AuthClient.Configuration.defaultLocalStorage, redirectToURL: redirectToURL, + storageKey: storageKey, flowType: flowType, encoder: encoder, decoder: decoder, diff --git a/Tests/AuthTests/MockHelpers.swift b/Tests/AuthTests/MockHelpers.swift index 561341d4..1ac595f9 100644 --- a/Tests/AuthTests/MockHelpers.swift +++ b/Tests/AuthTests/MockHelpers.swift @@ -1,4 +1,5 @@ import Foundation +import TestHelpers @testable import Auth @@ -12,3 +13,14 @@ extension Decodable { self = try! AuthClient.Configuration.jsonDecoder.decode(Self.self, from: json(named: name)) } } + +extension Dependencies { + static var mock = Dependencies( + configuration: AuthClient.Configuration( + url: URL(string: "https://project-id.supabase.com")!, + localStorage: InMemoryLocalStorage(), + logger: nil + ), + http: HTTPClientMock() + ) +} diff --git a/Tests/AuthTests/Resources/local-storage.json b/Tests/AuthTests/Resources/local-storage.json index 6f05c868..cd1514f7 100644 --- a/Tests/AuthTests/Resources/local-storage.json +++ b/Tests/AuthTests/Resources/local-storage.json @@ -1,5 +1,5 @@ { - "supabase.session" : { + "supabase.auth.token" : { "expiration_date" : "2024-04-01T13:25:07.000Z", "session" : { "access_token" : "accesstoken", diff --git a/Tests/AuthTests/StoredSessionTests.swift b/Tests/AuthTests/StoredSessionTests.swift index ecda04a4..93c9a4b2 100644 --- a/Tests/AuthTests/StoredSessionTests.swift +++ b/Tests/AuthTests/StoredSessionTests.swift @@ -4,13 +4,12 @@ import SnapshotTesting import XCTest final class StoredSessionTests: XCTestCase { - override func setUpWithError() throws { - try super.setUpWithError() - } - func testStoredSession() throws { let sut = try! DiskTestStorage() + Current = .mock + Current.configuration.storageKey = "supabase.auth.token" + let _ = try sut.getSession() let session = Session( diff --git a/Tests/SupabaseTests/SupabaseClientTests.swift b/Tests/SupabaseTests/SupabaseClientTests.swift index f8df2451..48075ded 100644 --- a/Tests/SupabaseTests/SupabaseClientTests.swift +++ b/Tests/SupabaseTests/SupabaseClientTests.swift @@ -81,6 +81,7 @@ final class SupabaseClientTests: XCTestCase { XCTAssertIdentical(realtimeOptions.logger as? Logger, logger) XCTAssertFalse(client.auth.configuration.autoRefreshToken) + XCTAssertEqual(client.auth.configuration.storageKey, "sb-project-ref-auth-token") } #if !os(Linux)