シングルログアウトの実行

Spring Security には、その他のログアウトメカニズムの中でも、RP および AP によって開始される SAML 2.0 シングルログアウトのサポートが付属しています。

簡単に言うと、Spring Security がサポートする 2 つのユースケースがあります。

  • RP 開始 - アプリケーションには、POST されると、ユーザーをログアウトして saml2:LogoutRequest をアサート側に送信するエンドポイントがあります。その後、アサート側は saml2:LogoutResponse を送り返し、アプリケーションが応答できるようにします。

  • AP-Initiated- アプリケーションには、アサート側から saml2:LogoutRequest を受信するエンドポイントがあります。アプリケーションはその時点でログアウトを完了し、saml2:LogoutResponse をアサート側に送信します。

AP が開始するシナリオでは、アプリケーションがログアウト後に実行するローカルリダイレクトは無効になります。アプリケーションが saml2:LogoutResponse を送信すると、ブラウザーを制御できなくなります。

シングルログアウトの最小構成

Spring Security の SAML 2.0 シングルログアウト機能を使用するには、次のものが必要です。

  • まず、アサート側は SAML 2.0 シングルログアウトをサポートする必要があります

  • 次に、アサート側は、アプリケーションの /logout/saml2/slo エンドポイントに saml2:LogoutRequest と saml2:LogoutResponse に署名して POST するように構成する必要があります。

  • 第 3 に、アプリケーションには、saml2:LogoutRequest と saml2:LogoutResponse に署名するための PKCS#8 秘密鍵と X.509 証明書が必要です。

Spring Boot では、次の方法でこれを実現できます。

spring:
  security:
    saml2:
      relyingparty:
        registration:
          metadata:
            signing.credentials: (3)
              - private-key-location: classpath:credentials/rp-private.key
                certificate-location: classpath:credentials/rp-certificate.crt
            singlelogout.url: "{baseUrl}/logout/saml2/slo" (2)
            assertingparty:
              metadata-uri: https://ap.example.com/metadata (1)
1- IDP のメタデータ URI。アプリケーションに SLO のサポートを示す
2- アプリケーションの SLO エンドポイント
3<saml2:LogoutRequest> および <saml2:LogoutResponse> に署名するための署名資格情報
An asserting party supports Single Logout if their metadata includes the `<SingleLogoutService>` element in their metadata.

以上です!

Spring Security のログアウトサポートには、いくつかの構成ポイントがあります。次のユースケースを検討してください。

スタートアップの期待

これらのプロパティを使用すると、ログインに加えて、SAML 2.0 サービスプロバイダーは、RP または AP によって開始されるログアウトを使用して、<saml2:LogoutRequest> および <saml2:LogoutResponse> によるログアウトを容易にするように自動的に構成されます。

これは、決定論的な起動プロセスを通じてこれを実現します。

  1. Identity Server メタデータエンドポイントで <SingleLogoutService> 要素を照会する

  2. メタデータをスキャンし、公開署名検証キーをキャッシュする

  3. 適切なエンドポイントを準備する

このプロセスの結果、サービスプロバイダーが正常に起動するには、アイデンティティサーバーが起動してリクエストを受信する必要があります。

サービスプロバイダーが ID サーバーにクエリを実行したときに ID サーバーがダウンしている場合 (適切なタイムアウトが指定されている場合)、起動は失敗します。

ランタイムの期待

上記の構成では、ログインしているユーザーは誰でもアプリケーションに POST /logout を送信して、RP 開始 SLO を実行できます。アプリケーションは次のことを実行します。

  1. ユーザーをログアウトし、セッションを無効にします

  2. <saml2:LogoutRequest> を生成し、関連するアサーションパーティの SLO エンドポイントに POST する

  3. 次に、アサーション側が <saml2:LogoutResponse> で応答した場合、アプリケーションはそれを検証し、設定された成功エンドポイントにリダイレクトします。

また、アサーション側が <saml2:LogoutRequest> を /logout/saml2/slo に送信すると、アプリケーションは AP 開始ログアウトに参加できます。この場合、アプリケーションは次の処理を実行します。

  1. <saml2:LogoutRequest> を確認する

  2. ユーザーをログアウトし、セッションを無効にします

  3. <saml2:LogoutResponse> を生成し、それをアサーション側の SLO エンドポイントに POST する

Boot なしの最小構成

Boot プロパティの代わりに、次のように Bean を直接公開することで同じ結果を実現することもできます。

  • Java

  • Kotlin

@Configuration
public class SecurityConfig {
    @Value("${private.key}") RSAPrivateKey key;
    @Value("${public.certificate}") X509Certificate certificate;

    @Bean
    RelyingPartyRegistrationRepository registrations() {
        Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate);
        RelyingPartyRegistration registration = RelyingPartyRegistrations
                .fromMetadataLocation("https://ap.example.org/metadata") (1)
                .registrationId("metadata")
                .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo") (2)
                .signingX509Credentials((signing) -> signing.add(credential)) (3)
                .build();
        return new InMemoryRelyingPartyRegistrationRepository(registration);
    }

    @Bean
    SecurityFilterChain web(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(withDefaults())
            .saml2Logout(withDefaults()); (4)

        return http.build();
    }
}
@Configuration
class SecurityConfig(@Value("${private.key}") val key: RSAPrivateKey,
        @Value("${public.certificate}") val certificate: X509Certificate) {

    @Bean
    fun registrations(): RelyingPartyRegistrationRepository {
        val credential = Saml2X509Credential.signing(key, certificate)
        val registration = RelyingPartyRegistrations
                .fromMetadataLocation("https://ap.example.org/metadata") (1)
                .registrationId("metadata")
                .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo") (2)
                .signingX509Credentials({ signing: List<Saml2X509Credential> -> signing.add(credential) }) (3)
                .build()
        return InMemoryRelyingPartyRegistrationRepository(registration)
    }

    @Bean
    fun web(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                anyRequest = authenticated
            }
            saml2Login {

            }
            saml2Logout { (4)

            }
        }

        return http.build()
    }
}
1- IDP のメタデータ URI。アプリケーションに SLO のサポートを示す
2- アプリケーションの SLO エンドポイント
3<saml2:LogoutRequest> および <saml2:LogoutResponse> に署名するための署名資格情報。複数の証明書利用者に追加することもできます。
4- 次に、アプリケーションが SAMLSLO を使用してエンドユーザーをログアウトすることを希望していることを示します
saml2Logout を追加すると、サービスプロバイダー全体にログアウト機能が追加されます。これはオプションの機能であるため、個々の RelyingPartyRegistration に対して有効にする必要があります。これを行うには、上記のように RelyingPartyRegistration.Builder#singleLogoutServiceLocation プロパティを設定します。

Saml 2.0 ログアウトの仕組み

次に、先ほど見たようなサーブレットベースのアプリケーションで Spring Security が SAML 2.0 ログアウト (英語) をサポートするために使用するアーキテクチャコンポーネントを見てみましょう。

RP によるログアウトの場合:

number 1Spring Security はログアウトフローを実行し、LogoutHandler を呼び出してセッションを無効化し、その他のクリーンアップを実行します。次に、Saml2RelyingPartyInitiatedLogoutSuccessHandler (Javadoc) を呼び出します。

number 2 ログアウト成功ハンドラーは、Saml2LogoutRequestResolver (Javadoc) のインスタンスを使用して、<saml2:LogoutRequest> を作成、署名、直列化します。現在の Saml2AuthenticatedPrincipal に関連付けられている RelyingPartyRegistration のキーと構成を使用します。次に、<saml2:LogoutRequest> をアサーションパーティの SLO エンドポイントにリダイレクト POST します。

ブラウザーは制御をアサーション側に引き渡します。アサーション側がリダイレクトした場合(そうならない可能性もあります)、アプリケーションはステップに進みます。number 3

number 3Saml2LogoutResponseFilter (Javadoc) は、Saml2LogoutResponseValidator (Javadoc) を使用して <saml2:LogoutResponse> をデシリアライズ、検証、処理します。

number 4 有効な場合は、/login?logout または設定されているものにリダイレクトして、ローカルログアウトフローを完了します。無効な場合は、400 で応答します。

AP によるログアウトの場合:

number 1Saml2LogoutRequestFilter (Javadoc) は、Saml2LogoutRequestValidator (Javadoc) を使用して <saml2:LogoutRequest> をデシリアライズ、検証、処理します。

number 2 有効な場合、フィルターは構成された LogoutHandler を呼び出し、セッションを無効にしてその他のクリーンアップを実行します。

number 3Saml2LogoutResponseResolver (Javadoc) を使用して、<saml2:LogoutResponse> を作成、署名、直列化します。エンドポイントまたは <saml2:LogoutRequest> の内容から派生した RelyingPartyRegistration のキーと構成を使用します。次に、<saml2:LogoutResponse> をアサーションパーティの SLO エンドポイントにリダイレクト POST します。

ブラウザーはアサーション側に制御を渡します。

ログアウトエンドポイントの構成

さまざまなエンドポイントによってトリガーされる可能性のある動作は 3 つあります。

  • RP によって開始されるログアウト。これにより、認証されたユーザーは POST を実行し、アサート側に <saml2:LogoutRequest> を送信してログアウトプロセスをトリガーできます。

  • AP が開始するログアウト。これにより、アサート側が <saml2:LogoutRequest> をアプリケーションに送信できるようになります。

  • AP ログアウトレスポンス。これにより、アサート側は RP によって開始された <saml2:LogoutRequest> にレスポンスして <saml2:LogoutResponse> を送信できます。

1 つ目は、プリンシパルが Saml2AuthenticatedPrincipal 型の場合に、通常の POST /logout を実行することによってトリガーされます。

2 つ目は、アサート側によって署名された SAMLRequest を使用して /logout/saml2/slo エンドポイントに POST することによってトリガーされます。

3 つ目は、アサート側によって署名された SAMLResponse を使用して /logout/saml2/slo エンドポイントに POST することによってトリガーされます。

ユーザーがすでにログインしているか、元のログアウトリクエストがわかっているため、registrationId はすでにわかっています。このため、{registrationId} はデフォルトではこれらの URL の一部ではありません。

この URL は DSL でカスタマイズ可能です。

例: 既存の証明書利用者を Spring Security に移行する場合、主張する当事者はすでに GET /SLOService.saml2 を指している可能性があります。アサーティングパーティの設定の変更を減らすために、DSL で次のようにフィルターを設定できます。

  • Java

  • Kotlin

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
        .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2"))
    );
http {
    saml2Logout {
        logoutRequest {
            logoutUrl = "/SLOService.saml2"
        }
        logoutResponse {
            logoutUrl = "/SLOService.saml2"
        }
    }
}

また、RelyingPartyRegistration でこれらのエンドポイントを構成する必要があります。

また、次のようにローカルでログアウトをトリガーするためのエンドポイントをカスタマイズすることもできます。

  • Java

  • Kotlin

http
    .saml2Logout((saml2) -> saml2.logoutUrl("/saml2/logout"));
http {
    saml2Logout {
        logoutUrl = "/saml2/logout"
    }
}

ローカルログアウトと SAML 2.0 ログアウトの分離

場合によっては、ローカルログアウト用に 1 つのログアウトエンドポイントを公開し、RP によって開始される SLO 用に別のログアウトエンドポイントを公開したいことがあります。他のログアウトメカニズムの場合と同様に、それぞれ異なるエンドポイントを持つ限り、複数のログアウトメカニズムを登録できます。

たとえば、DSL を次のように接続できます。

  • Java

  • Kotlin

http
    .logout((logout) -> logout.logoutUrl("/logout"))
    .saml2Logout((saml2) -> saml2.logoutUrl("/saml2/logout"));
http {
    logout {
        logoutUrl = "/logout"
    }
    saml2Logout {
        logoutUrl = "/saml2/logout"
    }
}

そして、クライアントが POST /logout を送信すると、セッションはクリアされますが、アサーションパーティに <saml2:LogoutRequest> は送信されません。ただし、クライアントが POST /saml2/logout を送信すると、アプリケーションは通常どおり SAML 2.0 SLO を開始します。

<saml2:LogoutRequest> 解決のカスタマイズ

Spring Security が提供するデフォルト以外の値を <saml2:LogoutRequest> に設定する必要があるのが一般的です。

デフォルトでは、Spring Security は <saml2:LogoutRequest> を発行し、以下を提供します。

  • Destination 属性 - RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation から

  • ID 属性 -GUID

  • <Issuer> 要素 - RelyingPartyRegistration#getEntityId から

  • <NameID> 要素 - Authentication#getName から

他の値を追加するには、次のように委譲を使用できます。

  • Java

  • Kotlin

@Bean
Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
	OpenSaml4LogoutRequestResolver logoutRequestResolver =
			new OpenSaml4LogoutRequestResolver(registrations);
	logoutRequestResolver.setParametersConsumer((parameters) -> {
		String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute");
		String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
		LogoutRequest logoutRequest = parameters.getLogoutRequest();
		NameID nameId = logoutRequest.getNameID();
		nameId.setValue(name);
		nameId.setFormat(format);
	});
	return logoutRequestResolver;
}
@Bean
open fun logoutRequestResolver(registrations:RelyingPartyRegistrationRepository?): Saml2LogoutRequestResolver {
    val logoutRequestResolver = OpenSaml4LogoutRequestResolver(registrations)
    logoutRequestResolver.setParametersConsumer { parameters: LogoutRequestParameters ->
        val name: String = (parameters.getAuthentication().getPrincipal() as Saml2AuthenticatedPrincipal).getFirstAttribute("CustomAttribute")
        val format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
        val logoutRequest: LogoutRequest = parameters.getLogoutRequest()
        val nameId: NameID = logoutRequest.getNameID()
        nameId.setValue(name)
        nameId.setFormat(format)
    }
    return logoutRequestResolver
}

次に、次のように DSL でカスタム Saml2LogoutRequestResolver を提供できます。

  • Java

  • Kotlin

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request
            .logoutRequestResolver(this.logoutRequestResolver)
        )
    );
http {
    saml2Logout {
        logoutRequest {
            logoutRequestResolver = this.logoutRequestResolver
        }
    }
}

<saml2:LogoutResponse> 解決のカスタマイズ

Spring Security が提供するデフォルト以外の値を <saml2:LogoutResponse> に設定する必要があるのが一般的です。

デフォルトでは、Spring Security は <saml2:LogoutResponse> を発行し、以下を提供します。

  • Destination 属性 - RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation から

  • ID 属性 -GUID

  • <Issuer> 要素 - RelyingPartyRegistration#getEntityId から

  • <Status> 要素 - SUCCESS

他の値を追加するには、次のように委譲を使用できます。

  • Java

  • Kotlin

@Bean
public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
	OpenSaml4LogoutResponseResolver logoutRequestResolver =
			new OpenSaml4LogoutResponseResolver(registrations);
	logoutRequestResolver.setParametersConsumer((parameters) -> {
		if (checkOtherPrevailingConditions(parameters.getRequest())) {
			parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT);
		}
	});
	return logoutRequestResolver;
}
@Bean
open fun logoutResponseResolver(registrations: RelyingPartyRegistrationRepository?): Saml2LogoutResponseResolver {
    val logoutRequestResolver = OpenSaml4LogoutResponseResolver(registrations)
    logoutRequestResolver.setParametersConsumer { LogoutResponseParameters parameters ->
        if (checkOtherPrevailingConditions(parameters.getRequest())) {
            parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT)
        }
    }
    return logoutRequestResolver
}

次に、次のように DSL でカスタム Saml2LogoutResponseResolver を提供できます。

  • Java

  • Kotlin

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request
            .logoutRequestResolver(this.logoutRequestResolver)
        )
    );
http {
    saml2Logout {
        logoutRequest {
            logoutRequestResolver = this.logoutRequestResolver
        }
    }
}

<saml2:LogoutRequest> 認証のカスタマイズ

検証をカスタマイズするために、独自の Saml2LogoutRequestValidator を実装できます。こでは、検証は最小限であるため、最初に次のようにデフォルトの Saml2LogoutRequestValidator に委譲できる場合があります。

  • Java

  • Kotlin

@Component
public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
	private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator();

	@Override
    public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) {
		 // verify signature, issuer, destination, and principal name
		Saml2LogoutValidatorResult result = delegate.authenticate(authentication);

		LogoutRequest logoutRequest = // ... parse using OpenSAML
        // perform custom validation
    }
}
@Component
open class MyOpenSamlLogoutRequestValidator: Saml2LogoutRequestValidator {
	private val delegate = OpenSamlLogoutRequestValidator()

	@Override
    fun logout(parameters: Saml2LogoutRequestValidatorParameters): Saml2LogoutRequestValidator {
		 // verify signature, issuer, destination, and principal name
		val result = delegate.authenticate(authentication)

		val logoutRequest: LogoutRequest = // ... parse using OpenSAML
        // perform custom validation
    }
}

次に、次のように DSL でカスタム Saml2LogoutRequestValidator を提供できます。

  • Java

  • Kotlin

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request
            .logoutRequestValidator(myOpenSamlLogoutRequestValidator)
        )
    );
http {
    saml2Logout {
        logoutRequest {
            logoutRequestValidator = myOpenSamlLogoutRequestValidator
        }
    }
}

<saml2:LogoutResponse> 認証のカスタマイズ

検証をカスタマイズするために、独自の Saml2LogoutResponseValidator を実装できます。こでは、検証は最小限であるため、最初に次のようにデフォルトの Saml2LogoutResponseValidator に委譲できる場合があります。

  • Java

  • Kotlin

@Component
public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
	private final Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator();

	@Override
    public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) {
		// verify signature, issuer, destination, and status
		Saml2LogoutValidatorResult result = delegate.authenticate(parameters);

		LogoutResponse logoutResponse = // ... parse using OpenSAML
        // perform custom validation
    }
}
@Component
open class MyOpenSamlLogoutResponseValidator: Saml2LogoutResponseValidator {
	private val delegate = OpenSamlLogoutResponseValidator()

	@Override
    fun logout(parameters: Saml2LogoutResponseValidatorParameters): Saml2LogoutResponseValidator {
		// verify signature, issuer, destination, and status
		val result = delegate.authenticate(authentication)

		val logoutResponse: LogoutResponse = // ... parse using OpenSAML
        // perform custom validation
    }
}

次に、次のように DSL でカスタム Saml2LogoutResponseValidator を提供できます。

  • Java

  • Kotlin

http
    .saml2Logout((saml2) -> saml2
        .logoutResponse((response) -> response
            .logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator)
        )
    );
http {
    saml2Logout {
        logoutResponse {
            logoutResponseValidator = myOpenSamlLogoutResponseValidator
        }
    }
}

<saml2:LogoutRequest> ストレージのカスタマイズ

アプリケーションが <saml2:LogoutRequest> を送信すると、値がセッションに格納されるため、RelayState パラメーターと <saml2:LogoutResponse> の InResponseTo 属性を検証できます。

セッション以外の場所にログアウトリクエストを保存する場合は、次のように DSL でカスタム実装を提供できます。

  • Java

  • Kotlin

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request
            .logoutRequestRepository(myCustomLogoutRequestRepository)
        )
    );
http {
    saml2Logout {
        logoutRequest {
            logoutRequestRepository = myCustomLogoutRequestRepository
        }
    }
}

その他のログアウト関連の参照