<saml2:Response> の認証

SAML 2.0 レスポンスを検証するために、Spring Security は Saml2AuthenticationTokenConverter を使用して Authentication リクエストを入力し、OpenSaml4AuthenticationProvider を使用してそれを認証します。

これは、次のようないくつかの方法で構成できます。

  1. RelyingPartyRegistration の検索方法の変更

  2. タイムスタンプ検証へのクロックスキューの設定

  3. レスポンスを GrantedAuthority インスタンスのリストにマッピングする

  4. アサーションを検証するための戦略のカスタマイズ

  5. レスポンス要素とアサーション要素を復号化するための戦略のカスタマイズ

これらを構成するには、DSL で saml2Login#authenticationManager メソッドを使用します。

SAML レスポンス処理エンドポイントの変更

デフォルトのエンドポイントは /login/saml2/sso/{registrationId} です。これは、DSL および関連するメタデータで次のように変更できます。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
	http
        // ...
        .saml2Login((saml2) -> saml2.loginProcessingUrl("/saml2/login/sso"))
        // ...

    return http.build();
}
@Bean
fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
	http {
        // ...
        .saml2Login {
            loginProcessingUrl = "/saml2/login/sso"
        }
        // ...
    }

    return http.build()
}

および:

  • Java

  • Kotlin

relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")

RelyingPartyRegistration ルックアップの変更

デフォルトでは、このコンバーターは、関連付けられている <saml2:AuthnRequest> または URL で見つかった registrationId と照合します。または、これらのケースのいずれかで見つからない場合は、<saml2:Response#Issuer> 要素によって検索を試みます。

ARTIFACT バインディングをサポートしている場合など、より洗練されたものが必要になる状況がいくつかあります。そのような場合、次のようにカスタマイズできるカスタム AuthenticationConverter を介してルックアップをカスタマイズできます。

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilters(HttpSecurity http, AuthenticationConverter authenticationConverter) throws Exception {
	http
        // ...
        .saml2Login((saml2) -> saml2.authenticationConverter(authenticationConverter))
        // ...

    return http.build();
}
@Bean
fun securityFilters(val http: HttpSecurity, val converter: AuthenticationConverter): SecurityFilterChain {
	http {
        // ...
        .saml2Login {
            authenticationConverter = converter
        }
        // ...
    }

    return http.build()
}

クロックスキューの設定

アサーティングパーティと依存パーティが完全に同期されていないシステムクロックを持つことは珍しくありません。そのため、OpenSaml4AuthenticationProvider のデフォルトのアサーションバリデーターをある程度の許容範囲で構成できます。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidatorWithParameters(assertionToken -> {
                    Map<String, Object> params = new HashMap<>();
                    params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
                    // ... other validation parameters
                    return new ValidationContext(params);
                })
        );

        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setAssertionValidator(
            OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidatorWithParameters(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
                    val params: MutableMap<String, Any> = HashMap()
                    params[CLOCK_SKEW] =
                        Duration.ofMinutes(10).toMillis()
                    ValidationContext(params)
                })
        )
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}

UserDetailsService との調整

または、レガシー UserDetailsService からのユーザーの詳細を含めることもできます。その場合、以下に示すように、レスポンス認証コンバーターが役立ちます。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
            Saml2Authentication authentication = OpenSaml4AuthenticationProvider
                    .createDefaultResponseAuthenticationConverter() (1)
                    .convert(responseToken);
            Assertion assertion = responseToken.getResponse().getAssertions().get(0);
            String username = assertion.getSubject().getNameID().getValue();
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); (2)
            return MySaml2Authentication(userDetails, authentication); (3)
        });

        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Autowired
    var userDetailsService: UserDetailsService? = null

    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
            val authentication = OpenSaml4AuthenticationProvider
                .createDefaultResponseAuthenticationConverter() (1)
                .convert(responseToken)
            val assertion: Assertion = responseToken.response.assertions[0]
            val username: String = assertion.subject.nameID.value
            val userDetails = userDetailsService!!.loadUserByUsername(username) (2)
            MySaml2Authentication(userDetails, authentication) (3)
        }
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}
1 最初に、レスポンスから属性と権限を抽出するデフォルトのコンバーターを呼び出します
2 次に、関連情報を使用して UserDetailsService を呼び出します
33 番目に、ユーザーの詳細を含むカスタム認証を返します
OpenSaml4AuthenticationProvider のデフォルトの認証コンバーターを呼び出す必要はありません。AttributeStatement から抽出した属性を含む Saml2AuthenticatedPrincipal と単一の ROLE_USER 権限を返します。

追加のレスポンス検証の実行

OpenSaml4AuthenticationProvider は、Response を復号化した直後に、Issuer 値と Destination 値を検証します。独自のレスポンスバリデーターと連結するデフォルトのバリデーターを継承することで検証をカスタマイズすることも、完全に自分のものに置き換えることもできます。

例: 次のように、Response オブジェクトで利用可能な追加情報を使用してカスタム例外をスローできます。

OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseValidator((responseToken) -> {
	Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
		.createDefaultResponseValidator()
		.convert(responseToken)
		.concat(myCustomValidator.convert(responseToken));
	if (!result.getErrors().isEmpty()) {
		String inResponseTo = responseToken.getInResponseTo();
		throw new CustomSaml2AuthenticationException(result, inResponseTo);
	}
	return result;
});

追加のアサーション検証の実行

OpenSaml4AuthenticationProvider は、SAML 2.0 アサーションに対して最小限の検証を実行します。署名を確認すると、次のようになります。

  1. <AudienceRestriction> および <DelegationRestriction> 条件を検証する

  2. <SubjectConfirmation> を検証し、任意の IP アドレス情報を期待する

追加の検証を実行するには、OpenSaml4AuthenticationProvider のデフォルトに委譲して独自に実行する独自のアサーションバリデーターを構成できます。

例: OpenSAML の OneTimeUseConditionValidator を使用して、<OneTimeUse> 条件を検証することもできます。

  • Java

  • Kotlin

OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
provider.setAssertionValidator(assertionToken -> {
    Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
            .createDefaultAssertionValidator()
            .convert(assertionToken);
    Assertion assertion = assertionToken.getAssertion();
    OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
    ValidationContext context = new ValidationContext();
    try {
        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
            return result;
        }
    } catch (Exception e) {
        return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
    }
    return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
});
var provider = OpenSaml4AuthenticationProvider()
var validator: OneTimeUseConditionValidator = ...
provider.setAssertionValidator { assertionToken ->
    val result = OpenSaml4AuthenticationProvider
        .createDefaultAssertionValidator()
        .convert(assertionToken)
    val assertion: Assertion = assertionToken.assertion
    val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
    val context = ValidationContext()
    try {
        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
            return@setAssertionValidator result
        }
    } catch (e: Exception) {
        return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
    }
    result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
}
推奨されていますが、OpenSaml4AuthenticationProvider のデフォルトのアサーションバリデーターを呼び出す必要はありません。スキップする状況は、<AudienceRestriction> または <SubjectConfirmation> を自分でチェックする必要がないためです。

復号化のカスタマイズ

Spring Security は、RelyingPartyRegistration に登録されている復号化 Saml2X509Credential インスタンスを使用して、<saml2:EncryptedAssertion><saml2:EncryptedAttribute><saml2:EncryptedID> 要素を自動的に復号化します。

OpenSaml4AuthenticationProvider は、2 つの復号化戦略を公開しています。レスポンス復号化機能は、<saml2:EncryptedAssertion> などの <saml2:Response> の暗号化された要素を復号化するためのものです。アサーション復号化機能は、<saml2:EncryptedAttribute> や <saml2:EncryptedID> などの <saml2:Assertion> の暗号化された要素を復号化するためのものです。

OpenSaml4AuthenticationProvider のデフォルトの復号化戦略を独自のものに置き換えることができます。例: <saml2:Response> のアサーションを復号化する別のサービスがある場合は、代わりに次のように使用できます。

  • Java

  • Kotlin

MyDecryptionService decryptionService = ...;
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml4AuthenticationProvider()
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }

<saml2:Assertion> の個々の要素も復号化する場合は、アサーション復号化機能をカスタマイズすることもできます。

  • Java

  • Kotlin

provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
アサーションはレスポンスとは別に署名できるため、2 つの別個の復号化機能があります。署名の検証前に署名されたアサーションの要素を復号化しようとすると、署名が無効になる場合があります。主張する当事者がレスポンスのみに署名する場合は、レスポンス復号化機能のみを使用してすべての要素を復号化しても安全です。

カスタム認証マネージャーの使用

もちろん、authenticationManager DSL メソッドを使用して、完全にカスタムの SAML 2.0 認証を実行することもできます。この認証マネージャーは、SAML 2.0 レスポンス XML データを含む Saml2AuthenticationToken オブジェクトを予期する必要があります。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(authenticationManager)
            )
        ;
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = customAuthenticationManager
            }
        }
        return http.build()
    }
}

Saml2AuthenticatedPrincipal を使用する

依存パーティが特定のアサーティングパーティに対して正しく構成されているため、アサーションを受け入れる準備ができています。依存パーティがアサーションを検証すると、結果は Saml2AuthenticatedPrincipal を含む Saml2Authentication になります。

つまり、次のようにコントローラーのプリンシパルにアクセスできます。

  • Java

  • Kotlin

@Controller
public class MainController {
	@GetMapping("/")
	public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
		String email = principal.getFirstAttribute("email");
		model.setAttribute("email", email);
		return "index";
	}
}
@Controller
class MainController {
    @GetMapping("/")
    fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
        val email = principal.getFirstAttribute<String>("email")
        model.setAttribute("email", email)
        return "index"
    }
}
SAML 2.0 仕様では、各属性に複数の値を設定できるため、getAttribute を呼び出して属性のリストを取得するか、getFirstAttribute を呼び出してリストの最初の属性を取得できます。getFirstAttribute は、値が 1 つしかないことがわかっている場合に非常に便利です。