Spring Boot (Spring Security) で Azure App Service の認証 (EasyAuth) を使う

.NET Framework1.0 が世に出た時から仕事で C# を使っていますが、Java は15年ぐらい前の少し触った程度の私です。これから IntelliJ IDEA と Spring Boot を使ってあれこれ試していきたいと思います。どっちも初心者なので、突っかかったところ正直に書いていきたいと思います。

## 別の IntelliJ + Spring Boot 記事

- [IntelliJ IDEAで Spring Boot を Azure App Service (WebApps)へデプロイする](https://qiita.com/takashiuesaka/items/ad63c7774fef3d75ebcc)

## 環境

- Window 11
- [IntelliJ IDEA Community Edition 2022.1.2](https://www.jetbrains.com/ja-jp/idea/download/#section=windows)
- [Microsoft Open JDK 17.0.3](https://docs.microsoft.com/ja-jp/java/openjdk/download)
- [Maven 3.8.3](https://maven.apache.org/index.html)
## App Service の認証機能の概要

Azure App Service にはとても簡単に認証機能を組み込むことができる機能があり、認証、英語だとAuthN/AuthZ、通称は EasyAuth と呼ばれます。この「認証」というのは外部の認証プロバイダーを利用する仕組みです。
公式：[Azure App Service および Azure Functions での認証と承認](https://docs.microsoft.com/ja-jp/azure/app-service/overview-authentication-authorization)

Easy Auth がサポートする外部プロバイダーは AzureAD, Facebook, Twitter, Google, Apple(Preview), 一般的な OpenIdConnect をサポートするプロバイダーです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8fa6c460-7cc3-b39f-d998-c602e66b06dd.png)
公式ページを開くと下の方にこの図が出てきます。この図で注目したいのは AuthN/AuthZ middleware が Your web app の前段に挟み込まれる、という点です。アプリに手を加えることなく、外部認証プロバイダーを使った認証が有効になる、ということです。めっちゃ便利です。

## なぜ Spring Security が必要なのか

外部認証プロバイダーを使用した OpenIdConnect 認証を行う場合、ほとんどの場合は認証プロバイダーが発行した IdToken を Validate し、 OK だったら IdToken（JWT）を decode して claim 情報を取り出します。よく使われるのが name や email, sub あたりでしょう。

認証機能を有効化するのは実装不要で Azure Portal 上でぽちぽちやればできてしまうので簡単なのですが、IdToken の取り扱いは自分で実装しなければなりません。今回、この IdToken の Validate と claim 情報の取り出しに Spring Security を使って楽をしよう、ということです。

## Azure AD を認証プロバイダーにした Spring Boot アプリを作成する

まずは Spring Boot(Spring Security)を使ったアプリを作る前に、AppService の認証機能を有効にします。

### App Service の認証機能を有効にする (AzureAD を使用)

機能を有効化するのは Azure Portal でボタンをぽちぽちするだけで完了してしまいます。今回は認証プロバイダーとして Azure AD を使用します。

App Service の 認証を選択して、「ID プロバイダーを追加」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/269f9d88-871b-aa41-a845-6870a01278a9.png)

ID プロバイダーの中から Microsoft を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/aad71475-3951-7d45-4672-2d726b77e9c3.png)

次の画面は、Azure AD 内部に作る「アプリケーション」の情報です。既定で App Service の名前がセットされていますが、変えることもできます。他は変更せずに追加ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/596b1313-7b71-c728-25fd-8a9527f6f21b.png)

追加した Azure AD のアプリケーションが登録されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ce1bbeee-3ada-0d27-51b7-427a59d0c71f.png)

EasyAuth の設定はこれで終わりです。 AzureAD の場合とても簡単ですね。

認証機能が本当に有効になっているのか、ブラウザで Webアプリケーションの URL にアクセスしてみましょう。まだ何もデプロイしていませんが、認証は動作するはずです。App Service の概要から URL をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8243911b-5873-df1d-8b60-872ae833c2c8.png)

サインインの画面が表示されます。ログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/bbbeef8e-58fe-bbf8-dc5c-67c0518d55b3.png)

Webアプリケーションに対して、自分の Claim 情報を渡しても良いかどうか聞かれますので許可を与えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8c0a0f51-815b-4f05-fac5-1200f21c8cf3.png)

すると、既定のページが表示されます。認証機能はこれで有効になっていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ce613f24-2b4e-2e22-c6a7-6c8038db27be.png)


さて、では自作の Spring Boot アプリで Claim 情報を取得する実装を行います。その前に Spring Securityが IdToken を Validation する時には Azure AD 内部に作られた「アプリケーション」の情報が必要なので、それを取得しておきます。

### Azure AD のアプリケーション？
今回、AppServiceでホスティングする Webアプリケーション の前段に認証機能が組み込まれて稼働するわけですが、この認証機能もWebアプリケーションの1つだと考えて良いわけです。そして、その認証 Web アプリが 認証プロバイダーとやり取りを行うわけですが、認証プロバイダー側からしてみると、得体のしれない Web アプリから認証してくれよ、とリクエストが来ても相手にするわけにいきません。素性の確かな Web アプリであることを証明してもらいたい訳です。

それはつまり Web アプリそのものがまず認証される必要がある、ということです。ということは、今回のように人間ではなく Web アプリケーションような「サービス」が認証プロバイダーにアクセスしたときに用いる ID/PW を、認証プロバイダー側に登録する必要がある訳です。

その登録した結果を Azure AD では「アプリケーション」と呼んでいます。アプリケーションは、ID/PWに相当するものとして ClientId/Secret を保持しています。

今回は認証用の Web アプリケーションが用いる Azure AD 内部の「アプリケーション」は EasyAuth の設定で登録を完了しています。

## Azure AD に登録されたアプリケーションから情報を取得する

Spring Security にとって必要な情報が3つあります。1つはどこの Azure AD テナントなのかを示す Tenant ID、そして Azure AD に登録されたアプリケーションが保持する ClientID/Secret(ID/PW)です。この3つの情報をあらかじめ取得しておきます。まずは一番簡単な方法で取得しましょう。 Azure AD のアプリケーションにアクセスして情報取得するのが本来ですが、アプリケーションへのアクセスの方法は後ほどご紹介します。（アクセスする必要があります）

### ClientID の取得

ID プロバイダーの一覧画面に登録済みの Azure AD 情報が1行あります。 ここに表示されているアプリ（クライアント）IDをコピーしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c2d7ccd3-f74f-e753-d2b5-5fdef0c44ad2.png)

### TenantId の取得

次は TenantId を取得します。TenantId とは、乱暴に言うと個々の Azure AD インスタンスが持つ Id のことです。（インスタンス、という表現は正確じゃないですけど）同じ画面の編集アイコンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/5b972a3c-3e10-fb4c-1d91-988b70d46549.png)

画面が切り替わります。この画面の発行者 URL に表示されている GUID 文字列が TenantId です。コピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/91c91c14-f23f-61d7-6a5f-a8179ab14e52.png)

### Secret の取得

最後に Password に相当する Secret を取得します。同じ画面の「クライアント シークレット設定の名前」を覚えておきます。次に「クリックしてシークレット値を編集します」リンクをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/3236ecdb-50fe-d897-56af-b04a8e9b65a9.png)


画面が App Service の構成に切り替わります。アプリケーション設定の中に、さきほど覚えておいた「クライアント シークレット設定の名前」が存在しています。この「値」が Secret です。非表示になっているので、クリックして表示させてコピーしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/4c4c0ea2-d267-311b-1347-1057e84ec8eb.png)

## Spring Boot アプリを作成する

App Service 側の準備は終わりましたので、Spring Boot アプリを作成しましょう。

### Spring Initializr

[Spring Initializr](https://start.spring.io/) を使って Spring Boot アプリのひな形を作成してダウンロードします。
Dependencies に次の2つの最低限に入れます。

- Azure Active Directory
- OAuth2 Client

Artifact は今回は AppServiceEasyAuth にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/f8ce62ed-b325-db2d-1d4e-8b058c3b1f1e.png)

### ひとまず Hello Worldする

IntelliJ IDEA で開き、まずは Controller と html を一つずつ用意して画面に Hello を表示させます。 Controller は controller ディレクトリを用意して、そこに HelloController.java を作成します。

```java:controller.HelloController
package com.example.AppServiceEasyAuth.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloController {

    @GetMapping("/")
    public String hello() {
        return "hello";
    }
}

```

HelloContoller で 文字列 "hello" を返しています。これは resources/templates に hello.html を用意してある前提です。 

```html:hello.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>Hello!</div>
</body>
</html>
```

Hello! がレンダリングされるだけです。
今回、Templateエンジンで Thymeleaf を使うように Dependencies に入れています。この後 Thymeleaf を使うので、今のうちから xmlnsに Thymeleaf を使うことを追加してエラーにならないようにしておきました。

まずはこの状態で問題なく稼働するかを確認します。実行して http://localhost:8080 にアクセスすると、作った覚えがない認証画面が表示されます。これは Spring Security の既定の認証画面です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c16c2d87-8360-3bd9-fc9d-8542b8d69c50.png)
 
Username には user、Password は、IntelliJ の実行タブを見ると、生成されたパスワードが表示されていますので、これを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/83c4f13a-c429-50e2-76b6-a09c5a1e3e43.png)

問題なく hello.html が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8235bedd-0686-a352-6d6c-daa03f5d8d4f.png)

### Spring Security をセットアップする

では、ひとまずローカルで Azure AD を認証プロバイダーとして認証するように Spring Security をセットアップします。

application.properties に次の情報書きます。\<TenantId>, \<ClientId>, \<Secret> の部分をコピーしておいた情報に置き換えてください。
一番下の scope でどんな情報を受け取りたいのかを Azure AD に通知しています。OpenIdConnect の仕様ですね。

```properties:application.properties
# Enable related features.
spring.cloud.azure.active-directory.enabled=true
# Specifies your Active Directory ID:
spring.cloud.azure.active-directory.profile.tenant-id=<TenantId>
# Specifies your App Registration's Application ID:
spring.cloud.azure.active-directory.credential.client-id=<ClientId>
# Specifies your App Registration's secret key:
spring.cloud.azure.active-directory.credential.client-secret=<Secret>
# Include email to scope
spring.cloud.azure.active-directory.authorization-clients.graph.scopes=profile,email
```

次に、IDTokenを decode した結果を Controller で受け取り、Secret を View へ渡すように実装します。

```java:controller.HelloContoller.java
package com.example.AppServiceEasyAuth.controller;

import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloController {

    @GetMapping("/")
    public String hello(Model model, OAuth2AuthenticationToken token) {
        if (token != null) {
            model.addAttribute("claims", token.getPrincipal().getAttributes());
        }
        return "hello";
    }
}
```
ポイントは、OAuth2AuthenticationToken という型で decode 結果を Injection してくれる、という点です。IdToken の Validation は Spring Security がやってくれている（はず）です。

受け取った Claim達を hello.html で表示します。

```html:hello.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>Hello!</div>
<hr />
<table>
    <tbody>
    <tr><th>name:</th><td th:text="${claims.name}"></td></tr>
    <tr><th>email:</th><td th:text="${claims.email}"></td></tr>
    <tr><th>sub:</th><td th:text="${claims.sub}"></td></tr>
    </tbody>
</table>
</body>
</html>
```
name, email, sub の3つだけ表示しています。

### ローカルで実行する

ブラウザで http://localhost:8080 にアクセスすると、Azure AD の認証画面が立ち上がります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e9da510a-92e2-f140-aa91-4b2c6354b004.png)

ログインすると、こんなエラー画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/af8305c5-2aa6-0b10-e8f6-b5e6dfc7a2bb.png)

ローカル実行用に redirect URI を AzureAD のアプリケーションに登録が必要だ、と言っています。登録しましょう。
まずはアプリケーションにアクセスします。方法は二つあって、一つはAzureADからアプリ登録を選び、アプリ名から登録されたアプリを探す方法。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/359149a0-0ae9-cbb6-20ef-c524836f5d10.png)

もう一つは App Service の認証からさきほど登録したID Provider に表示されているアプリケーション名をクリックする方法です。こっちの方が簡単ですかね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/96f8ed3b-8e6b-bbc5-21f8-486ab822c51a.png)

Azure AD のアプリケーションを開いたら、認証メニューを選択します。Web の URL の追加をクリックし、エラー画面に表示されていた redirect URI を記入して保存ボタンをクリックします。

> http://localhost:8080/login/oauth2/code/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ed67743f-fb2c-3ee3-16a5-58b27e84f972.png)

ここで1，2分待ちます。以前よりマシになりましたが、Azure AD への変更は反映まで少し時間がかかります。
再び http://localhost:8080 へアクセスしてみましょう。

シングルサインインが効いてしまうのでログイン画面が表示されないかもしれません。表示されたらログインします。無事に画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/b3e5a3ef-9380-3b13-fc34-fed6860af812.png)

## App Service へデプロイ

今のところローカルで動かしただけです。App Service へデプロイして挙動を確認しましょう。
IntelliJ から AppService へのデプロイは Azure Toolkit for IntelliJ を使うとボタンぽちぽちだけでデプロイできちゃいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/7188d621-20e1-f55a-4cc6-e128460ce35f.png)

プロジェクトを右クリック -> Azure -> Deploy to Azure WebApps を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/26723870-f2eb-f5a2-5ba8-5ec728570b08.png)

EasyAuth をセットアップしてある Azure App Servcice を選択し、実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/f53cd800-061d-fb54-fe85-b4c271682051.png)

ブラウザで AppService がホストする URL にアクセスすると・・・・再びエラーです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c46f6797-1161-1a1f-2dbe-205e8cf5312c.png)

また redirect URI が登録されていない、というエラーです。このエラー、EasyAuthではなく Spring Security 側のチェックに引っかかってしまっています。仕方がないので、再び Azure AD のアプリケーションを開いて、redirect URI を登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/3185d8ac-462a-c7fc-3c54-873340df2555.png)

1,2分待って、ブラウザでアクセスし直すと、今度は問題なく表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ac544ecd-da96-c36a-4315-eb5e57df6a1b.png)

## まとめ・感想

Spring Security を使うと、 IdToken の扱いを考えなくて済むようになるのは確かにいいですね。