Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 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
5 changes: 5 additions & 0 deletions App/Auth0DemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import SwiftUI
struct Auth0DemoApp: App {
var body: some Scene {
WindowGroup {
#if os(macOS)
ContentView()
#else
ContentView()
.withWindowReader()
#endif
}
}
}
171 changes: 134 additions & 37 deletions App/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,87 @@ import SwiftUI
import Combine
import Auth0

#if !os(macOS)
import UIKit
#else
import AppKit
#endif


struct ContentView: View {
@StateObject private var viewModel = ContentViewModel()


#if os(macOS)
@State private var currentWindow: Auth0WindowRepresentable?
#else
@Environment(\.window) private var window
#endif

var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 20) {

// Web Login Button (Universal Login)
Button {
Task {
await viewModel.webLogin()
}
} label: {
Text("Login with Browser")
}
.buttonStyle(SecondaryButtonStyle())
.disabled(viewModel.isLoading)

// Logout Button
Button {
Task {
await viewModel.logout()
}
} label: {
Text("Logout")
}
.buttonStyle(PrimaryButtonStyle())
.disabled(viewModel.isLoading || !viewModel.isAuthenticated)

// Authentication Status
if viewModel.isAuthenticated {
Text("✓ Authenticated")
.foregroundColor(.green)
.font(.caption)
}
}
.padding(.horizontal)
.padding(.top, 10)
VStack(spacing: 20) {

Button {
Task {
#if WEB_AUTH_PLATFORM
#if os(macOS)
await viewModel.webLogin(presentationWindow: currentWindow)
#else
await viewModel.webLogin(presentationWindow: window)
#endif
#endif
}
} label: {
VStack(spacing: 4) {
Text("Login")
}
}
.buttonStyle(PrimaryButtonStyle())
.disabled(viewModel.isLoading)

Divider()
.padding(.vertical)

Button {
Task {
#if WEB_AUTH_PLATFORM

#if os(macOS)
await viewModel.logout(presentationWindow: currentWindow)
#else
await viewModel.logout(presentationWindow: window)
#endif
#endif
}
} label: {
Text("Logout")
}
.buttonStyle(PrimaryButtonStyle())
.disabled(viewModel.isLoading || !viewModel.isAuthenticated)

if viewModel.isAuthenticated {
Text("✓ Authenticated")
.foregroundColor(.green)
.font(.caption)
}

if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
}
}
.padding(.horizontal)
.padding(.top, 10)
.task {
// Check for existing credentials on appear
await viewModel.checkAuthentication()
}
#if os(macOS)
.onAppear {
// Capture the window on appear for macOS
currentWindow = getCurrentWindow()
}
#endif
}
}

Expand Down Expand Up @@ -78,3 +114,64 @@ struct SecondaryButtonStyle: ButtonStyle {
}
}

#if os(macOS)
private func getCurrentWindow() -> NSWindow? {
if let keyWindow = NSApplication.shared.keyWindow {
return keyWindow
}

if let mainWindow = NSApplication.shared.mainWindow {
return mainWindow
}

return NSApplication.shared.windows.first
}

#else
private struct WindowKey: EnvironmentKey {
static let defaultValue: UIWindow? = nil
}

extension EnvironmentValues {
var window: UIWindow? {
get { self[WindowKey.self] }
set { self[WindowKey.self] = newValue }
}
}

struct WindowReaderModifier: ViewModifier {
@State private var window: UIWindow?

func body(content: Content) -> some View {
content
.environment(\.window, window)
.background(
WindowAccessor(window: $window)
)
}
}

struct WindowAccessor: UIViewRepresentable {
@Binding var window: UIWindow?

func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .clear
return view
}

func updateUIView(_ uiView: UIView, context: Context) {
DispatchQueue.main.async {
self.window = uiView.window
}
}
}

extension View {
func withWindowReader() -> some View {
self.modifier(WindowReaderModifier())
}
}

#endif

40 changes: 20 additions & 20 deletions App/ContentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,49 @@ final class ContentViewModel: ObservableObject {
@Published var errorMessage: String?
@Published var isAuthenticated: Bool = false
private let credentialsManager = CredentialsManager(authentication: Auth0.authentication())
/// Web Authentication using Universal Login (Recommended)
func webLogin() async {

#if WEB_AUTH_PLATFORM
func webLogin(presentationWindow window: Auth0WindowRepresentable? = nil) async {
isLoading = true
errorMessage = nil

#if !os(tvOS) && !os(watchOS)
do {

let credentials = try await Auth0
.webAuth()
.scope("openid profile email offline_access")
.start()

// Store credentials securely

let stored = credentialsManager.store(credentials: credentials)
if stored {
isAuthenticated = true
print("Access Token: \(credentials.accessToken)")
} else {
errorMessage = "Failed to store credentials"
}
} catch let error as Auth0Error {
errorMessage = "Login failed: \(error.localizedDescription)"
print("Web login failed with error: \(error)")
} catch {
errorMessage = "Unexpected error: \(error.localizedDescription)"
}
#endif

isLoading = false
}

/// Logout and clear stored credentials
func logout() async {

func logout(presentationWindow window: Auth0WindowRepresentable? = nil) async {
isLoading = true
errorMessage = nil
#if !os(tvOS) && !os(watchOS)
do {
try await Auth0
.webAuth()
.clearSession()

// Clear stored credentials
var webAuth = Auth0.webAuth()

if let window = window {
webAuth = webAuth.presentationWindow(window)
}

try await webAuth.clearSession()

let cleared = credentialsManager.clear()
if cleared {
isAuthenticated = false
Expand All @@ -63,11 +63,12 @@ final class ContentViewModel: ObservableObject {
errorMessage = "Unexpected error: \(error.localizedDescription)"
}
#endif

isLoading = false
}

/// Check if user has valid credentials stored
#endif

func checkAuthentication() async {
do {
let credentials = try await credentialsManager.credentials()
Expand All @@ -80,7 +81,6 @@ final class ContentViewModel: ObservableObject {
}
}


extension Array where Element: Hashable {
func uniqued() -> [Element] {
var seen = Set<Element>()
Expand Down
5 changes: 4 additions & 1 deletion App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>UIApplicationSceneManifest</key>
<dict/>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UILaunchScreen</key>
<dict/>
</dict>
Expand Down
16 changes: 11 additions & 5 deletions Auth0/ASProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import AuthenticationServices
typealias ASHandler = ASWebAuthenticationSession.CompletionHandler

extension WebAuthentication {

static func asProvider(redirectURL: URL,
ephemeralSession: Bool = false,
headers: [String: String]? = nil) -> WebAuthProvider {
headers: [String: String]? = nil,
presentationWindow: Auth0WindowRepresentable? = nil) -> WebAuthProvider {
return { url, callback in
let session: ASWebAuthenticationSession

Expand All @@ -32,11 +32,11 @@ extension WebAuthentication {

session.prefersEphemeralWebBrowserSession = ephemeralSession

return ASUserAgent(session: session, callback: callback)
return ASUserAgent(session: session, callback: callback, presentationWindow: presentationWindow)
}
}

static let completionHandler: (_ callback: @escaping WebAuthProviderCallback) -> ASHandler = { callback in
static let completionHandler: (_ callback: @escaping WebAuthProviderCallback) -> ASHandler = { callback in
return {
guard let callbackURL = $0, $1 == nil else {
if let error = $1 as? NSError,
Expand All @@ -60,8 +60,13 @@ class ASUserAgent: NSObject, WebAuthUserAgent {
private(set) static var currentSession: ASWebAuthenticationSession?
let callback: WebAuthProviderCallback

init(session: ASWebAuthenticationSession, callback: @escaping WebAuthProviderCallback) {
weak var presentationWindow: Auth0WindowRepresentable?

init(session: ASWebAuthenticationSession,
callback: @escaping WebAuthProviderCallback,
presentationWindow: Auth0WindowRepresentable? = nil) {
self.callback = callback
self.presentationWindow = presentationWindow
super.init()

session.presentationContextProvider = self
Expand All @@ -83,4 +88,5 @@ class ASUserAgent: NSObject, WebAuthUserAgent {
}

}

#endif
18 changes: 16 additions & 2 deletions Auth0/Auth0.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import Foundation

#if os(macOS)
import AppKit
#else
import UIKit
#endif

/**
`Result` wrapper for Authentication API operations.
*/
Expand All @@ -16,6 +22,14 @@ public typealias ManagementResult<T> = Result<T, ManagementError>
public typealias MyAccountResult<T> = Result<T, MyAccountError>

#if WEB_AUTH_PLATFORM
/**
`UIWindow or NSWindow` wrapper for presentation window
*/
#if canImport(UIKit)
public typealias Auth0WindowRepresentable = UIWindow
#elseif canImport(AppKit)
public typealias Auth0WindowRepresentable = NSWindow
#endif
/**
`Result` wrapper for Web Auth operations.
*/
Expand All @@ -28,9 +42,9 @@ public typealias WebAuthResult<T> = Result<T, WebAuthError>
public typealias CredentialsManagerResult<T> = Result<T, CredentialsManagerError>

/**
Default scope value used across Auth0.swift. Equals to `openid profile email`.
Default scope value used across Auth0.swift. Equals to `openid profile email offline_access`.
*/
public let defaultScope = "openid profile email"
public let defaultScope = "openid profile email offline_access"

/**
[Authentication API](https://auth0.com/docs/api/authentication) client to authenticate a user with Database, Social,
Expand Down
Loading
Loading