Skip to content

Commit

Permalink
feat(auth): add support for multiple auth instances (#445)
Browse files Browse the repository at this point in the history
* feat(auth): add support for multiple auth instances

* test: add tests for multiple auth client instances
  • Loading branch information
grdsdev authored Jul 9, 2024
1 parent 58ab9af commit 6803ddd
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 145 deletions.
8 changes: 5 additions & 3 deletions Sources/Auth/AuthAdmin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import Foundation
import Helpers

public struct AuthAdmin: Sendable {
var configuration: AuthClient.Configuration { Current.configuration }
var api: APIClient { Current.api }
var encoder: JSONEncoder { Current.encoder }
let clientID: AuthClientID

var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
var api: APIClient { Dependencies[clientID].api }
var encoder: JSONEncoder { Dependencies[clientID].encoder }

/// Delete a user. Requires `service_role` key.
/// - Parameter id: The id of the user you want to delete.
Expand Down
42 changes: 27 additions & 15 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import Helpers
import FoundationNetworking
#endif

typealias AuthClientID = UUID

public final class AuthClient: Sendable {
private var api: APIClient { Current.api }
var configuration: AuthClient.Configuration { Current.configuration }
private var codeVerifierStorage: CodeVerifierStorage { Current.codeVerifierStorage }
private var date: @Sendable () -> Date { Current.date }
private var sessionManager: SessionManager { Current.sessionManager }
private var eventEmitter: AuthStateChangeEventEmitter { Current.eventEmitter }
private var logger: (any SupabaseLogger)? { Current.configuration.logger }
private var storage: any AuthLocalStorage { Current.configuration.localStorage }
let clientID = AuthClientID()

private var api: APIClient { Dependencies[clientID].api }
var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
private var codeVerifierStorage: CodeVerifierStorage { Dependencies[clientID].codeVerifierStorage }
private var date: @Sendable () -> Date { Dependencies[clientID].date }
private var sessionManager: SessionManager { Dependencies[clientID].sessionManager }
private var eventEmitter: AuthStateChangeEventEmitter { Dependencies[clientID].eventEmitter }
private var logger: (any SupabaseLogger)? { Dependencies[clientID].configuration.logger }
private var sessionStorage: SessionStorage { Dependencies[clientID].sessionStorage }

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

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

/// Namespace for accessing multi-factor authentication API.
public let mfa = AuthMFA()
public var mfa: AuthMFA {
AuthMFA(clientID: clientID)
}

/// Namespace for the GoTrue admin methods.
/// - Warning: This methods requires `service_role` key, be careful to never expose `service_role`
/// key in the client.
public let admin = AuthAdmin()
public var admin: AuthAdmin {
AuthAdmin(clientID: clientID)
}

/// Initializes a AuthClient with a specific configuration.
///
/// - Parameters:
/// - configuration: The client configuration.
public init(configuration: Configuration) {
Current = Dependencies(
Dependencies[clientID] = Dependencies(
configuration: configuration,
http: HTTPClient(configuration: configuration)
http: HTTPClient(configuration: configuration),
api: APIClient(clientID: clientID),
sessionStorage: .live(clientID: clientID),
sessionManager: .live(clientID: clientID)
)
}

Expand Down Expand Up @@ -1065,7 +1077,7 @@ public final class AuthClient: Sendable {
scopes: scopes,
redirectTo: redirectTo,
queryParams: queryParams,
launchURL: { Current.urlOpener.open($0) }
launchURL: { Dependencies[clientID].urlOpener.open($0) }
)
}

Expand Down
14 changes: 8 additions & 6 deletions Sources/Auth/AuthMFA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import Helpers

/// Contains the full multi-factor authentication API.
public struct AuthMFA: Sendable {
var configuration: AuthClient.Configuration { Current.configuration }
var api: APIClient { Current.api }
var encoder: JSONEncoder { Current.encoder }
var decoder: JSONDecoder { Current.decoder }
var sessionManager: SessionManager { Current.sessionManager }
var eventEmitter: AuthStateChangeEventEmitter { Current.eventEmitter }
let clientID: AuthClientID

var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
var api: APIClient { Dependencies[clientID].api }
var encoder: JSONEncoder { Dependencies[clientID].encoder }
var decoder: JSONDecoder { Dependencies[clientID].decoder }
var sessionManager: SessionManager { Dependencies[clientID].sessionManager }
var eventEmitter: AuthStateChangeEventEmitter { Dependencies[clientID].eventEmitter }

/// Starts the enrollment process for a new Multi-Factor Authentication (MFA) factor. This method
/// creates a new `unverified` factor.
Expand Down
8 changes: 5 additions & 3 deletions Sources/Auth/Internal/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ extension HTTPClient {
}

struct APIClient: Sendable {
let clientID: AuthClientID

var configuration: AuthClient.Configuration {
Current.configuration
Dependencies[clientID].configuration
}

var http: any HTTPClientType {
Current.http
Dependencies[clientID].http
}

func execute(_ request: HTTPRequest) async throws -> HTTPResponse {
Expand Down Expand Up @@ -62,7 +64,7 @@ struct APIClient: Sendable {
@discardableResult
func authorizedExecute(_ request: HTTPRequest) async throws -> HTTPResponse {
var sessionManager: SessionManager {
Current.sessionManager
Dependencies[clientID].sessionManager
}

let session = try await sessionManager.session()
Expand Down
27 changes: 14 additions & 13 deletions Sources/Auth/Internal/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import Helpers
struct Dependencies: Sendable {
var configuration: AuthClient.Configuration
var http: any HTTPClientType
var sessionManager = SessionManager.live
var api = APIClient()
var api: APIClient
var sessionStorage: SessionStorage
var sessionManager: SessionManager

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

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

return instance
}
set {
_Current.withValue { Current in
Current = newValue
static subscript(_ id: AuthClientID) -> Dependencies {
get {
guard let instance = instances[id] else {
fatalError("Dependencies not found for id: \(id)")
}
return instance
}
set {
instances.withValue { $0[id] = newValue }
}
}
}
26 changes: 16 additions & 10 deletions Sources/Auth/Internal/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ struct SessionManager: Sendable {
}

extension SessionManager {
static var live: Self {
let instance = LiveSessionManager()
static func live(clientID: AuthClientID) -> Self {
let instance = LiveSessionManager(clientID: clientID)
return Self(
session: { try await instance.session() },
refreshSession: { try await instance.refreshSession($0) },
Expand All @@ -22,18 +22,24 @@ extension SessionManager {
}

private actor LiveSessionManager {
private var configuration: AuthClient.Configuration { Current.configuration }
private var storage: any AuthLocalStorage { Current.configuration.localStorage }
private var eventEmitter: AuthStateChangeEventEmitter { Current.eventEmitter }
private var logger: (any SupabaseLogger)? { Current.logger }
private var api: APIClient { Current.api }
private var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
private var sessionStorage: SessionStorage { Dependencies[clientID].sessionStorage }
private var eventEmitter: AuthStateChangeEventEmitter { Dependencies[clientID].eventEmitter }
private var logger: (any SupabaseLogger)? { Dependencies[clientID].logger }
private var api: APIClient { Dependencies[clientID].api }

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

let clientID: AuthClientID

init(clientID: AuthClientID) {
self.clientID = clientID
}

func session() async throws -> Session {
try await trace(using: logger) {
guard let currentSession = try storage.getSession() else {
guard let currentSession = try sessionStorage.get() else {
throw AuthError.sessionNotFound
}

Expand Down Expand Up @@ -92,15 +98,15 @@ private actor LiveSessionManager {

func update(_ session: Session) {
do {
try storage.storeSession(session)
try sessionStorage.store(session)
} catch {
logger?.error("Failed to store session: \(error)")
}
}

func remove() {
do {
try storage.deleteSession()
try sessionStorage.delete()
} catch {
logger?.error("Failed to remove session: \(error)")
}
Expand Down
64 changes: 38 additions & 26 deletions Sources/Auth/Internal/SessionStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,50 @@ struct StoredSession: Codable {
}
}

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

var oldKey: String { "supabase.session" }
extension SessionStorage {
static func live(clientID: AuthClientID) -> SessionStorage {
var key: String {
Dependencies[clientID].configuration.storageKey ?? AuthClient.Configuration.defaultStorageKey
}

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

if let storedData {
// migrate to new key.
try store(key: key, value: storedData)
try? remove(key: oldKey)
} else {
storedData = try retrieve(key: key)
var storage: any AuthLocalStorage {
Dependencies[clientID].configuration.localStorage
}

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

func storeSession(_ session: Session) throws {
try store(
key: key,
value: AuthClient.Configuration.jsonEncoder.encode(StoredSession(session: session))
)
}
if let storedData {
// migrate to new key.
try storage.store(key: key, value: storedData)
try? storage.remove(key: oldKey)
} else {
storedData = try storage.retrieve(key: key)
}

func deleteSession() throws {
try remove(key: key)
try? remove(key: oldKey)
return try storedData.flatMap {
try AuthClient.Configuration.jsonDecoder.decode(StoredSession.self, from: $0).session
}
},
store: { session in
try storage.store(
key: key,
value: AuthClient.Configuration.jsonEncoder.encode(StoredSession(session: session))
)
},
delete: {
try storage.remove(key: key)
try? storage.remove(key: oldKey)
}
)
}
}
46 changes: 46 additions & 0 deletions Tests/AuthTests/AuthClientMultipleInstancesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// AuthClientMultipleInstancesTests.swift
//
//
// Created by Guilherme Souza on 05/07/24.
//

@testable import Auth
import TestHelpers
import XCTest

final class AuthClientMultipleInstancesTests: XCTestCase {
func testMultipleAuthClientInstances() {
let url = URL(string: "http://localhost:54321/auth")!

let client1Storage = InMemoryLocalStorage()
let client2Storage = InMemoryLocalStorage()

let client1 = AuthClient(
configuration: AuthClient.Configuration(
url: url,
localStorage: client1Storage,
logger: nil
)
)

let client2 = AuthClient(
configuration: AuthClient.Configuration(
url: url,
localStorage: client2Storage,
logger: nil
)
)

XCTAssertNotEqual(client1.clientID, client2.clientID)

XCTAssertIdentical(
Dependencies[client1.clientID].configuration.localStorage as? InMemoryLocalStorage,
client1Storage
)
XCTAssertIdentical(
Dependencies[client2.clientID].configuration.localStorage as? InMemoryLocalStorage,
client2Storage
)
}
}
Loading

0 comments on commit 6803ddd

Please sign in to comment.