Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編

皆さん、こんにちは。技術開発グループのn-ozawanです。
今年は閏年だったんですね。閏年の条件は少し複雑で、①4で割り切れる年は閏年、②しかし100で割り切れる年は平年、③ただし400で割り切れる年は閏年、になります。

本題です。
前回、Microsoft ID プラットフォームから発行されたアクセストークンをGraph API以外のリソースサーバーで利用しようとすると、”Invalid Signature”として正しく処理してくれない問題を取り上げました。今回は、この問題に対する対処方法をSpring Securityで解決したいと思います。”Invalid Signature”に関する原因や理由などについては、前回を参照してください。

Graph API以外のアクセストークンを発行する方法

ゴール

解決の方針は前回と同じです。1回目のシーケンスでは、OIDC認証+Graph API用のアクセストークンを発行して貰います。2回目のシーケンスでは、OAuth2.0により自前で用意したリソースサーバーへのアクセストークンを発行して貰います。

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

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

# 1回目のシーケンス(OIDC認証+Graph API用アクセストークン発行)
spring.security.oauth2.client.registration.microsoftonline.client-name=microsoftonline
spring.security.oauth2.client.registration.microsoftonline.client-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline.client-secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline.provider=microsoftonline
spring.security.oauth2.client.registration.microsoftonline.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.microsoftonline.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.provider.microsoftonline.issuer-uri: https://login.microsoftonline.com/{tenantId}/v2.0
spring.security.oauth2.client.provider.microsoftonline.authorization-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
spring.security.oauth2.client.provider.microsoftonline.token-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
spring.security.oauth2.client.provider.microsoftonline.user-info-uri=https://graph.microsoft.com/oidc/userinfo
spring.security.oauth2.client.provider.microsoftonline.user-name-attribute=name

# 2回目のシーケンス(OAuth2.0によるアクセストークン発行)
spring.security.oauth2.client.registration.microsoftonline4rsc.client-name=microsoftonline4rsc
spring.security.oauth2.client.registration.microsoftonline4rsc.client-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline4rsc.client-secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline4rsc.provider=microsoftonline4rsc
spring.security.oauth2.client.registration.microsoftonline4rsc.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.microsoftonline4rsc.redirect-uri={baseUrl}/authorized/{registrationId}
spring.security.oauth2.client.registration.microsoftonline4rsc.scope=offline_access,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/.default
spring.security.oauth2.client.provider.microsoftonline4rsc.issuer-uri: https://login.microsoftonline.com/{tenantId}/v2.0
spring.security.oauth2.client.provider.microsoftonline4rsc.authorization-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
spring.security.oauth2.client.provider.microsoftonline4rsc.token-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token

1回目のシーケンスと、2回目のシーケンスに必要な情報をそれぞれ定義しています。設定内容に関しては、Express + Passport編(前々回前回)と同じなので説明を省略します。

定義する際のポイントとしては、プロパティ名の{registrationId}部分を適宜変更することです。例えば1回目シーケンスのClientIDでは、プロパティ名がspring.security.oauth2.client.registration.microsoftonline.client-idとなっています。対して、2回目シーケンスのClientIDでは、プロパティ名がspring.security.oauth2.client.registration.microsoftonline4rsc.client-idです。この赤字になっている個所により、それぞれを分けて定義しています。

SecurityFilterChainを生成する

以下は、1回目のシーケンスと、2回目のシーケンスの両方を行うSecurityFilterChainを生成するコードです。

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  // 1回目のシーケンス(OIDC認証+Graph API用アクセストークン発行)
  http
    .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
    .oauth2Login(oauth2 ->
      oauth2
        .loginPage(
          OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/microsoftonline"
        )
    );

  // 2回目のシーケンス(OAuth2.0によるアクセストークン発行)
  http
    .oauth2Client(Customizer.withDefaults())
    .addFilterAfter(
      this.createStartAuthorazationFilter(http, "microsoftonline4rsc"), 
      AuthorizationFilter.class
    );

  return http.build();
}

1回目のシーケンスではoauth2.loginPage()にて、ログインに使用する設定を指定しています。先ほど、application.propertiesにOIDC認証およびOAuth2.0で必要となる設定として、microsoftonlinemicrosoftonline4rscの2つを定義しました。もし、oauth2.loginPage()を省略すると、SpringSecurityはどちらの設定で認証すれば良いのか判断が出来ないため、以下の画面を表示してユーザーに選択させる動作となります。今回はmicrosoftonlineでOIDC認証したいので、oauth2.loginPage()で指定しています。

2回目のシーケンスではhttp.oauth2Client()でOAuth2.0を行うように指定しています。http.oauth2Client()http.oauth2Login()と同様に、/oauth2/authorization/{registrationId}へのリダイレクトをトリガーにシーケンスが開始されます。http.oauth2Login()の詳細な動作については、以前掲載した「Spring Security の基本とOIDC認証時の動作」を参照ください。

2回目シーケンスのregistrationIdmicrosoftonline4rscですので、2回目のシーケンスを開始するには、/oauth2/authorization/microsoftonline4rscへリダイレクトする必要があります。このリダイレクトする処理は自前で実装する必要があります。http.addFilterAfter()にて、/oauth2/authorization/microsoftonline4rscへリダイレクトするフィルタを、AuthorizationFilterの後ろに追加しています。

2回目のシーケンスを開始するフィルタ

2回目のシーケンスを開始するためのフィルタを実装します。

private Filter createStartAuthorazationFilter(HttpSecurity http, String registrationId) {
  return new OncePerRequestFilter() {
    @Override
    protected void doFilterInternal(
      HttpServletRequest request,
      HttpServletResponse response,
      FilterChain filterChain
    ) throws ServletException, IOException {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

      // 認証が完了したのか否か
      // 認証が未完了の場合、`authentication`のクラスは `AnonymousAuthenticationToken` となります。
      // 認証が完了した場合は、`OAuth2AuthenticationToken`になります。
      // クラスの違いで認証が完了したのか否かを判断しています。
      if (authentication instanceof OAuth2AuthenticationToken) {

        // 認可済みのクライアントを取得します。
        // もし、取得できない場合(authorizedClient == null)、まだアクセストークンが取得出来ていません。
        OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
          registrationId,
          authentication.getName()
        );

        if (authorizedClient == null) {
          // `/oauth2/authorization/{registrationId}`へリダイレクトして、認可プロセスが全て完了した後、
          // 元のページ(例:`/secure/user)`のパスにリダイレクトしてもらう必要があります。
          // 以下のコードにより、Spring Securityに対して、元のページへのリクエスト情報を保持しておくことが出来ます。
          RequestCache requestCache = http.getSharedObject(RequestCache.class);
          requestCache.saveRequest(request, response);

          // リダイレクト
          AuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint(
            OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + registrationId
          );
          entryPoint.commence(request, response, null);
        } else {
          filterChain.doFilter(request, response);
        }
      } else {
        filterChain.doFilter(request, response);
      }
    }
  };
}

フィルタでは、認証済み、かつ、自前で用意したリソースサーバーへのアクセストークンが未発行の場合、/oauth2/authorization/microsoftonline4rscへリダイレクトさせています。リダイレクトする際は、リクエスト内容を一時保存してから、リダイレクトするようにしています。

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

アクセストークンを取得するコードです。

@Service
public class OAuth2TokenService {

  @Autowired
  private OAuth2AuthorizedClientService clientService;

  public OAuth2AccessToken getAccessToken() {
    return getAccessToken(null);
  }

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

    OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
      registrationId == null ? authentication.getAuthorizedClientRegistrationId() : registrationId,
      authentication.getName()
    );

    return authorizedClient.getAccessToken();
  }
}

clientService.loadAuthorizedClient(...)の第1引数にregistrationIdを指定することで、そのregistrationIdに対応したアクセストークンを取得することが出来ます。なお、authentication.getAuthorizedClientRegistrationId()は認証で使用されたregistrationIdを返却します。今回の例で言うとmicrosoftonlineになります。

おわりに

1回目のシーケンスから、2回目のシーケンスを開始させる方法が見つからず、大分、苦戦しました。Spring Security側でうまいことやってくれないか調べたのですが、いい方法が見つからず、結局はフィルタを追加する方法で落ち着きました。ゴールに記載したようなシーケンス図がないと、Spring Securityのコードを読み解くのは難しいですね。

ではまた。

Recommendおすすめブログ