Skip to content

Commit 6803ddd

Browse files
authored
feat(auth): add support for multiple auth instances (#445)
* feat(auth): add support for multiple auth instances * test: add tests for multiple auth client instances
1 parent 58ab9af commit 6803ddd

14 files changed

+254
-145
lines changed

Sources/Auth/AuthAdmin.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import Foundation
99
import Helpers
1010

1111
public struct AuthAdmin: Sendable {
12-
var configuration: AuthClient.Configuration { Current.configuration }
13-
var api: APIClient { Current.api }
14-
var encoder: JSONEncoder { Current.encoder }
12+
let clientID: AuthClientID
13+
14+
var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
15+
var api: APIClient { Dependencies[clientID].api }
16+
var encoder: JSONEncoder { Dependencies[clientID].encoder }
1517

1618
/// Delete a user. Requires `service_role` key.
1719
/// - Parameter id: The id of the user you want to delete.

Sources/Auth/AuthClient.swift

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ import Helpers
1010
import FoundationNetworking
1111
#endif
1212

13+
typealias AuthClientID = UUID
14+
1315
public final class AuthClient: Sendable {
14-
private var api: APIClient { Current.api }
15-
var configuration: AuthClient.Configuration { Current.configuration }
16-
private var codeVerifierStorage: CodeVerifierStorage { Current.codeVerifierStorage }
17-
private var date: @Sendable () -> Date { Current.date }
18-
private var sessionManager: SessionManager { Current.sessionManager }
19-
private var eventEmitter: AuthStateChangeEventEmitter { Current.eventEmitter }
20-
private var logger: (any SupabaseLogger)? { Current.configuration.logger }
21-
private var storage: any AuthLocalStorage { Current.configuration.localStorage }
16+
let clientID = AuthClientID()
17+
18+
private var api: APIClient { Dependencies[clientID].api }
19+
var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
20+
private var codeVerifierStorage: CodeVerifierStorage { Dependencies[clientID].codeVerifierStorage }
21+
private var date: @Sendable () -> Date { Dependencies[clientID].date }
22+
private var sessionManager: SessionManager { Dependencies[clientID].sessionManager }
23+
private var eventEmitter: AuthStateChangeEventEmitter { Dependencies[clientID].eventEmitter }
24+
private var logger: (any SupabaseLogger)? { Dependencies[clientID].configuration.logger }
25+
private var sessionStorage: SessionStorage { Dependencies[clientID].sessionStorage }
2226

2327
/// Returns the session, refreshing it if necessary.
2428
///
@@ -33,31 +37,39 @@ public final class AuthClient: Sendable {
3337
///
3438
/// The session returned by this property may be expired. Use ``session`` for a session that is guaranteed to be valid.
3539
public var currentSession: Session? {
36-
try? storage.getSession()
40+
try? sessionStorage.get()
3741
}
3842

3943
/// Returns the current user, if any.
4044
///
4145
/// The user returned by this property may be outdated. Use ``user(jwt:)`` method to get an up-to-date user instance.
4246
public var currentUser: User? {
43-
try? storage.getSession()?.user
47+
try? sessionStorage.get()?.user
4448
}
4549

4650
/// Namespace for accessing multi-factor authentication API.
47-
public let mfa = AuthMFA()
51+
public var mfa: AuthMFA {
52+
AuthMFA(clientID: clientID)
53+
}
54+
4855
/// Namespace for the GoTrue admin methods.
4956
/// - Warning: This methods requires `service_role` key, be careful to never expose `service_role`
5057
/// key in the client.
51-
public let admin = AuthAdmin()
58+
public var admin: AuthAdmin {
59+
AuthAdmin(clientID: clientID)
60+
}
5261

5362
/// Initializes a AuthClient with a specific configuration.
5463
///
5564
/// - Parameters:
5665
/// - configuration: The client configuration.
5766
public init(configuration: Configuration) {
58-
Current = Dependencies(
67+
Dependencies[clientID] = Dependencies(
5968
configuration: configuration,
60-
http: HTTPClient(configuration: configuration)
69+
http: HTTPClient(configuration: configuration),
70+
api: APIClient(clientID: clientID),
71+
sessionStorage: .live(clientID: clientID),
72+
sessionManager: .live(clientID: clientID)
6173
)
6274
}
6375

@@ -1065,7 +1077,7 @@ public final class AuthClient: Sendable {
10651077
scopes: scopes,
10661078
redirectTo: redirectTo,
10671079
queryParams: queryParams,
1068-
launchURL: { Current.urlOpener.open($0) }
1080+
launchURL: { Dependencies[clientID].urlOpener.open($0) }
10691081
)
10701082
}
10711083

Sources/Auth/AuthMFA.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import Helpers
33

44
/// Contains the full multi-factor authentication API.
55
public struct AuthMFA: Sendable {
6-
var configuration: AuthClient.Configuration { Current.configuration }
7-
var api: APIClient { Current.api }
8-
var encoder: JSONEncoder { Current.encoder }
9-
var decoder: JSONDecoder { Current.decoder }
10-
var sessionManager: SessionManager { Current.sessionManager }
11-
var eventEmitter: AuthStateChangeEventEmitter { Current.eventEmitter }
6+
let clientID: AuthClientID
7+
8+
var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
9+
var api: APIClient { Dependencies[clientID].api }
10+
var encoder: JSONEncoder { Dependencies[clientID].encoder }
11+
var decoder: JSONDecoder { Dependencies[clientID].decoder }
12+
var sessionManager: SessionManager { Dependencies[clientID].sessionManager }
13+
var eventEmitter: AuthStateChangeEventEmitter { Dependencies[clientID].eventEmitter }
1214

1315
/// Starts the enrollment process for a new Multi-Factor Authentication (MFA) factor. This method
1416
/// creates a new `unverified` factor.

Sources/Auth/Internal/APIClient.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ extension HTTPClient {
2121
}
2222

2323
struct APIClient: Sendable {
24+
let clientID: AuthClientID
25+
2426
var configuration: AuthClient.Configuration {
25-
Current.configuration
27+
Dependencies[clientID].configuration
2628
}
2729

2830
var http: any HTTPClientType {
29-
Current.http
31+
Dependencies[clientID].http
3032
}
3133

3234
func execute(_ request: HTTPRequest) async throws -> HTTPResponse {
@@ -62,7 +64,7 @@ struct APIClient: Sendable {
6264
@discardableResult
6365
func authorizedExecute(_ request: HTTPRequest) async throws -> HTTPResponse {
6466
var sessionManager: SessionManager {
65-
Current.sessionManager
67+
Dependencies[clientID].sessionManager
6668
}
6769

6870
let session = try await sessionManager.session()

Sources/Auth/Internal/Dependencies.swift

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import Helpers
55
struct Dependencies: Sendable {
66
var configuration: AuthClient.Configuration
77
var http: any HTTPClientType
8-
var sessionManager = SessionManager.live
9-
var api = APIClient()
8+
var api: APIClient
9+
var sessionStorage: SessionStorage
10+
var sessionManager: SessionManager
1011

1112
var eventEmitter: AuthStateChangeEventEmitter = .shared
1213
var date: @Sendable () -> Date = { Date() }
@@ -18,18 +19,18 @@ struct Dependencies: Sendable {
1819
var logger: (any SupabaseLogger)? { configuration.logger }
1920
}
2021

21-
private let _Current = LockIsolated<Dependencies?>(nil)
22-
var Current: Dependencies {
23-
get {
24-
guard let instance = _Current.value else {
25-
fatalError("Current should be set before usage.")
26-
}
22+
extension Dependencies {
23+
static let instances = LockIsolated([AuthClientID: Dependencies]())
2724

28-
return instance
29-
}
30-
set {
31-
_Current.withValue { Current in
32-
Current = newValue
25+
static subscript(_ id: AuthClientID) -> Dependencies {
26+
get {
27+
guard let instance = instances[id] else {
28+
fatalError("Dependencies not found for id: \(id)")
29+
}
30+
return instance
31+
}
32+
set {
33+
instances.withValue { $0[id] = newValue }
3334
}
3435
}
3536
}

Sources/Auth/Internal/SessionManager.swift

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ struct SessionManager: Sendable {
1010
}
1111

1212
extension SessionManager {
13-
static var live: Self {
14-
let instance = LiveSessionManager()
13+
static func live(clientID: AuthClientID) -> Self {
14+
let instance = LiveSessionManager(clientID: clientID)
1515
return Self(
1616
session: { try await instance.session() },
1717
refreshSession: { try await instance.refreshSession($0) },
@@ -22,18 +22,24 @@ extension SessionManager {
2222
}
2323

2424
private actor LiveSessionManager {
25-
private var configuration: AuthClient.Configuration { Current.configuration }
26-
private var storage: any AuthLocalStorage { Current.configuration.localStorage }
27-
private var eventEmitter: AuthStateChangeEventEmitter { Current.eventEmitter }
28-
private var logger: (any SupabaseLogger)? { Current.logger }
29-
private var api: APIClient { Current.api }
25+
private var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
26+
private var sessionStorage: SessionStorage { Dependencies[clientID].sessionStorage }
27+
private var eventEmitter: AuthStateChangeEventEmitter { Dependencies[clientID].eventEmitter }
28+
private var logger: (any SupabaseLogger)? { Dependencies[clientID].logger }
29+
private var api: APIClient { Dependencies[clientID].api }
3030

3131
private var inFlightRefreshTask: Task<Session, any Error>?
3232
private var scheduledNextRefreshTask: Task<Void, Never>?
3333

34+
let clientID: AuthClientID
35+
36+
init(clientID: AuthClientID) {
37+
self.clientID = clientID
38+
}
39+
3440
func session() async throws -> Session {
3541
try await trace(using: logger) {
36-
guard let currentSession = try storage.getSession() else {
42+
guard let currentSession = try sessionStorage.get() else {
3743
throw AuthError.sessionNotFound
3844
}
3945

@@ -92,15 +98,15 @@ private actor LiveSessionManager {
9298

9399
func update(_ session: Session) {
94100
do {
95-
try storage.storeSession(session)
101+
try sessionStorage.store(session)
96102
} catch {
97103
logger?.error("Failed to store session: \(error)")
98104
}
99105
}
100106

101107
func remove() {
102108
do {
103-
try storage.deleteSession()
109+
try sessionStorage.delete()
104110
} catch {
105111
logger?.error("Failed to remove session: \(error)")
106112
}

Sources/Auth/Internal/SessionStorage.swift

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,50 @@ struct StoredSession: Codable {
1919
}
2020
}
2121

22-
extension AuthLocalStorage {
23-
var key: String {
24-
Current.configuration.storageKey ?? AuthClient.Configuration.defaultStorageKey
25-
}
22+
struct SessionStorage {
23+
var get: @Sendable () throws -> Session?
24+
var store: @Sendable (_ session: Session) throws -> Void
25+
var delete: @Sendable () throws -> Void
26+
}
2627

27-
var oldKey: String { "supabase.session" }
28+
extension SessionStorage {
29+
static func live(clientID: AuthClientID) -> SessionStorage {
30+
var key: String {
31+
Dependencies[clientID].configuration.storageKey ?? AuthClient.Configuration.defaultStorageKey
32+
}
2833

29-
func getSession() throws -> Session? {
30-
var storedData = try? retrieve(key: oldKey)
34+
var oldKey: String { "supabase.session" }
3135

32-
if let storedData {
33-
// migrate to new key.
34-
try store(key: key, value: storedData)
35-
try? remove(key: oldKey)
36-
} else {
37-
storedData = try retrieve(key: key)
36+
var storage: any AuthLocalStorage {
37+
Dependencies[clientID].configuration.localStorage
3838
}
3939

40-
return try storedData.flatMap {
41-
try AuthClient.Configuration.jsonDecoder.decode(StoredSession.self, from: $0).session
42-
}
43-
}
40+
return SessionStorage(
41+
get: {
42+
var storedData = try? storage.retrieve(key: oldKey)
4443

45-
func storeSession(_ session: Session) throws {
46-
try store(
47-
key: key,
48-
value: AuthClient.Configuration.jsonEncoder.encode(StoredSession(session: session))
49-
)
50-
}
44+
if let storedData {
45+
// migrate to new key.
46+
try storage.store(key: key, value: storedData)
47+
try? storage.remove(key: oldKey)
48+
} else {
49+
storedData = try storage.retrieve(key: key)
50+
}
5151

52-
func deleteSession() throws {
53-
try remove(key: key)
54-
try? remove(key: oldKey)
52+
return try storedData.flatMap {
53+
try AuthClient.Configuration.jsonDecoder.decode(StoredSession.self, from: $0).session
54+
}
55+
},
56+
store: { session in
57+
try storage.store(
58+
key: key,
59+
value: AuthClient.Configuration.jsonEncoder.encode(StoredSession(session: session))
60+
)
61+
},
62+
delete: {
63+
try storage.remove(key: key)
64+
try? storage.remove(key: oldKey)
65+
}
66+
)
5567
}
5668
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// AuthClientMultipleInstancesTests.swift
3+
//
4+
//
5+
// Created by Guilherme Souza on 05/07/24.
6+
//
7+
8+
@testable import Auth
9+
import TestHelpers
10+
import XCTest
11+
12+
final class AuthClientMultipleInstancesTests: XCTestCase {
13+
func testMultipleAuthClientInstances() {
14+
let url = URL(string: "http://localhost:54321/auth")!
15+
16+
let client1Storage = InMemoryLocalStorage()
17+
let client2Storage = InMemoryLocalStorage()
18+
19+
let client1 = AuthClient(
20+
configuration: AuthClient.Configuration(
21+
url: url,
22+
localStorage: client1Storage,
23+
logger: nil
24+
)
25+
)
26+
27+
let client2 = AuthClient(
28+
configuration: AuthClient.Configuration(
29+
url: url,
30+
localStorage: client2Storage,
31+
logger: nil
32+
)
33+
)
34+
35+
XCTAssertNotEqual(client1.clientID, client2.clientID)
36+
37+
XCTAssertIdentical(
38+
Dependencies[client1.clientID].configuration.localStorage as? InMemoryLocalStorage,
39+
client1Storage
40+
)
41+
XCTAssertIdentical(
42+
Dependencies[client2.clientID].configuration.localStorage as? InMemoryLocalStorage,
43+
client2Storage
44+
)
45+
}
46+
}

0 commit comments

Comments
 (0)