SpringBootのWebアプリケーションにMicrosoft IDプラットフォームでサインインさせる


**2021/07/26 追記　[AzureADスターターの新しいバージョン(3.6.x)を使った記事](https://qiita.com/talesleaves/items/fed1f39dfca9ae8efa3e)を書きました**

##概要
Microsoftの公式ドキュメントが混乱していたり、ライブラリの更新が追いついていなかったりで最新のSpring BootからAzure ADテナントアカウント[^1]でサインインさせることが難しくなっています。
現状でサインインを実装するためのあれこれをまとめました。

- （記事の作成時現在）Azure AD用のSpring Boot Starterはそのままでは起動できない
- Microsoftのドキュメントも大体更新が追いついておらず、古い
- 認証のみならSpring Securityの~~OAuth2~~OpenID Connectだけで可能
- ほとんど日本語資料がない
- 認証後にMicrosoftGraphを通じて各機能にアクセスするなら[Microsoft Graph SDK for Java](https://github.com/microsoftgraph/msgraph-sdk-java#microsoft-graph-sdk-for-java)を使用する

##前提条件
- Java11
- Spring Boot2.3.5（最新は2.4）
- 基本の依存性はSpring Initialzrで書き出す
- Gredle6.7
- 筆者のOSはWindows10
- ここではAzureADのテナント内のアカウント（職場または学校のアカウント）を対象としますが、個人用Microsoftアカウントを対象に含める場合も基本的な流れは同じはずです
- ログインエラー時の画面処理やパスごとのセキュリティ設定の方法（WebSecurityConfigurerAdapterの拡張）はここでは範囲外

##[Azure AD Spring Boot Starter client library for Java(2.3.5)](https://github.com/Azure/azure-sdk-for-java/tree/407b4e0bffbb75b461c0512e669a6d43376fa7ab/sdk/spring/azure-spring-boot-starter-active-directory)で実装する
まず最初に当たるのはこのライブラリ[^3]だと思います。Microsoftが主導するプロジェクトです。
Spring Boot/Spring SecurityのOauth2クライアントの機能を利用しているため、設定項目や実装範囲が他の選択肢より少なくなります。

Azure ADとSpringの連携についてはMicrosoftのサイトの様々な場所にドキュメントが散在していますが、現時点では[Githubのドキュメント](https://github.com/Azure/azure-sdk-for-java/tree/407b4e0bffbb75b461c0512e669a6d43376fa7ab/sdk/spring/azure-spring-boot-starter-active-directory)を参照してください。最新の記述のはずです。
この記事で主な対象にしているシナリオは[バックエンドでの認証フロー](https://github.com/Azure/azure-sdk-for-java/tree/407b4e0bffbb75b461c0512e669a6d43376fa7ab/sdk/spring/azure-spring-boot-starter-active-directory#authorization-code-mode-usage)となります。

このドキュメントを基に作業を進めるわけですが、**現状ではReadMe通りの設定では動きません→報告をもとに修正されましたがmavenへの反映をお待ちください**。

理由についても突き止めて[GitHubで報告してあります](https://github.com/Azure/azure-sdk-for-java/issues/15180#issuecomment-726564519)が、Spring Boot2.3.5の依存性によりAAD-Starterが依存するnimbus-jose-jwtのバージョンが上がってしまい、起動できなくなっています。
これはnimbus-jose-jwtの8.10以降DefaultJWKSetCacheのコンストラクタにリフレッシュサイクルを指定する引数が追加されたために起こっています。
まずはこれに対応します。

###起動可能にするためにAADAuthenticationFilterAutoConfigurationの設定を上書きする
幸いにこのバグの原因がAutoConfigのBean定義ににあるため、**実行時にこれを解決する**ことが可能です。（記事を書いている間にこの問題のソースコードは修正されました）
ここではnimbus-jose-jwtバージョンを下げて対応するのは良しとせず、それを修正して使用します。
AAD-Starterの利用には基本的にWebSecurityConfigurerAdapterを継承してConfigを書くことになりますので、そこで古いBean定義を上書きします。

```java:AADOAuth2LoginConfig.java
/*ReadMeのサンプルにコメント部分を追加*/

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class AADOAuth2LoginConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;

    //設定プロパティをインジェクション
    @Autowired
    private AADAuthenticationProperties aadAuthProps;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .oauth2Login()
            .userInfoEndpoint()
            .oidcUserService(oidcUserService);
    }

    //問題の設定を上書きするBean定義
    @Bean
    public JWKSetCache getJWKSetCache() {
        long lifespan = aadAuthenticationProperties.getJwkSetCacheLifespan();
        return new DefaultJWKSetCache(lifespan, lifespan, TimeUnit.MILLISECONDS);
    }
}
```
このBean定義はバージョンアップ前と同じ動作を再現しますが、問題が含まれています。
キャッシュの寿命とリフレッシュサイクルが同じになるため、実行時に不規則なエラーが発生する可能性があります[^4]。
これを解決するにはDefaultJWKSetCacheの二つ目の引数を一つ目の引数よりも小さい数値に設定する必要があり、プロパティを自作するかコード内で指定する必要があります。
また、引数なしのコンストラクタは寿命を15分、リフレッシュサイクルを5分に設定されているようですので、そのサイクルに問題がなさそうであれば引数なしのコンストラクタを使用するという選択もあります。

###アプリをArueに登録
これは基本的に[ドキュメント通りの設定](https://github.com/Azure/azure-sdk-for-java/blob/407b4e0bffbb75b461c0512e669a6d43376fa7ab/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend/README.md)で問題なく動きます。
実際にAzure ADにアプリを登録するときに参考になる[図つきのチュートリアル](https://docs.microsoft.com/ja-jp/powerapps/developer/common-data-service/walkthrough-register-app-azure-active-directory)がありますが、AAD-Starterを使用する場合では変更点が必要なことに注意してください。

####リダイレクトURL
注意しなくてはいけないのは、リダイレクトURLはSpringSecurityのデフォルトとなる`{baseURL}/login/oauth2/code/azure`でなくてはならないことです。
Spring Securityのプロパティで変更できそうな気がしますが、私は成功しませんでした。[^5]

####クライアント シークレット
登録自体は特に問題ないのですが、シークレットの値自体は**登録直後の画面でしか表示されません**。必ずコピーしましょう。
とはいえ、メモり損ねてもミスしたものを削除して新しく作成すれば大丈夫です。反映も早いので大きな問題にはなりません。

####APIアクセスの許可
そしてもう一つ重要なのが、**アプリに与えるAPIへのアクセス権限**です。
> In the Supported legacy APIs section, click on **Azure Active Directory Graph**
> In the Delegated permissions section, ensure that the right permissions are checked: Directory.AccessAsUser.All
> Select the Add permissions button


これは完全に罠なのですが、AAD-Starterのドキュメントの内容で**必要なのはAzureADGraphの権限**であって、MicroSoft Graphの権限ではありません。
![許可するAPI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/d0855d61-a664-1119-33b9-b7bd744052a7.png "Azure ADのAPIの許可")

現在のAzureのWeb画面ではAzure AD Graphの表示は最下部にあり、目立っているMicroSoft Graphの権限を付与してしまっていくら頑張ってもAADSTS90008(アプリケーションにAPIアクセスの許可が設定されていない時のエラー)を拝むことになります。
これについてはうっかりミスですが、気付くのにはかなり時間がかかりました（図入りのチュートリアルを見ていたのとコードのバグを疑っていたので…）。

委任されたアクセス許可を選択してから、
![アプリケーションに必要な許可の種類.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/558c8926-2636-e003-7e0c-e5edc5b2bf0a.png)
`Directory.AccessAsUser.All`にチェックを入れます。

これに気づいた後はすんなり部署やセキュリティーグループを利用したアクセス許可のコントロールも成功しました。

###apllication.propertiesの編集
アプリ登録時のクライアントIDやシークレット、テナントIDなどをコピペします。

```yaml:application.properties
#他の設定に以下を追加
spring.security.oauth2.client.registration.azure.client-id=xxxxxx-your-client-id-xxxxxx
spring.security.oauth2.client.registration.azure.client-secret=xxxxxx-your-client-secret-xxxxxx
azure.activedirectory.tenant-id=xxxxxx-your-tenant-id-xxxxxx
# It's suggested the logged in user should at least belong to one of the below groups
# If not, the logged in user will not be able to access any authorization controller rest APIs
azure.activedirectory.user-group.allowed-groups=group1, group2
```

上記の通り、4項目を増やすだけでSpring security＋Azure ADでの認証・許可が可能になります。
また、AAD-Starterを利用すると**Azure ADに登録されているグループ名でのアクセス許可が可能**になるのが特徴です。
逆にテスト時には実在しているグループ名を指定しないとアクセス拒否されますので、テスト用のグループを作成しておくと良いでしょう。

後はアプリを起動して、Microsoftアカウントのログイン画面に転送された後に、アプリの画面へ戻ってくるかを確認すれば終了です。


##Microsoft GraphのAPIを使ってみる
過渡期でテスト的な立場のようですが、Azure AD graphからではなく、Microsoft Graphでグループ情報を取得するサンプルがあります。
サンプルのドキュメントは[azure-spring-boot-sample-active-directory-backend-v2](https://github.com/Azure/azure-sdk-for-java/tree/407b4e0bffbb75b461c0512e669a6d43376fa7ab/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend-v2)となります。

###apllication.propertiesの編集
前述の設定に、さらに下記を追加します。

```yaml:application.properties
#グループ情報をMicrosoft Graphから取得する設定
azure.activedirectory.environment=global-v2-graph
azure.activedirectory.user-group.key=@odata.type
azure.activedirectory.user-group.value=#microsoft.graph.group
azure.activedirectory.user-group.object-id-key=id
```

その他の変更は必要なく、起動して登録したアプリのページにアクセスするとMicrosoftのログイン画面に飛び、認証後にアプリのページへ戻ってくるはずです。

しかしながら、内部的に認可に使うグループ情報をMicrosoft Graphから取得しているだけですので、把握できる動作からは差異はわかりません。(実は機能していない？)



##Microsoft ID プラットフォーム(v2.0エンドポイント)でログインさせる
###Azure AD Graph v1.6とMicrosoft ID プラットフォーム
ここまでのログイン処理は、アプリ登録時のAPIアクセスの許可でもわかるとおり、Azure AD Graphに対して行われていました。
[Microsoft ID プラットフォーム (v2.0) に更新する理由](https://docs.microsoft.com/ja-jp/azure/active-directory/azuread-dev/azure-ad-endpoint-comparison)によると、Microsoft ID プラットフォームではAzure AD Graphによる認証はv1.0エンドポイントとされ、現在は利用を推奨されていません。現在推奨されるエンドポイントはMicrosoft ID プラットフォーム (v2.0) エンドポイントとされています。 
ADD-Starterのデフォルトではv1.0エンドポイントに対して認証を行いますので、次これをこれを2.0エンドポイントに変更する方法を確認します。

###認証エンドポイント
AAD-Starterの内部的には、MASL for javaでAzure ADへアクセスしており、そのプロパティを一部、Spring Securityと共有しています。

そこでSpring SecurityのOpenID Connectによる認証のドキュメントを元にプロパティを構成し、AAD-Starterのデフォルト設定を上書きすることでOpenID Connectの仕様に従っているMicrosoft ID プラットフォーム(v2.0エンドポイント)で認証を行ってみます。

###アプリケーション登録の変更
アプリケーションの登録手順自体は標準の場合と同様です。唯一、変更が必要なのがAPIの許可です。

####APIアクセスの許可
こちらでは**Microsoft Graph**からアクセス権を設定します。
こちらはOpenID Connectの仕様となるため、
OpenIDアクセス許可から、最低でもopenidが必要になります。通常のアプリでの使い勝手を考えると、emailやprofileも許可が必要かもしれません。
続いて、`Directory.AccessAsUser.All`にも許可があるかを確認します。
ADD-Starterでは認証に引き続きAzure AD上のGroupを参照して、要求されたサイトへのアクセスを認可しますので、このAPI許可が必要になります。 

####エンドポイントの確認
apllication.propertiesに記述するエンドポイントを確認しておきます。
下図のボタンから確認できます。
![Graphエンドポイント.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357184/d059c121-cba0-1d34-9791-6e4bd6b439b2.png)

このうち、OAuth 2.0 承認エンドポイント (v2)、OAuth 2.0 トークン エンドポイント (v2)が必要になります。
また、Userinfoエンドポイントが必要になるのですが、これはOpenID Connectの仕様により`https://graph.microsoft.com/oidc/userinfo`で固定となります。

###apllication.propertiesの編集
これらのエンドポイントの設定はSpring Securityの設定プロパティとなります。

```yaml:apllication.properties
spring.security.oauth2.client.registration.azure.scope=openid #(APIアクセスの許可で許可していればemailやprofileも)
spring.security.oauth2.client.provider.azure.authorization-uri=承認エンドポイント (v2)
spring.security.oauth2.client.provider.azure.token-uri=トークン エンドポイント (v2)
spring.security.oauth2.client.provider.azure.user-info-uri=https://graph.microsoft.com/oidc/userinfo

#OpenID Connect仕様だと下記が設定されていれば他のエンドポイントを検索できるはずですが、ADD-Starterの設定が優先されるのか機能しませんでした
#spring.security.oauth2.client.provider.azure.issuer-uri=
```
上記のプロパティを追加すると、Microsoft IDプラットフォームv2（OpenID Connect）によって認証されるようになります。
Spring SecurityのOAuth、OpenID Connect関連のプロパティの詳細は[Spring Boot 2.x プロパティマッピング](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#oauth2login-boot-property-mappings)を参照してください。

####認証エンドポイントの確認方法
Spring BootのStarterによってThymeleafが導入されていれば、テンプレート上で`#authentication`オブジェクト参照が使用できるため、適当なdivを追加して`th:text="${#authentication}"`のようにすることでサイトにアクセスしているユーザーの認証情報が確認できます。この中に存在する情報に「iss」≒認証者の情報があるため、これによってどのエンドポイントで認証されたかを確認することができます。

##~~Spring SecurityのOAuth2.0クライアントで実装してみる~~
かなりの勢いで車輪の再発明になるため取りやめました…

##記述途中→ログイン後にGraphの機能でいろいろする
この記事自体が長くなってしまったこととまだ確認中のため、別の記事を作成したいと思います。

##まとめ
このライブラリの問題回避とアクセス権限の調査で、Spring SecurityでのOpenID connect(OAuth2)認証とAzureAD（Microsoft　IDプラットフォーム）の連携の仕組みが良くわかりました。
まず認証を受けてトークンを受け取り、グループ情報を取得してそれを元に認可を行うという独特なフローも、ADD-Starterがあるとプロパティ操作だけで実現できました（アップデートによる不具合回避を除く）。

Azure AD Spring Boot Starter client library for Javaは次バージョンで大きくアップデートされるようですが、現状は問題が多く含まれているようです。
私は初めてGithub上で修正案をコメントしましたが、意外にあっさりと取り込まれました。皆さんもライブラリの調査でちょっとでも問題に気づいたら報告してみるといいかもしれません。

###追記：2020/11/18
azure-spring-boot-starterの3.0.0-beta.1がprereleaseになってます。
また変更点確認しなきゃ…

[^1]: 私が触れた範囲がAzureADのシングルテナントのみのためこのように限定しています
[^3]: 以下AAD-Starerと省略
[^4]: nimbus-jose-jwtの修正もこれが原因だと思われ、この修正はその場しのぎであることはGitHubで共有されました
[^5]: 私の理解、検証不足の可能性があります

##参考資料
[Azure AD Spring Boot Starter client library for Java](https://github.com/Azure/azure-sdk-for-java/blob/407b4e0bffbb75b461c0512e669a6d43376fa7ab/sdk/spring/azure-spring-boot-starter-active-directory/README.md)[^10]
[^10]: 多分一番新しいStarterのドキュメント
[OAuth 2.0 Sample for Azure AD Spring Boot Starter client library for Java](https://github.com/Azure/azure-sdk-for-java/tree/master/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend)
[A Java web application using Spring security which signs in users with the Microsoft identity platform](https://github.com/Azure-Samples/ms-identity-java-webapp/tree/master/spring-security-web-app)
[【Microsoft ID プラットフォーム】(Azure AD) Java Web アプリ (WAR file)-設定編-](https://qiita.com/Design_Tshirt7/items/df8dd7799bfdc32c9078)
[【spring-projects/spring-security】OAuth 2.0 Migration Guide](https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide)
[Spring Security Reference12.1. OAuth 2.0 Login](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2login)