Skip to content

Commit d760f2d

Browse files
authored
feat: add third-party auth support (#423)
* feat: add third-party auth support * test 3p auth * fix test on linux * Use IssueReporting * fix linux test
1 parent 93f4ff5 commit d760f2d

File tree

5 files changed

+104
-18
lines changed

5 files changed

+104
-18
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ let package = Package(
137137
name: "Supabase",
138138
dependencies: [
139139
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
140+
.product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
140141
"Auth",
141142
"Functions",
142143
"PostgREST",

Sources/Supabase/SupabaseClient.swift

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ConcurrencyExtras
33
import Foundation
44
@_exported import Functions
55
import Helpers
6+
import IssueReporting
67
@_exported import PostgREST
78
@_exported import Realtime
89
@_exported import Storage
@@ -26,9 +27,18 @@ public final class SupabaseClient: Sendable {
2627
let databaseURL: URL
2728
let functionsURL: URL
2829

29-
/// Supabase Auth allows you to create and manage user sessions for access to data that is secured
30-
/// by access policies.
31-
public let auth: AuthClient
30+
private let _auth: AuthClient
31+
32+
/// Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies.
33+
public var auth: AuthClient {
34+
if options.auth.accessToken != nil {
35+
reportIssue("""
36+
Supabase Client is configured with the auth.accessToken option,
37+
accessing supabase.auth is not possible.
38+
""")
39+
}
40+
return _auth
41+
}
3242

3343
var rest: PostgrestClient {
3444
mutableState.withValue {
@@ -105,7 +115,7 @@ public final class SupabaseClient: Sendable {
105115
var changedAccessToken: String?
106116
}
107117

108-
private let mutableState = LockIsolated(MutableState())
118+
let mutableState = LockIsolated(MutableState())
109119

110120
private var session: URLSession {
111121
options.global.session
@@ -153,7 +163,7 @@ public final class SupabaseClient: Sendable {
153163
// default storage key uses the supabase project ref as a namespace
154164
let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token"
155165

156-
auth = AuthClient(
166+
_auth = AuthClient(
157167
url: supabaseURL.appendingPathComponent("/auth/v1"),
158168
headers: _headers.dictionary,
159169
flowType: options.auth.flowType,
@@ -190,7 +200,9 @@ public final class SupabaseClient: Sendable {
190200
options: realtimeOptions
191201
)
192202

193-
listenForAuthEvents()
203+
if options.auth.accessToken == nil {
204+
listenForAuthEvents()
205+
}
194206
}
195207

196208
/// Performs a query on a table or a view.
@@ -240,9 +252,7 @@ public final class SupabaseClient: Sendable {
240252

241253
/// Returns all Realtime channels.
242254
public var channels: [RealtimeChannelV2] {
243-
get async {
244-
await Array(realtimeV2.subscriptions.values)
245-
}
255+
Array(realtimeV2.subscriptions.values)
246256
}
247257

248258
/// Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.
@@ -252,8 +262,8 @@ public final class SupabaseClient: Sendable {
252262
public func channel(
253263
_ name: String,
254264
options: @Sendable (inout RealtimeChannelConfig) -> Void = { _ in }
255-
) async -> RealtimeChannelV2 {
256-
await realtimeV2.channel(name, options: options)
265+
) -> RealtimeChannelV2 {
266+
realtimeV2.channel(name, options: options)
257267
}
258268

259269
/// Unsubscribes and removes Realtime channel from Realtime client.
@@ -340,9 +350,15 @@ public final class SupabaseClient: Sendable {
340350
}
341351

342352
private func adapt(request: URLRequest) async -> URLRequest {
353+
let token: String? = if let accessToken = options.auth.accessToken {
354+
try? await accessToken()
355+
} else {
356+
try? await auth.session.accessToken
357+
}
358+
343359
var request = request
344-
if let accessToken = try? await auth.session.accessToken {
345-
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
360+
if let token {
361+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
346362
}
347363
return request
348364
}

Sources/Supabase/Types.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,21 @@ public struct SupabaseClientOptions: Sendable {
6060
/// Set to `true` if you want to automatically refresh the token before expiring.
6161
public let autoRefreshToken: Bool
6262

63+
/// 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.
64+
/// 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.
65+
/// When set, the `auth` namespace of the Supabase client cannot be used.
66+
/// Create another client if you wish to use Supabase Auth and third-party authentications concurrently in the same application.
67+
public let accessToken: (@Sendable () async throws -> String)?
68+
6369
public init(
6470
storage: any AuthLocalStorage,
6571
redirectToURL: URL? = nil,
6672
storageKey: String? = nil,
6773
flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType,
6874
encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder,
6975
decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder,
70-
autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken
76+
autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken,
77+
accessToken: (@Sendable () async throws -> String)? = nil
7178
) {
7279
self.storage = storage
7380
self.redirectToURL = redirectToURL
@@ -76,6 +83,7 @@ public struct SupabaseClientOptions: Sendable {
7683
self.encoder = encoder
7784
self.decoder = decoder
7885
self.autoRefreshToken = autoRefreshToken
86+
self.accessToken = accessToken
7987
}
8088
}
8189

@@ -154,7 +162,8 @@ extension SupabaseClientOptions.AuthOptions {
154162
flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType,
155163
encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder,
156164
decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder,
157-
autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken
165+
autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken,
166+
accessToken: (@Sendable () async throws -> String)? = nil
158167
) {
159168
self.init(
160169
storage: AuthClient.Configuration.defaultLocalStorage,
@@ -163,7 +172,8 @@ extension SupabaseClientOptions.AuthOptions {
163172
flowType: flowType,
164173
encoder: encoder,
165174
decoder: decoder,
166-
autoRefreshToken: autoRefreshToken
175+
autoRefreshToken: autoRefreshToken,
176+
accessToken: accessToken
167177
)
168178
}
169179
#endif

Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"originHash" : "7cc8037abb258fd1406effe711922c8d309c2e7a93147bfe68753fd05392a24a",
32
"pins" : [
43
{
54
"identity" : "appauth-ios",
@@ -64,6 +63,24 @@
6463
"version" : "1.1.2"
6564
}
6665
},
66+
{
67+
"identity" : "swift-concurrency-extras",
68+
"kind" : "remoteSourceControl",
69+
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
70+
"state" : {
71+
"revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
72+
"version" : "1.1.0"
73+
}
74+
},
75+
{
76+
"identity" : "swift-crypto",
77+
"kind" : "remoteSourceControl",
78+
"location" : "https://github.com/apple/swift-crypto.git",
79+
"state" : {
80+
"revision" : "46072478ca365fe48370993833cb22de9b41567f",
81+
"version" : "3.5.2"
82+
}
83+
},
6784
{
6885
"identity" : "swift-custom-dump",
6986
"kind" : "remoteSourceControl",
@@ -82,6 +99,15 @@
8299
"version" : "1.1.0"
83100
}
84101
},
102+
{
103+
"identity" : "swift-snapshot-testing",
104+
"kind" : "remoteSourceControl",
105+
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
106+
"state" : {
107+
"revision" : "c097f955b4e724690f0fc8ffb7a6d4b881c9c4e3",
108+
"version" : "1.17.2"
109+
}
110+
},
85111
{
86112
"identity" : "swift-syntax",
87113
"kind" : "remoteSourceControl",
@@ -110,5 +136,5 @@
110136
}
111137
}
112138
],
113-
"version" : 3
139+
"version" : 2
114140
}

Tests/SupabaseTests/SupabaseClientTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@testable import Auth
22
import CustomDump
33
@testable import Functions
4+
import IssueReporting
45
@testable import Realtime
56
@testable import Supabase
67
import XCTest
@@ -83,6 +84,11 @@ final class SupabaseClientTests: XCTestCase {
8384

8485
XCTAssertFalse(client.auth.configuration.autoRefreshToken)
8586
XCTAssertEqual(client.auth.configuration.storageKey, "sb-project-ref-auth-token")
87+
88+
XCTAssertNotNil(
89+
client.mutableState.listenForAuthEventsTask,
90+
"should listen for internal auth events"
91+
)
8692
}
8793

8894
#if !os(Linux)
@@ -93,4 +99,31 @@ final class SupabaseClientTests: XCTestCase {
9399
)
94100
}
95101
#endif
102+
103+
func testClientInitWithCustomAccessToken() async {
104+
let localStorage = AuthLocalStorageMock()
105+
106+
let client = SupabaseClient(
107+
supabaseURL: URL(string: "https://project-ref.supabase.co")!,
108+
supabaseKey: "ANON_KEY",
109+
options: .init(
110+
auth: .init(
111+
storage: localStorage,
112+
accessToken: { "jwt" }
113+
)
114+
)
115+
)
116+
117+
XCTAssertNil(
118+
client.mutableState.listenForAuthEventsTask,
119+
"should not listen for internal auth events when using 3p authentication"
120+
)
121+
122+
#if canImport(Darwin)
123+
// withExpectedIssue is unavailable on non-Darwin platform.
124+
withExpectedIssue {
125+
_ = client.auth
126+
}
127+
#endif
128+
}
96129
}

0 commit comments

Comments
 (0)