---
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


**目次**
<!-- toc -->

### 雛形プロジェクトの作成

まずは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
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-saml2-service-provider</artifactId>
		</dependency>
```

`spring-security-saml2-service-provider`で使われているライブラリであるOpenSAMLは、[諸事情](https://shibboleth.atlassian.net/wiki/spaces/DEV/pages/1123844333/Use+of+Maven+Central)によりMaven Centralで利用できないため、
以下のMavenレポジトリを追加します。

```xml
<project>
	<!-- ... -->
	<repositories>
		<repository>
			<id>shibboleth</id>
			<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
		</repository>
	</repositories>
	<!-- ... -->
</project>
```

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<String, Object> 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"ボタンをクリックします。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/c0135227-fb39-4af4-9a00-e6823b5ebc39">

"I have an application I want ot set up"を選択します。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/f81f2802-7306-473b-9d1c-9c77e7777a51">

"Application type"は"SAML 2.0"を選択します。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/f880b4d6-0cf0-4e31-bdd1-e35bb0dda5f7">

"Display name"は"Hello SAML"にします。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/8ecaffa3-e9d3-41d0-b0b8-7174bd851b3f">

ここで、"IAM Identity Center metadata"の"IAM Identity Center SAML metadata file"のURLをコピーしてください。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/91ac5f11-d040-4f62-8227-49f180cf16a4">


このURLを次のように`application.properties`の`spring.security.saml2.relyingparty.registration.<registrationId>.assertingparty.metadata-uri`に設定します。

```properties
METADATA_URL=https://portal.sso.****.amazonaws.com/saml/metadata/****
cat <<EOF > 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/<registrationId>`で公開されるので、ダウンロードします。

```bash
curl -s http://localhost:8080/saml2/metadata/awssso -o ~/Downloads/metadata-awssso.xml
```

ダウンロードしたメタデータを"Application metadata"でアップロードし、"Submit"をクリックします。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/c491cde6-6a57-46f2-9fe4-6864c4288519">

アプリケーションが作成され、"Status"が"Active"になればOKです。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/16683266-4415-402e-9fd2-795f904a3c46">

次に、ユーザー・グループをこのアプリケーションにアサインします。"Assigned users adn groups"で"Assign users and groups"ボタンをクリックします。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/aa932676-5834-4513-8af4-58dcebcbad10">

ユーザーあるいはグループが存在しない場合は、新規作成してください。ここでは`developers`グループと`administrators`グループをアサインしました。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/ce0f387a-64b6-4ed7-b085-e4f6ece30391">

次のAttributeのマッピングを行います。アプリケーションの"Details"の"Actions"をクリックし、"Edit attribute mappings"を選択してください。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/a78e4d85-d9ef-4e51-a5e0-1eb0ff5bf458">

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`型)

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/94237934-6bd7-4e42-a5ea-81da17bf454c">

"Save changes"をクリックして設定を保存したら、先ほど起動したアプリケーション(http://localhost:8080)にアクセスしてください。

IAM Identity Centerのログイン画面にリダイレクトされます。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/ebc4493f-00ab-46ce-a6bf-37f409a4027b">

IAM Identity Centerにログインすると再度再度リダイレクトされます。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/40538bfb-613f-42ae-b51c-4fb56ca33553">

http://localhost:8080 にリダイレクトされ、ログインユーザー情報が表示されます。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/bbcd62f1-f478-4250-ba98-de3f6e2eacc3">

ユーザーが所属しているグループの情報が`groups` attributeに含まれますが、名前ではなくIDが取得できます。

IAM Identity Centerのグループの詳細画面でGroup IDは確認可能です。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/2441a59d-74c4-4e97-8d7b-48cb4cfca3a0">

これで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 にアクセスしてください。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/1b4c36ac-011a-4967-b1e9-6ee75f540a00">

"Log Out"ボタンをクリックしてください。

そうするとアプリケーションからログアウトし、IAM Identity Centerからもログアウトされ...ません。
IAM Identity Centerにログイン後のポータル画面にリダイレクトされます。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/5e6740de-5eb1-4efd-835b-a715f946c01f">

本来ならこの設定で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<T> implements AuthorizationManager<RequestAuthorizationContext> {


	private final Function<Saml2AuthenticatedPrincipal, T> applyer;

	private final Predicate<T> predicate;

	public SamlAuthorizationManager(Function<Saml2AuthenticatedPrincipal, T> applyer, Predicate<T> predicate) {
		this.applyer = applyer;
		this.predicate = predicate;
	}

	public static SamlAuthorizationManager<List<String>> attribute(String name, Predicate<List<String>> predicate) {
		return new SamlAuthorizationManager<>(principal -> principal.getAttribute(name), predicate);
	}

	public static SamlAuthorizationManager<String> firstAttribute(String name, Predicate<String> predicate) {
		return new SamlAuthorizationManager<>(principal -> principal.getFirstAttribute(name), predicate);
	}

	public static SamlAuthorizationManager<String> username(Predicate<String> predicate) {
		return new SamlAuthorizationManager<>(AuthenticatedPrincipal::getName, predicate);
	}

	@Override
	public AuthorizationDecision check(Supplier<Authentication> 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 にアクセスします。

権限のあるユーザーでログインすると、次のようにページが表示されます。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/3cd16f67-1598-478d-907a-f5e52c32f26a">

権限のあるユーザーでログインすると、403エラーが表示されます。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/eeeb3605-2fd0-44c0-add9-d12f01299652">

---

Spring BootでIAM Identity CenterとSAML2連携することにより、AWSのユーザーを使ってアプリケーションにログインすることができました。
シングルログアウトが機能しませんでしたが、すでにIAM Identity Centerでユーザーを管理していて、別途ユーザー管理を重複したくない場合に便利な手段です。
