最新の安定バージョンについては、Spring Security 6.3.1 を使用してください!

SAML 2.0 ログインの概要

SAML 2.0 証明書利用者認証が Spring Security 内でどのように機能するかを見てみましょう。まず、OAuth 2.0 ログインと同様に、Spring Security はユーザーをサードパーティに誘導して認証を実行することがわかります。これは、一連のリダイレクトを通じて行われます。

saml2webssoauthenticationrequestfilter
図 1: アサーティングパーティ認証へのリダイレクト

上の図は、SecurityFilterChain および AbstractAuthenticationProcessingFilter 図を基にしています。

number 1 最初に、ユーザーが認可されていないリソース /private に対して認証されていないリクエストを行います。

number 2Spring Security の FilterSecurityInterceptor は、認証されていないリクエストが AccessDeniedException をスローすることによって拒否されたことを示します。

number 3 ユーザーに認可がないため、ExceptionTranslationFilter認証開始を開始します。構成された AuthenticationEntryPoint は、<saml2:AuthnRequest> 生成エンドポイントSaml2WebSsoAuthenticationRequestFilter にリダイレクトする LoginUrlAuthenticationEntryPoint (Javadoc) のインスタンスです。または、複数のアサートパーティを設定した場合は、最初にピッカーページにリダイレクトされます。

number 4 次に、Saml2WebSsoAuthenticationRequestFilter は、構成された Saml2AuthenticationRequestFactory を使用して <saml2:AuthnRequest> を作成、署名、シリアライズ、エンコードします。

number 5 次に、ブラウザーはこの <saml2:AuthnRequest> を受け取り、アサーティングパーティに提示します。アサーティングパーティは、ユーザーの認証を試みます。成功すると、ブラウザーに <saml2:Response> が返されます。

number 6 次に、ブラウザーは <saml2:Response> をアサーションコンシューマーサービスエンドポイントに POST します。

saml2webssoauthenticationfilter
図 2: <saml2:Response> の認証

この図は、SecurityFilterChain ダイアグラムから構築されています。

number 1 ブラウザーが <saml2:Response> をアプリケーションに送信すると、ブラウザーは Saml2WebSsoAuthenticationFilter委譲します。このフィルターは、構成された AuthenticationConverter を呼び出して、HttpServletRequest からレスポンスを抽出することによって Saml2AuthenticationToken を作成します。このコンバーターはさらに RelyingPartyRegistration を解決し、Saml2AuthenticationToken に供給します。

number 2 次に、フィルターはトークンを構成済みの AuthenticationManager に渡します。デフォルトでは、OpenSAML authentication provider を使用します。

number 3 認証に失敗した場合、Failure

number 4 認証が成功した場合は、Success .

  • AuthenticationSecurityContextHolder に設定されます。

  • Saml2WebSsoAuthenticationFilter は FilterChain#doFilter(request,response) を呼び出して、残りのアプリケーションロジックを続行します。

最小限の依存関係

SAML 2.0 サービスプロバイダーのサポートは spring-security-saml2-service-provider にあります。OpenSAML ライブラリを基に構築されています。

最小構成

Spring Boot を使用する場合、アプリケーションをサービスプロバイダーとして設定するには、2 つの基本的な手順があります。最初に、必要な依存関係を含め、次に必要なアサーティングパーティのメタデータを示します。

また、これは、すでにあなたの主張する当事者に依拠当事者を登録していることを前提としています。

ID プロバイダーのメタデータの指定

Spring Boot アプリケーションで ID プロバイダーのメタデータを指定するには、次のようにします。

spring:
  security:
    saml2:
      relyingparty:
        registration:
          adfs:
            identityprovider:
              entity-id: https://idp.example.com/issuer
              verification.credentials:
                - certificate-location: "classpath:idp.crt"
              singlesignon.url: https://idp.example.com/issuer/sso
              singlesignon.sign-request: false

where

以上です!

ID プロバイダーとアサーティングパーティはシノニムであり、サービスプロバイダと依存パーティも同義です。これらは、それぞれ AP および RP と略されることがよくあります。

ランタイムの期待

上で設定したように、アプリケーションは SAMLResponse パラメーターを含む POST /login/saml2/sso/{registrationId} リクエストを処理します。

POST /login/saml2/sso/adfs HTTP/1.1

SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...

アサーティングパーティが SAMLResponse を生成するように誘導するには、2 つの方法があります。

  • まず、アサーティングパーティに移動できます。おそらく、SAMLResponse を送信するためにクリックできる、登録済みの証明書利用者ごとに何らかのリンクまたはボタンがあります。

  • 次に、アプリ内の保護されたページ(localhost:8080 など)に移動できます。次に、アプリは構成されたアサーティングパーティーにリダイレクトし、アサーティングパーティーが SAMLResponse を送信します。

ここから、次へのジャンプを検討してください。

SAML 2.0 ログインが OpenSAML と統合する方法

Spring Security の SAML 2.0 サポートには、いくつかの設計ゴールがあります。

  • まず、SAML 2.0 操作とドメインオブジェクトのライブラリに依存します。これを達成するために、Spring Security は OpenSAML を使用します。

  • 次に、Spring Security の SAML サポートを使用するときにこのライブラリが不要であることを確認します。これを実現するために、Spring Security が契約で OpenSAML を使用するすべてのインターフェースまたはクラスは、カプセル化されたままです。これにより、OpenSAML を他のいくつかのライブラリ、サポートされていないバージョンの OpenSAML に切り替えることができます。

上記の 2 つのゴールの当然の結果として、Spring Security の SAML API は他のモジュールに比べてかなり小さいです。代わりに、OpenSaml4AuthenticationRequestFactory や OpenSaml4AuthenticationProvider などのクラスは、認証プロセスのさまざまなステップをカスタマイズする Converter を公開します。

例: アプリケーションが SAMLResponse を受け取り、Saml2WebSsoAuthenticationFilter に委譲すると、フィルターは OpenSaml4AuthenticationProvider に委譲します。

下位互換性のために、Spring Security はデフォルトで最新の OpenSAML3 を使用します。OpenSAML 3 はサポートが終了したため、OpenSAML 4.x に更新することをお勧めします。そのため、Spring Security は OpenSAML3.x および 4.x の両方をサポートしています。OpenSAML の 4.x への依存関係を管理する場合、Spring Security は OpenSAML4.x 実装を選択します。
OpenSAML Response の認証

opensamlauthenticationprovider

この図は、Saml2WebSsoAuthenticationFilter 図に基づいています。

number 1Saml2WebSsoAuthenticationFilter は Saml2AuthenticationToken を作成し、AuthenticationManager を呼び出します。

number 2AuthenticationManager は、OpenSAML 認証プロバイダーを呼び出します。

number 3 認証プロバイダーは、レスポンスを OpenSAML Response に逆直列化し、その署名をチェックします。署名が無効な場合、認証は失敗します。

number 4 次に、プロバイダーは EncryptedAssertion 要素を復号化します。復号化が失敗すると、認証は失敗します。

number 5 次に、プロバイダーはレスポンスの Issuer および Destination 値を検証します。それらが RelyingPartyRegistration の内容と一致しない場合、認証は失敗します。

number 6 その後、プロバイダーは各 Assertion の署名を検証します。いずれかの署名が無効な場合、認証は失敗します。また、レスポンスにもアサーションにも署名がない場合、認証は失敗します。レスポンスまたはすべてのアサーションに署名が必要です。

number 7 次に、プロバイダーは、EncryptedID または EncryptedAttribute 要素を復号化します。]。復号化が失敗すると、認証は失敗します。

number 8 次に、プロバイダは各アサーションの ExpiresAt および NotBefore タイムスタンプ、<Subject> およびすべての <AudienceRestriction> 条件を検証します。いずれかの検証が失敗すると、認証は失敗します。

number 9 その後、プロバイダーは最初のアサーションの AttributeStatement を取得し、それを Map<String, List<Object>> にマップします。また、ROLE_USER 付与権限も付与します。

number 10 そして最後に、最初のアサーションから NameID、属性の MapGrantedAuthority を取得し、Saml2AuthenticatedPrincipal を構築します。次に、そのプリンシパルと権限を Saml2Authentication に配置します。

結果の Authentication#getPrincipal は Spring Security Saml2AuthenticatedPrincipal オブジェクトであり、Authentication#getName は最初のアサーションの NameID 要素にマップされます。Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId は、関連付けられた RelyingPartyRegistration の識別子を保持します。

OpenSAML 構成のカスタマイズ

次のように、Spring Security と OpenSAML の両方を使用するクラスは、クラスの先頭で OpenSamlInitializationService を静的に初期化する必要があります。

  • Java

  • Kotlin

static {
	OpenSamlInitializationService.initialize();
}
companion object {
    init {
        OpenSamlInitializationService.initialize()
    }
}

これは OpenSAML の InitializationService#initialize に代わるものです。

OpenSAML が SAML オブジェクトを構築、マーシャリング、アンマーシャリングする方法をカスタマイズすることが役立つ場合があります。このような状況では、代わりに OpenSAML の XMLObjectProviderFactory へのアクセスを提供する OpenSamlInitializationService#requireInitialize(Consumer) を呼び出すことができます。

例: 署名されていない AuthNRequest を送信する場合、再認証を強制することができます。その場合、次のように独自の AuthnRequestMarshaller を登録できます。

  • Java

  • Kotlin

static {
    OpenSamlInitializationService.requireInitialize(factory -> {
        AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
            @Override
            public Element marshall(XMLObject object, Element element) throws MarshallingException {
                configureAuthnRequest((AuthnRequest) object);
                return super.marshall(object, element);
            }

            public Element marshall(XMLObject object, Document document) throws MarshallingException {
                configureAuthnRequest((AuthnRequest) object);
                return super.marshall(object, document);
            }

            private void configureAuthnRequest(AuthnRequest authnRequest) {
                authnRequest.setForceAuthn(true);
            }
        }

        factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
    });
}
companion object {
    init {
        OpenSamlInitializationService.requireInitialize {
            val marshaller = object : AuthnRequestMarshaller() {
                override fun marshall(xmlObject: XMLObject, element: Element): Element {
                    configureAuthnRequest(xmlObject as AuthnRequest)
                    return super.marshall(xmlObject, element)
                }

                override fun marshall(xmlObject: XMLObject, document: Document): Element {
                    configureAuthnRequest(xmlObject as AuthnRequest)
                    return super.marshall(xmlObject, document)
                }

                private fun configureAuthnRequest(authnRequest: AuthnRequest) {
                    authnRequest.isForceAuthn = true
                }
            }
            it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
        }
    }
}

requireInitialize メソッドは、アプリケーションインスタンスごとに 1 回だけ呼び出すことができます。

Boot 自動構成のオーバーライドまたは置換

Spring Boot が依存パーティ用に生成する 2 つの @Bean があります。

1 つ目は、アプリを証明書利用者として構成する SecurityFilterChain です。spring-security-saml2-service-provider を含めると、SecurityFilterChain は次のようになります。

デフォルトの SAML 2.0 ログイン構成
  • Java

  • Kotlin

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .saml2Login(withDefaults());
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        saml2Login { }
    }
    return http.build()
}

アプリケーションが SecurityFilterChain Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

これを置き換えるには、アプリケーション内で Bean を公開します。

カスタム SAML 2.0 ログイン構成
  • Java

  • Kotlin

@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(withDefaults());
        return http.build();
    }
}
@EnableWebSecurity
class MyCustomSecurityConfiguration {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("ROLE_USER"))
                authorize(anyRequest, authenticated)
            }
            saml2Login {
            }
        }
        return http.build()
    }
}

上記では、/messages/ で始まる URL の USER のロールが必要です。

2 番目の @Bean Spring Boot が作成する RelyingPartyRegistrationRepository (Javadoc) は、アサーティングパーティと依存パーティのメタデータを表します。これには、アサーティングパーティに認証をリクエストするときに依存パーティが使用する必要がある SSO エンドポイントの場所などが含まれます。

独自の RelyingPartyRegistrationRepository Bean を公開することにより、デフォルトを上書きできます。例: 次のようにメタデータエンドポイントにアクセスすることにより、アサーティングパーティの構成を検索できます。

証明書利用者登録リポジトリ
  • Java

  • Kotlin

@Value("${metadata.location}")
String assertingPartyMetadataLocation;

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
    RelyingPartyRegistration registration = RelyingPartyRegistrations
            .fromMetadataLocation(assertingPartyMetadataLocation)
            .registrationId("example")
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Value("\${metadata.location}")
var assertingPartyMetadataLocation: String? = null

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
    val registration = RelyingPartyRegistrations
        .fromMetadataLocation(assertingPartyMetadataLocation)
        .registrationId("example")
        .build()
    return InMemoryRelyingPartyRegistrationRepository(registration)
}
registrationId は、登録を区別するために選択する任意の値です。

または、以下に示すように、各詳細を手動で提供できます。

証明書利用者登録リポジトリの手動設定
  • Java

  • Kotlin

@Value("${verification.key}")
File verificationKey;

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
    X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
    Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
    RelyingPartyRegistration registration = RelyingPartyRegistration
            .withRegistrationId("example")
            .assertingPartyDetails(party -> party
                .entityId("https://idp.example.com/issuer")
                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
                .wantAuthnRequestsSigned(false)
                .verificationX509Credentials(c -> c.add(credential))
            )
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Value("\${verification.key}")
var verificationKey: File? = null

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
    val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
    val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
    val registration = RelyingPartyRegistration
        .withRegistrationId("example")
        .assertingPartyDetails { party: AssertingPartyDetails.Builder ->
            party
                .entityId("https://idp.example.com/issuer")
                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
                .wantAuthnRequestsSigned(false)
                .verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
                    c.add(
                        credential
                    )
                }
        }
        .build()
    return InMemoryRelyingPartyRegistrationRepository(registration)
}
X509Support は OpenSAML クラスであり、簡潔にするためにスニペットで使用されていることに注意してください

または、DSL を使用してリポジトリを直接接続することもできます。これにより、自動構成された SecurityFilterChain もオーバーライドされます。

カスタム依拠当事者登録 DSL
  • Java

  • Kotlin

@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .relyingPartyRegistrationRepository(relyingPartyRegistrations())
            );
        return http.build();
    }
}
@EnableWebSecurity
class MyCustomSecurityConfiguration {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("ROLE_USER"))
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                relyingPartyRegistrationRepository = relyingPartyRegistrations()
            }
        }
        return http.build()
    }
}
RelyingPartyRegistrationRepository に複数の証明書利用者を登録することにより、証明書利用者をマルチテナントにすることができます。

RelyingPartyRegistration

RelyingPartyRegistration (Javadoc) インスタンスは、依存パーティとアサーティングパーティのメタデータ間のリンクを表します。

RelyingPartyRegistration では、Issuer 値のような依存パーティメタデータを提供できます。このメタデータは、SAML レスポンスが送信されることを期待し、ペイロードに署名または復号化するために所有する資格情報を提供します。

また、AuthnRequests の送信先となる Issuer 値などのアサーティングパーティのメタデータと、依存パーティがペイロードを検証または暗号化する目的で所有するパブリック資格情報を提供できます。

次の RelyingPartyRegistration は、ほとんどのセットアップに最低限必要なものです。

  • Java

  • Kotlin

RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
        .fromMetadataLocation("https://ap.example.org/metadata")
        .registrationId("my-id")
        .build();
val relyingPartyRegistration = RelyingPartyRegistrations
    .fromMetadataLocation("https://ap.example.org/metadata")
    .registrationId("my-id")
    .build()

任意の InputStream ソースから RelyingPartyRegistration を作成することもできることに注意してください。そのような例の 1 つは、メタデータがデータベースに保存されている場合です。

String xml = fromDatabase();
try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
            .fromMetadata(source)
            .registrationId("my-id")
            .build();
}

次のように、より高度な設定も可能です。

  • Java

  • Kotlin

RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
        .entityId("{baseUrl}/{registrationId}")
        .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
        .assertingPartyDetails(party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        )
        .build();
val relyingPartyRegistration =
    RelyingPartyRegistration.withRegistrationId("my-id")
        .entityId("{baseUrl}/{registrationId}")
        .decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
            c.add(relyingPartyDecryptingCredential())
        }
        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
        .assertingPartyDetails { party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        }
        .build()
最上位のメタデータメソッドは、証明書利用者に関する詳細です。assertingPartyDetails 内のメソッドは、アサーティングパーティに関する詳細です。
依存パーティが SAML レスポンスを期待している場所は、アサーションコンシューマーサービスの場所です。

証明書利用者の entityId のデフォルトは {baseUrl}/saml2/service-provider-metadata/{registrationId} です。これは、アサーティングパーティが依存パーティについて知るように設定するときに必要なこの値です。

assertionConsumerServiceLocation のデフォルトは /login/saml2/sso/{registrationId} です。デフォルトでは、フィルターチェーンの Saml2WebSsoAuthenticationFilter にマッピングされます。

URI パターン

上記の例で、{baseUrl} および {registrationId} プレースホルダーに気付いたと思います。

これらは、URI の生成に役立ちます。そのため、依存パーティの entityId および assertionConsumerServiceLocation は、次のプレースホルダーをサポートしています。

  • baseUrl - デプロイされたアプリケーションのスキーム、ホスト、ポート

  • registrationId - この依存パーティの登録 ID

  • baseScheme - デプロイされたアプリケーションのスキーム

  • baseHost - デプロイされたアプリケーションのホスト

  • basePort - デプロイされたアプリケーションのポート

例: 上記で定義された assertionConsumerServiceLocation は次のとおりです。

/my-login-endpoint/{registrationId}

デプロイされたアプリケーションではこれに変換されます

/my-login-endpoint/adfs

上記の entityId は次のように定義されました。

{baseUrl}/{registrationId}

デプロイされたアプリケーションではこれに変換されます

https://rp.example.com/adfs

一般的な URI パターンは次のとおりです。

registrationId は RelyingPartyRegistration の主要な識別子であるため、認証されていないシナリオの URL で必要になります。何らかの理由で URL から registrationId を削除したい場合は、RelyingPartyRegistrationResolver を指定して Spring Security に registrationId を検索する方法を伝えることができます。

資格情報

また、使用された認証情報にも気づいたでしょう。

多くの場合、証明書利用者は、同じキーを使用してペイロードに署名し、ペイロードを復号化します。または、同じキーを使用してペイロードを検証し、暗号化します。

このため、Spring Security には Saml2X509Credential が同梱されており、これは SAML 固有の認証情報であり、さまざまなユースケースで同じキーを簡単に構成できます。

少なくとも、アサーティングパーティの署名されたレスポンスを検証できるように、アサーティングパーティからの証明書が必要です。

アサーティングパーティからのアサーションを検証するために使用する Saml2X509Credential を構築するには、ファイルをロードして次のように CertificateFactory を使用できます。

  • Java

  • Kotlin

Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
    X509Certificate certificate = (X509Certificate)
            CertificateFactory.getInstance("X.509").generateCertificate(is);
    return Saml2X509Credential.verification(certificate);
}
val resource = ClassPathResource("ap.crt")
resource.inputStream.use {
    return Saml2X509Credential.verification(
        CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
    )
}

アサーティングパーティがアサーションも暗号化するとします。その場合、依存パーティは、暗号化された値を復号化できるようにするための秘密キーを必要とします。

その場合、RSAPrivateKey とそれに対応する X509Certificate が必要になります。1 つ目は Spring Security の RsaKeyConverters ユーティリティクラスを使用して、2 つ目は以前と同様にロードできます。

  • Java

  • Kotlin

X509Certificate certificate = relyingPartyDecryptionCertificate();
Resource resource = new ClassPathResource("rp.crt");
try (InputStream is = resource.getInputStream()) {
    RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
    return Saml2X509Credential.decryption(rsa, certificate);
}
val certificate: X509Certificate = relyingPartyDecryptionCertificate()
val resource = ClassPathResource("rp.crt")
resource.inputStream.use {
    val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
    return Saml2X509Credential.decryption(rsa, certificate)
}
これらのファイルの場所を適切な Spring Boot プロパティとして指定すると、Spring Boot がこれらの変換を実行します。

証明書利用者構成の複製

アプリケーションが複数のアサーティングパーティを使用する場合、いくつかの構成が RelyingPartyRegistration インスタンス間で複製されます。

  • 依存パーティの entityId

  • その assertionConsumerServiceLocation、および

  • その署名、たとえば署名または復号化の証明書

この設定の良いところは、一部の ID プロバイダーと他の ID プロバイダーで資格情報を簡単にローテーションできることです。

重複はいくつかの方法で軽減できます。

まず、YAML では、次のように参照でこれを軽減できます。

spring:
  security:
    saml2:
      relyingparty:
        okta:
          signing.credentials: &relying-party-credentials
            - private-key-location: classpath:rp.key
              certificate-location: classpath:rp.crt
          identityprovider:
            entity-id: ...
        azure:
          signing.credentials: *relying-party-credentials
          identityprovider:
            entity-id: ...

次に、データベースでは、RelyingPartyRegistration のモデルを複製する必要はありません。

3 番目に、Java では、次のようにカスタム構成メソッドを作成できます。

  • Java

  • Kotlin

private RelyingPartyRegistration.Builder
        addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {

    Saml2X509Credential signingCredential = ...
    builder.signingX509Credentials(c -> c.addAll(signingCredential));
    // ... other relying party configurations
}

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
    RelyingPartyRegistration okta = addRelyingPartyDetails(
            RelyingPartyRegistrations
                .fromMetadataLocation(oktaMetadataUrl)
                .registrationId("okta")).build();

    RelyingPartyRegistration azure = addRelyingPartyDetails(
            RelyingPartyRegistrations
                .fromMetadataLocation(oktaMetadataUrl)
                .registrationId("azure")).build();

    return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
}
private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
    val signingCredential: Saml2X509Credential = ...
    builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
        c.add(
            signingCredential
        )
    }
    // ... other relying party configurations
}

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
    val okta = addRelyingPartyDetails(
        RelyingPartyRegistrations
            .fromMetadataLocation(oktaMetadataUrl)
            .registrationId("okta")
    ).build()
    val azure = addRelyingPartyDetails(
        RelyingPartyRegistrations
            .fromMetadataLocation(oktaMetadataUrl)
            .registrationId("azure")
    ).build()
    return InMemoryRelyingPartyRegistrationRepository(okta, azure)
}

リクエストからの RelyingPartyRegistration の解決

これまで見てきたように、Spring Security は URI パスで登録 ID を探すことによって RelyingPartyRegistration を解決します。

カスタマイズする理由はいくつかあります。その中で:

RelyingPartyRegistration の解決方法をカスタマイズするために、カスタム RelyingPartyRegistrationResolver を構成できます。デフォルトでは、URI の最後のパス要素から登録 ID を検索し、RelyingPartyRegistrationRepository で検索します。

RelyingPartyRegistration にプレースホルダーがある場合は、リゾルバー実装が解決する必要があることに注意してください。

単一の一貫した RelyingPartyRegistration への解決

たとえば、常に同じ RelyingPartyRegistration を返すリゾルバーを提供できます。

  • Java

  • Kotlin

public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {

    private final RelyingPartyRegistrationResolver delegate;

    public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
        this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
    }

    @Override
    public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
        return this.delegate.resolve(request, "single");
    }
}
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
    override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
        return this.delegate.resolve(request, "single")
    }
}
次に、このリゾルバーを使用して <saml2:SPSSODescriptor> メタデータの生成をカスタマイズする方法を見てみましょう。

<saml2:Response#Issuer> に基づく解決

複数のアサーティングパーティからのアサーションを受け入れることができる 1 つの依存パーティがある場合、アサーティングパーティと同数の RelyingPartyRegistration が存在し、依存パーティ情報が各インスタンス間で複製されます

これは、アサーションコンシューマーサービスエンドポイントがアサーティングパーティごとに異なることを意味しますが、これは望ましくない場合があります。

代わりに、Issuer を介して registrationId を解決できます。これを行う RelyingPartyRegistrationResolver のカスタム実装は次のようになります。

  • Java

  • Kotlin

public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
	private final InMemoryRelyingPartyRegistrationRepository registrations;

	// ... constructor

    @Override
    RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
		if (registrationId != null) {
			return this.registrations.findByRegistrationId(registrationId);
		}
        String entityId = resolveEntityIdFromSamlResponse(request);
        for (RelyingPartyRegistration registration : this.registrations) {
            if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
                return registration;
            }
        }
        return null;
    }

	private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
		// ...
	}
}
class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
        RelyingPartyRegistrationResolver {
    @Override
    fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
		if (registrationId != null) {
			return this.registrations.findByRegistrationId(registrationId)
		}
        String entityId = resolveEntityIdFromSamlResponse(request)
        for (val registration : this.registrations) {
            if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
                return registration
            }
        }
        return null
    }

	private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
		// ...
	}
}
次に、このリゾルバーを使用して <saml2:Response> 認証をカスタマイズする方法を見てみましょう。

連携ログイン

SAML 2.0 の一般的な取り決めの 1 つは、複数のアサーティングパーティを持つ ID プロバイダーです。この場合、ID プロバイダーのメタデータエンドポイントは複数の <md:IDPSSODescriptor> 要素を返します。

これらの複数のアサーティングパーティは、次のように RelyingPartyRegistrations への 1 回の呼び出しでアクセスできます。

  • Java

  • Kotlin

Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
        .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
        .stream().map((builder) -> builder
            .registrationId(UUID.randomUUID().toString())
            .entityId("https://example.org/saml2/sp")
            .build()
        )
        .collect(Collectors.toList()));
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
        .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
        .stream().map { builder : RelyingPartyRegistration.Builder -> builder
            .registrationId(UUID.randomUUID().toString())
            .entityId("https://example.org/saml2/sp")
            .build()
        }
        .collect(Collectors.toList()));

登録 ID がランダムな値に設定されているため、これにより特定の SAML 2.0 エンドポイントが予測不能になることに注意してください。これに対処するにはいくつかの方法があります。フェデレーションの特定のユースケースに適した方法に焦点を当てましょう。

多くのフェデレーションの場合、すべてのアサーティングパーティがサービスプロバイダーの設定を共有します。Spring Security がデフォルトで、その SAML 2.0 URI のすべてに registrationId を含めることを考えると、次のステップは、多くの場合、これらの URI を変更して registrationId を除外することです。

これらの行に沿って変更したい主な URI が 2 つあります。

必要に応じて、認証リクエストの場所を変更することもできますが、これはアプリ内部の URI であり、アサーティングパーティには公開されないため、多くの場合、メリットは最小限です。

この完全な例は、saml-extension-federation サンプル [GitHub] (英語) で確認できます。

Spring Security SAML 拡張 URI の使用

Spring Security SAML 拡張機能から移行する場合、SAML 拡張 URI のデフォルトを使用するようにアプリケーションを構成すると、利点が得られる場合があります。