--- title: Spring BootでIAM Identity Center(旧AWS SSO)とSAML2連携してログインするメモ tags: ["Java", "Spring Boot", "Spring Security", "SAML", "OpenSSL", "IAM Identity Center", "AWS"] categories: ["Programming", "Java", "org", "springframework", "security", "saml2"] date: 2024-09-04T08:50:20Z updated: 2024-09-04T10:45:12Z --- IAM Identity Center(旧AWS SSO)で管理されているユーザーでSpring Bootにログインしたい時のメモ。 IAM Identity CenterはOIDC Providerの機能はありませんが、SAML2のIdentity Providerとして利用できるので、 Spring BootでSAML2連携により、AWSのユーザーを使ったログインを実装してみます。 SAML2連携はSpring Securityで用意されています。 https://docs.spring.io/spring-security/reference/servlet/saml2/index.html **目次** ### 雛形プロジェクトの作成 まずはSpring Initializrで雛形プロジェクトを作成します。 ```bash curl -s https://start.spring.io/starter.tgz \ -d artifactId=hello-saml \ -d baseDir=hello-saml \ -d packageName=com.example \ -d dependencies=web,actuator,security,configuration-processor \ -d type=maven-project \ -d name=hello-saml \ -d applicationName=HelloSamlApplication | tar -xzvf - cd hello-saml ``` SAML2連携を使うために、以下のdependencyを`pom.xml`に追加します。 ```xml org.springframework.security spring-security-saml2-service-provider ``` `spring-security-saml2-service-provider`で使われているライブラリであるOpenSAMLは、[諸事情](https://shibboleth.atlassian.net/wiki/spaces/DEV/pages/1123844333/Use+of+Maven+Central)によりMaven Centralで利用できないため、 以下のMavenレポジトリを追加します。 ```xml shibboleth https://build.shibboleth.net/nexus/content/repositories/releases/ ``` SAML2でログインするための設定を`SecurityConfig`に記述します。 ```java cat <<'EOF' > src/main/java/com/example/SecurityConfig.java package com.example; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import static org.springframework.security.config.Customizer.withDefaults; @Configuration(proxyBeanMethods = false) public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(authz -> authz .requestMatchers("/error").permitAll() .anyRequest().authenticated()) .saml2Login(withDefaults()) .saml2Metadata(withDefaults()) .build(); } } EOF ``` SAML2で認証されたユーザー情報を表示するControllerを作成します。 ```java cat <<'EOF' > src/main/java/com/example/HelloController.java package com.example; import java.util.Map; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping(path = "/") public Map hello(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) { return Map.of("username", principal.getName(), "attributes", principal.getAttributes()); } } EOF ``` ### Identity Provider (Relying Party)の設定 次にIAM Identity CenterでIdentity Provider (Relying Party)の設定を行います。 まずはアプリケーションを登録します。"Applications"画面で"Add application"ボタンをクリックします。 image "I have an application I want ot set up"を選択します。 image "Application type"は"SAML 2.0"を選択します。 image "Display name"は"Hello SAML"にします。 image ここで、"IAM Identity Center metadata"の"IAM Identity Center SAML metadata file"のURLをコピーしてください。 image このURLを次のように`application.properties`の`spring.security.saml2.relyingparty.registration..assertingparty.metadata-uri`に設定します。 ```properties METADATA_URL=https://portal.sso.****.amazonaws.com/saml/metadata/**** cat < src/main/resources/application.properties spring.application.name=hello-saml spring.security.saml2.relyingparty.registration.awssso.entity-id=hello-saml spring.security.saml2.relyingparty.registration.awssso.assertingparty.metadata-uri=$METADATA_URL EOF ``` `application.properties`の設定ができたら、一旦アプリケーションを起動してください。 ```bash ./mvnw spring-boot:run ``` Service Provider側のメタデータが`/saml2/metadata/`で公開されるので、ダウンロードします。 ```bash curl -s http://localhost:8080/saml2/metadata/awssso -o ~/Downloads/metadata-awssso.xml ``` ダウンロードしたメタデータを"Application metadata"でアップロードし、"Submit"をクリックします。 image アプリケーションが作成され、"Status"が"Active"になればOKです。 image 次に、ユーザー・グループをこのアプリケーションにアサインします。"Assigned users adn groups"で"Assign users and groups"ボタンをクリックします。 image ユーザーあるいはグループが存在しない場合は、新規作成してください。ここでは`developers`グループと`administrators`グループをアサインしました。 image 次のAttributeのマッピングを行います。アプリケーションの"Details"の"Actions"をクリックし、"Edit attribute mappings"を選択してください。 image https://docs.aws.amazon.com/singlesignon/latest/userguide/attributemappingsconcept.html を参考にマッピングを行います。 ここでは次のようにマッピングしました。 * `Subject` (ユーザー名として使われる) ... `${user.email}` (`emailAddress`型) * `firstName` ... `${user.givenName}` (`unspecified`型) * `lastName` ... `${user.familyName}` (`unspecified`型) * `groups` ... `${user.groups}` (`unspecified`型) image "Save changes"をクリックして設定を保存したら、先ほど起動したアプリケーション(http://localhost:8080)にアクセスしてください。 IAM Identity Centerのログイン画面にリダイレクトされます。 image IAM Identity Centerにログインすると再度再度リダイレクトされます。 image http://localhost:8080 にリダイレクトされ、ログインユーザー情報が表示されます。 image ユーザーが所属しているグループの情報が`groups` attributeに含まれますが、名前ではなくIDが取得できます。 IAM Identity Centerのグループの詳細画面でGroup IDは確認可能です。 image これでIAM Identity Centerのユーザーを使い、SAML2でSpring Bootのアプリにログインできました。 ### シングルログアウト 次にシングルログアウトの設定を行います。 `SecurityConfig`に次の設定を追加します。 ```java @Configuration(proxyBeanMethods = false) public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(authz -> authz .requestMatchers("/error").permitAll() .anyRequest().authenticated()) .saml2Login(withDefaults()) .saml2Logout(withDefaults()) // <--- .saml2Metadata(withDefaults()) .build(); } } ``` シングルログアウトのリクエストは署名が必要となります。次のコマンドで秘密鍵と公開鍵を設定します。 ```bash openssl req -x509 -newkey rsa:4096 -keyout src/main/resources/key.pem -out src/main/resources/cert.pem -sha256 -days 3650 -nodes -subj "/CN=@making/O=LOL.MAKI/C=JP" ``` `application.properties`に次の設定を追記します。 ```properties cat <<'EOF' >> src/main/resources/application.properties spring.security.saml2.relyingparty.registration.awssso.signing.credentials[0].certificate-location=classpath:cert.pem spring.security.saml2.relyingparty.registration.awssso.signing.credentials[0].private-key-location.=classpath:key.pem spring.security.saml2.relyingparty.registration.awssso.singlelogout.binding=post spring.security.saml2.relyingparty.registration.awssso.singlelogout.response-url={baseUrl}/logout/saml2/slo/{registrationId} EOF ``` アプリケーションを停止し、再起動します。 ```bash ./mvnw spring-boot:run ``` 一度、 http://localhost:8080 にアクセスした後、 http://localhost:8080/logout にアクセスしてください。 image "Log Out"ボタンをクリックしてください。 そうするとアプリケーションからログアウトし、IAM Identity Centerからもログアウトされ...ません。 IAM Identity Centerにログイン後のポータル画面にリダイレクトされます。 image 本来ならこの設定でIdentity Providerからのログアウトもできるのですが、IAM Identity Centerではできませんでした。 次の記述を見つけましたので、IAM Identity Centerではシングルログアウトはサポートされていないかもしれません。 https://github.com/aws-mwaa/upstream-to-airflow/blob/0a816c6f0b500e1b0515452e38e3446412f3e8e3/airflow/providers/amazon/aws/auth_manager/views/auth.py#L105 実際に[Microsoft Entra ID(旧Azure AD)の場合](/entries/820)は同じ設定でシングルログアウトできました。 ここはIAM Identity Centerの制約かもしれないので、このままにします。 ### Attributesによる認可 SAML2でログインしたユーザー情報にはデフォルトで`ROLE_USER` authorityがついています。 [ドキュメント](https://docs.spring.io/spring-security/reference/servlet/saml2/login/authentication.html)では作成されるユーザー情報をカスタマイズする方法が紹介されています。 この方法を使って、ユーザーに任意のauthorityを追加することができます。 それよりもユーザー情報が持つattributesを使って直接認可設定できた方が便利なので、次のような`AuthorizationManager`を実装します。 ```java cat <<'EOF' > src/main/java/com/example/SamlAuthorizationManager.java package com.example; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.core.Authentication; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; public class SamlAuthorizationManager implements AuthorizationManager { private final Function applyer; private final Predicate predicate; public SamlAuthorizationManager(Function applyer, Predicate predicate) { this.applyer = applyer; this.predicate = predicate; } public static SamlAuthorizationManager> attribute(String name, Predicate> predicate) { return new SamlAuthorizationManager<>(principal -> principal.getAttribute(name), predicate); } public static SamlAuthorizationManager firstAttribute(String name, Predicate predicate) { return new SamlAuthorizationManager<>(principal -> principal.getFirstAttribute(name), predicate); } public static SamlAuthorizationManager username(Predicate predicate) { return new SamlAuthorizationManager<>(AuthenticatedPrincipal::getName, predicate); } @Override public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext object) { Authentication auth = authentication.get(); if (auth instanceof Saml2Authentication && auth.getPrincipal() instanceof Saml2AuthenticatedPrincipal principal) { T target = this.applyer.apply(principal); if (target == null) { return new AuthorizationDecision(false); } return new AuthorizationDecision(this.predicate.test(target)); } else { return new AuthorizationDecision(false); } } } EOF ``` 例えば、`/admin`のパスにアクセスするには、以下のいずれかを満たす必要があるとします。 * `groups` attributeに`a7443a38-70d1-709f-aa2c-4841adf65ed1`を含む * `email` attribute(先頭の要素のみ対象)が`@example.com`で終わる * ユーザー名が`admin`または`makingx@gmail.com`である この条件を`SamlAuthorizationManager`を使って次のように定義できます。 ```java // ... import static com.example.SamlAuthorizationManager.attribute; import static com.example.SamlAuthorizationManager.firstAttribute; import static com.example.SamlAuthorizationManager.username; import static org.springframework.security.authorization.AuthorizationManagers.anyOf; // ... @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(authz -> authz .requestMatchers("/error").permitAll() // <--- .requestMatchers("/admin").access(anyOf( attribute("groups", groups -> groups.contains("a7443a38-70d1-709f-aa2c-4841adf65ed1")), firstAttribute("email", email -> email.endsWith("@example.com")), username(username -> username.equals("admin") || username.equals("makingx@gmail.com")))) // ---> .anyRequest().authenticated()) .saml2Login(withDefaults()) .saml2Metadata(withDefaults()) .saml2Logout(withDefaults()) .build(); } ``` `/admin`パスに対するControllerを作成します。 ```java cat <<'EOF' > src/main/java/com/example/AdminController.java package com.example; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AdminController { @GetMapping(path = "/admin") public String admin() { return "admin page"; } } EOF ``` アプリケーションを再起動して、 http://localhost:8080/admin にアクセスします。 権限のあるユーザーでログインすると、次のようにページが表示されます。 image 権限のあるユーザーでログインすると、403エラーが表示されます。 image --- Spring BootでIAM Identity CenterとSAML2連携することにより、AWSのユーザーを使ってアプリケーションにログインすることができました。 シングルログアウトが機能しませんでしたが、すでにIAM Identity Centerでユーザーを管理していて、別途ユーザー管理を重複したくない場合に便利な手段です。