Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(auth)!: serialize AuthClient to @MainActor #619

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/Auth/AuthAdmin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import Foundation
import Helpers
import HTTPTypes

public struct AuthAdmin: Sendable {
@MainActor
public struct AuthAdmin {
let clientID: AuthClientID

var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
Expand Down
9 changes: 5 additions & 4 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import Helpers

typealias AuthClientID = UUID

public final class AuthClient: Sendable {
@MainActor
public final class AuthClient {
let clientID = AuthClientID()

private var api: APIClient { Dependencies[clientID].api }
Expand Down Expand Up @@ -79,11 +80,10 @@ public final class AuthClient: Sendable {
sessionManager: .live(clientID: clientID)
)

Task { @MainActor in observeAppLifecycleChanges() }
observeAppLifecycleChanges()
}

#if canImport(ObjectiveC)
@MainActor
private func observeAppLifecycleChanges() {
#if canImport(UIKit)
#if canImport(WatchKit)
Expand Down Expand Up @@ -149,6 +149,7 @@ public final class AuthClient: Sendable {
// no-op
}
#endif

/// Listen for auth state changes.
/// - Parameter listener: Block that executes when a new event is emitted.
/// - Returns: A handle that can be used to manually unsubscribe.
Expand Down Expand Up @@ -1161,7 +1162,7 @@ public final class AuthClient: Sendable {
queryParams: queryParams
)

await launchURL(response.url)
launchURL(response.url)
}

/// Links an OAuth identity to an existing user.
Expand Down
4 changes: 2 additions & 2 deletions Sources/Auth/AuthMFA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import Foundation
import Helpers

/// Contains the full multi-factor authentication API.
public struct AuthMFA: Sendable {
@MainActor
public struct AuthMFA {
let clientID: AuthClientID

var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
Expand Down Expand Up @@ -64,7 +65,6 @@ public struct AuthMFA: Sendable {
).decoded(decoder: decoder)

await sessionManager.update(response)

eventEmitter.emit(.mfaChallengeVerified, session: response, token: nil)

return response
Expand Down
1 change: 1 addition & 0 deletions Sources/Auth/Internal/EventEmitter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Foundation
import Helpers

@MainActor
struct AuthStateChangeEventEmitter {
var emitter = EventEmitter<(AuthChangeEvent, Session?)?>(initialEvent: nil, emitsLastEventWhenAttaching: false)
var logger: (any SupabaseLogger)?
Expand All @@ -11,7 +12,7 @@
guard let event else { return }
listener(event.0, event.1)

logger?.verbose("Auth state changed: \(event)")

Check failure on line 15 in Sources/Auth/Internal/EventEmitter.swift

View workflow job for this annotation

GitHub Actions / xcodebuild-macOS-14 (IOS)

main actor-isolated property 'logger' can not be referenced from a Sendable closure
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Auth/Internal/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private actor LiveSessionManager {
.decoded(as: Session.self, decoder: configuration.decoder)

update(session)
eventEmitter.emit(.tokenRefreshed, session: session)
await eventEmitter.emit(.tokenRefreshed, session: session)

return session
} catch {
Expand Down
83 changes: 49 additions & 34 deletions Sources/Supabase/SupabaseClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import ConcurrencyExtras
import Foundation
@_exported import Functions
import HTTPTypes
import Helpers
import IssueReporting
@_exported import PostgREST
@_exported import Realtime
@_exported import Storage
import HTTPTypes

#if canImport(FoundationNetworking)
import FoundationNetworking
Expand All @@ -28,17 +28,25 @@ public final class SupabaseClient: Sendable {
let databaseURL: URL
let functionsURL: URL

private let _auth: AuthClient
@MainActor
private var _auth: AuthClient?

/// Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies.
@MainActor
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.
""")
reportIssue(
"""
Supabase Client is configured with the auth.accessToken option,
accessing supabase.auth is not possible.
""")
}

if _auth == nil {
_auth = _initAuthClient()
}
return _auth

return _auth!
}

var rest: PostgrestClient {
Expand Down Expand Up @@ -161,26 +169,6 @@ public final class SupabaseClient: Sendable {
])
.merging(with: HTTPFields(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: _headers.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,
decoder: options.auth.decoder,
fetch: {
// DON'T use `fetchWithAuth` method within the AuthClient as it may cause a deadlock.
try await options.global.session.data(for: $0)
},
autoRefreshToken: options.auth.autoRefreshToken
)

_realtime = UncheckedSendable(
RealtimeClient(
supabaseURL.appendingPathComponent("/realtime/v1").absoluteString,
Expand Down Expand Up @@ -329,6 +317,7 @@ public final class SupabaseClient: Sendable {
/// supabase.handle(url)
/// }
/// ```
@MainActor
public func handle(_ url: URL) {
auth.handle(url)
}
Expand All @@ -351,11 +340,12 @@ 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
}
let token: String? =
if let accessToken = options.auth.accessToken {
try? await accessToken()
} else {
try? await auth.session.accessToken
}

var request = request
if let token {
Expand All @@ -366,7 +356,7 @@ public final class SupabaseClient: Sendable {

private func listenForAuthEvents() {
let task = Task {
for await (event, session) in auth.authStateChanges {
for await (event, session) in await auth.authStateChanges {
await handleTokenChanged(event: event, session: session)
}
}
Expand All @@ -377,7 +367,9 @@ public final class SupabaseClient: Sendable {

private func handleTokenChanged(event: AuthChangeEvent, session: Session?) async {
let accessToken: String? = mutableState.withValue {
if [.initialSession, .signedIn, .tokenRefreshed].contains(event), $0.changedAccessToken != session?.accessToken {
if [.initialSession, .signedIn, .tokenRefreshed].contains(event),
$0.changedAccessToken != session?.accessToken
{
$0.changedAccessToken = session?.accessToken
return session?.accessToken ?? supabaseKey
}
Expand All @@ -393,4 +385,27 @@ public final class SupabaseClient: Sendable {
realtime.setAuth(accessToken)
await realtimeV2.setAuth(accessToken)
}

@MainActor
private func _initAuthClient() -> AuthClient {
// default storage key uses the supabase project ref as a namespace
let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token"

return AuthClient(
url: supabaseURL.appendingPathComponent("/auth/v1"),
headers: _headers.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,
decoder: options.auth.decoder,
fetch: { [options] in
// DON'T use `fetchWithAuth` method within the AuthClient as it may cause a deadlock.
try await options.global.session.data(for: $0)
},
autoRefreshToken: options.auth.autoRefreshToken
)
}
}
4 changes: 3 additions & 1 deletion Tests/AuthTests/AuthClientMultipleInstancesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
// Created by Guilherme Souza on 05/07/24.
//

@testable import Auth
import TestHelpers
import XCTest

@testable import Auth

@MainActor
final class AuthClientMultipleInstancesTests: XCTestCase {
func testMultipleAuthClientInstances() {
let url = URL(string: "http://localhost:54321/auth")!
Expand Down
1 change: 1 addition & 0 deletions Tests/AuthTests/AuthClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import XCTest
import FoundationNetworking
#endif

@MainActor
final class AuthClientTests: XCTestCase {
var sessionManager: SessionManager!

Expand Down
16 changes: 10 additions & 6 deletions Tests/AuthTests/RequestsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@
// Created by Guilherme Souza on 07/10/23.
//

@testable import Auth
import Helpers
import InlineSnapshotTesting
import SnapshotTesting
import TestHelpers
import XCTest

@testable import Auth

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

struct UnimplementedError: Error {}

@MainActor
final class RequestsTests: XCTestCase {
func testSignUpWithEmailAndPassword() async {
let sut = makeSUT()
Expand Down Expand Up @@ -126,7 +128,7 @@ final class RequestsTests: XCTestCase {
url,
URL(
string:
"http://localhost:54321/auth/v1/authorize?provider=github&scopes=read,write&redirect_to=https://dummy-url.com/redirect&extra_key=extra_value"
"http://localhost:54321/auth/v1/authorize?provider=github&scopes=read,write&redirect_to=https://dummy-url.com/redirect&extra_key=extra_value"
)!
)
}
Expand All @@ -152,7 +154,7 @@ final class RequestsTests: XCTestCase {

let url = URL(
string:
"https://dummy-url.com/callback#access_token=accesstoken&expires_in=60&refresh_token=refreshtoken&token_type=bearer"
"https://dummy-url.com/callback#access_token=accesstoken&expires_in=60&refresh_token=refreshtoken&token_type=bearer"
)!

let session = try await sut.session(from: url)
Expand All @@ -173,7 +175,7 @@ final class RequestsTests: XCTestCase {

let url = URL(
string:
"https://dummy-url.com/callback#access_token=accesstoken&expires_in=60&refresh_token=refreshtoken"
"https://dummy-url.com/callback#access_token=accesstoken&expires_in=60&refresh_token=refreshtoken"
)!

do {
Expand Down Expand Up @@ -430,7 +432,8 @@ final class RequestsTests: XCTestCase {
Dependencies[sut.clientID].sessionStorage.store(.validSession)

await assert {
_ = try await sut.mfa.enroll(params: MFAEnrollParams(issuer: "supabase.com", friendlyName: "test"))
_ = try await sut.mfa.enroll(
params: MFAEnrollParams(issuer: "supabase.com", friendlyName: "test"))
}
}

Expand Down Expand Up @@ -480,7 +483,8 @@ final class RequestsTests: XCTestCase {
Dependencies[sut.clientID].sessionStorage.store(.validSession)

await assert {
_ = try await sut.mfa.verify(params: .init(factorId: "123", challengeId: "123", code: "123456"))
_ = try await sut.mfa.verify(
params: .init(factorId: "123", challengeId: "123", code: "123456"))
}
}

Expand Down
Loading