AzureAD認証の導入手順(SpringBoot)

SpringBoot(Java)プロジェクトでAzureAD認証を追加するための実装方法について記載していく。

依存関係の追加

Azure AD認証を導入したいプロジェクトのbuild.gradleに「Spring Cloud Azure Starter Active Directory」の依存関係を追加する。

dependencies {
    // Spring Cloud Azure Starter Active Directory
    implementation 'com.azure.spring:spring-cloud-azure-starter-active-directory'
}

application.ymlに設定追加

application.ymlに下記の設定を追加する。どのAzureADで認証を行うのかを設定している。
テナントID、クライアントID、クライアントシークレットはAzure Portal上で確認可能。

spring:
    cloud:
        azure:
            active-directory:
                application-type: web-application
                # Azure AD認証を有効化する
                enabled: true
                profile:
                    # Azure Tenant ID
                    tenant-id: xxxxx-xxxx-xxx-xxx-xxxxx
                credential:
                    # Azureでサービス プリンシパル認証を実行する際に使用するクライアントID
                    client-id: xxxxxx-xxx-xxx-xxxx-xxxxxxxx
                    # Azureでサービス プリンシパル認証を実行する際に使用するクライアントシークレット
                    client-secret: xxxxx~xxxxxxx
                authorization-clients:
                    graph:
                        scopes: email,openid

    # AzureADのautoconfigureを無効化する(Spring Securityの設定方法が古くアプリ起動時に例外が発生するため)
    autoconfigure:
        exclude: com.azure.spring.cloud.autoconfigure.aad.AadAutoConfiguration

AuzreAD認証のユーザー情報を構築するサービス

リクエストからログイン者のメールアドレスを取得して、対象のメールアドレスがDBに登録されているかどうか検索する。
今回は標準のユーザインスタンスを返却しているが、このサービスクラスの中で独自ユーザクラスにラップしたものを作成して返却することも可能。

※AzureAD認証自体はアプリの外側で実施されるため、このクラスにリクエストが送られてきたタイミングで既にAzureAD認証は完了している。

import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;

/**
 * AuzreAD認証のユーザー情報を構築するサービス。<br>
 */
@Transactional
public class AzureAd2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {

  /** {@link LoginUserDao} */
  private LoginUserDao loginUserDao;

  /**
   * コンストラクタ。<br>
   */
  public AzureAd2UserService() {
  }

  /**
   * コンストラクタ。<br>
   * 
   * @param loginUserDao
   *          {@link LoginUserDao}
   */
  public AzureAd2VeUserService(LoginUserDao loginUserDao) {
    this.loginUserDao = loginUserDao;
  }

  /**
   * 認証後のユーザー情報を元に、DBを検索し拡張したユーザー情報を返す
   * 
   * @param userRequest
   *          認証リクエスト
   * @return 拡張ユーザー情報
   */
  @Override
  public OidcUser loadUser(OidcUserRequest userRequest) {
    OidcUserService service = new OidcUserService();

    // 認証処理後のユーザー情報を取得
    OidcUser oidcUser = service.loadUser(userRequest);

    // ログインユーザーがDBに登録されているかどうかを確認する
    LoginUser loginUser = loginUserDao.selectById(oidcUser.getEmail());

    if (loginUser == null) {
      // nullの場合は、アプリへのアクセス権限が付与されていないためエラーとする
      throw new AccessDeniedException(
          "[ログインユーザ]テーブルに該当のメールアドレスがされていません。 メールアドレス=[%s]".formatted(oidcUser.getEmail()));
    }

    return oidcUser;
  }
}

JavaConfig

Azure AD認証周りの設定を定義するためのJavaConfigクラス。
application.ymlでautoconfigurationをOFFにしたため、最低限必要なJavaConfigについてこのクラスでインポートを実施する。

@Import({ AadPropertiesConfiguration.class, AadResourceServerConfiguration.class, AadOAuth2ClientConfiguration.class })
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
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.authentication.logout.LogoutHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.azure.spring.cloud.autoconfigure.aad.configuration.AadOAuth2ClientConfiguration;
import com.azure.spring.cloud.autoconfigure.aad.configuration.AadPropertiesConfiguration;
import com.azure.spring.cloud.autoconfigure.aad.configuration.AadResourceServerConfiguration;

/**
 * AzureAD認証を利用を前提としたSpring Securityの定義クラス。
 */
@EnableWebSecurity
@Import({ AadPropertiesConfiguration.class, AadResourceServerConfiguration.class,
    AadOAuth2ClientConfiguration.class })
public class SecurityConfiguration {

  /** ログアウトハンドラー */
  private LogoutHandler logoutHandler;

  /** {@link ServiceAdminAuthorityComplexDao} */
  @Autowired
  private LoginUserDao loginUserDao;

  /**
   * コンストラクタ<br>
   * 
   * @param logoutHandler
   *          ログアウトハンドラー
   */
  public SecurityConfiguration(LogoutHandler logoutHandler) {
    this.logoutHandler = logoutHandler;
  }

  /**
   * {@link SecurityFilterChain}をDIコンテナに登録する。<br>
   * 
   * @param http
   *          {@link HttpSecurity}
   * @return {@link SecurityFilterChain}
   * @throws Exception
   *           セキュリティ設定時に例外が発生した場合
   */
  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // 認証処理の設定
    http.authorizeHttpRequests()
        // 認証対象外のURL指定
        .mvcMatchers("/images/**", "/css/**", "/js/**", "/webjars/**")
        .permitAll()
        // 上記以外は認証が必要
        .anyRequest().authenticated()
        // 認可エラーが発生した際にハンドリングする設定
        .and().exceptionHandling().accessDeniedHandler(this.getForbiddenAccessDeniedHandler())
        // AzureAD認証はOAuth2認証を使用して設定する
        .and()
        .oauth2Login(oauth2 -> {
          oauth2.userInfoEndpoint(
                userInfo -> userInfo.oidcUserService(request -> {
                var service = getAzureAd2VeUserService();
                return service.loadUser(request);
              }));
          // 認証時に例外が発生した場合にハンドリングする設定
          oauth2.failureHandler(this.getDefaultAuthenticationFailureHandler());
        });

    // ログアウト処理の設定
    http.logout()
        .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
        .addLogoutHandler(logoutHandler)
        .invalidateHttpSession(true) // ログアウト時にセッションを無効にする
        .clearAuthentication(true) // ログアウト時に認証情報を削除する
        .deleteCookies("JSESSIONID").permitAll(); // ログアウト時にCookieを削除する

    return http.build();
  }

  /**
   * {@link DefaultAuthenticationFailureHandler}のオブジェクトを返す。
   * 
   * @return DefaultAuthenticationFailureHandlerのオブジェクト
   */
  private DefaultAuthenticationFailureHandler getDefaultAuthenticationFailureHandler() {
    return new DefaultAuthenticationFailureHandler();
  }

  /**
   * {@link ForbiddenAccessDeniedHandler}のオブジェクトを返す。
   * 
   * @return ForbiddenAccessDeniedHandlerのオブジェクト
   */
  private ForbiddenAccessDeniedHandler getForbiddenAccessDeniedHandler() {
    return new ForbiddenAccessDeniedHandler();
  }

  /**
   * {@link AzureAd2UserService}を取得する
   * 
   * @return {@link AzureAd2UserService}
   */
  private AzureAd2UserService getAzureAd2UserService() {
    return new AzureAd2UserService(loginUserDao);
  }

}

参考

Microsoft Entra 用の Spring Boot Starter の開発者ガイド - Java on Azure | Microsoft Learn