Spring BootでIAM Identity Center(旧AWS SSO)とSAML2連携してログインするメモ

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で雛形プロジェクトを作成します。

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に追加します。

		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-saml2-service-provider</artifactId>
		</dependency>

spring-security-saml2-service-providerで使われているライブラリであるOpenSAMLは、諸事情によりMaven Centralで利用できないため、 以下のMavenレポジトリを追加します。

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

SAML2でログインするための設定をSecurityConfigに記述します。

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を作成します。

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

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.propertiesspring.security.saml2.relyingparty.registration.<registrationId>.assertingparty.metadata-uriに設定します。

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の設定ができたら、一旦アプリケーションを起動してください。

./mvnw spring-boot:run

Service Provider側のメタデータが/saml2/metadata/<registrationId>で公開されるので、ダウンロードします。

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に次の設定を追加します。

@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();
	}
}

シングルログアウトのリクエストは署名が必要となります。次のコマンドで秘密鍵と公開鍵を設定します。

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に次の設定を追記します。

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

アプリケーションを停止し、再起動します。

./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)の場合は同じ設定でシングルログアウトできました。

ここはIAM Identity Centerの制約かもしれないので、このままにします。

Attributesによる認可

SAML2でログインしたユーザー情報にはデフォルトでROLE_USER authorityがついています。 ドキュメントでは作成されるユーザー情報をカスタマイズする方法が紹介されています。 この方法を使って、ユーザーに任意のauthorityを追加することができます。

それよりもユーザー情報が持つattributesを使って直接認可設定できた方が便利なので、次のようなAuthorizationManagerを実装します。

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を使って次のように定義できます。

// ...
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を作成します。

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でユーザーを管理していて、別途ユーザー管理を重複したくない場合に便利な手段です。