SAML 2.0 ログインの概要

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

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

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

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

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

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

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

number 5 次に、ブラウザーはこの <saml2:AuthnRequest> を取得し、アサート側に提示します。アサート側はユーザーの認証を試みます。成功すると、<saml2:Response> がブラウザーに返されます。

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

次のイメージは、Spring Security が <saml2:Response> を認証する方法を示しています。

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

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

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

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

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

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

  • AuthenticationSecurityContextHolder に設定されます。

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

最小限の依存関係

SAML 2.0 サービスプロバイダーのサポートは spring-security-saml2-service-provider にあります。これは OpenSAML ライブラリから構築されているため、ビルド構成に Shibboleth Maven リポジトリも含める必要があります。別のリポジトリが必要な理由の詳細については、このリンク (英語) を確認してください。

<repositories>
    <!-- ... -->
    <repository>
        <id>shibboleth-releases</id>
        <url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
    </repository>
</repositories>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
repositories {
    // ...
    maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
    // ...
    implementation 'org.springframework.security:spring-security-saml2-service-provider'
}

最小構成

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

内容:

以上です!

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 は他のモジュールに比べて非常に小さいです。代わりに、OpenSamlAuthenticationRequestFactory や OpenSamlAuthenticationProvider などのクラスは、認証プロセスのさまざまなステップをカスタマイズする Converter 実装を公開します。

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

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

@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(withDefaults());
        return http.build();
    }
}
@Configuration
@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")
            .assertingPartyMetadata(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")
        .assertingPartyMetadata { party: AssertingPartyMetadata.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

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

メタデータを定期的にリフレッシュしたい場合は、次のようにリポジトリを CachingRelyingPartyRegistrationRepository でラップします。

キャッシュ依存パーティ登録リポジトリ
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public RelyingPartyRegistrationRepository registrations(CacheManager cacheManager) {
		Supplier<IterableRelyingPartyRegistrationRepository> delegate = () ->
            new InMemoryRelyingPartyRegistrationRepository(RelyingPartyRegistrations
                .fromMetadataLocation("https://idp.example.org/ap/metadata")
                .registrationId("ap").build());
		CachingRelyingPartyRegistrationRepository registrations =
            new CachingRelyingPartyRegistrationRepository(delegate);
		registrations.setCache(cacheManager.getCache("my-cache-name"));
        return registrations;
    }
}
@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration  {
    @Bean
    fun registrations(cacheManager: CacheManager): RelyingPartyRegistrationRepository {
        val delegate = Supplier<IterableRelyingPartyRegistrationRepository> {
             InMemoryRelyingPartyRegistrationRepository(RelyingPartyRegistrations
                .fromMetadataLocation("https://idp.example.org/ap/metadata")
                .registrationId("ap").build())
        }
        val registrations = CachingRelyingPartyRegistrationRepository(delegate)
        registrations.setCache(cacheManager.getCache("my-cache-name"))
        return registrations
    }
}

このようにして、 `RelyingPartyRegistration` のセットはキャッシュの削除スケジュールに基づいてリフレッシュされます。

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}")
        .assertingPartyMetadata(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}")
        .assertingPartyMetadata { party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        }
        .build()

最上位のメタデータメソッドは、証明書利用者に関する詳細です。AssertingPartyMetadata 内のメソッドは、アサーティングパーティに関する詳細です。

依存パーティが 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 を解決します。

ユースケースに応じて、他の多くの戦略を使用して 1 つを導き出します。例:

  • 処理のために、<saml2:Response>`s, the `RelyingPartyRegistration は関連する <saml2:AuthRequest> または <saml2:Response#Issuer> 要素から検索されます。

  • <saml2:LogoutRequest>`s, the `RelyingPartyRegistration を処理するために、現在ログインしているユーザーまたは <saml2:LogoutRequest#Issuer> 要素から検索されます

  • メタデータを公開する場合、RelyingPartyRegistration`s are looked up from any repository that also implements `Iterable<RelyingPartyRegistration>

これを調整する必要がある場合は、これをカスタマイズすることを目的としたこれらの各エンドポイントの特定のコンポーネントに目を向けることができます。

  • SAML レスポンスの場合は、AuthenticationConverter をカスタマイズします

  • ログアウトリクエストの場合、Saml2LogoutRequestValidatorParametersResolver をカスタマイズします

  • メタデータについては、Saml2MetadataResponseResolver をカスタマイズします

連携ログイン

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")
            .assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso")
            .build()
        }
        .collect(Collectors.toList())

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

多くのフェデレーションの場合、すべてのアサーティングパーティがサービスプロバイダーの設定を共有します。Spring Security がデフォルトでサービスプロバイダーのメタデータに registrationId を含めることを考えると、もう 1 つのステップは、対応する URI を変更して registrationId を除外することです。これは、entityId と assertionConsumerServiceLocation が静的エンドポイントで構成されている上記のサンプルですでに行われていることがわかります。

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

Spring Security SAML 拡張 URI の使用

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