はじめに
Spring Securityを使っているアプリで、OpenID Connect(以下OIDC)を別のものに移行する機会があり、その際にOIDC周りの設定について見直しました。そのときに得られてた知見のまとめです。
OIDCの仕様についてはここでは特に解説はしません。
Auth屋さんのスライドや書籍が参考になりますので、気になる方はこちらもどうぞ。
まとめ
項目 | 概要 |
---|---|
application.yamlでの設定できること | OIDCの接続などの基本的なことの設定 |
JWTの署名のアルゴリズム | ID tokenの署名のアルゴリズム |
SecurityFilterChain | セキュリテイの設定をするためのオブジェクト |
OIDC user service | ID tokenからログインユーザを生成する部分 |
Authentication success handler | 認証処理が成功したときの処理 |
Authentivation failure handler | 認証処理が失敗したときの処理 |
application.ymlで設定できること
yamlから設定できる内容の詳細は公式ドキュメントあります。
ほとんどの場合、OIDCの認可コードフローを使うので、その場合は下記のようになります。
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
authorization-grant-type: authorization_code
redirect-url: {baseUrl}/login/oauth2/code/{registrationId}
scope:
- openid
- profile
provider:
okta:
authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
user-name-attribute: sub
jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys
ログイン方法を複数提供したい場合は、registartion,providerの組を、複数設定できます。
spring:
security:
oauth2:
client:
registration:
okta:
...
google:
...
provider:
okta:
...
google:
well-known endpoint
OIDCの仕様で、/.well-known/openid-configuration
から取得できます。
例としてyahooのものを一部抜粋したものをあげます。
https://auth.login.yahoo.co.jp/yconnect/v2/.well-known/openid-configuration
{
"issuer": "https://auth.login.yahoo.co.jp/yconnect/v2",
"authorization_endpoint": "https://auth.login.yahoo.co.jp/yconnect/v2/authorization",
"token_endpoint": "https://auth.login.yahoo.co.jp/yconnect/v2/token",
"userinfo_endpoint": "https://userinfo.yahooapis.jp/yconnect/v2/attribute",
"jwks_uri": "https://auth.login.yahoo.co.jp/yconnect/v2/jwks",
}
ここにあるissuerのURLを、providerのissuer-uriを設定すれば、このwell-known endpointから取得して、必要な設定をしてくれます。provider側の設定を減らすことができます。
provider:
yahoo:
issuer-uri: https://auth.login.yahoo.co.jp/yconnect/v2
また、well-known endpointがend_session_endpointをもっている場合、OidcClientInitiatedLogoutSuccessHandler
を設定することで、OIDC側も含めてログアウト処理を実装できます。
注意するのは、issuer-uriにアクセスできない場合は、このやり方はつかえません。
spring securityのissuer-uriへのアクセスはRestTemplateが使われていますが、隠蔽されているので、この部分だけにproxyを適用することが難しいです。
max_age
OIDC側の仕様で、ログインしてから再認証が必要になる経過時間を設定できます。
max_age
OPTIONAL. Authentication Age の最大値. End-User が OP によって明示的に認証されてからの経過時間の最大許容値 (秒). もし経過時間がこの値より大きい場合, OP は End-User を明示的に再認証しなければならない (MUST). (max_age リクエストパラメータは OpenID 2.0 PAPE [OpenID.PAPE] の max_auth_age リクエストパラメータに相当する) max_age が指定された場合, 発行される ID Token は auth_time Claim を含まねばならない (MUST).
この仕組みによって、自分たちのアプリのセッションタイムアウトまでの時間を制御できます。
一番簡単な使い方は、authorization-uriでの指定です。
provider:
yahoo:
authorization-url: https://auth.login.yahoo.co.jp/yconnect/v2/authorization?max_age=3600
prompt
OIDCの仕様で、再認証やアカウントの切り替え・確認を要求するためのパラメタです。
prompt=login
とすれば、認証を再度要求できます。決済やアカウント情報の変更など、高いセキュリテイが要求される場面で使えます。
prompt
OPTIONAL. Authorization Server が End-User に再認証および同意を再度要求するかどうか指定するための, スペース区切りの ASCII 文字列のリスト. 以下の値が定義されている.
login
Authorization Server は End-User を再認証するべきである (SHOULD). 再認証が不可能な場合はエラーを返す (MUST). 典型的なエラーコードは login_required である.
こちらも、authorization-uriでの設定が一番簡単なやり方です。
provider:
yahoo:
authorization-url: https://auth.login.yahoo.co.jp/yconnect/v2/authorization?prompt=login
JWTの署名のアルゴリズム
ID tokenは署名付きJWT(JWS)で、署名のアルゴリズムの設定方法です。
ECDSAでキーペアをつくることが標準になりつつあるので、ES256を設定したい場合は下記のようにすればいいです。
@Bean
JwtDecoderFactory<ClientRegistartion> decoderFactory() {
OidcIdTokenDecoderFactory decoderFactory = new OidcIdTokenFactory();
decoderFactory.setJwsAlgorithmResolver(clientRegistration -> SignatureAlgorithm.ES256);
return decoderFactory;
}
SecurityFilterChain
requestに対して、どのようなセキュリテイの処理をするかを決められるオブジェクトです。
OIDCの場合、下記のようになります。
@Bean
SecurityFilterChain oidc(HttpSecurity http,
SampleUserService userService,
SampleAuthenticationSuccessHandler successHandler,
SampleAuthenticationFailuerHandler failureHandler) {
http.authorizeHttpRequest(authz ->
authz.anyRequest().authenticated())
.oauth2Login(login -> login
.userInfoEndPoint(userinfo -> userinfo.oidcUserService(userService))
.successHandler(successHandler)
.failureHandler(failureHandler)
);
return http.build();
}
URLごとにセキュリティの設定を変えたい場合は、HttpSecurity#securityMatcherなどで、URLパターンを指定すればいいです。
http.securityMatcher("/okta/**")
....;
http.securityMatcher("/google/**")
...;
OIDC user service
ID tokenからログインユーザを生成する部分です。Spring Securityでデフォルトの挙動を実現するためのサービスクラスがあります。ID tokenに加えて、自分たちのDBの情報も付け加えるなど、追加の処理がいる場合は、OAuth2UserService<OidcUserRequest, OidcUser>
を実装したクラスを作成すればいいです。また、ログインユーザを表すオブジェクトは、OidcUserを実装していればよいです。
デフォルトで問題ない場合でも、委譲の形でラップしておくとその後に変更があっても対応しやすいです。
public class SampleOidcUser implements OidcUser, Serializable {
private final OidcUser oidcUser;
public SampleOidcUser(OidcUser oidcUser) {
this.oidcUser = oidcUser;
}
...
// OidcUserに必要な実装はthis.oidcUserのものをそのまま使う
@Override
public Map<String, Object> getAttributes() {
return oidcUser.getAttributes();
}
...
}
// DIしやすくするためBeanに登録
@Bean
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
return new OidcUserService();
}
@Service
public class SampleUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private final OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;
// 実際にはOidcUserServiceのインスタンスを使う想定
// 変更に強くするために、型の指定はゆるめている
public SampleUserService(OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService) {
this.oidcUserService = oidcService;
}
@Override
public OidcUser loadUser(OidcUserRequest userRequest) {
// ID tokenを取得して、OidcUserを作成
OidcUser oidcUser = oidcUserService.loadUser(userRequest);
// 必要ならここで追加処理をいれる
...
// 作成したログインユーザクラスを返す
return new SampleOidcUser(oidcUser);
}
}
Authentication success handler
認証成功後の処理です。Spring securityのデフォルトの挙動では、認証前に最初にアクセスしたURLへリダイレクトします。
SavedRequestAwareAuthenticationSuccessHandlerがそのためのhandlerです。
この挙動を維持したまま、追加の処理をいれたい場合、委譲の形でAuthticationSuccessHandlerを実装するといいでしょう。
public class SampleAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final SavedRequestAwareAuthenticationSuccessHandler delegatedHandler;
...
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 必要ならここに追加処理をいれる
delegatedHandler.onAuthentication(request, response, authentication);
}
}
RequestCache
SavedRequestAwareAuthenticationSuccessHandlerは、RequestCacheに最初のrequestを保持しています。
この保持したものをつかって、最初のURLへのリダイレクトを実現しています。そこの設定を変えたい場合は、下記のようにします。
@Bean
RequestCache requetCache() {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setSessionAttrName("sample_saved_request")
return requetCache;
}
@Bean
SavedRequestAwareAuthenticationSuccessHandler(RequestCache requestCache) {
SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
handler.setRequestCache(requestCache);
return handler;
}
Authentication failure handler
認証に失敗したときの処理です。認証処理中に、AuthenticationExceptionが投げられると、このhandlerに入ってきます。自分たちの処理で例外をなげてfailure handlerでまとめて処理させたい場合は、AuthenticationExceptionや、それを継承した例外を投げればいいです。
ただし、Success handler内でAuthenticationExceptionを投げても、failure handlerには入ってこないです。ログインページや、専用のエラーページにリダイレクトさせることが多いです。
SimpleUrlAuthenticationFailureHandlerを使えば簡単にできます。
このクラスを継承してもいいですが、委譲とDIの形にしておくと変更やテストがやりやすいです。
public class SampleAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final SimpleUrlAuthenticationFailureHandler delegatedHandler;
...
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 必要ならここに追加の処理をいれる
delegatedHandler.onAuthenticationFailure(request, response, exception);
}
}