SpringbootでのSAML認証デモアプリ作成（Keycloak）

業務で急遽Springbootアプリケーション（MVC）にSAML認証をかけたものを触らなければならなくなったので、勉強がてら作成したものを、忘備録として記載。

# デモアプリケーション repository
https://github.com/ryoheiY/Spring-mvc-demo

# 参考文献
## 実装
https://www.a-frontier.jp/technology/security11/

https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/saml2/login

# Java

## pom.xml抜粋
以下をSpringbootInitializerでの設定以外で追加する。
Java一時ディレクトリの使用に関連する脆弱性があるようなのでそこだけ注意。

~~~xml

        <!-- SAML認証のためにはSpringbootの初期設定だけでなく、追加で必要な依存 -->
        <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-saml2-service-provider -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-saml2-service-provider</artifactId>
            <version>6.2.4</version>
        </dependency>
~~~

## application.yaml
security以下設定を参照。
秘密鍵は以下で生成。
~~~cmd
openssl req -newkey rsa:2048 -nodes -keyout local.key -x509 -days 365 -out local.crt
~~~
~~~yaml

spring:
  application:
    name: demo
  #DB
  sql:
    init:
      schema-locations: classpath:schema.sql
      data-locations: classpath:data.sql
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password: sa
  #h2
  h2:
    console:
      enabled: true
  #saml
  security:
    saml2:
      relyingparty:
        registration:
          my-client: #任意の値
            entity-id: spring-saml-demo #keycloakのクライアントIDの指定
            signing:
              credentials:
                - private-key-location: classpath:credentials/local.key
                  certificate-location: classpath:credentials/local.crt 
            assertingparty:
              metadata-uri: http://localhost:18080/realms/demo/protocol/saml/descriptor

#LOG
logging:
  level:
    org:
      springframework.web: debug

#Mybatis
mybatis:
  configuration:
    map-underscore-to-camel-case: true
~~~

## SecurityConfig
SpringSecurityの設定実装。
saml2Metadataの記載によって、特定URLにアクセスすることでSP、つまり本アプリケーションのメタデータXMLを生成することができる。

上記yaml設定で、my-clientとしてsaml設定を用意しているので、接続URLは以下となる。

http://localhost:8080/saml2/service-provider-metadata/my-client



疑問：
デフォルトサクセスURLを指定していても、リダイレクト前に指定したURLに飛ぶ。
どういう挙動が正解なのか、SpringSecurityの理解が必要。
~~~Java
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /*
     * 認可設定とログインの設定を定義
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);

        http
                .authorizeHttpRequests(authz -> authz
                        // URLごとの認可設定
                        .anyRequest().authenticated()
                ).saml2Login(saml2Login -> saml2Login
                        // SAML 2.0プロトコルの認証設定をカスタマイズする場合は、ここにCustomizerを指定します
                        .defaultSuccessUrl("/search") // 認証成功後のデフォルトのリダイレクト先を設定する場合など)
                ).saml2Metadata(
                        Customizer.withDefaults()
                ).csrf(
                        // CSRFキー有効化設定（デフォルト：有効）
                        Customizer.withDefaults()
                );
        return http.build();
    }
}
~~~

## Controller
Token情報の受け取り例。
~~~Java
    /**
     * 初期表示想定
     * @param principal 認証ユーザ情報の入れ物
     * @param modelAndView モデルとviewの入れ物
     * @return
     */
    @GetMapping("/search")
    public ModelAndView initPage(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, ModelAndView modelAndView) {
        SearchCondition condition = new SearchCondition();
        searchService.init(condition);
        //Tokenから情報を取得
        //Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        //view, model
        modelAndView.setViewName("index");
        modelAndView.addObject("searchCondition", condition);
        return modelAndView;
    }
~~~

# Keycloak

## Docker
物理でもいいが、再現性向上のためにDockerで用意する。
~~~yaml
version: '3'
services:
  keycloak:
    image: quay.io/keycloak/keycloak:19.0.2
    container_name: keycloak
    tty: true
    stdin_open: true
    ports:
      - "18080:8080" # 8080は被りやすいので別のポートに設定
    volumes:
      - ./data/keycloak:/opt/keycloak/data # DB情報等が格納されます
    environment:
      KEYCLOAK_ADMIN: admin # 管理ユーザーIDを設定
      KEYCLOAK_ADMIN_PASSWORD: admin # 管理ユーザーのパスワードを設定
    command:
      - start-dev # 開発モードで起動
~~~


## Keycloakにメタデータのインポート
詳細は割愛するが、Clients画面でキャプチャの丸部分をクリックし、前述したメタデータXMLをインポートすれば自動的にクライアントを登録できる。
![スクリーンショット 2024-05-11 15.03.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673518/8f1c032a-cd72-9fc1-f5e7-8285696073d3.png)

# 動作確認
アプリケーション（SP）にアクセスすると、、、
![スクリーンショット 2024-05-11 15.08.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673518/5cf36e73-fc9f-22c7-dc19-9fb709591ea2.png)

自動的にKeycloakへリダイレクト！![スクリーンショット 2024-05-11 15.12.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673518/c07fbc0e-8afd-6fd6-351e-0f9c7bf6d1b0.png)

事前に用意したユーザでログインしてみると、認証成功して先ほどアクセスしたURLのアプリケーションへ再リダイレクト
![スクリーンショット 2024-05-11 15.10.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673518/dd76c305-7ae8-9aee-e1ec-ae086e7345e3.png)

![スクリーンショット 2024-05-11 15.13.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673518/8efa10b3-d740-e95a-6f7b-1ed3d451c0db.png)