🍻

SwiftUI App で Azure AD B2C のログインを実装してみた

2021/07/03に公開

SwiftUI App で msal ライブラリを使って Azure AD B2C でログインをするまでを実装してみます。取得したアクセストークンを使用して API から値を取得するのは(思いの外長くなってしまったので)次回にします。

UIKit App ベースでのサンプルコードがあるので、これを SwiftUI App ベースで書き換えてみます。
https://docs.microsoft.com/ja-jp/azure/active-directory-b2c/code-samples

SwiftUI App って簡単に UI 作れるので( M1 買ったのもあるけど、何より苦手であきらめた InterfaceBuilder が不要で)楽しそうって最近始めたのですが、ライブラリの使い方や UIKit App との違いをよく理解してないので結構ハマったという話です。

動作環境

  • Mac Book Air M1
  • Xcode 12.5

Azure AD B2C でのアプリケーションの登録

まず Azure AD B2C のテナントを作成します。下記を参考にしてください。
https://docs.microsoft.com/ja-jp/azure/active-directory-b2c/tutorial-create-tenant

次にアプリケーションの登録をおこないます。下記を参考にしてください。
https://docs.microsoft.com/ja-jp/azure/active-directory-b2c/tutorial-register-applications?tabs=app-reg-ga
これに加えて、msal で使用する場合は、「プラットフォームを追加」→「モバイルアプリケーションとデスクトップアプリケーション」→ 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 AsSource 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
}

https://docs.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-ios#for-ios-only-handle-the-sign-in-callback

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 では取得できないとのこと。

https://qiita.com/shiz/items/3b829b1521f9723aa875

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を利用する」をそのまま実装します。
https://qiita.com/shiz/items/3b829b1521f9723aa875#environmentを利用する

ということで、View クラスから UIWindow が参照できるようになったので、先ほどのログイン処理を書き換えます。
(修正前)

let viewController = UIHostingController(rootView: self)

(修正後)

let viewController = self.window?.rootViewController

これで動作しました。

(参考)msal のライブラリを追加しても Xcode でビルドエラーが出る場合

Xcode で下記のビルドエラーが出る場合は、ライブラリをアップデートする必要があります。

  • Xcode のプロジェクトで Validate Workspaceyes にする
  • carthage update --use-xcframeworks でライブラリをアップデートする

詳しくは下記を参考にしてください。
https://github.com/Carthage/Carthage#building-platform-independent-xcframeworks-xcode-12-and-above

最後に

次回は、今回取得したアクセストークンを使って認証された API で値を取得するところを書きます(予定)。

Discussion