SwiftUI App で Azure AD B2C のログインを実装してみた
SwiftUI App で msal ライブラリを使って Azure AD B2C でログインをするまでを実装してみます。取得したアクセストークンを使用して API から値を取得するのは(思いの外長くなってしまったので)次回にします。
UIKit App ベースでのサンプルコードがあるので、これを SwiftUI App ベースで書き換えてみます。
SwiftUI App って簡単に UI 作れるので( M1 買ったのもあるけど、何より苦手であきらめた InterfaceBuilder が不要で)楽しそうって最近始めたのですが、ライブラリの使い方や UIKit App との違いをよく理解してないので結構ハマったという話です。
動作環境
- Mac Book Air M1
- Xcode 12.5
Azure AD B2C でのアプリケーションの登録
まず Azure AD B2C のテナントを作成します。下記を参考にしてください。
次にアプリケーションの登録をおこないます。下記を参考にしてください。msal<your-client-id>://auth
を選択して、「構成」をクリックして追加をしてください。
「パブリッククライアントフローを許可する」は「はい」を選択してください。
Xcode で SwiftUI App の作成
Xcode でアプリケーションを作成します。「SwiftUI App」を選択してください。
ボタンを1つ配置してタップしたときに login メソッドが実行されるようにします。Azure AD B2C に必要な情報はこの後に追加します。
struct ContentView: View {
var body: some View {
VStack{
Button( action: { login() } ) {
Text("Authorize")
}
}.padding()
}
func login(){
// ここにログインを実装します
}
}
msal ライブラリのビルドおよびインポート
msal のライブラリは Carthage
を使ってビルドします。
-
brew install carthage
で Carthage をインストールします。brew が入っていない場合はここからインストールします。 - プロジェクトに
Cartfile
というファイル名を作成しその中に下記のテキストをペーストして保存します。
github "AzureAD/microsoft-authentication-library-for-objc" "master"
-
carthage bootstrap --platform iOS
を実行してライブラリをビルドします。完了するとCarthage/Checkouts
フォルダに MSAL ライブラリが作成されます。 - Xcode でプロジェクトを開いて、プロジェクトを選択して、 "General" タブを選択し、 "Frameworks, Libraries, and Embedded Content" セクションに
Carthage/Build/iOS
フォルダにあるMSAL.framework
フォルダをドラッグ&ドロップします。
ここでひとまずビルドしておきましょう。
ビルドエラーが出る場合は下記の「ビルドエラーが出る場合」を参照してください。
info.plist の編集①: msal の URL スキームを登録する
info.plist に下記を追加します。プロジェクトから info.plist を選択して、Open As
→ Source Code
で編集できます。your-client-id-here
には Azure AD B2C で作成したアプリケーションのクライアント ID をコピペします。
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleURLSchemes</key>
<array>
<string>msal{your-client-id-here}</string>
</array>
</dict>
</array>
swift での実装① AppDelegate を追加する
SwiftUI App には AppDelegate
クラスがないので、@UIApplicationDelegateAdaptor
を使用することで、既存の AppDelegate
を利用することができるようになります。
@main
struct B2CLoginApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
}
swift での実装②: ログインした時の callback を追加する
AppDelegate クラスに Azure AD B2C でログイン(sign-in)した場合の callback を追加します。
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
guard let sourceApp = sourceApplication else { return false }
MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: sourceApp)
return true
}
swift での実装③: Azure AD B2C に必要な定数を定義する
Azure AD B2C に必要な定数を定義します。
struct ContentView: View {
let kTenantName = "<tenant>.onmicrosoft.com" // テナント名
let kClientID = "<your-client-id>" // クライント ID
let kSignupOrSigninPolicy = "<your-signin-policy>" // サインインポリシー
let kScopes: [String] = ["<Your backend API>/demo.read"] // スコープ
...
}
swift での実装④: アプリ起動時に msal アプリケーションを初期化する
アプリの起動時に(今回は View の初期化時に) msal のアプリケーションのインスタンスを取得します。
@State var application: MSALPublicClientApplication!
var body: some View {
VStack{
Button( action: { login() } ) {
Text("Authorize")
}
}
.padding()
.onAppear(perform: initApp)
}
func initApp(){
do {
let siginPolicyAuthority = try self.getAuthority(forPolicy: self.kSignupOrSigninPolicy)
let pcaConfig = MSALPublicClientApplicationConfig(clientId: kClientID, redirectUri: nil, authority: siginPolicyAuthority)
pcaConfig.knownAuthorities = [siginPolicyAuthority]
self.application = try MSALPublicClientApplication(configuration: pcaConfig)
} catch {
print("Unable to create application \(error)")
}
}
swift での実装⑤: ボタンをタップしたらログインする
ログイン処理を実装します。ログインを実行するには親のウィンドウである UIViewController
が必要なので、View クラスを UIHostingController
を使って変換します。
func login(){
do {
let authority = try self.getAuthority(forPolicy: self.kSignupOrSigninPolicy)
let viewController = UIHostingController(rootView: self)
let webViewParameters = MSALWebviewParameters(authPresentationViewController: viewController)
let interactiveParameters = MSALInteractiveTokenParameters(scopes: kScopes, webviewParameters: webViewParameters)
interactiveParameters.promptType = .selectAccount
interactiveParameters.authority = authority
application.acquireToken(with: interactiveParameters) { (result, error) in
guard let result = result else {
return
}
let accessToken = result.accessToken
print("accessToken=\(accessToken)")
}
catch {
print(error) /* MSALPublicClientApplication creation failed, check error information */
}
}
これで動作すると思ったら動かない!
渡した View のウィンドウが nil とのこと。
Error Domain=MSALErrorDomain Code=-50000 "(null)" UserInfo={MSALErrorDescriptionKey=parentViewController has no window! Provide a valid controller with view and window., MSALInternalErrorCodeKey=-42000}
このエラー箇所のソースコードをよく見てみると、View はあるけど UIWindow が nil であることが原因とのこと。UIKit App で UIViewControler.view.window
プロパティで取得できていた UIWindow(現在のアクティブなウィンドウという言い方で正しい?)が SwiftUI App では取得できないとのこと。
iOS13以降ではiOSアプリでもマルチウィンドウで使えるようになり
アプリの起動経路が変わりました。
※ マルチウィンドウ自体はオプトインの仕組みで
ON にするには info.plist のUIApplicationSceneManifest
の設定が必要になります。
ということでやっていきます。
info.plist の編集②: UIApplicationSceneManifest を追加する
info.plist に下記を追加します。
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
swift での実装⑥: SceneDelegate を追加する
SceneDelegate
クラスを追加して下記を追加します。
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Use a UIHostingController as window root view controller
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
}
}
}
swift での実装⑦: @Environment を使って UIWindow インスタンスをDIする
この記事の「@Environmentを利用する」をそのまま実装します。
ということで、View クラスから UIWindow が参照できるようになったので、先ほどのログイン処理を書き換えます。
(修正前)
let viewController = UIHostingController(rootView: self)
(修正後)
let viewController = self.window?.rootViewController
これで動作しました。
(参考)msal のライブラリを追加しても Xcode でビルドエラーが出る場合
Xcode で下記のビルドエラーが出る場合は、ライブラリをアップデートする必要があります。
- Xcode のプロジェクトで
Validate Workspace
にyes
にする -
carthage update --use-xcframeworks
でライブラリをアップデートする
詳しくは下記を参考にしてください。
最後に
次回は、今回取得したアクセストークンを使って認証された API で値を取得するところを書きます(予定)。
Discussion