これは、なにをしたくて書いたもの?
前に、Spring SecurityでKeycloakのクライアントスコープを使った認可を試してみました。
Spring SecurityのOAuth 2.0サポートで、Keycloak 19.0のクライアントスコープを使った認可を試す - CLOVER🍀
今度は、Realmロールを使った認可を試してみたいと思います。
Realmロール
Realmロールを使用することで、ユーザーに対する権限の割り当てを表現することができます。
Server Administration Guide / Assigning permissions using roles and groups
Realmロールを作成して
Server Administration Guide / Assigning permissions using roles and groups / Creating a realm role
ユーザーに割り当てて使用します。
Server Administration Guide / Assigning permissions using roles and groups / Assigning role mappings
そして、クライアントスコープの設定でアクセストークンやIDトークンのクレーム、User Infoエンドポイントのレスポンスに含めることもできます。
You can use most OIDC mappers to control where the claim gets placed. You opt to include or exclude the claim from the id and access tokens by adjusting the Add to ID token and Add to access token switches.
今回は、こちらを使ってRealmロールをIDトークンに含め、アプリケーションでの認可に使いたいと思います。
なお、RealmロールをIDトークンやアクセストークン、およびUser Infoエンドポイントのレスポンスに含めるかどうかの制御は、
roles
クライアントスコープのrealm roles
マッパーで行います。
※使用したKeycloak 19.0.1の問題なのかはわかりませんが、ハマりどころがありました
ところで今回は扱いませんが、クライアントにもロールを割り当てられるようです。
Server Administration Guide / Assigning permissions using roles and groups / Client roles
それでは、進めていきましょう。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.4 2022-07-19 OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04) OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.4, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-125-generic", arch: "amd64", family: "unix"
Keycloakは19.0.1を使い、172.17.0.2で動作しているものとします。
$ bin/kc.sh --version Keycloak 19.0.1 JVM: 17.0.4 (Eclipse Adoptium OpenJDK 64-Bit Server VM 17.0.4+8) OS: Linux 5.4.0-125-generic amd64
Keycloakの管理ユーザーは、起動時に作成。
$ KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=password bin/kc.sh start-dev
お題
KeycloakのRealm内に、以下の2種類のユーザーとRealmロールの割り当てを行います。
token-role-user
…token
というRealmロールを割り当てるmessage-role-user
…message
というRealmロールを割り当てる
名前は適当ですが、この名前に応じた認可制御を行います。
RealmロールはIDトークンに含め、アプリケーション側で利用する形にします。
Keycloakの準備
まずは、Keycloakの準備をしておきます。
以下の情報、手順で作成していきます。
- Realm
- Realmの名前を
sample-realm
として作成
- Realmの名前を
特に、デフォルト設定から変更するところがわかるようにしたいと思います。
- クライアント(Client)
- 作成時
- 作成後
- Settingsタブ
- Root URLを
http://localhost:8080
に設定 - Valid redirect URIsに
http://localhost:8080/login/oauth2/code/keycloak
を設定- ※アプリケーション側の
spring.security.oauth2.client.registration.keycloak.redirect-uri
に指定する値と同等
- ※アプリケーション側の
- 保存
- Root URLを
- Settingsタブ
また、CredentialsタブのClient secretの値を控えておきます。
- ユーザー(User)
token-role-user
の作成- 作成時
- Usernameを
token-role-user
として作成
- Usernameを
- 作成後
- Credentialsタブ
- パスワードを設定
- TemporaryをOffに設定
- 保存
- Credentialsタブ
- 作成時
message-role-user
の作成- 作成時
- Usernameを
message-role-user
として作成
- Usernameを
- 作成後
- Credentialsタブ
- パスワードを設定
- TemporaryをOffに設定
- 保存
- Credentialsタブ
- 作成時
Realmロールは次で作成、ユーザーへの割り当てを行います。
Realmロールの作成とユーザーへの割り当て
では、Realmロールを作成します。
今回は、以下の2つのRealmロールを作成します。
token
message
メニューの「Realm roles」を選択して
「Create role」を選択して、Realmロールを作成します。
入力したのは、今回はNameのみです。
作成したRealmロールは、token-role-user
ユーザーに割り当てます。ユーザーを選択して、「Role mapping」タブを選択。
付与するRealmロールを選択して、「Assign」。
message
ロールも、同様にmessage-role-user
ユーザーに割り当てます。
ここで、クライアントスコープを確認します。「Client scopes」から
roles
スコープの「Mappers」タブを選択します。
「Token Claim Name」を見るとrealm_access.roles
と書かれています。
「Token Claim Name」はトークン内のクレームにどのような名前で含めるかを示しています。.
はネストしたオブジェクトを指すので、
realm_access
内にroles
がある、ということになりますね。
ここで「Add to ID token」を1度「Off」にして「Save」、そしてもう1度「On」にして「Save」と繰り返します。
※「Add to userinfo」でもOKみたいです
Keycloak 19.0.1の問題なのかもしれませんが、なぜこれが必要は後で書きます。
これで、Keycloak側の準備は完了です。
Spring Bootプロジェクトを作成する
では、アプリケーションを作成していきます。依存関係にweb
とoauth2-client
を指定して、プロジェクトを作成。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=2.7.3 \ -d javaVersion=17 \ -d name=spring-security-oauth2-client-realm-role-authz-example \ -d groupId=org.littlewings \ -d artifactId=spring-security-oauth2-client-realm-role-authz-example \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.keycloak.spring \ -d dependencies=web,oauth2-client \ -d baseDir=spring-security-oauth2-client-realm-role-authz-example | tar zxvf -
プロジェクト内に移動。
$ cd spring-security-oauth2-client-realm-role-authz-example
<properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
自動生成されたソースコードは、削除しておきます。
$ rm src/main/java/org/littlewings/keycloak/spring/SpringSecurityOauth2ClientRealmRoleAuthzExampleApplication.java src/test/java/org/littlewings/keycloak/spring/SpringSecurityOauth2ClientRealmRoleAuthzExampleApplicationTests.java
ソースコードを作成しましょう。
Spring SecurityのOAuth 2.0に関する設定。こちらは、また後で説明していきます。
src/main/java/org/littlewings/keycloak/spring/OAuth2ClientSecurityConfig.java
package org.littlewings.keycloak.spring; import java.util.HashSet; import java.util.Map; import java.util.Set; import com.nimbusds.jose.shaded.json.JSONArray; import com.nimbusds.jose.shaded.json.JSONObject; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.web.SecurityFilterChain; @EnableWebSecurity public class OAuth2ClientSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/auth/**").authenticated() .mvcMatchers("/role/message").hasRole("message") .mvcMatchers("/role/token").hasRole("token") .anyRequest().permitAll()) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .oidcUserService(oidcUserService()))) //.userAuthoritiesMapper(userAuthoritiesMapper()))) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/")); return http.build(); } OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() { OidcUserService delegate = new OidcUserService(); return userRequest -> { OidcUser oidcUser = delegate.loadUser(userRequest); System.out.println("access_token value = " + userRequest.getAccessToken().getTokenValue()); System.out.println("id_token value = " + oidcUser.getIdToken().getTokenValue()); System.out.println("claims = " + oidcUser.getClaims()); System.out.println("id_token claims = " + oidcUser.getIdToken().getClaims()); System.out.println("userInfo claims = " + oidcUser.getUserInfo().getClaims()); Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities()); Map<String, Object> claims = oidcUser.getIdToken().getClaims(); // Map<String, Object> claims = oidcUser.getClaims(); JSONObject realmAccess = (JSONObject) claims.get("realm_access"); JSONArray roles = (JSONArray) realmAccess.get("roles"); roles.forEach(role -> { String roleName = (String) role; mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleName)); }); System.out.println("authorities = " + mappedAuthorities); return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo()); }; } GrantedAuthoritiesMapper userAuthoritiesMapper() { return authorities -> { System.out.println("authorities = " + authorities); Set<GrantedAuthority> mappedAuthorities = new HashSet<>(authorities); authorities.forEach(authority -> { if (authority instanceof OidcUserAuthority) { System.out.println("oidc user authority = " + authority); OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority; OidcIdToken idToken = oidcUserAuthority.getIdToken(); Map<String, Object> claims = idToken.getClaims(); System.out.println("oidc user authority claims = " + claims); JSONObject realmAccess = (JSONObject) claims.get("realm_access"); JSONArray roles = (JSONArray) realmAccess.get("roles"); roles.forEach(role -> { String roleName = (String) role; mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleName)); }); } /* else if (authority instanceof OAuth2UserAuthority) { OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority) authority; Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes(); // ... } */ }); System.out.println("authorities = " + mappedAuthorities); return mappedAuthorities; }; } }
先に、パスの認証・認可設定だけ取り上げておきましょう。
/auth
配下は認証必須、/role/message
はmessage
(ROLE_message
)を権限として要求し、/role/token
は同じくtoken
(ROLE_token
)
ロールを要求します。それ以外のパスは、認証なしでアクセスできるようにしています。
http .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/auth/**").authenticated() .mvcMatchers("/role/message").hasRole("message") .mvcMatchers("/role/token").hasRole("token") .anyRequest().permitAll())
RestController。
src/main/java/org/littlewings/keycloak/spring/RoleAuthzController.java
package org.littlewings.keycloak.spring; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class RoleAuthzController { @GetMapping public String index() { return "Hello, Spring Security Application!!"; } @GetMapping("token") public Object token(OAuth2AuthenticationToken authenticationToken) { return authenticationToken != null ? authenticationToken : "not login"; } @GetMapping("auth/message") public String authenticatedMessage() { return "Authenticated!!"; } @GetMapping("role/message") public String authorizedMessage() { return "Authorized!!"; } @GetMapping("role/token") public OAuth2AuthenticationToken authorizedToken(OAuth2AuthenticationToken authenticationToken) { return authenticationToken; } }
以下の3つのメソッドは、それぞれ認証必須、message
ロール、token
ロールを要求するメソッドです。
@GetMapping("auth/message") public String authenticatedMessage() { return "Authenticated!!"; } @GetMapping("role/message") public String authorizedMessage() { return "Authorized!!"; } @GetMapping("role/token") public OAuth2AuthenticationToken authorizedToken(OAuth2AuthenticationToken authenticationToken) { return authenticationToken; }
それ以外は、ログインしていなくてもアクセス可能です。
OAuth2AuthenticationToken
クラスは、Spring SecurityのOAuth 2.0サポートにおける認証情報です。
main
クラス。
src/main/java/org/littlewings/keycloak/spring/App.java
package org.littlewings.keycloak.spring; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
設定。
src/main/resources/application.properties
spring.security.oauth2.client.registration.keycloak.client-id=spring-security-client spring.security.oauth2.client.registration.keycloak.client-secret=B3lqawajjRxyZxy13dWPEGAPCCqiRRTI spring.security.oauth2.client.registration.keycloak.client-name=spring security oauth2 authorization example spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.keycloak.scope=openid spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} spring.security.oauth2.client.provider.keycloak.authorization-uri=http://172.17.0.2:8080/realms/sample-realm/protocol/openid-connect/auth spring.security.oauth2.client.provider.keycloak.token-uri=http://172.17.0.2:8080/realms/sample-realm/protocol/openid-connect/token spring.security.oauth2.client.provider.keycloak.user-info-uri=http://172.17.0.2:8080/realms/sample-realm/protocol/openid-connect/userinfo spring.security.oauth2.client.provider.keycloak.jwk-set-uri=http://172.17.0.2:8080/realms/sample-realm/protocol/openid-connect/certs spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username spring.jackson.serialization.indent_output=true #logging.level.org.springframework.security=DEBUG
Spring SecurityのOAuth 2.0を使ったクラスの掘り下げ
OAuth2ClientSecurityConfig
というクラスに認証・認可設定をまとめましたが、こちらに関する内容をもうちょっと掘り下げましょう。
片方コメントアウトされていますが、oauth2Login
→ userInfoEndpoint
で、OAuth2UserService
またはGrantedAuthoritiesMapper
を
設定しています。
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/auth/**").authenticated() .mvcMatchers("/role/message").hasRole("message") .mvcMatchers("/role/token").hasRole("token") .anyRequest().permitAll()) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .oidcUserService(oidcUserService()))) //.userAuthoritiesMapper(userAuthoritiesMapper()))) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/")); return http.build(); }
これは、以下に書かれている内容を参考に作成したもので、OAuth 2.0のシーケンス完了時にユーザーの権限をマッピングのカスタマイズを
行うものです。
Advanced Configuration / UserInfo Endpoint / Mapping User Authorities
方法としては、2つ記載があります。
- Advanced Configuration / UserInfo Endpoint / Mapping User Authorities / Using a GrantedAuthoritiesMapper
- Advanced Configuration / UserInfo Endpoint / Mapping User Authorities / Delegation-based strategy with OAuth2UserService
記載したソースコードは両方のパターンを書いており、うち片方はコメントアウトしています。
どちらのパターンも、realm_access.roles
をGrantedAuthority
にマッピングするものになります。
まずはOAuth2UserService
の委譲パターンから。
コメントアウト箇所と、System.out.println
を削除するとこんな感じになります。
@EnableWebSecurity public class OAuth2ClientSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/auth/**").authenticated() .mvcMatchers("/role/message").hasRole("message") .mvcMatchers("/role/token").hasRole("token") .anyRequest().permitAll()) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .oidcUserService(oidcUserService()))) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/")); return http.build(); } OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() { OidcUserService delegate = new OidcUserService(); return userRequest -> { OidcUser oidcUser = delegate.loadUser(userRequest); Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities()); Map<String, Object> claims = oidcUser.getIdToken().getClaims(); // Map<String, Object> claims = oidcUser.getClaims(); JSONObject realmAccess = (JSONObject) claims.get("realm_access"); JSONArray roles = (JSONArray) realmAccess.get("roles"); roles.forEach(role -> { String roleName = (String) role; mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleName)); }); return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo()); }; } }
ポイントはこちらと
.oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .oidcUserService(oidcUserService())))
IDトークンまたはOidcUser#getClaims
の結果からrealm_access.roles
に含まれる値を取得し、GrantedAuthority
に変換しているところですね。
Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities()); Map<String, Object> claims = oidcUser.getIdToken().getClaims(); // Map<String, Object> claims = oidcUser.getClaims(); JSONObject realmAccess = (JSONObject) claims.get("realm_access"); JSONArray roles = (JSONArray) realmAccess.get("roles"); roles.forEach(role -> { String roleName = (String) role; mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleName)); }); return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
認証後に保持しているもともとのGrantedAuthority
はクライアントスコープのものになるのですが、
Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities());
今回はクライアントスコープとRealmロールの内容の両方を権限として扱うようにしました。
SimpleGrantedAuthority
を使ったロールへのマッピングの際には、ROLE_
接頭辞を付与しています。
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleName));
ところでコメントアウトしていますが、IDトークンのクレームかOidcUser#getClaims
の違いについて。後者はuserInfo+IDトークンの
クレームを含めたものになっています。
今回の内容だと、どちらを使っても良いと言えます。
ただOAuth 2.0のクライアントとして扱う場合は、アクセストークンを使ってrealm_access.roles
にアクセスするのはかなり面倒な感じが
したので、この点には注意ですね(今回、それなりに時間がかかったのですが、この方法を模索したいのが理由のひとつです)。
次に、こちら。
@EnableWebSecurity public class OAuth2ClientSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/auth/**").authenticated() .mvcMatchers("/role/message").hasRole("message") .mvcMatchers("/role/token").hasRole("token") .anyRequest().permitAll()) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .userAuthoritiesMapper(userAuthoritiesMapper()))) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/")); return http.build(); } GrantedAuthoritiesMapper userAuthoritiesMapper() { return authorities -> { Set<GrantedAuthority> mappedAuthorities = new HashSet<>(authorities); authorities.forEach(authority -> { if (authority instanceof OidcUserAuthority) { OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority; OidcIdToken idToken = oidcUserAuthority.getIdToken(); Map<String, Object> claims = idToken.getClaims(); JSONObject realmAccess = (JSONObject) claims.get("realm_access"); JSONArray roles = (JSONArray) realmAccess.get("roles"); roles.forEach(role -> { String roleName = (String) role; mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleName)); }); } /* else if (authority instanceof OAuth2UserAuthority) { OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority) authority; Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes(); // ... } */ }); return mappedAuthorities; }; } }
こちらが変わり
.oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .userAuthoritiesMapper(userAuthoritiesMapper())))
権限の読み替えは、こういう処理になりました。
GrantedAuthoritiesMapper userAuthoritiesMapper() { return authorities -> { Set<GrantedAuthority> mappedAuthorities = new HashSet<>(authorities); authorities.forEach(authority -> { if (authority instanceof OidcUserAuthority) { OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority; OidcIdToken idToken = oidcUserAuthority.getIdToken(); Map<String, Object> claims = idToken.getClaims(); JSONObject realmAccess = (JSONObject) claims.get("realm_access"); JSONArray roles = (JSONArray) realmAccess.get("roles"); roles.forEach(role -> { String roleName = (String) role; mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleName)); }); } /* else if (authority instanceof OAuth2UserAuthority) { OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority) authority; Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes(); // ... } */ }); return mappedAuthorities; }; }
結局のところ、realm_access.roles
クレームを読み取ってGrantedAuthority
に変換するという点では先ほどのパターンと同じですね。
なお、ドキュメントではOAuth2UserAuthority
に関する分岐もあるのですが、今回はOidcUserAuthority
の方だけを残しています。
今回の構成ではOidcUserAuthority
のみが使われ、OAuth2UserAuthority
は使われないからです(動作確認ができないので)。
OidcUserAuthority
のインスタンスを作成している箇所は、こちら。
2パターン書きましたが、今回のテーマにおける認可に関する動作はどちらも同じ挙動になるので、結果記載は1パターンのみとします。
動作確認してみる
それでは、動作確認をしていきましょう。
適用するのは、こちらの処理とします。
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() { OidcUserService delegate = new OidcUserService(); return userRequest -> { OidcUser oidcUser = delegate.loadUser(userRequest); System.out.println("access_token value = " + userRequest.getAccessToken().getTokenValue()); System.out.println("id_token value = " + oidcUser.getIdToken().getTokenValue()); System.out.println("claims = " + oidcUser.getClaims()); System.out.println("id_token claims = " + oidcUser.getIdToken().getClaims()); System.out.println("userInfo claims = " + oidcUser.getUserInfo().getClaims()); Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities()); Map<String, Object> claims = oidcUser.getIdToken().getClaims(); // Map<String, Object> claims = oidcUser.getClaims(); JSONObject realmAccess = (JSONObject) claims.get("realm_access"); JSONArray roles = (JSONArray) realmAccess.get("roles"); roles.forEach(role -> { String roleName = (String) role; mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleName)); }); System.out.println("authorities = " + mappedAuthorities); return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo()); }; }
アプリケーションを起動。
$ mvn spring-boot:run
まずはログインしないまま、http://localhost:8080/
にアクセスしてみます。
http://localhost:8080/token
にアクセス。
ログインを必要とするhttp://localhost:8080/auth/message
にアクセスしてみます。
すると、Keycloakのログインページにリダイレクトされるので、先ほど作成したユーザー名とパスワード入力してログイン。
最初はtoken-role-user
を使うことにします。
ログイン後、リダイレクトしてhttp://localhost:8080/auth/message
にアクセスできました。
System.out.println
で出力していた、トークンやクレームの情報。
access_token value = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3T0Fqa3Vxekw0THlqNUJUR0JVa2xlQnFuYVd5SHRWTWs4cXlKbDhfcWM4In0.eyJleHAiOjE2NjI1NjgyODcsImlhdCI6MTY2MjU2Nzk4NywiYXV0aF90aW1lIjoxNjYyNTY3ODQ2LCJqdGkiOiJjZTAyYTRiMi1kYmY5LTQ5M2MtOGIwYS1jNDFlNzQzY2RkYmQiLCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMjo4MDgwL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMDkxMDdiZDItOWQ5Ny00OTcwLWE2Y2ItZGViNzhjYzU2MzE0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic3ByaW5nLXNlY3VyaXR5LWNsaWVudCIsIm5vbmNlIjoia3RFazV2YXFsa0hCdFhYTDhoMVhRVG92bC1BMzZMUXpxd040WF9QTzhQbyIsInNlc3Npb25fc3RhdGUiOiJiNzEwZWZhMy0xNGJiLTQ0ZGItYjhjOS1jOTBiNDBlY2EwZTgiLCJhY3IiOiIwIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtc2FtcGxlLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsInRva2VuIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsInNpZCI6ImI3MTBlZmEzLTE0YmItNDRkYi1iOGM5LWM5MGI0MGVjYTBlOCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoidG9rZW4tcm9sZS11c2VyIiwiZ2l2ZW5fbmFtZSI6IiIsImZhbWlseV9uYW1lIjoiIn0.VlAgieBkufKAwNhUXNOj1myP76Cp1bnZDusn4VbR0DWDbXOWegWsxc1m73mtCwAEZ81jaVD5rCXV6DfjdIYh7kw0dbyJSmoDxb-69To7Ur7804OMoQQU0OudCvTV9sEphe9M8MHOyzV2Hhs1097DbazNmFE9rUD3ZZ9gQIwfupXy0FQk5pmszkKrnZIUL8RhnU71rxy4YMp2aNR5IlSy2lB4jMdSgqaK9Iw93cH-qlb9rP3uV_fy6jT0mra9BbmjKDgU2n6zjei8QLBmVgYhryskzt1BIUYwDH31NmQzVFpH1JHEMkRW9U5gwE6CmoKpZPMFJavmvJGJwMNQujCyig id_token value = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3T0Fqa3Vxekw0THlqNUJUR0JVa2xlQnFuYVd5SHRWTWs4cXlKbDhfcWM4In0.eyJleHAiOjE2NjI1NjgyODcsImlhdCI6MTY2MjU2Nzk4NywiYXV0aF90aW1lIjoxNjYyNTY3ODQ2LCJqdGkiOiJiZmRiY2RmNC00OTlmLTQyM2EtOTlhNC03MmFkYjY2MGE3NzIiLCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMjo4MDgwL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJzcHJpbmctc2VjdXJpdHktY2xpZW50Iiwic3ViIjoiMDkxMDdiZDItOWQ5Ny00OTcwLWE2Y2ItZGViNzhjYzU2MzE0IiwidHlwIjoiSUQiLCJhenAiOiJzcHJpbmctc2VjdXJpdHktY2xpZW50Iiwibm9uY2UiOiJrdEVrNXZhcWxrSEJ0WFhMOGgxWFFUb3ZsLUEzNkxRenF3TjRYX1BPOFBvIiwic2Vzc2lvbl9zdGF0ZSI6ImI3MTBlZmEzLTE0YmItNDRkYi1iOGM5LWM5MGI0MGVjYTBlOCIsImF0X2hhc2giOiJKRDRaWjZ4YzY3WlNQSWRRRHZLd3lnIiwiYWNyIjoiMCIsInNpZCI6ImI3MTBlZmEzLTE0YmItNDRkYi1iOGM5LWM5MGI0MGVjYTBlOCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtc2FtcGxlLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsInRva2VuIl19LCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0b2tlbi1yb2xlLXVzZXIiLCJnaXZlbl9uYW1lIjoiIiwiZmFtaWx5X25hbWUiOiIifQ.cSUJFkTmqQXo0uJaogFmW8hkZQl4XzFgBBS5a3VH6kFm9VTuUKSLHIQUSZtK4i8D_aPuX_MMxLtkm5f38Mhr55sZmlbsxMZfif5hHvPMs2Vd-rUMWRSe5h-JAiKPMYpQf8KsUcfb-C3sBfUUCt9TPQUAjYUG6LOTF2XMPAI64K3LTZxw-BoieQuv1rqL-AAOzxIRrfC17E0a22NI8tmZdsI4uNGv9ueAKwQDyIXIX7Mwy5PWYhcIgnTRmy8X5hMKtxiehJiHDH8z7o6kfvEQY97y9OGE9UNvN0pQtfLjvTqHkzkjibYe9BscYEdTXIqRsUJxlV5JqALB4-vuxMbEKg claims = {at_hash=JD4ZZ6xc67ZSPIdQDvKwyg, sub=09107bd2-9d97-4970-a6cb-deb78cc56314, email_verified=false, iss=http://172.17.0.2:8080/realms/sample-realm, typ=ID, preferred_username=token-role-user, given_name=, nonce=ktEk5vaqlkHBtXXL8h1XQTovl-A36LQzqwN4X_PO8Po, sid=b710efa3-14bb-44db-b8c9-c90b40eca0e8, aud=[spring-security-client], acr=0, realm_access={"roles":["default-roles-sample-realm","offline_access","uma_authorization","token"]}, azp=spring-security-client, auth_time=2022-09-07T16:24:06Z, exp=2022-09-07T16:31:27Z, session_state=b710efa3-14bb-44db-b8c9-c90b40eca0e8, family_name=, iat=2022-09-07T16:26:27Z, jti=bfdbcdf4-499f-423a-99a4-72adb660a772} id_token claims = {at_hash=JD4ZZ6xc67ZSPIdQDvKwyg, sub=09107bd2-9d97-4970-a6cb-deb78cc56314, email_verified=false, iss=http://172.17.0.2:8080/realms/sample-realm, typ=ID, preferred_username=token-role-user, given_name=, nonce=ktEk5vaqlkHBtXXL8h1XQTovl-A36LQzqwN4X_PO8Po, sid=b710efa3-14bb-44db-b8c9-c90b40eca0e8, aud=[spring-security-client], acr=0, realm_access={"roles":["default-roles-sample-realm","offline_access","uma_authorization","token"]}, azp=spring-security-client, auth_time=2022-09-07T16:24:06Z, exp=2022-09-07T16:31:27Z, session_state=b710efa3-14bb-44db-b8c9-c90b40eca0e8, iat=2022-09-07T16:26:27Z, family_name=, jti=bfdbcdf4-499f-423a-99a4-72adb660a772} userInfo claims = {sub=09107bd2-9d97-4970-a6cb-deb78cc56314, email_verified=false, realm_access={roles=[default-roles-sample-realm, offline_access, uma_authorization, token]}, preferred_username=token-role-user, given_name=, family_name=}
IDトークンのJWTをデコードすると、こんな感じになっています。
{ "exp": 1662568287, "iat": 1662567987, "auth_time": 1662567846, "jti": "bfdbcdf4-499f-423a-99a4-72adb660a772", "iss": "http://172.17.0.2:8080/realms/sample-realm", "aud": "spring-security-client", "sub": "09107bd2-9d97-4970-a6cb-deb78cc56314", "typ": "ID", "azp": "spring-security-client", "nonce": "ktEk5vaqlkHBtXXL8h1XQTovl-A36LQzqwN4X_PO8Po", "session_state": "b710efa3-14bb-44db-b8c9-c90b40eca0e8", "at_hash": "JD4ZZ6xc67ZSPIdQDvKwyg", "acr": "0", "sid": "b710efa3-14bb-44db-b8c9-c90b40eca0e8", "email_verified": false, "realm_access": { "roles": [ "default-roles-sample-realm", "offline_access", "uma_authorization", "token" ] }, "preferred_username": "token-role-user", "given_name": "", "family_name": "" }
付与したロールが含まれていることが確認できます。
そして、Spring Securityとして認識する権限はこのようになりました。
authorities = [ROLE_offline_access, SCOPE_openid, SCOPE_email, ROLE_uma_authorization, ROLE_default-roles-sample-realm, SCOPE_profile, ROLE_token, ROLE_USER]
次に、認可が必要なページにアクセスしてみます。
今のユーザーはtoken
ロールを保持しているので、http://localhost:8080/role/token
にはアクセスできます。
message
ロールは保持していないのでhttp://localhost:8080/role/message
にはアクセスできず、403になります。
OKそうですね。次は1度ログアウトして、今度はmessage-role-user
でアクセスしてみます。
message-role-user
の場合はmessage
ロールを保持しているので、http://localhost:8080/role/message
にアクセスすることができます。
反対に、token
ロールを保持していないのでhttp://localhost:8080/role/token
にはアクセスできなくなります。
OKそうですね。
message-role-user
でのログイン時に出力される情報も、参考までに。
access_token value = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3T0Fqa3Vxekw0THlqNUJUR0JVa2xlQnFuYVd5SHRWTWs4cXlKbDhfcWM4In0.eyJleHAiOjE2NjI1Njg2OTksImlhdCI6MTY2MjU2ODM5OSwiYXV0aF90aW1lIjoxNjYyNTY4Mzk5LCJqdGkiOiJlNjFjYTBjNC1hMzgwLTRjOTgtODkxOS1jY2IzZmRiMjNiYzUiLCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMjo4MDgwL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiZjc5ZjFkZjgtYTNkZC00MjIwLTgwOTYtOGQwZjI5MTBhNWQ0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic3ByaW5nLXNlY3VyaXR5LWNsaWVudCIsIm5vbmNlIjoibU9rU29jUkZtcjBuSHh2RFNDNWNMWVBkWW93M3duU1lxbURpX2tsdS1uUSIsInNlc3Npb25fc3RhdGUiOiI2YmY0MjhmOC1lYzNjLTRlNzEtYjZlYy00ZDg4MDM5MGNjNDIiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtc2FtcGxlLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsIm1lc3NhZ2UiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwic2lkIjoiNmJmNDI4ZjgtZWMzYy00ZTcxLWI2ZWMtNGQ4ODAzOTBjYzQyIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJtZXNzYWdlLXJvbGUtdXNlciIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiJ9.dElZjCV7BUZIgHgmRYwuqQpHwDr9GfWGlktfUpwavahjJN-vJkLtn0srX8JH8prSUSn-LxF0EbpPo6tCsK5kH53t0Y6K1-8bGUl0P6MYFU1q11Y8I5-hQ8JGfKSsiZQhmos16iJ700555LIi2vGO_8aNyF8aVem75teMiU7-e1e7ZSnPr5fBVa_hr0Ny3AfaJvjBIAIUvkMTTpR4pksZv5224OhmUTXDYPU0zMTN1sMMx34LKg13Bg5vJ8MkQp9VoyUx9C4SdhvD3uL5T2od_J-JgwfrrYPNLZyiBxrt-Tm27NjOmo9fhIzWt3j-xrd3nYfgpD2KjpS185eMz56_5A id_token value = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3T0Fqa3Vxekw0THlqNUJUR0JVa2xlQnFuYVd5SHRWTWs4cXlKbDhfcWM4In0.eyJleHAiOjE2NjI1Njg2OTksImlhdCI6MTY2MjU2ODM5OSwiYXV0aF90aW1lIjoxNjYyNTY4Mzk5LCJqdGkiOiJjZjg5ZGM1NS0zZmRlLTQ2MjAtYmJjZC0xN2I5MmNjZjcyNzMiLCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMjo4MDgwL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJzcHJpbmctc2VjdXJpdHktY2xpZW50Iiwic3ViIjoiZjc5ZjFkZjgtYTNkZC00MjIwLTgwOTYtOGQwZjI5MTBhNWQ0IiwidHlwIjoiSUQiLCJhenAiOiJzcHJpbmctc2VjdXJpdHktY2xpZW50Iiwibm9uY2UiOiJtT2tTb2NSRm1yMG5IeHZEU0M1Y0xZUGRZb3czd25TWXFtRGlfa2x1LW5RIiwic2Vzc2lvbl9zdGF0ZSI6IjZiZjQyOGY4LWVjM2MtNGU3MS1iNmVjLTRkODgwMzkwY2M0MiIsImF0X2hhc2giOiJZc2doQXI2cS0xSUNFR0RpUTdZREpnIiwiYWNyIjoiMSIsInNpZCI6IjZiZjQyOGY4LWVjM2MtNGU3MS1iNmVjLTRkODgwMzkwY2M0MiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtc2FtcGxlLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsIm1lc3NhZ2UiXX0sInByZWZlcnJlZF91c2VybmFtZSI6Im1lc3NhZ2Utcm9sZS11c2VyIiwiZ2l2ZW5fbmFtZSI6IiIsImZhbWlseV9uYW1lIjoiIn0.L4ZVVgoDGsI6FNzM6DIWSpfpJCQoUqzeXab8Gb-En4RziWIQFT1kzBw4GANPJSlITahz3zPWk6PyLi6e4SGEvsBbUcGugvayf_orn2puKVh412V4cMGQHwGZT91toYChzlI6b3sW25PrDqgsquz_0s_3nE1njfDmBTqh7XYY0eSnzg46BBJN_uXO2rWNZv_6aOEK4G6UnwXQeztgWk0-0NnESO9UWDMA_Cw-xzbzCHL3E-4Od48nh5M4bu4EcN7XsUAVvKz8u8LY1fUTh9hFaVAZ04mUFg4Cd7O_yEgXOxoB-HYgh6074Yu9SifQGk5hqHMNltFM832cv6LbyhNixg claims = {at_hash=YsghAr6q-1ICEGDiQ7YDJg, sub=f79f1df8-a3dd-4220-8096-8d0f2910a5d4, email_verified=false, iss=http://172.17.0.2:8080/realms/sample-realm, typ=ID, preferred_username=message-role-user, given_name=, nonce=mOkSocRFmr0nHxvDSC5cLYPdYow3wnSYqmDi_klu-nQ, sid=6bf428f8-ec3c-4e71-b6ec-4d880390cc42, aud=[spring-security-client], acr=1, realm_access={"roles":["default-roles-sample-realm","offline_access","uma_authorization","message"]}, azp=spring-security-client, auth_time=2022-09-07T16:33:19Z, exp=2022-09-07T16:38:19Z, session_state=6bf428f8-ec3c-4e71-b6ec-4d880390cc42, family_name=, iat=2022-09-07T16:33:19Z, jti=cf89dc55-3fde-4620-bbcd-17b92ccf7273} id_token claims = {at_hash=YsghAr6q-1ICEGDiQ7YDJg, sub=f79f1df8-a3dd-4220-8096-8d0f2910a5d4, email_verified=false, iss=http://172.17.0.2:8080/realms/sample-realm, typ=ID, preferred_username=message-role-user, given_name=, nonce=mOkSocRFmr0nHxvDSC5cLYPdYow3wnSYqmDi_klu-nQ, sid=6bf428f8-ec3c-4e71-b6ec-4d880390cc42, aud=[spring-security-client], acr=1, realm_access={"roles":["default-roles-sample-realm","offline_access","uma_authorization","message"]}, azp=spring-security-client, auth_time=2022-09-07T16:33:19Z, exp=2022-09-07T16:38:19Z, session_state=6bf428f8-ec3c-4e71-b6ec-4d880390cc42, iat=2022-09-07T16:33:19Z, family_name=, jti=cf89dc55-3fde-4620-bbcd-17b92ccf7273} userInfo claims = {sub=f79f1df8-a3dd-4220-8096-8d0f2910a5d4, email_verified=false, realm_access={roles=[default-roles-sample-realm, offline_access, uma_authorization, message]}, preferred_username=message-role-user, given_name=, family_name=} authorities = [ROLE_offline_access, SCOPE_openid, ROLE_USER, SCOPE_email, ROLE_uma_authorization, ROLE_default-roles-sample-realm, ROLE_message, SCOPE_profile]
これで、今回確認したかった内容は見ることができました。
オマケ
最後に、Keycloakの設定時に「Add to ID token」を1度「Off」にして「Save」、そしてもう1度「On」みたいなことをしていた理由を
書いておきます。
結局、やったことは(見た目は)初期状態に戻しただけなのですが。
デフォルトで、roles
クライアントスコープのrealm roles
の内容としては、以下の状態になっています。
なのですが、実際の動作はどうなるかというとアクセストークンにしかクレームが含まれない状態になります。
access_token value = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3T0Fqa3Vxekw0THlqNUJUR0JVa2xlQnFuYVd5SHRWTWs4cXlKbDhfcWM4In0.eyJleHAiOjE2NjI1NjgxNDYsImlhdCI6MTY2MjU2Nzg0NiwiYXV0aF90aW1lIjoxNjYyNTY3ODQ2LCJqdGkiOiIzMTY3OWRlYy00MzViLTQ4ZmMtYjY3ZS1iMTJjYTQ4NjFmYWEiLCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMjo4MDgwL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMDkxMDdiZDItOWQ5Ny00OTcwLWE2Y2ItZGViNzhjYzU2MzE0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic3ByaW5nLXNlY3VyaXR5LWNsaWVudCIsIm5vbmNlIjoiYzdMR2hlVTNaVnRIQ19ydUlweWdsRU8xTGdxYUlTM0xyYlJBRWVUdkREOCIsInNlc3Npb25fc3RhdGUiOiJiNzEwZWZhMy0xNGJiLTQ0ZGItYjhjOS1jOTBiNDBlY2EwZTgiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtc2FtcGxlLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsInRva2VuIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsInNpZCI6ImI3MTBlZmEzLTE0YmItNDRkYi1iOGM5LWM5MGI0MGVjYTBlOCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoidG9rZW4tcm9sZS11c2VyIiwiZ2l2ZW5fbmFtZSI6IiIsImZhbWlseV9uYW1lIjoiIn0.EZN2CCAJq_D8jAntWNIZMq_1biRGhLikiVYYlv2ChqFOLrYTu8A-N-u4Rl5bFV1Svt6_a4Pnevwql2Dtyz5SD5NE5Gf7LHWE13KYTjkI97lBXTNXcDC9XROsATFB3VEq76U6cqA1Bt79AbIzkH6q5RRHvlWE8IDdF2ashvfqjcJqFJ_fsjY0x7vbjex32ULQE3r6W7vYom8lhhSvIJgd1JeEtR4ev_8d3y9sP-9Jh4pNbktkxDo5q1nrNxG4rm4qKm9T1xknmx-8eZsE__jnu0scwkUGOesU6OPJWQSAiNgWdyWarAWwUgQ2qnobjWoyAbJUHVJgCBc9P3eMCYWvoA id_token value = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3T0Fqa3Vxekw0THlqNUJUR0JVa2xlQnFuYVd5SHRWTWs4cXlKbDhfcWM4In0.eyJleHAiOjE2NjI1NjgxNDYsImlhdCI6MTY2MjU2Nzg0NiwiYXV0aF90aW1lIjoxNjYyNTY3ODQ2LCJqdGkiOiJlNmE4NTFhZi0yYjIzLTRiMmMtODUzYi05ZGViN2FjYTg3ZjgiLCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMjo4MDgwL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJzcHJpbmctc2VjdXJpdHktY2xpZW50Iiwic3ViIjoiMDkxMDdiZDItOWQ5Ny00OTcwLWE2Y2ItZGViNzhjYzU2MzE0IiwidHlwIjoiSUQiLCJhenAiOiJzcHJpbmctc2VjdXJpdHktY2xpZW50Iiwibm9uY2UiOiJjN0xHaGVVM1pWdEhDX3J1SXB5Z2xFTzFMZ3FhSVMzTHJiUkFFZVR2REQ4Iiwic2Vzc2lvbl9zdGF0ZSI6ImI3MTBlZmEzLTE0YmItNDRkYi1iOGM5LWM5MGI0MGVjYTBlOCIsImF0X2hhc2giOiItdnhqd2pJZExnSm1nX3BLRHlEYjlnIiwiYWNyIjoiMSIsInNpZCI6ImI3MTBlZmEzLTE0YmItNDRkYi1iOGM5LWM5MGI0MGVjYTBlOCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoidG9rZW4tcm9sZS11c2VyIiwiZ2l2ZW5fbmFtZSI6IiIsImZhbWlseV9uYW1lIjoiIn0.obijJwONjgsRchUkXzfS3UIBaV1VanXFb1CAD0RO917M_TiY0uyMSXOVwOWcKwotc7LnPHrXs00tXRqPFB6uWCTUWOwfhE4Iim2jTk-A1ziLOvqfhQ1p36ChKna0-XmJ1kI_x3pA63TKN9gOYmPtbIP_RCgS1EEYwg1xhthr342O117sgbmWQrQhMxLZK4Mlt_O4UItAp2nU1cILqpMbeKN4QWqHHbdPdcTWMGkBlBUmFA0sUQ9qGmQq9nizy2uUzZec-Y7Mnj7pSzoB7BsV7vCQ8hfcSvbH0UFP1KjGhgbofPPnWEEWpiZwZmftWWkCjSQMLlFEFb59j3DEhHvVgQ claims = {at_hash=-vxjwjIdLgJmg_pKDyDb9g, sub=09107bd2-9d97-4970-a6cb-deb78cc56314, email_verified=false, iss=http://172.17.0.2:8080/realms/sample-realm, typ=ID, preferred_username=token-role-user, given_name=, nonce=c7LGheU3ZVtHC_ruIpyglEO1LgqaIS3LrbRAEeTvDD8, sid=b710efa3-14bb-44db-b8c9-c90b40eca0e8, aud=[spring-security-client], acr=1, azp=spring-security-client, auth_time=2022-09-07T16:24:06Z, exp=2022-09-07T16:29:06Z, session_state=b710efa3-14bb-44db-b8c9-c90b40eca0e8, family_name=, iat=2022-09-07T16:24:06Z, jti=e6a851af-2b23-4b2c-853b-9deb7aca87f8} id_token claims = {at_hash=-vxjwjIdLgJmg_pKDyDb9g, sub=09107bd2-9d97-4970-a6cb-deb78cc56314, email_verified=false, iss=http://172.17.0.2:8080/realms/sample-realm, typ=ID, preferred_username=token-role-user, given_name=, nonce=c7LGheU3ZVtHC_ruIpyglEO1LgqaIS3LrbRAEeTvDD8, sid=b710efa3-14bb-44db-b8c9-c90b40eca0e8, aud=[spring-security-client], acr=1, azp=spring-security-client, auth_time=2022-09-07T16:24:06Z, exp=2022-09-07T16:29:06Z, session_state=b710efa3-14bb-44db-b8c9-c90b40eca0e8, iat=2022-09-07T16:24:06Z, family_name=, jti=e6a851af-2b23-4b2c-853b-9deb7aca87f8} userInfo claims = {sub=09107bd2-9d97-4970-a6cb-deb78cc56314, email_verified=false, preferred_username=token-role-user, given_name=, family_name=}
よって、今回のソースコードではRealmロールがIDトークンに含まれていないことを想定していないのでエラーになります。
2022-09-08 01:24:07.054 ERROR 20716 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception java.lang.NullPointerException: Cannot invoke "com.nimbusds.jose.shaded.json.JSONObject.get(Object)" because "realmAccess" is null at org.littlewings.keycloak.spring.OAuth2ClientSecurityConfig.lambda$oidcUserService$5(OAuth2ClientSecurityConfig.java:63) ~[classes/:na] at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.authenticate(OidcAuthorizationCodeAuthenticationProvider.java:156) ~[spring-security-oauth2-client-5.7.3.jar:5.7.3] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) ~[spring-security-core-5.7.3.jar:5.7.3] at org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.attemptAuthentication(OAuth2LoginAuthenticationFilter.java:195) ~[spring-security-oauth2-client-5.7.3.jar:5.7.3] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227) ~[spring-security-web-5.7.3.jar:5.7.3] 〜省略〜
結局デフォルト値に戻しているだけなのですが、なぜか「Off」、「On」と操作するとIDトークン等にもクレームが含まれるようになったので、
今回はこのようにしました。
どうしてでしょうね?
まとめ
Spring SecurityのOAuth 2.0サポートで、KeycloakのRealmロールを使った認可を試してみました。
クライアントスコープの方はすんなりいきましたが、こちらはかなりてこずりました。
困ったのは
あたりですね。
最終的に、目的は達成できたのでよかったのですが。