diff --git a/Package.swift b/Package.swift index 0c2324c1..6105e5a6 100644 --- a/Package.swift +++ b/Package.swift @@ -137,6 +137,7 @@ let package = Package( name: "Supabase", dependencies: [ .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), + .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), "Auth", "Functions", "PostgREST", diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index 632d63db..65218754 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -3,6 +3,7 @@ import ConcurrencyExtras import Foundation @_exported import Functions import Helpers +import IssueReporting @_exported import PostgREST @_exported import Realtime @_exported import Storage @@ -26,9 +27,18 @@ public final class SupabaseClient: Sendable { let databaseURL: URL let functionsURL: URL - /// Supabase Auth allows you to create and manage user sessions for access to data that is secured - /// by access policies. - public let auth: AuthClient + private let _auth: AuthClient + + /// Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies. + public var auth: AuthClient { + if options.auth.accessToken != nil { + reportIssue(""" + Supabase Client is configured with the auth.accessToken option, + accessing supabase.auth is not possible. + """) + } + return _auth + } var rest: PostgrestClient { mutableState.withValue { @@ -105,7 +115,7 @@ public final class SupabaseClient: Sendable { var changedAccessToken: String? } - private let mutableState = LockIsolated(MutableState()) + let mutableState = LockIsolated(MutableState()) private var session: URLSession { options.global.session @@ -153,7 +163,7 @@ public final class SupabaseClient: Sendable { // default storage key uses the supabase project ref as a namespace let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token" - auth = AuthClient( + _auth = AuthClient( url: supabaseURL.appendingPathComponent("/auth/v1"), headers: _headers.dictionary, flowType: options.auth.flowType, @@ -190,7 +200,9 @@ public final class SupabaseClient: Sendable { options: realtimeOptions ) - listenForAuthEvents() + if options.auth.accessToken == nil { + listenForAuthEvents() + } } /// Performs a query on a table or a view. @@ -240,9 +252,7 @@ public final class SupabaseClient: Sendable { /// Returns all Realtime channels. public var channels: [RealtimeChannelV2] { - get async { - await Array(realtimeV2.subscriptions.values) - } + Array(realtimeV2.subscriptions.values) } /// Creates a Realtime channel with Broadcast, Presence, and Postgres Changes. @@ -252,8 +262,8 @@ public final class SupabaseClient: Sendable { public func channel( _ name: String, options: @Sendable (inout RealtimeChannelConfig) -> Void = { _ in } - ) async -> RealtimeChannelV2 { - await realtimeV2.channel(name, options: options) + ) -> RealtimeChannelV2 { + realtimeV2.channel(name, options: options) } /// Unsubscribes and removes Realtime channel from Realtime client. @@ -340,9 +350,15 @@ public final class SupabaseClient: Sendable { } private func adapt(request: URLRequest) async -> URLRequest { + let token: String? = if let accessToken = options.auth.accessToken { + try? await accessToken() + } else { + try? await auth.session.accessToken + } + var request = request - if let accessToken = try? await auth.session.accessToken { - request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + if let token { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } return request } diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 7c354d77..754515ca 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -60,6 +60,12 @@ public struct SupabaseClientOptions: Sendable { /// Set to `true` if you want to automatically refresh the token before expiring. public let autoRefreshToken: Bool + /// Optional function for using a third-party authentication system with Supabase. The function should return an access token or ID token (JWT) by obtaining it from the third-party auth client library. + /// Note that this function may be called concurrently and many times. Use memoization and locking techniques if this is not supported by the client libraries. + /// When set, the `auth` namespace of the Supabase client cannot be used. + /// Create another client if you wish to use Supabase Auth and third-party authentications concurrently in the same application. + public let accessToken: (@Sendable () async throws -> String)? + public init( storage: any AuthLocalStorage, redirectToURL: URL? = nil, @@ -67,7 +73,8 @@ public struct SupabaseClientOptions: Sendable { flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken + autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken, + accessToken: (@Sendable () async throws -> String)? = nil ) { self.storage = storage self.redirectToURL = redirectToURL @@ -76,6 +83,7 @@ public struct SupabaseClientOptions: Sendable { self.encoder = encoder self.decoder = decoder self.autoRefreshToken = autoRefreshToken + self.accessToken = accessToken } } @@ -154,7 +162,8 @@ extension SupabaseClientOptions.AuthOptions { flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken + autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken, + accessToken: (@Sendable () async throws -> String)? = nil ) { self.init( storage: AuthClient.Configuration.defaultLocalStorage, @@ -163,7 +172,8 @@ extension SupabaseClientOptions.AuthOptions { flowType: flowType, encoder: encoder, decoder: decoder, - autoRefreshToken: autoRefreshToken + autoRefreshToken: autoRefreshToken, + accessToken: accessToken ) } #endif diff --git a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved index a3bc22bb..a0a59d70 100644 --- a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,4 @@ { - "originHash" : "7cc8037abb258fd1406effe711922c8d309c2e7a93147bfe68753fd05392a24a", "pins" : [ { "identity" : "appauth-ios", @@ -64,6 +63,24 @@ "version" : "1.1.2" } }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "46072478ca365fe48370993833cb22de9b41567f", + "version" : "3.5.2" + } + }, { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", @@ -82,6 +99,15 @@ "version" : "1.1.0" } }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "c097f955b4e724690f0fc8ffb7a6d4b881c9c4e3", + "version" : "1.17.2" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -110,5 +136,5 @@ } } ], - "version" : 3 + "version" : 2 } diff --git a/Tests/SupabaseTests/SupabaseClientTests.swift b/Tests/SupabaseTests/SupabaseClientTests.swift index 5864c0e6..d11a3e31 100644 --- a/Tests/SupabaseTests/SupabaseClientTests.swift +++ b/Tests/SupabaseTests/SupabaseClientTests.swift @@ -1,6 +1,7 @@ @testable import Auth import CustomDump @testable import Functions +import IssueReporting @testable import Realtime @testable import Supabase import XCTest @@ -83,6 +84,11 @@ final class SupabaseClientTests: XCTestCase { XCTAssertFalse(client.auth.configuration.autoRefreshToken) XCTAssertEqual(client.auth.configuration.storageKey, "sb-project-ref-auth-token") + + XCTAssertNotNil( + client.mutableState.listenForAuthEventsTask, + "should listen for internal auth events" + ) } #if !os(Linux) @@ -93,4 +99,31 @@ final class SupabaseClientTests: XCTestCase { ) } #endif + + func testClientInitWithCustomAccessToken() async { + let localStorage = AuthLocalStorageMock() + + let client = SupabaseClient( + supabaseURL: URL(string: "https://project-ref.supabase.co")!, + supabaseKey: "ANON_KEY", + options: .init( + auth: .init( + storage: localStorage, + accessToken: { "jwt" } + ) + ) + ) + + XCTAssertNil( + client.mutableState.listenForAuthEventsTask, + "should not listen for internal auth events when using 3p authentication" + ) + + #if canImport(Darwin) + // withExpectedIssue is unavailable on non-Darwin platform. + withExpectedIssue { + _ = client.auth + } + #endif + } }