Skip to content

Commit

Permalink
feat: add third-party auth support (#423)
Browse files Browse the repository at this point in the history
* feat: add third-party auth support

* test 3p auth

* fix test on linux

* Use IssueReporting

* fix linux test
  • Loading branch information
grdsdev authored Jul 29, 2024
1 parent 93f4ff5 commit d760f2d
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 18 deletions.
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 29 additions & 13 deletions Sources/Supabase/SupabaseClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ConcurrencyExtras
import Foundation
@_exported import Functions
import Helpers
import IssueReporting
@_exported import PostgREST
@_exported import Realtime
@_exported import Storage
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
16 changes: 13 additions & 3 deletions Sources/Supabase/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,21 @@ 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,
storageKey: String? = nil,
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
Expand All @@ -76,6 +83,7 @@ public struct SupabaseClientOptions: Sendable {
self.encoder = encoder
self.decoder = decoder
self.autoRefreshToken = autoRefreshToken
self.accessToken = accessToken
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -163,7 +172,8 @@ extension SupabaseClientOptions.AuthOptions {
flowType: flowType,
encoder: encoder,
decoder: decoder,
autoRefreshToken: autoRefreshToken
autoRefreshToken: autoRefreshToken,
accessToken: accessToken
)
}
#endif
Expand Down
30 changes: 28 additions & 2 deletions Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"originHash" : "7cc8037abb258fd1406effe711922c8d309c2e7a93147bfe68753fd05392a24a",
"pins" : [
{
"identity" : "appauth-ios",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -110,5 +136,5 @@
}
}
],
"version" : 3
"version" : 2
}
33 changes: 33 additions & 0 deletions Tests/SupabaseTests/SupabaseClientTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@testable import Auth
import CustomDump
@testable import Functions
import IssueReporting
@testable import Realtime
@testable import Supabase
import XCTest
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}

0 comments on commit d760f2d

Please sign in to comment.