microsoft-authentication-library-for-objc
microsoft-authentication-library-for-objc copied to clipboard
SwiftUI support for fetching tokens interactively without using ViewController
Hi there,
I want to integrate MSAL into my new SwiftUI app, but based on the sample code, it seems that I need to work with UIKit. I'm not familiar with UIKit at all. How do I get a token interactively using just SwiftUI paradigms?
Here's the sample code that I'm confused about, since SwiftUI doesn't use controllers:

hi @dpaulino, we don't have SwiftUI sample at the moment. I recommend you to take a look at this tutorial: "Interfacing with UIKit".
Is there any plan to support SwiftUI? Apple seems heavily invested in this moving forward, and new devs like me only or mostly know SwiftUI. Without support, my colleagues and I may not be able to add Microsoft logins to our products
I posted this last night for authenticating against a B2C tenant. The code snippets show everything that you need: https://medium.com/neudesic-innovation/using-azure-ad-b2c-to-authenticate-ios-app-users-ef3f82435f7d
This issue has been automatically marked as stale because it has not had recent activity. Please provide additional information if requested. Thank you for your contributions.
Full SwiftUI support would be awesome. Apple is really pushing SwiftUI.
@kaisong1990 and @antrix1989 and word on this feature? Do we have a target date for this?
We don't have the ETA at the moment, but we are always encouraging contributions.
Hi y'all,
I'm trying to integrate MSAL with SwiftUI app as well.
I'm acquiring the token interactively from one of my SwiftUI Views upon appearing, see code below:
.onAppear {
viewModel.startEnrollment(with: self)
}
In my view model I've this code:
class MainViewModel: ObservableObject {
@Published private (set) var accessToken: String = ""
@Published private (set) var error: Error?
var bag: Set<AnyCancellable> = Set<AnyCancellable>()
private var enrollmentManager: EnrollmentManager
init(enrollmentManager: EnrollmentManager = EnrollmentManager.shared) {
self.enrollmentManager = enrollmentManager
}
func startEnrollment<T: View>(with view: T) {
do {
let application = try enrollmentManager.create()
enrollmentManager.acquireToken(for: application, target: view).sink(receiveCompletion: { [weak self] completion in
switch completion {
case .finished:
return
case .failure(let error):
self?.error = error
}
}) { [weak self] (accessToken: String, accountIdentifier: String?) in
self?.accessToken = accessToken
}.store(in: &bag)
} catch {
self.error = error
}
}
}
the self
here is the SwiftUI view, and on my enrollment service/manager I've something like this:
func acquireToken<T: View>(for application: MSALPublicClientApplication, target: T) -> AnyPublisher<(accessToken: String, accountIdentifier: String?), Error> {
let resultPublisher: PassthroughSubject<(accessToken: String, accountIdentifier: String?), Error> = PassthroughSubject<(accessToken: String, accountIdentifier: String?), Error>()
let viewController = UIHostingController(rootView: target)
let webViewParameters = MSALWebviewParameters(authPresentationViewController: viewController)
let interactiveParameters = MSALInteractiveTokenParameters(scopes: [], webviewParameters: webViewParameters)
application.acquireToken(with: interactiveParameters) { (result, error) in
guard let result = result, error == nil else {
resultPublisher.send(completion: .failure(error!))
return
}
let accessToken = result.accessToken
let accountIdentifier = result.account.identifier
resultPublisher.send((accessToken, accountIdentifier))
}
return resultPublisher.eraseToAnyPublisher()
}
Unfortunately, I'm getting the following error:
Error Domain=MSALErrorDomain Code=-50000 "(null)" UserInfo={MSALErrorDescriptionKey=parentViewController has no window! Provide a valid controller with view and window., MSALInternalErrorCodeKey=-42000}
Any idea on why this is happening and how to fix it? Because this seems to be a bug for me as I'm providing the parentViewController as a UIViewController here.
Hi y'all,
I'm trying to integrate MSAL with SwiftUI app as well.
I'm acquiring the token interactively from one of my SwiftUI Views upon appearing, see code below:
.onAppear { viewModel.startEnrollment(with: self) }
In my view model I've this code:
class MainViewModel: ObservableObject { @Published private (set) var accessToken: String = "" @Published private (set) var error: Error? var bag: Set<AnyCancellable> = Set<AnyCancellable>() private var enrollmentManager: EnrollmentManager init(enrollmentManager: EnrollmentManager = EnrollmentManager.shared) { self.enrollmentManager = enrollmentManager } func startEnrollment<T: View>(with view: T) { do { let application = try enrollmentManager.create() enrollmentManager.acquireToken(for: application, target: view).sink(receiveCompletion: { [weak self] completion in switch completion { case .finished: return case .failure(let error): self?.error = error } }) { [weak self] (accessToken: String, accountIdentifier: String?) in self?.accessToken = accessToken }.store(in: &bag) } catch { self.error = error } } }
the
self
here is the SwiftUI view, and on my enrollment service/manager I've something like this:func acquireToken<T: View>(for application: MSALPublicClientApplication, target: T) -> AnyPublisher<(accessToken: String, accountIdentifier: String?), Error> { let resultPublisher: PassthroughSubject<(accessToken: String, accountIdentifier: String?), Error> = PassthroughSubject<(accessToken: String, accountIdentifier: String?), Error>() let viewController = UIHostingController(rootView: target) let webViewParameters = MSALWebviewParameters(authPresentationViewController: viewController) let interactiveParameters = MSALInteractiveTokenParameters(scopes: [], webviewParameters: webViewParameters) application.acquireToken(with: interactiveParameters) { (result, error) in guard let result = result, error == nil else { resultPublisher.send(completion: .failure(error!)) return } let accessToken = result.accessToken let accountIdentifier = result.account.identifier resultPublisher.send((accessToken, accountIdentifier)) } return resultPublisher.eraseToAnyPublisher() }
Unfortunately, I'm getting the following error:
Error Domain=MSALErrorDomain Code=-50000 "(null)" UserInfo={MSALErrorDescriptionKey=parentViewController has no window! Provide a valid controller with view and window., MSALInternalErrorCodeKey=-42000}
Any idea on why this is happening and how to fix it? Because this seems to be a bug for me as I'm providing the parentViewController as a UIViewController here.
This is Working for fresh installation of the App, but not running afterward without signing in on 1st launch of the app. So basically the error is thrown from 2nd launch of the app and up.
a simple workaround is to create a UIControllerView
wrapper View in the SwiftUI.
struct MSALAuthPresentationView: UIViewControllerRepresentable {
@Binding var showingMSALAuthPresentaion: Bool
func makeUIViewController(context: Context) -> UIMSALAuthPresentationViewController {
return UIMSALAuthPresentationViewController(showingMSALAuthPresentaion: $showingMSALAuthPresentaion)
}
func updateUIViewController(_ uiViewController: UIMSALAuthPresentationViewController, context: Context) {
}
}
class UIMSALAuthPresentationViewController: UIViewController {
var showingMSALAuthPresentaion: Binding<Bool>
init(showingMSALAuthPresentaion: Binding<Bool>, nibName nibNameOrNil: String? = nil,
bundle nibBundleOrNil: Bundle? = nil) {
self.showingMSALAuthPresentaion = showingMSALAuthPresentaion
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
doAuth()
}
private func doAuth() {
let config = MSALPublicClientApplicationConfig(clientId: "")
let scopes = ["user.read"]
let application = try? MSALPublicClientApplication(configuration: config)
guard let application = application else {
print("doAuth: load application failed")
showingMSALAuthPresentaion.wrappedValue = false
return
}
#if os(iOS)
let viewController = self // Pass a reference to the view controller that should be used when getting a token interactively
let webviewParameters = MSALWebviewParameters(authPresentationViewController: viewController)
#else
let webviewParameters = MSALWebviewParameters()
#endif
let interactiveParameters = MSALInteractiveTokenParameters(scopes: scopes, webviewParameters: webviewParameters)
application.acquireToken(with: interactiveParameters, completionBlock: { (result, error) in
defer {
self.showingMSALAuthPresentaion.wrappedValue = false
}
guard let authResult = result, error == nil else {
print("doAuth acquireToken error: \(error!)")
return
}
// Get access token from result
let accessToken = authResult.accessToken
// You'll want to get the account identifier to retrieve and reuse the account for later acquireToken calls
let accountIdentifier = authResult.account.identifier
})
}
}
then put the MSALAuthPresentationView
under the ZStack
in the parent View
The article by Michael Collins does work, but it really seems like getting it set up and configured is challenging. One of the advantages of other solutions is the simplicity. Is this on the roadmap? It seems like it would be a high priority given iOS mobile development is a pretty significant audience and SwiftUI is the future. This should be a few lines of code and a few configuration settings in an xcode project.
any news? would be awesome if the sdk provide the implementation
Not sure if this would help anyone but I made a repo where I use MSAL with @Environment and @Observable class here: https://github.com/carr0495/MSALSwiftUI/tree/main
The UIViewController is placed at the root of the application and all logic is extracted to an Observable object in the Environment. This allows you to login and logout from anywhere within your SwiftUI Application
Simple Workaround
Create a shared static Utilities class for returning the top view controller
import UIKit
import SwiftUI
final class Utilities {
static let shared = Utilities()
private init() {}
@MainActor
func topViewController(controller: UIViewController? = nil) -> UIViewController? {
let controller = controller ?? UIApplication.shared.keyWindow?.rootViewController
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
Can be implemented like so:
let application = try MSALPublicClientApplication(configuration: config)
guard let viewController = Utilities.shared.topViewController() else { return }
let webviewParameters = MSALWebviewParameters(authPresentationViewController: viewController)
let interactiveParameters = MSALInteractiveTokenParameters(scopes: scopes, webviewParameters: webviewParameters)
application.acquireToken(with: interactiveParameters){ result, error in...
Simple Workaround
Create a shared static Utilities class for returning the top view controller
import UIKit import SwiftUI final class Utilities { static let shared = Utilities() private init() {} @MainActor func topViewController(controller: UIViewController? = nil) -> UIViewController? { let controller = controller ?? UIApplication.shared.keyWindow?.rootViewController if let navigationController = controller as? UINavigationController { return topViewController(controller: navigationController.visibleViewController) } if let tabController = controller as? UITabBarController { if let selected = tabController.selectedViewController { return topViewController(controller: selected) } } if let presented = controller?.presentedViewController { return topViewController(controller: presented) } return controller } }
Can be implemented like so:
let application = try MSALPublicClientApplication(configuration: config) guard let viewController = Utilities.shared.topViewController() else { return } let webviewParameters = MSALWebviewParameters(authPresentationViewController: viewController) let interactiveParameters = MSALInteractiveTokenParameters(scopes: scopes, webviewParameters: webviewParameters) application.acquireToken(with: interactiveParameters){ result, error in...
This is a nice workaround, though in iOS16+ it provokes the following warning:
'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes
I believe I feel the need to propose another workaround, that works flawlessly as of iOS 16.2+:
In your AppDelegate, setup your UISceneConfiguration with a delegateClass of type sceneDelegate implementing UIWindowSceneDelegate.
In your sceneDelegate cast your UIScene as a UIWindowScene -> keyWindow and from that get the rootViewController. Store the reference somewhere where you can access it or use it to create the MSALWebviewParameters.
Please note that this doesent woth in Preview, but works fine running normally.
import Foundation
import UIKit
import MSAL
class AppDelegate: NSObject, UIApplicationDelegate {
// Configure MainScene to provide a root UIViewController for MSAL authentication
var test: UIViewController?
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
let configuration = UISceneConfiguration(
name: "MainScene",
sessionRole: .windowApplication
)
configuration.delegateClass = MainSceneDelegate.self
return configuration
}
// Used for MSAL callbacks
func application(
_ application: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String)
}
}
// MSAL needs to be provided a UIViewController to presents its UI along with a rootVC
class MainSceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
var authController = Container.authController
var logController = Container.logController
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene, let window = windowScene.keyWindow, let rootViewController = window.rootViewController else {
logController.log(logString: "MSAL root view controller not found")
return
}
authController.setRootViewController(viewController: rootViewController)
}
}
I believe I feel the need to propose another workaround, that works flawlessly as of iOS 16.2+:
In your AppDelegate, setup your UISceneConfiguration with a delegateClass of type sceneDelegate implementing UIWindowSceneDelegate.
In your sceneDelegate cast your UIScene as a UIWindowScene -> keyWindow and from that get the rootViewController. Store the reference somewhere where you can access it or use it to create the MSALWebviewParameters.
Please note that this doesent woth in Preview, but works fine running normally.
import Foundation import UIKit import MSAL class AppDelegate: NSObject, UIApplicationDelegate { // Configure MainScene to provide a root UIViewController for MSAL authentication var test: UIViewController? func application( _ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions ) -> UISceneConfiguration { let configuration = UISceneConfiguration( name: "MainScene", sessionRole: .windowApplication ) configuration.delegateClass = MainSceneDelegate.self return configuration } // Used for MSAL callbacks func application( _ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool { return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String) } } // MSAL needs to be provided a UIViewController to presents its UI along with a rootVC class MainSceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject { var authController = Container.authController var logController = Container.logController func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { guard let windowScene = scene as? UIWindowScene, let window = windowScene.keyWindow, let rootViewController = window.rootViewController else { logController.log(logString: "MSAL root view controller not found") return } authController.setRootViewController(viewController: rootViewController) } }
Elegant solution! Implemented it today and it works great!