Keycloakで、SpringSecurityによるOIDC認証をする

皆さん、こんにちは。技術開発グループのn-ozawanです。
今年は辰年ですね。辰年と言えばタツノオトシゴです。タツノオトシゴのオスは、メスが産んだ卵を稚魚になるまで腹部にある袋に入れて大切に保護します。その姿はさながらオスが妊娠して出産している様です。

本題です。
前回はTypeScript言語で、Express + Passportを利用したOIDC認証を紹介しました。今回はJava言語です。SpringSecurityでOIDC認証するコードを実装したいと思います。

SpringSecurityでOIDC認証

ゴール

SpringSecurityを利用して、Keycloakに対してOIDC認証を行います。付与方式は前回と同様に「認可コードによる認証方式」になります。なお、本稿ではKeycloak側の設定などは扱いません。正しく設定されていることを前提としています。

Spring Security とは

Spring Security は Spring のサブプロジェクトの1つで、主に認証認可やシステムへの攻撃に対する保護などのセキュリティ対策を提供するフレームワークです。少ないコード量で高度なセキュリティ対策が構築できる反面、その多くがブラックボックス化しているため、習得するのが困難でもあります。人によっては「Springのサブプロジェクトの中で最も難しい」とも言われているようです。

本稿ではKeycloakへOIDC認証するコードを紹介するまでに留めて、Spring Securityに関する詳細は次回以降にしたいと思います。

OIDC認証に必要な情報を設定する

OIDC認証で必要となる情報をapplication.propertiesに定義します。

spring.security.oauth2.client.registration.keycloak.client-id=client.java
spring.security.oauth2.client.registration.keycloak.client-secret=ZqvbEgW55uqlNoO9zABpGSGGvkpQmRlP
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}

spring.security.oauth2.client.provider.keycloak.issuer-uri: http://localhost:8080/realms/myrealm
spring.security.oauth2.client.provider.keycloak.authorization-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/auth
spring.security.oauth2.client.provider.keycloak.token-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/token
spring.security.oauth2.client.provider.keycloak.user-info-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/userinfo
spring.security.oauth2.client.provider.keycloak.jwk-set-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/certs
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username

Express + Passportで設定した内容とほぼ同じなので、個別の説明は省きます。ただ、その中で注意が必要なのがspring.security.oauth2.client.registration.keycloak.redirect-uri (以降、redirect-uriと表現)です。

redirect-uri はその名の通り、認証後にリダイレクト先となるURIになります。Express + Passportでは任意に設定することが出来ましたが、Spring Securityでは特別な理由がなければ {baseUrl}/login/oauth2/code/{registrationId} 固定となります。Spring Security の決まり事です。もちろん他のURIにすることは可能ですが、その場合はその為のコードを実装する必要があります。

OIDC認証をする

Spring Security でOIDC認証を行う最小のコードは以下となります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

SecurityConfigクラスには、@EnableWebSecurityアノテーションを追加します。また、Spring Securityはサーブレットのフィルタで動作しますので、そのフィルタで動作するためにSecurityFilterChainのBeanを生成するメソッドを定義します。

authorizeHttpRequests(...)は、ユーザーがアクセスしたパスに対して、ユーザーが適切な権限を有しているかチェックするためのメソッドです。authorize -> authorize.anyRequest().authenticated()を指定することで、ユーザーは全てのパスにアクセスする際は、認証されていることが必要となります。もし、アクセスしてきたユーザーがまだ認証されていない場合は、認証プロセスが行われます。ユーザーが認証プロセスに失敗した場合は「Access Denied」となり、ユーザーはそのページを表示することが出来ません。

oauth2Login(...)は、OIDC認証を行うためのメソッドです。Customizer.withDefaults()を指定することで、Spring Securityのデフォルト設定で動作することを示します。もし、カスタマイズが必要な場合は、Customizer.withDefaults()のところに別途実装をすることになります。例えば、redirect-uriを{baseUrl}/login/oauth2/code/{registrationId}以外のURIにしたい場合は、以下のように実装します。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(oauth2 -> 
        oauth2.redirectionEndpoint(redirection -> redirection.baseUri("/my_redirect"))
      );

    return http.build();
  }
}

認証したユーザーの情報を取得する方法

認証したユーザーの情報やIDトークンは以下のようにして取得することが出来ます。

DefaultOidcUser oidcUser = (DefaultOidcUser) SecurityContextHolder
  .getContext()
  .getAuthentication()
  .getPrincipal();

// ログインしたユーザーの名前を取得
model.addAttribute("displayName", oidcUser.getFullName());

SecurityContextHolderは認証されたユーザーの情報を保持します。これはSpring Securityがセッション情報として保持しています。SecurityContextHolderには1つのコンテキスト情報があり、そのコンテキスト情報にユーザーの認証情報(Authentication)が保持されています。この認証情報から、getPrincipal()メソッドを呼ぶことで、ユーザー情報を取得することが出来ます。

アクセストークンを取得する方法

認証時に取得したアクセストークンは以下のように取得することが出来ます。

@Service
public class OAuth2TokenService {

  @Autowired
  private OAuth2AuthorizedClientService clientService;

  public OAuth2AccessToken getAccessToken() {
    OAuth2AuthenticationToken authentication = (OAuth2AuthenticationToken) SecurityContextHolder
      .getContext()
      .getAuthentication();

    OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
      authentication.getAuthorizedClientRegistrationId(),
      authentication.getName()
    );

    return authorizedClient.getAccessToken();
  }
}

OAuth2AuthorizedClientServiceは認証済みのクライアントを提供するサービスです。SecurityContextHolderに保持されている認証情報を元に、OAuth2AuthorizedClientServiceから認証済みクライアントOAuth2AuthorizedClientを受け取り、アクセストークンを取得することが出来ます。

おわりに

Spring Securityはコード量が少なくて驚きます。authorizeHttpRequestsoauth2Loginの2ステップで、複雑なシーケンスが必要なOIDC認証が出来てしまうなんて、なんだか魔法にかかった気分ですね。ただし、便利な反面、デメリットもあります。OIDC認証で必要な多くのロジックがブラックボックス化しているため、エラーなどが発生するとその原因究明が困難になります。

次回はそのブラックボックスを少しでも紐解きたいと思います。

ではまた。

Recommendおすすめブログ