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で必要となる設定として、microsoftonline
とmicrosoftonline4rsc
の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回目シーケンスのregistrationId
はmicrosoftonline4rsc
ですので、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のコードを読み解くのは難しいですね。
ではまた。