Spring Boot 2 + OAuth2 Autoconfig で超シンプルにOAuth2クライアントを実現する

今回は、Spring Boot 2とOAuth2 Auto Configを利用してシンプルにOAuth2クライアント（フロント）アプリを作成する方法について解説します。

OAuth2クライアントで実現する要件は、以下のとおりです。

* OAuth2認可サーバを利用したシングルサインオン（SSO）
* SSOで取得したアクセストークンを利用したOAuth2リソースサーバ（API）へのアクセス

プロバイダとして、今回はGithub APIを利用します。

## 今回使用するライブラリ

* Spring Boot 2 (Spring & Spring Security 5)
  - spring-boot-starter-web 2.0.1.RELEASE
  - spring-boot-starter-security 2.0.1.RELEASE
* OAuth2 Autoconfig (Spring Security OAuth2)
  - spring-security-oauth2-autoconfigure 2.0.0.RELEASE
* Thymeleaf 3
  - spring-boot-starter-thymeleaf 2.0.1.RELEASE
  - thymeleaf-extras-springsecurity4 3.0.2.RELEASE
* Coding Support
  - lombok 1.16.20

Spring Security OAuth2の設定をシンプルに実現するため、`spring-security-oauth2-autoconfigure`を利用するのがポイントです。

## Spring Securityによる通常のログイン

まずはSpring Boot 2 (Spring & Spring Security 5)を利用して通常のフォームログインを確認します。

### 依存ライブラリの設定

* pom.xml

```xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
</parent>

<dependencies>
    <!-- Spring Boot 2 (Spring & Spring Security 5) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- Thymeleaf 3 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    </dependency>
    <!-- Coding Support -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>
```

`thymeleaf-extras-springsecurity4`は、ログイン後の画面で認証情報を表示するために利用しています。Spring Security 5用のモジュールがリリースされていないため、Spring Security 4用のもので代用しています。

### アプリケーションのエントリポイント

* Application.java

```java
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

エントリポイントに`@EnableWebSecurity`を付与せずとも、クラスパスに`spring-boot-starter-security`を含めるだけで自動的にSpring Securityが有効になり、デフォルトですべてのパスに`isAuthenticated()`の認可がかかります。

### ホーム画面

* Controller.java

```java
@Controller
public class Controller {
    @GetMapping("/")
    public String home() {
        return "home";
    }
}
```

* home.html

```html
<html xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="utf-8">
<title>Home</title>
</head>
<body>
    <h1>Welcome to Home <span sec:authentication="name"></span></h1>
</body>
</html>
```

この時点でアプリケーションを実行しコンテキストルートにアクセスすると、Spring Securityのフォームログイン機能によりデフォルトのログイン画面が表示されます。ログインしてホーム画面にアクセスすると「Welcome to Home user」と表示され、ログインしたことが確認できます。

> Spring Securityのデフォルトでは、ユーザ名`user`と起動時にログ出力されるランダム生成されたパスワードを利用してアクセスすることができます。

## OAuth2を利用したシングルサインオン（SSO）

通常のSpring Securityによるログインを確認したら、次にOAuth2によるSSOに変更します。

### 依存ライブラリに`spring-security-oauth2-autoconfigure`を追加

* pom.xml

```xml
<dependencies>
    <!-- OAuth2 Autoconfig (Spring Security OAuth2) -->
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>
</dependencies>
```

### エントリポイントに`@EnableOAuth2Sso`を追加

* Application.java

```java
@SpringBootApplication
@EnableWebSecurity
@EnableOAuth2Sso
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

`@EnableOAuth2Sso`を追加することで、Spring Securityの認証処理をフックしてOAuth2による認証を行うようになります。

ここでのポイントは、`@EnableOAuth2Sso`だけでなく`@EnableWebSecurity`も追加することです。
`@EnableOAuth2Sso`でOAuth2認証は有効になりますが、Spring Securityの認証情報に引き継がれない（＝ログインしない）ため、リクエストごとに認証が必要となり、全く実用的ではありませんでした。SSOするなら必ず`@EnableWebSecurity`を追加しましょう。

### Githubにアプリケーションを登録

[Github APIリファレンス](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/)を参考にアプリケーションをGithubに登録し、`client-id`と`client-secret`を入手しましょう。
Githubユーザページのユーザアイコンから「Settings>Developer settings>OAuth Apps>New OAuth App」と移動し、指示に従い登録します。

アプリケーションの登録時にはAuthorization callback URL（認証時にコールバックするURL）が必要となるので、アプリケーションのポート番号とコンテキストルートURLの設定方法を示します。

* application.yml

```yaml
server:
  port: 8080
  servlet:
    context-path: /demo
```

### OAuth2のクライアント認証情報を登録

* application.yml

```yaml
security:
  oauth2:
    client:
      client-id: xxxx # (1)
      client-secret: xxxx
      access-token-uri: https://github.com/login/oauth/access_token # (2)
      user-authorization-uri: https://github.com/login/oauth/authorize
      client-authentication-scheme: form # (3)
    resource:
      user-info-uri: https://api.github.com/user # (4)
      prefer-token-info: false
    sso:
      login-path: /login # (5)
```

(1) さきほど入手した`client-id`と`client-secret`を登録します。
(2) [Github APIリファレンス](https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/#web-application-flow)を参考に認証URL（`user-authorization-uri`）やアクセストークン取得のURL（`user-authorization-uri`）を登録します。
(3) 今回はリクエストボディに認証情報を載せるので、`client-authentication-scheme`に`form`をセットします。
(4) OAuth2クライアントとして利用するだけなら上記のみでOKですが、今回はシングルサインオンのためユーザ情報を取得するURL（`user-info-uri`）を登録する必要があります。（加えて、ユーザ情報を`token-info-uri`から取得せず`user-info-uri`から取得するため、`prefer-token-info`に`false`をセットして無効化しています。）
(5) SSOログインURL（認証していないときにリダイレクトするURL）を変更したい場合は、`login-path`を変更します。今回はデフォルト（`/login`）で良いので設定する必要はありませんが、プロバイダが複数だったり、認可制御してログインURLを認可から除外したいときに設定します。

この時点でアプリケーションを実行しコンテキストルートにアクセスすると、Githubのログイン画面が表示されます。
Githubでアプリケーションを認可するとホーム画面に遷移し、「Welcome to Home yoshikawaa」と表示され、SSOによりログインしGithubの認証情報がSpring Securityの認証情報に連携されたことが確認できます。

> 認証情報が`OAuth2Authentication`に置き換わるため、例えば`sec:authentication="oAuth2Request.clientId"`で画面上で`client-id`が確認できます。

## OAuth2を利用したリソースサーバへのアクセス

SSOによるログインを確認したら、次に取得したアクセストークンを利用してリソースサーバ（Github API）へのアクセスができることを確認します。

今回は、[Github APIリファレンス](https://developer.github.com/v3/repos/#list-your-repositories)を参考にログインユーザのリポジトリ一覧を取得して画面に表示してみます。
試しにブラウザでURL`https://api.github.com/user/repos`にアクセスしてみると、`Requires authentication`と表示され認証が必要なことが分かります。

### エントリポイントに`OAuth2RestTemplate`のBean定義を追加

* Application.java

```java
    @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
            OAuth2ProtectedResourceDetails details) {
        return new OAuth2RestTemplate(details, oauth2ClientContext);
    }
```

クラスに`@EnableOAuth2Client`を付与することで`OAuth2ClientContext`と`OAuth2ProtectedResourceDetails`が自動的に設定されますが、`@EnableOAuth2Sso`が内部的に`@EnableOAuth2Client`を付与するため、ここではアノテーションを付与する必要はありません。

### 取得したリポジトリ情報を格納するBean

* Repository.java

```java
@Getter
@Setter
public class Repository {
    private String name;
    @JsonProperty("html_url")
    private String htmlUrl;
}
```

ここで初めてLombokを使います。。。

### ログインユーザのリポジトリ一覧を表示する画面

* Controller.java

```java
    @Autowired
    private OAuth2RestTemplate auth2RestTemplate;

    @GetMapping("/repos")
    public String repositories(Model model) {
        URI uri = UriComponentsBuilder.fromUriString("https://api.github.com/user/repos").build().toUri();
        model.addAttribute("repos", auth2RestTemplate.getForEntity(uri, Repository[].class).getBody());
        return "repos";
    }
```

* repos.html

```html
<html xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="utf-8">
<title>Repos</title>
</head>
<body>
    <h1>Listing Repositories owned <span sec:authentication="name"></span></h1>
    <ul>
        <li th:each="repo : ${repos}" th:object="${repo}">
            <a th:text="*{name}" th:href="*{htmlUrl}"></a>
        </li>
    </ul>
</body>
</html>
```

この時点でアプリケーションを実行しコンテキストルートにアクセスし、SSOによりログインします。
その後、パス`/repos`にアクセスすると、取得したアクセストークンを利用してGithub APIからリポジトリの一覧を取得できることが確認できます。
ブラウザのネットワーク履歴を見ると、パス`repos`にアクセスする際に再ログインなどはしていないことも確認できますね。

> ちなみに、途中でアクセストークンの有効期限が切れると認可サーバへ問い合わせが走るため、現在アクセス中のURLにGETによるリダイレクトが走る可能性があります。その際、POSTで送信していたリクエストパラメータが失われてしまうため、基本的にリソースサーバにアクセスするURLにはGETでアクセスさせる、またはリクエストパラメータが失われても再入力すればOKな作りにしておくのが無難なようです。

## まとめ

Spring Security OAuth2は設定がやたら難しくて実装が大変な印象ですが、OAuth2 Autoconfigを利用すると難しい設定はほとんどなく、超シンプルにシングルサインオンとOAuth2クライアントが実現できました。

ただ、Spring Securityと連携する方法がドキュメントのどこにも書いてなく、最初はかなりハマったので、今後改善されるといいなーと思います。

## 参考

* [(Getting Started) Building an Application with Spring Boot](https://spring.io/guides/gs/spring-boot/)
* [(Tutorial) Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/)
* [OAuth2 Autoconfig](https://docs.spring.io/spring-security-oauth2-boot/docs/2.0.0.RELEASE/reference/htmlsingle/)
* [Github API v3](https://developer.github.com/v3/)
* [Building OAuth Apps](https://developer.github.com/apps/building-oauth-apps/)