SpringSecurity + OpenAM + OpenIDConnect

SpringSecurity OpenIDConnectでOpenAMとの連携を試します。

/privateは認証を必要とするエンドポイントで、/publicは認証が不要なエンドポイントです。
また、ログは可能な限り出力する方針とします。SpringSecurityのデバッグログに加えてRestTemplateのリクエストとレスポンスもログに出力します。

OAuth 2.0とOpenID Connectについては以下の記事がとてもわかりやすいです。

https://logmi.jp/tech/articles/322839

1. OpenAMの実行と設定

まずはOpenAMを実行します。前回実行していたら不要です。

1
docker run -h openam-01.example.com -p 8080:8080 --name openam-01 openidentityplatform/openam:14.6.5
  1. パスワードの設定しTopLevelのレルムにOAuth2のアプリケーションを作成します。名前は openid_test、パスワードは password としました。
  2. Redirection URIshttp://localhost:9000/login/oauth2/code/openam を追加します。
  3. Scopeemailopenid を追加します。

2. アプリケーションの作成

application.yml

まずは設定ファイル。8080ポートはOpenAMが使っているので9000にしました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
server:
port: 9000

spring:
security:
oauth2:
client:
registration:
openid_test:
client-id: openid_test
client-secret: password
authorization-grant-type: authorization_code
scope: email,openid
redirect-uri: http://localhost:9000/login/oauth2/code/openid_test
client-authentication-method: Basic
client-name: clientName
provider:
openid_test:
authorization-uri: http://localhost:8080/openam/oauth2/authorize
user-info-uri: http://localhost:8080/openam/oauth2/userinfo
token-uri: http://localhost:8080/openam/oauth2/access_token
jwk-set-uri: http://localhost:8080/openam/oauth2/connect/jwk_uri
user-name-attribute: sub
user-info-authentication-method: authorization_code

# http://localhost:9000/private

logging:
level:
root: DEBUG

CustomOidcUserService.java

ログを出力するように、OidcUserServiceOauth2UserServiceを上書きします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Slf4j
@RequiredArgsConstructor
@Component
public class CustomOidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {

private final OidcUserService oidcUserService= new OidcUserService();
private final CustomOAuth2UserService delegate;

@PostConstruct
public void postConstruct() {
oidcUserService.setOauth2UserService(delegate);
}

@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
log.info("CustomOAuth2UserService.loadUser");
OAuth2AccessToken accessToken = userRequest.getAccessToken();
log.info("accessToken: " + accessToken.getTokenValue());

// Delegate to the default implementation for loading a user
var oidcUser1 = oidcUserService.loadUser(userRequest);

// Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

var oidcUser2 = new DefaultOidcUser(oidcUser1.getAuthorities(), oidcUser1.getIdToken(), oidcUser1.getUserInfo(),"sub");
return oidcUser2;
}
}

SecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {

private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

private final CustomOAuth2UserService customOAuth2UserService;

private final CustomOidcUserService customOidcUserService;

@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
var accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
var tokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
var restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
restTemplate.setInterceptors(List.of(new RestTemplateLoggingInterceptor()));

accessTokenResponseClient.setRestOperations(restTemplate);
return accessTokenResponseClient;
}

/*
http://localhost:9000/private
*/

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.oauth2Login()
.successHandler(customAuthenticationSuccessHandler)
.tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient()).and()
.userInfoEndpoint().userService(customOAuth2UserService).and()
.userInfoEndpoint().oidcUserService(customOidcUserService).and()
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.mvcMatchers("/", "/public").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}

ほかは、前回のOAuth2と同じです。

処理の流れ

処理の流れは以下のとおりです。

  1. ブラウザで http://localhost:9000/private にアクセスする
  2. アプリケーションからOpenAMへのリダイレクト指示が返るので、OpenAMのログインページにリダイレクトする
    http://localhost:8080/openam/oauth2/authorize?response_type=code&client_id=openid_test&scope=email%20openid&state=SpFiRmKQyJG42n6HrTDr4E6aHBt3r5l7GDNEBikzM2s%3D&redirect_uri=http://localhost:9000/login/oauth2/code/openid_test&nonce=2mPPqOqM1fLpudfAnyUWMU3RKx4pCL1k12mEp9MTHoY
  3. ID、パスワードを入力しOpenAMにログインする
  4. OpenAMのレスポンスからアプリケーションへのリダイレクト指示が返るので、アプリケーションにリダイレクトする
    http://localhost:9000/login/oauth2/code/openid_test?code=d009a213-a43c-4314-b6e8-9cb565eb5f2a&scope=openid%20email&iss=http%3A%2F%2Flocalhost%3A8080%2Fopenam%2Foauth2&state=SpFiRmKQyJG42n6HrTDr4E6aHBt3r5l7GDNEBikzM2s%3D&client_id=openid_test
  5. アプリケーションがOpenAMのAPIを呼び出してアクセストークンとIDトークンを取得する
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ===========================request begin================================================
    URI : http://localhost:8080/openam/oauth2/access_token
    Method : POST
    Headers : [Accept:"application/json;charset=UTF-8", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8", Authorization:"Basic b3BlbmlkX3Rlc3Q6cGFzc3dvcmQ=", Content-Length:"152"]
    Request body: grant_type=authorization_code&code=d009a213-a43c-4314-b6e8-9cb565eb5f2a&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Flogin%2Foauth2%2Fcode%2Fopenid_test
    ==========================request end================================================
    ============================response begin==========================================
    Status code : 200 OK
    Status text :
    Headers : [X-Frame-Options:"SAMEORIGIN", Cache-Control:"no-store", Date:"Sat, 11 Jun 2022 08:34:13 GMT", Accept-Ranges:"bytes", Server:"Restlet-Framework/2.4.0", Vary:"Accept-Charset, Accept-Encoding, Accept-Language, Accept", Pragma:"no-cache", Content-Type:"application/json", Transfer-Encoding:"chunked", Keep-Alive:"timeout=20", Connection:"keep-alive"]
    Response body: {"access_token":"c170e237-39ee-4901-941f-589d781f87b9","scope":"openid email","id_token":"eyJ0eXAiOiJKV1QiLCJraWQiOiJ6aEJvZmJadytqa1paalhzMjhmR2Z6eFpnTTg9IiwiYWxnIjoiUlMyNTYifQ.eyJhdF9oYXNoIjoidVZrSXRFZlpJQWJheGVJZW9mOTlWUSIsInN1YiI6IlRhcm8iLCJhdWRpdFRyYWNraW5nSWQiOiI1YzY1MWRkNC1hOTc2LTQ3YzAtOGE3Ni1mNWUyYjcyMWRjODgtMTAwMjAwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL29wZW5hbS9vYXV0aDIiLCJ0b2tlbk5hbWUiOiJpZF90b2tlbiIsIm5vbmNlIjoiMm1QUHFPcU0xZkxwdWRmQW55VVdNVTNSS3g0cENMMWsxMm1FcDlNVEhvWSIsImF1ZCI6Im9wZW5pZF90ZXN0IiwiY19oYXNoIjoiYkVqMnkwYngyRmhKOHpqblNDY0l2USIsIm9yZy5mb3JnZXJvY2sub3BlbmlkY29ubmVjdC5vcHMiOiI3NDE2YThkNC1kZjE1LTRhMWEtOWQ0Mi0xOWY4MjhiNDdlMjEiLCJhdXRoOnRpbWUiOjE2NTQ5MzUzODYsImF6cCI6Im9wZW5pZF90ZXN0IiwicmVhbG0iOiIvIiwiZXhwIjoxNjU0OTQwMDUzLCJ0b2tlblR5cGUiOiJKV1RUb2tlbiIsImlhdCI6MTY1NDkzNjQ1M30.fpIw-9ZaYqIteeyU2QVAh6yvpm1A-JxhihtIFXfRpPriSb1cdYh1glsH0LqVDL9TurPZX56rq0UFvgwfDI9O45j48aTIfslUumI5tVWyVm4E5H0vDVuvxVo7ZYHyTwPkAKn2BTfItS0c4-7Nf_RTGXewwbjZefPgQEYEzhtK4ycvOkNEXapR6lvXNTPMZiC9uT-wyWl3_IfEjQFaa7lY2H4kIf4ochuLENlwC4f7nOGDTYZLlu8AX62hL_HIb-Rf6J-8N7AeNHRQHjK_H4cslltQSMROdZ7SYibWTU7IOEx4ILG2bTnPyj-wGj0V9WAs6giCsBt-r-eIqplpwiymCg","token_type":"Bearer","expires_in":3599,"nonce":"2mPPqOqM1fLpudfAnyUWMU3RKx4pCL1k12mEp9MTHoY"}
    =======================response end=================================================
  6. アプリケーションがOpenAMのAPIを呼び出してトークン検証用のJWKを取得する
    GET http://localhost:8080/openam/oauth2/connect/jwk_uri
  7. アプリケーションがOpenAMのAPIを呼び出してユーザー情報を取得する
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ===========================request begin================================================
    URI : http://localhost:8080/openam/oauth2/userinfo
    Method : GET
    Headers : [Accept:"application/json", Authorization:"Bearer c170e237-39ee-4901-941f-589d781f87b9", Content-Length:"0"]
    Request body:
    ==========================request end================================================
    ============================response begin==========================================
    Status code : 200 OK
    Status text :
    Headers : [X-Frame-Options:"SAMEORIGIN", Date:"Sat, 11 Jun 2022 08:34:13 GMT", Accept-Ranges:"bytes", Server:"Restlet-Framework/2.4.0", Vary:"Accept-Charset, Accept-Encoding, Accept-Language, Accept", Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Keep-Alive:"timeout=20", Connection:"keep-alive"]
    Response body: {"sub":"Taro","email":"taro@example.com"}
    =======================response end=================================================
  8. もともとアクセスしようとしていた http://localhost:9000/private にリダイレクトする

ソースコード

https://github.com/pecolynx/spring-security-openam-oidc