Microsoft Entra ID(旧Azure AD)とSpring BootをSAML2連携するメモ。
Entra IDはOIDC Providerの機能もあるので、通常はそちらを使う方が良いです。
先の記事でIAM Identity CenterとのSAML2連携を行いましたが、動作比較をするために、あえてEntra IDでもSAML2で連携してみます。
以下はIAM Identity Centerの場合とほぼ同じですが、IAM Identity Centerでは機能しなかったシングルログアウトができています。
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)の設定
次にEntra IDでIdentity Provider (Relying Party)の設定を行います。
まずはアプリケーションを登録します。"Enterprise applications"画面で"New application"をクリックします。
"Create your own application"をクリックします。
アプリ名はhello-samlを記入、"Integrate any other application you don't find int the gallery (Non-gallery)"を選択し、"Create"ボタンをクリックします。
"single-sign on method"には"SAML"を選択します。
ここで、"SAML Certificates"で"App Federation Metadata Url"のURLをコピーしてください。
このURLを次のようにapplication.propertiesのspring.security.saml2.relyingparty.registration.<registrationId>.assertingparty.metadata-uriに設定します。
METADATA_URL=https://login.microsoftonline.com/****/federationmetadata/2007-06/federationmetadata.xml?appid=****
cat <<EOF > src/main/resources/application.properties
spring.application.name=hello-saml
spring.security.saml2.relyingparty.registration.entraid.entity-id=hello-saml
spring.security.saml2.relyingparty.registration.entraid.assertingparty.metadata-uri=$METADATA_URL
EOF
application.propertiesの設定ができたら、一旦アプリケーションを起動してください。
./mvnw spring-boot:run
Service Provider側のメタデータが/saml2/metadata/<registrationId>で公開されるので、ダウンロードします。
curl -s http://localhost:8080/saml2/metadata/entraid -o ~/Downloads/metadata-entraid.xml
ダウンロードしたメタデータを"Upload metadata file"でアップロードし、"Add"をクリックします。
アップロードした情報が"Basic SAML Configuration"に表示されます。"Save"をクリックします。
"Test single sign-on with ..."のダイアログは"No, I'll test later"ボタンをクリックしてください。
次に、ユーザー・グループをこのアプリケーションにアサインします。"Users adn groups"で"Add user/group"をクリックします。
ユーザーあるいはグループが存在しない場合は、新規作成してください。
任意のユーザーまたはグループをアサインしてください。
"Assign"ボタンをクリックして設定を保存したら、一度ログアウトします。
ログアウト後、先ほど起動したアプリケーション(http://localhost:8080)にアクセスしてください。
Entra IDのログイン画面にリダイレクトされます。
http://localhost:8080 にリダイレクトされ、ログインユーザー情報が表示されます。
デフォルトのattributesをそのまま使用しても良いですが、
ここではAttributeのマッピングを行ってみます。
"Single sign-on"で"Attributes & Claim"の"Edit"ボタンをクリックしてください。
アプリケーションの"Details"の"Actions"をクリックし、"Edit attribute mappings"を選択してください。
デフォルトのマッピングは次のようになっています。
ここではデフォルトのマッピングを削除し、次のようにマッピングしました。
Unique User Identitfier(ユーザー名として使われる) ...user.mailfirstName...user.givennamelastName...user.surnamename...user.principalname
アプリケーションを再起動して、再度 http://localhost:8080 にアクセスします。今度はマッピングしたattributesが表示されます。
これでEntra IDのユーザーを使い、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.entraid.signing.credentials[0].certificate-location=classpath:cert.pem
spring.security.saml2.relyingparty.registration.entraid.signing.credentials[0].private-key-location.=classpath:key.pem
spring.security.saml2.relyingparty.registration.entraid.singlelogout.binding=post
spring.security.saml2.relyingparty.registration.entraid.singlelogout.response-url={baseUrl}/logout/saml2/slo/{registrationId}
EOF
アプリケーションを停止し、再起動します。
./mvnw spring-boot:run
一度、 http://localhost:8080 にアクセスした後、 http://localhost:8080/logout にアクセスしてください。
"Log Out"ボタンをクリックしてください。
そうするとアプリケーションからログアウトし、Entra IDからもログアウトされます。
再度 http://localhost:8080 にアクセスするとEntra IDの再ログインが求められます。
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のパスにアクセスするには、以下のいずれかを満たす必要があるとします。
groupsattributeにa7443a38-70d1-709f-aa2c-4841adf65ed1を含むemailattribute(先頭の要素のみ対象)が@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 にアクセスします。
権限のあるユーザーでログインすると、次のようにページが表示されます。
権限のあるユーザーでログインすると、403エラーが表示されます。
Spring BootでEntra IDとSAML2連携することにより、Entra IDのユーザーを使ってアプリケーションにログインすることができました。
Entra IDの場合は、OIDCも対応しているので、あまりSAML2を使うケースはないかもしれません。