Skip to content

Commit

Permalink
Showing 14 changed files with 468 additions and 102 deletions.
12 changes: 8 additions & 4 deletions Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -14,7 +14,8 @@
793E03092B2CED5D00AC7DED /* Contants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E03082B2CED5D00AC7DED /* Contants.swift */; };
793E030B2B2CEDDA00AC7DED /* ActionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E030A2B2CEDDA00AC7DED /* ActionState.swift */; };
793E030D2B2DAB5700AC7DED /* SignInWithApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E030C2B2DAB5700AC7DED /* SignInWithApple.swift */; };
7940E3152B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7940E3142B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift */; };
79401F332BC6FEAE004C9C0F /* SignInWithOAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79401F322BC6FEAE004C9C0F /* SignInWithOAuth.swift */; };
79401F352BC708C8004C9C0F /* UIViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79401F342BC708C8004C9C0F /* UIViewControllerWrapper.swift */; };
794C61D62BAD1E12000E6B0F /* UserIdentityList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C61D52BAD1E12000E6B0F /* UserIdentityList.swift */; };
794EF1222955F26A008C9526 /* AddTodoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794EF1212955F26A008C9526 /* AddTodoListView.swift */; };
794EF1242955F3DE008C9526 /* TodoListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794EF1232955F3DE008C9526 /* TodoListRow.swift */; };
@@ -83,7 +84,8 @@
793E03082B2CED5D00AC7DED /* Contants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contants.swift; sourceTree = "<group>"; };
793E030A2B2CEDDA00AC7DED /* ActionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionState.swift; sourceTree = "<group>"; };
793E030C2B2DAB5700AC7DED /* SignInWithApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithApple.swift; sourceTree = "<group>"; };
7940E3142B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleSignInWithWebFlow.swift; sourceTree = "<group>"; };
79401F322BC6FEAE004C9C0F /* SignInWithOAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithOAuth.swift; sourceTree = "<group>"; };
79401F342BC708C8004C9C0F /* UIViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerWrapper.swift; sourceTree = "<group>"; };
794C61D52BAD1E12000E6B0F /* UserIdentityList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentityList.swift; sourceTree = "<group>"; };
794EF1212955F26A008C9526 /* AddTodoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTodoListView.swift; sourceTree = "<group>"; };
794EF1232955F3DE008C9526 /* TodoListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListRow.swift; sourceTree = "<group>"; };
@@ -221,6 +223,7 @@
79E2B55B2B97A2310042CD21 /* UIApplicationExtensions.swift */,
797EFB672BABD90500098D6B /* Stringfy.swift */,
79B1C80D2BAC017C00D991AA /* AnyJSONView.swift */,
79401F342BC708C8004C9C0F /* UIViewControllerWrapper.swift */,
);
path = Examples;
sourceTree = "<group>";
@@ -258,9 +261,9 @@
79AF047E2B2CE207008761AD /* AuthWithEmailAndPassword.swift */,
79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */,
793E030C2B2DAB5700AC7DED /* SignInWithApple.swift */,
7940E3142B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift */,
79E2B5542B9788BF0042CD21 /* GoogleSignInSDKFlow.swift */,
79C9B8E42BBB16C0003AD942 /* SignInAnonymously.swift */,
79401F322BC6FEAE004C9C0F /* SignInWithOAuth.swift */,
);
path = Auth;
sourceTree = "<group>";
@@ -486,6 +489,8 @@
796298992AEBBA77000AA957 /* MFAFlow.swift in Sources */,
79AF04862B2CE586008761AD /* Debug.swift in Sources */,
79AF04842B2CE408008761AD /* AuthWithMagicLink.swift in Sources */,
79401F352BC708C8004C9C0F /* UIViewControllerWrapper.swift in Sources */,
79401F332BC6FEAE004C9C0F /* SignInWithOAuth.swift in Sources */,
79B1C80E2BAC017C00D991AA /* AnyJSONView.swift in Sources */,
79E2B5552B9788BF0042CD21 /* GoogleSignInSDKFlow.swift in Sources */,
793E03092B2CED5D00AC7DED /* Contants.swift in Sources */,
@@ -504,7 +509,6 @@
795640602954AE140088A06F /* AuthController.swift in Sources */,
79AF047F2B2CE207008761AD /* AuthWithEmailAndPassword.swift in Sources */,
795640622955AD2B0088A06F /* HomeView.swift in Sources */,
7940E3152B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift in Sources */,
793895CA2954ABFF0044F2B8 /* ExamplesApp.swift in Sources */,
797EFB682BABD90500098D6B /* Stringfy.swift in Sources */,
797EFB6C2BABE1B800098D6B /* FileObjectDetailView.swift in Sources */,
10 changes: 7 additions & 3 deletions Examples/Examples/Auth/AuthView.swift
Original file line number Diff line number Diff line change
@@ -12,7 +12,8 @@ struct AuthView: View {
case emailAndPassword
case magicLink
case signInWithApple
case googleSignInWebFlow
case signInWithOAuth
case signInWithOAuthUsingUIKit
case googleSignInSDKFlow
case signInAnonymously

@@ -21,7 +22,8 @@ struct AuthView: View {
case .emailAndPassword: "Auth with Email & Password"
case .magicLink: "Auth with Magic Link"
case .signInWithApple: "Sign in with Apple"
case .googleSignInWebFlow: "Google Sign in (Web Flow)"
case .signInWithOAuth: "Sign in with OAuth flow"
case .signInWithOAuthUsingUIKit: "Sign in with OAuth flow (UIKit)"
case .googleSignInSDKFlow: "Google Sign in (GIDSignIn SDK Flow)"
case .signInAnonymously: "Sign in Anonymously"
}
@@ -50,7 +52,9 @@ extension AuthView.Option: View {
case .emailAndPassword: AuthWithEmailAndPassword()
case .magicLink: AuthWithMagicLink()
case .signInWithApple: SignInWithApple()
case .googleSignInWebFlow: GoogleSignInWithWebFlow()
case .signInWithOAuth: SignInWithOAuth()
case .signInWithOAuthUsingUIKit: UIViewControllerWrapper(SignInWithOAuthViewController())
.edgesIgnoringSafeArea(.all)
case .googleSignInSDKFlow: GoogleSignInSDKFlow()
case .signInAnonymously: SignInAnonymously()
}
41 changes: 0 additions & 41 deletions Examples/Examples/Auth/GoogleSignInWithWebFlow.swift

This file was deleted.

122 changes: 122 additions & 0 deletions Examples/Examples/Auth/SignInWithOAuth.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//
// SignInWithOAuth.swift
// Examples
//
// Created by Guilherme Souza on 10/04/24.
//

import AuthenticationServices
import Supabase
import SwiftUI

struct SignInWithOAuth: View {
let providers = Provider.allCases

@State var provider = Provider.allCases[0]
@Environment(\.webAuthenticationSession) var webAuthenticationSession

var body: some View {
VStack {
Picker("Provider", selection: $provider) {
ForEach(providers) { provider in
Text("\(provider)").tag(provider)
}
}

Button("Start Sign-in Flow") {
Task {
do {
try await supabase.auth.signInWithOAuth(
provider: provider,
redirectTo: Constants.redirectToURL,
launchFlow: { @MainActor url in
try await webAuthenticationSession.authenticate(
using: url,
callbackURLScheme: Constants.redirectToURL.scheme!
)
}
)
} catch {
debug("Failed to sign-in with OAuth flow: \(error)")
}
}
}
}
}
}

final class SignInWithOAuthViewController: UIViewController, UIPickerViewDataSource,
UIPickerViewDelegate
{
let providers = Provider.allCases
var provider = Provider.allCases[0]

let providerPicker = UIPickerView()
let signInButton = UIButton(type: .system)

override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}

func setupViews() {
view.backgroundColor = .white

providerPicker.dataSource = self
providerPicker.delegate = self
view.addSubview(providerPicker)
providerPicker.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
providerPicker.centerXAnchor.constraint(equalTo: view.centerXAnchor),
providerPicker.centerYAnchor.constraint(equalTo: view.centerYAnchor),
providerPicker.widthAnchor.constraint(equalToConstant: 200),
providerPicker.heightAnchor.constraint(equalToConstant: 100),
])

signInButton.setTitle("Start Sign-in Flow", for: .normal)
signInButton.addTarget(self, action: #selector(signInButtonTapped), for: .touchUpInside)
view.addSubview(signInButton)
signInButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
signInButton.topAnchor.constraint(equalTo: providerPicker.bottomAnchor, constant: 20),
])
}

@objc func signInButtonTapped() {
Task {
do {
try await supabase.auth.signInWithOAuth(
provider: provider,
redirectTo: Constants.redirectToURL
)
} catch {
debug("Failed to sign-in with OAuth flow: \(error)")
}
}
}

func numberOfComponents(in _: UIPickerView) -> Int {
1
}

func pickerView(_: UIPickerView, numberOfRowsInComponent _: Int) -> Int {
providers.count
}

func pickerView(_: UIPickerView, titleForRow row: Int, forComponent _: Int) -> String? {
"\(providers[row])"
}

func pickerView(_: UIPickerView, didSelectRow row: Int, inComponent _: Int) {
provider = providers[row]
}
}

#Preview("SwiftUI") {
SignInWithOAuth()
}

#Preview("UIKit") {
SignInWithOAuthViewController()
}
7 changes: 6 additions & 1 deletion Examples/Examples/ExamplesApp.swift
Original file line number Diff line number Diff line change
@@ -22,7 +22,12 @@ struct ExamplesApp: App {
let supabase = SupabaseClient(
supabaseURL: Secrets.supabaseURL,
supabaseKey: Secrets.supabaseAnonKey,
options: .init(global: .init(logger: ConsoleLogger()))
options: .init(
auth: .init(redirectToURL: Constants.redirectToURL),
global: .init(
logger: ConsoleLogger()
)
)
)

struct ConsoleLogger: SupabaseLogger {
26 changes: 26 additions & 0 deletions Examples/Examples/UIViewControllerWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// UIViewControllerWrapper.swift
// Examples
//
// Created by Guilherme Souza on 10/04/24.
//

import SwiftUI

struct UIViewControllerWrapper<T: UIViewController>: UIViewControllerRepresentable {
typealias UIViewControllerType = T

let viewController: T

init(_ viewController: T) {
self.viewController = viewController
}

func makeUIViewController(context _: Context) -> T {
viewController
}

func updateUIViewController(_: T, context _: Context) {
// Update the view controller if needed
}
}
171 changes: 159 additions & 12 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import _Helpers
import AuthenticationServices
import Foundation

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public actor AuthClient {
public final class AuthClient: @unchecked Sendable {
/// FetchHandler is a type alias for asynchronous network request handling.
public typealias FetchHandler = @Sendable (
_ request: URLRequest
@@ -16,6 +17,7 @@ public actor AuthClient {
public let url: URL
public var headers: [String: String]
public let flowType: AuthFlowType
public let redirectToURL: URL?
public let localStorage: any AuthLocalStorage
public let logger: (any SupabaseLogger)?
public let encoder: JSONEncoder
@@ -28,6 +30,7 @@ public actor AuthClient {
/// - url: The base URL of the Auth server.
/// - headers: Custom headers to be included in requests.
/// - flowType: The authentication flow type.
/// - redirectToURL: Default URL to be used for redirect on the flows that requires it.
/// - localStorage: The storage mechanism for local data.
/// - logger: The logger to use.
/// - encoder: The JSON encoder to use for encoding requests.
@@ -37,6 +40,7 @@ public actor AuthClient {
url: URL,
headers: [String: String] = [:],
flowType: AuthFlowType = Configuration.defaultFlowType,
redirectToURL: URL? = nil,
localStorage: any AuthLocalStorage,
logger: (any SupabaseLogger)? = nil,
encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder,
@@ -48,6 +52,7 @@ public actor AuthClient {
self.url = url
self.headers = headers
self.flowType = flowType
self.redirectToURL = redirectToURL
self.localStorage = localStorage
self.logger = logger
self.encoder = encoder
@@ -100,15 +105,17 @@ public actor AuthClient {
/// - url: The base URL of the Auth server.
/// - headers: Custom headers to be included in requests.
/// - flowType: The authentication flow type..
/// - redirectToURL: Default URL to be used for redirect on the flows that requires it.
/// - localStorage: The storage mechanism for local data..
/// - logger: The logger to use.
/// - encoder: The JSON encoder to use for encoding requests.
/// - decoder: The JSON decoder to use for decoding responses.
/// - fetch: The asynchronous fetch handler for network requests.
public init(
public convenience init(
url: URL,
headers: [String: String] = [:],
flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType,
redirectToURL: URL? = nil,
localStorage: any AuthLocalStorage,
logger: (any SupabaseLogger)? = nil,
encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder,
@@ -120,6 +127,7 @@ public actor AuthClient {
url: url,
headers: headers,
flowType: flowType,
redirectToURL: redirectToURL,
localStorage: localStorage,
logger: logger,
encoder: encoder,
@@ -133,7 +141,7 @@ public actor AuthClient {
///
/// - Parameters:
/// - configuration: The client configuration.
public init(configuration: Configuration) {
public convenience init(configuration: Configuration) {
let api = APIClient.live(
configuration: configuration,
http: HTTPClient(
@@ -243,7 +251,10 @@ public actor AuthClient {
path: "/signup",
method: .post,
query: [
redirectTo.map { URLQueryItem(name: "redirect_to", value: $0.absoluteString) },
(redirectTo ?? configuration.redirectToURL).map { URLQueryItem(
name: "redirect_to",
value: $0.absoluteString
) },
].compactMap { $0 },
body: configuration.encoder.encode(
SignUpRequest(
@@ -428,7 +439,10 @@ public actor AuthClient {
path: "/otp",
method: .post,
query: [
redirectTo.map { URLQueryItem(name: "redirect_to", value: $0.absoluteString) },
(redirectTo ?? configuration.redirectToURL).map { URLQueryItem(
name: "redirect_to",
value: $0.absoluteString
) },
].compactMap { $0 },
body: configuration.encoder.encode(
OTPParams(
@@ -504,7 +518,7 @@ public actor AuthClient {
SignInWithSSORequest(
providerId: nil,
domain: domain,
redirectTo: redirectTo,
redirectTo: redirectTo ?? configuration.redirectToURL,
gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) },
codeChallenge: codeChallenge,
codeChallengeMethod: codeChallengeMethod
@@ -539,7 +553,7 @@ public actor AuthClient {
SignInWithSSORequest(
providerId: providerId,
domain: nil,
redirectTo: redirectTo,
redirectTo: redirectTo ?? configuration.redirectToURL,
gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) },
codeChallenge: codeChallenge,
codeChallengeMethod: codeChallengeMethod
@@ -579,7 +593,15 @@ public actor AuthClient {
return session
}

/// Log in an existing user via a third-party provider.
/// Get a URL which you can use to start an OAuth flow for a third-party provider.
///
/// Use this method if you want to have full control over the OAuth flow implementation, once you
/// have result URL with a OAuth token, use method ``session(from:)`` to load the session
/// into the client.
///
/// If that isn't the case, you should consider using
/// ``signInWithOAuth(provider:redirectTo:scopes:queryParams:launchFlow:)`` or
/// ``signInWithOAuth(provider:redirectTo:scopes:queryParams:configure:)``.
public func getOAuthSignInURL(
provider: Provider,
scopes: String? = nil,
@@ -595,6 +617,111 @@ public actor AuthClient {
)
}

/// Sign-in an existing user via a third-party provider.
///
/// - Parameters:
/// - provider: The third-party provider.
/// - redirectTo: A URL to send the user to after they are confirmed.
/// - scopes: A space-separated list of scopes granted to the OAuth application.
/// - queryParams: Additional query params.
/// - launchFlow: A launch closure that you can use to implement the authentication flow. Use
/// the `url` to initiate the flow and return a `URL` that contains the OAuth result.
///
/// - Note: This method support the PKCE flow.
@discardableResult
public func signInWithOAuth(
provider: Provider,
redirectTo: URL? = nil,
scopes: String? = nil,
queryParams: [(name: String, value: String?)] = [],
launchFlow: @MainActor @Sendable (_ url: URL) async throws -> URL
) async throws -> Session {
guard let redirectTo = (redirectTo ?? configuration.redirectToURL) else {
throw AuthError.invalidRedirectScheme
}

let url = try getOAuthSignInURL(
provider: provider,
scopes: scopes,
redirectTo: redirectTo,
queryParams: queryParams
)

let resultURL = try await launchFlow(url)

return try await session(from: resultURL)
}

/// Sign-in an existing user via a third-party provider using ``ASWebAuthenticationSession``.
///
/// - Parameters:
/// - provider: The third-party provider.
/// - redirectTo: A URL to send the user to after they are confirmed.
/// - scopes: A space-separated list of scopes granted to the OAuth application.
/// - queryParams: Additional query params.
/// - configure: A configuration closure that you can use to customize the internal
/// ``ASWebAuthenticationSession`` object.
///
/// - Note: This method support the PKCE flow.
/// - Warning: Do not call `start()` on the `ASWebAuthenticationSession` object inside the
/// `configure` closure, as the method implementation calls it already.
@available(watchOS 6.2, tvOS 16.0, *)
@discardableResult
public func signInWithOAuth(
provider: Provider,
redirectTo: URL? = nil,
scopes: String? = nil,
queryParams: [(name: String, value: String?)] = [],
configure: @Sendable (_ session: ASWebAuthenticationSession) -> Void = { _ in }
) async throws -> Session {
try await signInWithOAuth(
provider: provider,
redirectTo: redirectTo,
scopes: scopes,
queryParams: queryParams
) { @MainActor url in
try await withCheckedThrowingContinuation { continuation in
guard let callbackScheme = (configuration.redirectToURL ?? redirectTo)?.scheme else {
continuation.resume(throwing: AuthError.invalidRedirectScheme)
return
}

#if !os(tvOS) && !os(watchOS)
var presentationContextProvider: DefaultPresentationContextProvider?
#endif

let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: callbackScheme
) { url, error in
if let error {
continuation.resume(throwing: error)
} else if let url {
continuation.resume(returning: url)
} else {
continuation.resume(throwing: AuthError.missingURL)
}

#if !os(tvOS) && !os(watchOS)
// Keep a strong reference to presentationContextProvider until the flow completes.
_ = presentationContextProvider
#endif
}

configure(session)

#if !os(tvOS) && !os(watchOS)
if session.presentationContextProvider == nil {
presentationContextProvider = DefaultPresentationContextProvider()
session.presentationContextProvider = presentationContextProvider
}
#endif

session.start()
}
}
}

/// Gets the session data from a OAuth2 callback URL.
@discardableResult
public func session(from url: URL) async throws -> Session {
@@ -756,7 +883,10 @@ public actor AuthClient {
path: "/verify",
method: .post,
query: [
redirectTo.map { URLQueryItem(name: "redirect_to", value: $0.absoluteString) },
(redirectTo ?? configuration.redirectToURL).map { URLQueryItem(
name: "redirect_to",
value: $0.absoluteString
) },
].compactMap { $0 },
body: configuration.encoder.encode(
VerifyOTPParams.email(
@@ -840,7 +970,10 @@ public actor AuthClient {
path: "/resend",
method: .post,
query: [
emailRedirectTo.map { URLQueryItem(name: "redirect_to", value: $0.absoluteString) },
(emailRedirectTo ?? configuration.redirectToURL).map { URLQueryItem(
name: "redirect_to",
value: $0.absoluteString
) },
].compactMap { $0 },
body: configuration.encoder.encode(
ResendEmailParams(
@@ -983,7 +1116,10 @@ public actor AuthClient {
path: "/recover",
method: .post,
query: [
redirectTo.map { URLQueryItem(name: "redirect_to", value: $0.absoluteString) },
(redirectTo ?? configuration.redirectToURL).map { URLQueryItem(
name: "redirect_to",
value: $0.absoluteString
) },
].compactMap { $0 },
body: configuration.encoder.encode(
RecoverParams(
@@ -1084,7 +1220,7 @@ public actor AuthClient {
queryItems.append(URLQueryItem(name: "scopes", value: scopes))
}

if let redirectTo {
if let redirectTo = redirectTo ?? configuration.redirectToURL {
queryItems.append(URLQueryItem(name: "redirect_to", value: redirectTo.absoluteString))
}

@@ -1124,3 +1260,14 @@ extension AuthClient {
/// ``AuthClient/didChangeAuthStateNotification`` notification.
public static let authChangeSessionInfoKey = "AuthClient.authChangeSession"
}

#if !os(tvOS) && !os(watchOS)
@MainActor
final class DefaultPresentationContextProvider: NSObject,
ASWebAuthenticationPresentationContextProviding
{
func presentationAnchor(for _: ASWebAuthenticationSession) -> ASPresentationAnchor {
ASPresentationAnchor()
}
}
#endif
7 changes: 5 additions & 2 deletions Sources/Auth/AuthError.swift
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ public enum AuthError: LocalizedError, Sendable, Equatable {
case api(APIError)
case pkce(PKCEFailureReason)
case invalidImplicitGrantFlowURL
case missingURL
case invalidRedirectScheme

public struct APIError: Error, Decodable, Sendable, Equatable {
/// A basic message describing the problem with the request. Usually missing if
@@ -49,8 +51,9 @@ public enum AuthError: LocalizedError, Sendable, Equatable {
case .sessionNotFound: return "Unable to get a valid session."
case .pkce(.codeVerifierNotFound): return "A code verifier wasn't found in PKCE flow."
case .pkce(.invalidPKCEFlowURL): return "Not a valid PKCE flow url."
case .invalidImplicitGrantFlowURL:
return "Not a valid implicit grant flow url."
case .invalidImplicitGrantFlowURL: return "Not a valid implicit grant flow url."
case .missingURL: return "Missing URL."
case .invalidRedirectScheme: return "Invalid redirect scheme."
}
}
}
2 changes: 1 addition & 1 deletion Sources/Auth/Deprecated.swift
Original file line number Diff line number Diff line change
@@ -105,7 +105,7 @@ extension AuthClient {
deprecated,
message: "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)"
)
public init(
public convenience init(
url: URL,
headers: [String: String] = [:],
flowType: AuthFlowType = Configuration.defaultFlowType,
8 changes: 4 additions & 4 deletions Sources/Auth/Internal/Dependencies.swift
Original file line number Diff line number Diff line change
@@ -31,14 +31,14 @@ var Current: Dependencies {
}

@propertyWrapper
struct Dependency<Value> {
struct Dependency<Value: Sendable>: Sendable {
var wrappedValue: Value {
Current[keyPath: keyPath]
Current[keyPath: keyPath.value]
}

let keyPath: KeyPath<Dependencies, Value>
let keyPath: UncheckedSendable<KeyPath<Dependencies, Value>>

init(_ keyPath: KeyPath<Dependencies, Value>) {
self.keyPath = keyPath
self.keyPath = UncheckedSendable(keyPath)
}
}
3 changes: 2 additions & 1 deletion Sources/Supabase/SupabaseClient.swift
Original file line number Diff line number Diff line change
@@ -126,6 +126,7 @@ public final class SupabaseClient: @unchecked Sendable {
url: supabaseURL.appendingPathComponent("/auth/v1"),
headers: defaultHeaders,
flowType: options.auth.flowType,
redirectToURL: options.auth.redirectToURL,
localStorage: options.auth.storage,
logger: options.global.logger,
encoder: options.auth.encoder,
@@ -256,7 +257,7 @@ public final class SupabaseClient: @unchecked Sendable {
private func listenForAuthEvents() {
listenForAuthEventsTask.setValue(
Task {
for await (event, session) in await auth.authStateChanges {
for await (event, session) in auth.authStateChanges {
await handleTokenChanged(event: event, session: session)
}
}
7 changes: 7 additions & 0 deletions Sources/Supabase/Types.swift
Original file line number Diff line number Diff line change
@@ -38,6 +38,9 @@ public struct SupabaseClientOptions: Sendable {
/// A storage provider. Used to store the logged-in session.
public let storage: any AuthLocalStorage

/// Default URL to be used for redirect on the flows that requires it.
public let redirectToURL: URL?

/// OAuth flow to use - defaults to PKCE flow. PKCE is recommended for mobile and server-side
/// applications.
public let flowType: AuthFlowType
@@ -50,11 +53,13 @@ public struct SupabaseClientOptions: Sendable {

public init(
storage: any AuthLocalStorage,
redirectToURL: URL? = nil,
flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType,
encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder,
decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder
) {
self.storage = storage
self.redirectToURL = redirectToURL
self.flowType = flowType
self.encoder = encoder
self.decoder = decoder
@@ -109,12 +114,14 @@ extension SupabaseClientOptions {
extension SupabaseClientOptions.AuthOptions {
#if !os(Linux)
public init(
redirectToURL: URL? = nil,
flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType,
encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder,
decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder
) {
self.init(
storage: AuthClient.Configuration.defaultLocalStorage,
redirectToURL: redirectToURL,
flowType: flowType,
encoder: encoder,
decoder: decoder
96 changes: 94 additions & 2 deletions Tests/AuthTests/AuthClientTests.swift
Original file line number Diff line number Diff line change
@@ -20,6 +20,8 @@ final class AuthClientTests: XCTestCase {
var eventEmitter: Auth.EventEmitter!
var sessionManager: SessionManager!

var sessionStorage: SessionStorage!
var codeVerifierStorage: CodeVerifierStorage!
var api: APIClient!
var sut: AuthClient!

@@ -33,6 +35,8 @@ final class AuthClientTests: XCTestCase {
super.setUp()
Current = .mock

sessionStorage = .mock
codeVerifierStorage = .mock
eventEmitter = .mock
sessionManager = .mock
api = .mock
@@ -250,6 +254,94 @@ final class AuthClientTests: XCTestCase {
XCTAssertEqual(events, [.signedIn])
}

func testSignInWithOAuth() async throws {
let emitReceivedEvents = LockIsolated<[(AuthChangeEvent, Session?)]>([])

eventEmitter.emit = { @Sendable event, session, _ in
emitReceivedEvents.withValue {
$0.append((event, session))
}
}

sessionStorage = .live
codeVerifierStorage = .live
sessionManager = .live

api.execute = { @Sendable _ in
.stub(
"""
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6Imd1aWxoZXJtZTJAZ3Jkcy5kZXYiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifQ.4lMvmz2pJkWu1hMsBgXP98Fwz4rbvFYl4VA9joRv6kY",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "GGduTeu95GraIXQ56jppkw",
"user": {
"id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8",
"aud": "authenticated",
"role": "authenticated",
"email": "guilherme@binaryscraping.co",
"email_confirmed_at": "2022-03-30T10:33:41.018575157Z",
"phone": "",
"last_sign_in_at": "2022-03-30T10:33:41.021531328Z",
"app_metadata": {
"provider": "email",
"providers": [
"email"
]
},
"user_metadata": {},
"identities": [
{
"id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8",
"user_id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8",
"identity_id": "859f402d-b3de-4105-a1b9-932836d9193b",
"identity_data": {
"sub": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8"
},
"provider": "email",
"last_sign_in_at": "2022-03-30T10:33:41.015557063Z",
"created_at": "2022-03-30T10:33:41.015612Z",
"updated_at": "2022-03-30T10:33:41.015616Z"
}
],
"created_at": "2022-03-30T10:33:41.005433Z",
"updated_at": "2022-03-30T10:33:41.022688Z"
}
}
"""
)
}

let sut = makeSUT()

try await sut.signInWithOAuth(
provider: .google,
redirectTo: URL(string: "supabase://auth-callback")
) { (url: URL) in
URL(string: "supabase://auth-callback?code=12345") ?? url
}

XCTAssertEqual(emitReceivedEvents.value.map(\.0), [.signedIn])
}

@available(watchOS 6.2, tvOS 16.0, *)
func testSignInWithOAuthWithInvalidRedirecTo() async {
let sut = makeSUT()

do {
try await sut.signInWithOAuth(
provider: .google,
redirectTo: nil
) { _ in
XCTFail("Should not call launchFlow.")
}
} catch let error as AuthError {
XCTAssertEqual(error, .invalidRedirectScheme)
} catch {
XCTFail("Unexcpted error: \(error)")
}
}

private func makeSUT() -> AuthClient {
let configuration = AuthClient.Configuration(
url: clientURL,
@@ -261,10 +353,10 @@ final class AuthClientTests: XCTestCase {
let sut = AuthClient(
configuration: configuration,
sessionManager: sessionManager,
codeVerifierStorage: .mock,
codeVerifierStorage: codeVerifierStorage,
api: api,
eventEmitter: eventEmitter,
sessionStorage: .mock,
sessionStorage: sessionStorage,
logger: nil
)

58 changes: 27 additions & 31 deletions Tests/AuthTests/RequestsTests.swift
Original file line number Diff line number Diff line change
@@ -125,7 +125,7 @@ final class RequestsTests: XCTestCase {

func testGetOAuthSignInURL() async throws {
let sut = makeSUT()
let url = try await sut.getOAuthSignInURL(
let url = try sut.getOAuthSignInURL(
provider: .github, scopes: "read,write",
redirectTo: URL(string: "https://dummy-url.com/redirect")!,
queryParams: [("extra_key", "extra_value")]
@@ -146,40 +146,36 @@ final class RequestsTests: XCTestCase {
}
}

#if !os(Windows) && !os(Linux)
// For some reason this crashes the testing bundle
// on Linux and Windows, skipping it.
func testSessionFromURL() async throws {
let sut = makeSUT(fetch: { request in
let authorizationHeader = request.allHTTPHeaderFields?["Authorization"]
XCTAssertEqual(authorizationHeader, "bearer accesstoken")
return (json(named: "user"), HTTPURLResponse())
})
func testSessionFromURL() async throws {
let sut = makeSUT(fetch: { request in
let authorizationHeader = request.allHTTPHeaderFields?["Authorization"]
XCTAssertEqual(authorizationHeader, "bearer accesstoken")
return (json(named: "user"), HTTPURLResponse())
})

let currentDate = Date()
let currentDate = Date()

Current.sessionManager = .live
Current.sessionStorage.storeSession = { _ in }
Current.codeVerifierStorage.get = { nil }
Current.currentDate = { currentDate }
Current.sessionManager = .live
Current.sessionStorage.storeSession = { _ in }
Current.codeVerifierStorage.get = { nil }
Current.currentDate = { currentDate }

let url = URL(
string:
"https://dummy-url.com/callback#access_token=accesstoken&expires_in=60&refresh_token=refreshtoken&token_type=bearer"
)!
let url = URL(
string:
"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)
let expectedSession = Session(
accessToken: "accesstoken",
tokenType: "bearer",
expiresIn: 60,
expiresAt: currentDate.addingTimeInterval(60).timeIntervalSince1970,
refreshToken: "refreshtoken",
user: User(fromMockNamed: "user")
)
XCTAssertEqual(session, expectedSession)
}
#endif
let session = try await sut.session(from: url)
let expectedSession = Session(
accessToken: "accesstoken",
tokenType: "bearer",
expiresIn: 60,
expiresAt: currentDate.addingTimeInterval(60).timeIntervalSince1970,
refreshToken: "refreshtoken",
user: User(fromMockNamed: "user")
)
XCTAssertEqual(session, expectedSession)
}

func testSessionFromURLWithMissingComponent() async {
let sut = makeSUT()

0 comments on commit 1290bcf

Please sign in to comment.