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

image

"Create your own application"をクリックします。

image

アプリ名はhello-samlを記入、"Integrate any other application you don't find int the gallery (Non-gallery)"を選択し、"Create"ボタンをクリックします。

image

"single-sign on method"には"SAML"を選択します。

image

ここで、"SAML Certificates"で"App Federation Metadata Url"のURLをコピーしてください。

image

このURLを次のようにapplication.propertiesspring.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"をクリックします。

image

アップロードした情報が"Basic SAML Configuration"に表示されます。"Save"をクリックします。

image

"Test single sign-on with ..."のダイアログは"No, I'll test later"ボタンをクリックしてください。

image

次に、ユーザー・グループをこのアプリケーションにアサインします。"Users adn groups"で"Add user/group"をクリックします。

ユーザーあるいはグループが存在しない場合は、新規作成してください。

image

任意のユーザーまたはグループをアサインしてください。

image

"Assign"ボタンをクリックして設定を保存したら、一度ログアウトします。

image

ログアウト後、先ほど起動したアプリケーション(http://localhost:8080)にアクセスしてください。

Entra IDのログイン画面にリダイレクトされます。

image

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

image

デフォルトのattributesをそのまま使用しても良いですが、

ここではAttributeのマッピングを行ってみます。

"Single sign-on"で"Attributes & Claim"の"Edit"ボタンをクリックしてください。

アプリケーションの"Details"の"Actions"をクリックし、"Edit attribute mappings"を選択してください。

image

デフォルトのマッピングは次のようになっています。

image

ここではデフォルトのマッピングを削除し、次のようにマッピングしました。

  • Unique User Identitfier (ユーザー名として使われる) ... user.mail
  • firstName ... user.givenname
  • lastName ... user.surname
  • name ... user.principalname
image

アプリケーションを再起動して、再度 http://localhost:8080 にアクセスします。今度はマッピングしたattributesが表示されます。

image

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

image

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

そうするとアプリケーションからログアウトし、Entra IDからもログアウトされます。

image

再度 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のパスにアクセスするには、以下のいずれかを満たす必要があるとします。

  • 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でEntra IDとSAML2連携することにより、Entra IDのユーザーを使ってアプリケーションにログインすることができました。
Entra IDの場合は、OIDCも対応しているので、あまりSAML2を使うケースはないかもしれません。

Found a mistake? Update the entry.
Share this article: