DefaultAzureCredential では環境変数を使うと便利です（Java on Azure）

## はじめに

Azure で Java アプリケーションを開発していたとき、認証方式の設計で悩む機会がありました。

本記事では、Java アプリケーションで実装可能な認証方式である `DefaultAzureCredential` を使った認証について、実開発でつまづきがちなポイントをまとめます。

<b>この記事に記載の内容は、あくまで私個人の見解であり、所属する会社＆組織の見解を必ずしも反映したものではありません。ご了承ください。</b>

## DefaultAzureCredential

`DefaultAzureCredential` とは、開発環境・Azure デプロイ後それぞれで使用される資格情報を組み合わせて使用することができる認証方式です。

>DefaultAzureCredential は、アプリケーションが最終的に Azure クラウドで実行されるほとんどのシナリオに適しています。 DefaultAzureCredential は、デプロイ時の認証に一般的に使用される資格情報と、開発環境での認証に使用される資格情報を組み合わせたものです。

引用: [Azure でホストされる Java アプリケーションを認証する](https://docs.microsoft.com/ja-jp/azure/developer/java/sdk/identity-azure-hosted-auth)

Docs にも記載の通り、`DefaultAzureCredential` を使用すると、既定では、以下の順序で認証を実施します。
|順序|認証方式|
|:---|:---|
|1|環境変数|
|2|マネージド ID|
|3|IntelliJ アカウント|
|4|Visual Studio Code|
|5|Azure CLI |

>環境 - DefaultAzureCredential は、DefaultAzureCredentialで指定されたアカウント情報を読み取り、それを使用して認証を行います。
マネージド ID - マネージド ID が有効になっている Azure ホストにアプリケーションがデプロイされている場合、DefaultAzureCredential はそのアカウントを使用して認証を行います。
IntelliJ - Azure Toolkit for IntelliJ 経由で認証した場合、DefaultAzureCredential はそのアカウントを使用して認証を行います。
Visual Studio Code - Visual Studio Code Azure Account プラグインを使用して認証した場合、DefaultAzureCredential はそのアカウントを使用して認証を行います。
Azure CLI - Azure CLI の az login コマンドを使用してアカウントを認証した場合、DefaultAzureCredential はそのアカウントを使用して認証を行います。

引用: [既定の Azure 資格情報](https://docs.microsoft.com/ja-jp/azure/developer/java/sdk/identity-azure-hosted-auth#default-azure-credential)

### 環境変数を使った認証フロー
`DefaultAzureCredential` を認証方式として採用すると、環境変数を使った認証が最優先で実行されます。
環境変数を使った認証を実行するには、以下の 3 つの環境変数が全てセットされている必要があります。

|変数名|値|
|:---|:---|
|AZURE_CLIENT_ID|Azure AD アプリケーション ID|
|AZURE_CLIENT_SECRET|Azure AD アプリケーションに設定されたクライアントシークレット|
|AZURE_TENANT_ID|Azure AD テナント ID|

個人的な見解ですが、**認証フローに関して、特別な要件がない限り、環境変数を使って認証を実行することをおすすめします。**

理由は、ローカル開発時とクラウドへのデプロイ後とで、同様の方式で認証を実施することが可能だからです。詳細は後述します。

## サンプルプログラム

`DefaultAzureCredential` を使って認証し、Key Vault に格納されているシークレットを取得するサンプルプログラムを書きました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/0ed6e694-5bfd-89a5-2563-41e09ad7d4ae.png)


今回作成したプログラムは、以下に配置しています。

Gitリポジトリ: 『[kohei3110/JavaOnAzureAuthDemo](https://github.com/kohei3110/JavaOnAzureAuthDemo)』

開発はローカルマシンにて行い、Web Apps にアプリケーションをデプロイします。

## サービスプリンシパルの作成

まず、Azure 上のリソースにアクセスするために、開発において使用する資格情報を作成する必要があります。ここで使用する資格情報は、**なるべく人に依存させず、アプリケーションそのものに資格情報を設定したい**ですよね。

なぜなら、仮に開発者の方の資格情報を使って認証していた場合、その開発者が退職してしまったらどうでしょう。その開発者のユーザーが削除された場合、認証そのものがその時点で通らなくなってしまうなどのケースが考えられますよね。

こういうケースに対応するため、Azure では**サービスプリンシパル**という機能が用意されています。
今回も、新たにサービスプリンシパルを作成しました。サービスプリンシパルの作成方法については、以下 Docs をご確認ください。

Docs: 『[リソースにアクセスできる Azure AD アプリケーションとサービス プリンシパルをポータルで作成する](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/howto-create-service-principal-portal)』

## 環境変数の設定

サービスプリンシパルが作成されると、アプリケーションID、クライアントシークレットが払い出されます。`DefaultAzureCredential` で環境変数を用いた認証を行う際には、これらを環境変数としてあらかじめ設定しておく必要があります。

今回は、以下のようにシェルスクリプトを用いて環境変数をセットしました。
```shell: env.sh
export AZURE_CLIENT_ID=<Azure AD アプリケーション ID>
export AZURE_CLIENT_SECRET=<Azure AD アプリケーションに設定されたクライアントシークレット>
export AZURE_TENANT_ID=<Azure AD テナント ID>
```

## サンプルプログラムの作成

今回は、上記の資格情報を用いて、あらかじめ Key Vault に格納されたシークレットを読み込むプログラムを書きました。

サンプルプログラムは下記の通りです。

```java:GetSecret.java
package com.koheisaito.javaonazureauthdemo;

import java.util.logging.Logger;

import com.azure.identity.DefaultAzureCredential;
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import com.azure.security.keyvault.secrets.models.KeyVaultSecret;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GetSecret {
    private static final String SECRET_NAME = "storageaccountconnectionstring";
    private static final String keyVaultName = "key-b1f4eaa7158ef33d";

    Logger logger = Logger.getLogger(GetSecretTest.class.getName());

    private String keyVaultUri = "https://" + keyVaultName + ".vault.azure.net";

    @GetMapping("/secrets")
    public void getSecret() {
        DefaultAzureCredential defaultAzureCredential = buildCredential();
        SecretClient secretClient = buildSecretClient(keyVaultUri, defaultAzureCredential);
        KeyVaultSecret keyVaultSecret = secretClient.getSecret(SECRET_NAME);
        String secret = keyVaultSecret.getValue();
        logger.info("Secret: " + secret);
    }

    public DefaultAzureCredential buildCredential() {
        DefaultAzureCredential defaultAzureCredential = new DefaultAzureCredentialBuilder().build();
        return defaultAzureCredential;
    }

    public SecretClient buildSecretClient(String vaultUrl, DefaultAzureCredential credential) {
        SecretClient client = new SecretClientBuilder()
            .vaultUrl(vaultUrl)
            .credential(credential)
            .buildClient();
        return client;
    }
}
```

上記コードを実行すると、以下のように出力されます。認証が成功し、Key Vault シークレットが取得できていることがわかります。

```
2022-03-18 10:49:59.629  INFO 125200 --- [nio-8080-exec-1] c.azure.identity.EnvironmentCredential   : Azure Identity => EnvironmentCredential invoking ClientSecretCredential
2022-03-18 10:49:59.697 ERROR 125200 --- [nio-8080-exec-1] c.a.i.i.IntelliJCacheAccessor            : IntelliJ Authentication not available. Please log in with Azure Tools for IntelliJ plugin in the IDE.
2022-03-18 10:49:59.883  INFO 125200 --- [nio-8080-exec-1] c.a.s.k.secrets.SecretAsyncClient        : Retrieving secret - storageaccountconnectionstring
2022-03-18 10:50:22.704  INFO 125200 --- [onPool-worker-3] c.azure.identity.ClientSecretCredential  : Azure Identity => getToken() result for scopes [https://vault.azure.net/.default]: SUCCESS
2022-03-18 10:50:22.719  INFO 125200 --- [onPool-worker-3] c.azure.identity.DefaultAzureCredential  : Azure Identity => Attempted credential EnvironmentCredential returns a token      
2022-03-18 10:50:33.378  INFO 125200 --- [ctor-http-nio-5] c.a.s.k.secrets.SecretAsyncClient        : Retrieved secret - storageaccountconnectionstring
2022-03-18 10:50:33.382  INFO 125200 --- [nio-8080-exec-1] c.k.javaonazureauthdemo.GetSecret        : Secret: xxxxxxx
```

### 補足: 環境変数が正しくセットされていない場合

環境変数が正しく設定されていない場合は、以下のように、マネージド ID を使った認証、IntelliJ アカウントを使った認証、、と、あらかじめ決められた順番（先述）に沿って認証が実行されます。これは、[Docs](https://docs.microsoft.com/ja-jp/azure/developer/java/sdk/identity-azure-hosted-auth#default-azure-credential) にも記載されているように、想定通りの挙動です。

```
2022-03-15 16:00:49.288 ERROR 21016 --- [nio-8080-exec-1] c.azure.identity.EnvironmentCredential   : Azure Identity => ERROR in EnvironmentCredential: Missing required environment variable AZURE_CLIENT_ID
2022-03-15 16:00:49.342 ERROR 21016 --- [nio-8080-exec-1] c.a.i.i.IntelliJCacheAccessor            : IntelliJ Authentication not available. Please log in with Azure Tools for IntelliJ plugin in the IDE.
2022-03-15 16:00:49.484  INFO 21016 --- [nio-8080-exec-1] c.a.s.k.secrets.SecretAsyncClient        : Retrieving secret - storageaccountconnectionstring
2022-03-15 16:01:01.316 ERROR 21016 --- [ctor-http-nio-2] c.azure.identity.EnvironmentCredential   : EnvironmentCredential authentication unavailable. Environment variables are not fully configured.
2022-03-15 16:01:01.318  INFO 21016 --- [ctor-http-nio-2] c.azure.identity.DefaultAzureCredential  : Azure Identity => Attempted credential EnvironmentCredential is unavailable.       
2022-03-15 16:01:01.341 ERROR 21016 --- [ctor-http-nio-2] c.a.i.implementation.IdentityClient      : ManagedIdentityCredential authentication unavailable. Connection to IMDS endpoint cannot be established, Network is unreachable: connect.
2022-03-15 16:01:01.344 ERROR 21016 --- [ctor-http-nio-2] c.a.identity.ManagedIdentityCredential   : Azure Identity => ERROR in getToken() call for scopes [https://vault.azure.net/.default]: ManagedIdentityCredential authentication unavailable. Connection to IMDS endpoint cannot be established, Network is unreachable: connect.
2022-03-15 16:01:01.345  INFO 21016 --- [ctor-http-nio-2] c.azure.identity.DefaultAzureCredential  : Azure Identity => Attempted credential ManagedIdentityCredential is unavailable.   
2022-03-15 16:01:11.660 ERROR 21016 --- [onPool-worker-3] c.a.identity.SharedTokenCacheCredential  : Azure Identity => ERROR in getToken() call for scopes [https://vault.azure.net/.default]: SharedTokenCacheCredential authentication unavailable. No accounts were found in the cache.
2022-03-15 16:01:11.663  INFO 21016 --- [onPool-worker-3] c.azure.identity.DefaultAzureCredential  : Azure Identity => Attempted credential SharedTokenCacheCredential is unavailable.  
2022-03-15 16:01:11.665 ERROR 21016 --- [onPool-worker-3] c.a.i.i.IntelliJCacheAccessor            : IntelliJ Authentication not available. Please log in with Azure Tools for IntelliJ plugin in the IDE.
2022-03-15 16:01:11.666 ERROR 21016 --- [onPool-worker-3] com.azure.identity.IntelliJCredential    : Azure Identity => ERROR in getToken() call for scopes [https://vault.azure.net/.default]: IntelliJ Authentication not available. Please log in with Azure Tools for IntelliJ plugin in the IDE.
2022-03-15 16:01:11.667  INFO 21016 --- [onPool-worker-3] c.azure.identity.DefaultAzureCredential  : Azure Identity => Attempted credential IntelliJCredential is unavailable.
2022-03-15 16:01:12.060 ERROR 21016 --- [onPool-worker-5] c.m.aad.msal4j.PublicClientApplication   : [Correlation ID: 793da894-e800-4e09-bf6b-c3dadae35d5a] Execution of class com.microsoft.aad.msal4j.AcquireTokenByAuthorizationGrantSupplier failed.
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/5764a476-0c67-91b7-369a-9b15a1ee65a9.png)


引用: 『[Default Azure credential](https://docs.microsoft.com/en-us/azure/developer/java/sdk/identity-azure-hosted-auth#default-azure-credential)』

## DefaultAzureCredential - 環境変数以外の認証方式

環境変数が設定されていない場合でも、マネージド ID を使った認証、IntelliJ アカウントによる認証、、と、認証フローは遷移します。ただ、開発環境では通っていた認証が Azure 環境へのデプロイ後は通らないなど、**認証方式に環境差異が発生してしまう問題が生じます**。

それぞれの認証方式で想定される、環境差異に起因する課題を、以下のようにまとめました。`DefaultAzureCredential` では、環境変数を使った認証が最初に実行されるため、順番は 2 から記述しています。

|順番|認証方式|課題|
|:---|:---|:---|
|2|マネージド ID|ローカル開発時、ローカル PC から Azure Instance Metadata Service (169.254.169.254) への名前解決ができない。|
|3|IntelliJ|開発マシンに IDE ・拡張機能のインストールが必要。社内規定などで IntelliJ を使えない場合は認証不可。|
|4|VSCode|開発マシンに IDE ・拡張機能のインストールが必要。社内規定などで VS Code を使えない場合は認証不可。|
|5|Azure CLI|アプリケーションのデプロイ先に Azure CLI のインストールが必要。|

参考: 『[Azure Instance Metadata Service (Windows)](https://docs.microsoft.com/ja-jp/azure/virtual-machines/windows/instance-metadata-service?tabs=windows)』

上記 2～5 の認証方式は、認証に必要な情報をトークンとして取得するため、環境変数をセットするためのファイルを Git リポジトリで共有する必要がありません。
そのため、誤って認証情報を記述したファイルを Git リポジトリに Push してしまう等、ファイル管理における事故を防ぐ意味では、よりセキュアな認証方式といえます。

ただ、トークンを使った認証方式は、ID トークンの期限切れが発生した場合、**現時点の仕様では**アプリケーションやユーザー側でトークンの取得をハンドリングする必要があるため、開発の生産性が落ちてしまうと私は考えています。

一例として、VS Code 認証方式を使用した場合、90 日経過した ID トークンをリフレッシュする機能をサポートしていません（2022年3月18日時点）。より正確に書くと、VS Code バージョン 0.9.11 以前はサポートされていましたが、それ以降のバージョンでは、キャッシュされたリフレッシュトークンを保存する方式に変更が加えられました。

近々この機能はサポートされる予定ですが、それでも Azure へデプロイ後は別の認証方式を採用する必要があります。

```shell
2022-03-02 10:07:45.090 ERROR 64800 --- [onPool-worker-5] c.m.aad.msal4j.PublicClientApplication   : [Correlation ID: 8bf2bdbb-0c76-45a4-9a66-eb1d371eb864] Execution of class com.microsoft.aad.msal4j.AcquireTokenByAuthorizationGrantSupplier failed.

com.microsoft.aad.msal4j.MsalInteractionRequiredException: AADSTS700082: The refresh token has expired due to inactivity.?The token was issued on 2021-11-28T15:18:11.7012365Z and was inactive for 90.00:00:00.
```

よって、環境差異に起因するを認証の手間を減らすためには、環境変数を使った認証を採用することを**個人的には**おすすめします。

## Azure 上のアプリケーション実行サービスでの環境変数設定方法

補足として、Java に関わらず、App Service 等 Azure 上のアプリケーション実行サービスでももちろん環境変数をセット可能です。
詳細は、以下の Docs を参考にしてください。

Docs: 『[App Service アプリを構成する](https://docs.microsoft.com/ja-jp/azure/app-service/configure-common?tabs=portal)』


## 参考

- [Microsoft ID プラットフォームと OAuth 2.0 認証コード フロー](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v2-oauth2-auth-code-flow)
- [MSAL4J JavaDoc](https://javadoc.io/doc/com.microsoft.azure/msal4j/latest/index.html)
- [microsoft-authentication-library-for-java
: Acquiring Token Interactively](https://github.com/AzureAD/microsoft-authentication-library-for-java/wiki/Acquiring-Token-Interactively)