Android端末におけるMSALを使用したSSO認証

急に「これやりたいから調べてくれない？」と言われた自分用メモ

## 結論
https://docs.microsoft.com/ja-jp/azure/active-directory/develop/quickstart-v2-android
https://github.com/Azure-Samples/ms-identity-android-java
公式のクイックスタートとサンプルリポジトリのREADMEとソースを読むと良い。~~ドキュメントたくさんあるし自分がこれを書く必要なくない？~~

## アプリの初起動
`PublicClientApplication.createSingleAccountPublicClientApplication()`を叩いて`ISingleAccountPublicClientApplication`インスタンスを取得する。
このインスタンスを使用して、サインインやサインアウト、アカウントの状態確認等ができる。

`auth_config_single_account.json`はMSALで使用する構成ファイル
Azureにアプリ登録した際のIDとか、リダイレクトURIとかの設定が記載されている。

``` java
PublicClientApplication.createSingleAccountPublicClientApplication(
    getContext(),
    R.raw.auth_config_single_account, // res/raw/auth_config_single_account.jsonのリソースID
    new IPublicClientApplication.ISingleAccountApplicationCreatedListener() {
        @Override
        public void onCreated(ISingleAccountPublicClientApplication application) {
            mSingleAccountApp = application;
            // 現在のアカウント状態を読み込む
        }

        @Override
        public void onError(MsalException exception) {
            displayError(exception);
        }
    });
```

アプリの構成が正しく読み込めたら、`mSingleAccountApp.getCurrentAccountAsync`を呼び出して、現在のアカウントの状態を読み込む。
処理が終われば引数に設定したリスナーの`onAccountLoaded`が呼び出されて、引数にアカウント情報が入ってくる。
一度もサインインしていない場合は引数にnullが設定されるので、サインインする。

``` java
mSingleAccountApp.getCurrentAccountAsync(new ISingleAccountPublicClientApplication.CurrentAccountCallback() {
    @Override
    public void onAccountLoaded(@Nullable IAccount activeAccount) {
        if(activeAccount == null){
            // サインインの処理
        } else {
            // 必要であればトークンの更新
        }
    }

    @Override
    public void onAccountChanged(@Nullable IAccount priorAccount, @Nullable IAccount currentAccount) {
        // ログインアカウントを変更する場合に呼び出される
    }

    @Override
    public void onError(@NonNull MsalException exception) {
        displayError(exception);
    }
});
```

## サインイン
サインインは`mSingleAccountApp.signIn(activity, hint, scope, callback); ` を呼び出せばOK
`activity`は現在のアクティビティ
`hint`は事前入力用の文字列(nullable)
`scope`は要求する権限
`callback`はサインインの結果を受け取るリスナー

呼び出すとMSAL側のログインアクティビティが立ち上がってユーザーに認証を求めてくる。
サインインできればコールバックの`onSuccess`メソッドが呼び出される。

要望としてこっそりサインインさせたいんだけどって言われたけど
引数にアクティビティは必須だし、ログイン用のアクティビティが立ち上がるので
こっそりログインさせるのは無理そう

## 2回目以降の起動(トークンの取得・更新)
アプリ構成とログインしているアカウントの情報がコールバックの引数に入ってくるはずなので、そこから確認できる。
トークンを取得したい場合は`mSingleAccountApp.acquireToken`メソッドか
`mSingleAccountApp.acquireTokenSilentAsync`メソッドを呼び出すことで取得できる。
有効なアクセストークンがなくても、自動的にリフレッシュトークンを使用して更新してくれる。

基本は`mSingleAccountApp.acquireTokenSilentAsync`を使用して、
エラーが発生する等のユーザが操作する必要がある場合に`mSingleAccountApp.acquireToken`を使う感じだと思います。

## トークンってどこに保存されているの
https://docs.microsoft.com/ja-jp/azure/active-directory/develop/msal-android-single-sign-on

[Intune](https://play.google.com/store/apps/details?id=com.microsoft.windowsintune.companyportal)もしくは[Microsoft Authenticator](https://play.google.com/store/apps/details?id=com.azure.authenticator)が入っていて、会社等でデバイスを管理していれば、このアプリがトークンを保存している。

もしもIntuneとAuthenticatorが入っていない場合は、Androidで設定しているブラウザもしくはWebViewを使用する。
開くブラウザが異なると何度もサインインを求められるので注意。

## その他ハマったポイント

### リダイレクトURIについて
MSAL構成ファイルのリダイレクトURIはパーセントエンコーディングする必要があり、
`AndroidManifest.xml`に記載する`path`は、頭に`/`をつけてデコードした文字列を設定する必要がある。
例えば、以下のようなMSAL構成ファイルだった場合は

``` auth_config_single_account.json
{
  "client_id" : "0984a7b6-bc13-4141-8b0d-8f767e136bb7",
  "authorization_user_agent" : "DEFAULT",
  "redirect_uri" : "msauth://com.azuresamples.msalandroidapp/1wIqXSqBj7w%2Bh11ZifsnqwgyKrY%3D",
  "account_mode" : "SINGLE",
  "broker_redirect_uri_registered": true,
  "authorities" : [
    {
      "type": "AAD",
      "authority_url": "https://login.microsoftonline.com/common"
    }
  ]
}
```

以下のような`AndroidManifest.xml`になる。
(`%2B`が`+`に、`%3D`が`=`に変わっている。)

``` AndroidManifest.xml
<activity android:name="com.microsoft.identity.client.BrowserTabActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data
            android:host="com.azuresamples.msalandroidapp"
            android:path="/1wIqXSqBj7w+h11ZifsnqwgyKrY="
            android:scheme="msauth" />
    </intent-filter>
</activity>
```

疲れたので終わり