<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()
    }
}

オープン SAML 5 を使用している場合は、OpenSaml5AuthenticationProvider.AssertionValidator を使用するより簡単な方法があります。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
        AssertionValidator assertionValidator = AssertionValidator.builder()
                .clockSkew(Duration.ofMinutes(10)).build();
		authenticationProvider.setAssertionValidator(assertionValidator);
        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
	}
}
@Configuration @EnableWebSecurity
class SecurityConfig {
    @Bean
    @Throws(Exception::class)
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml5AuthenticationProvider()
        val assertionValidator = AssertionValidator.builder().clockSkew(Duration.ofMinutes(10)).build()
        authenticationProvider.setAssertionValidator(assertionValidator)
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}

Assertion を Authentication に変換する

OpenSamlXAuthenticationProvider#setResponseAuthenticationConverter は、アサーションを Authentication インスタンスに変換する方法を変更する方法を提供します。

カスタムコンバーターは次の方法で設定できます。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    Converter<ResponseToken, Saml2Authentication> authenticationConverter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
        authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter);

        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated())
            .saml2Login((saml2) -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
    }

}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Autowired
    var authenticationConverter: Converter<ResponseToken, Saml2Authentication>? = null

    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml5AuthenticationProvider()
        authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter)
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}

続く例はすべてこの共通構造に基づいて構築されており、このコンバーターが役立つさまざまな方法を示しています。

UserDetailsService との調整

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

  • Java

  • Kotlin

@Component
class MyUserDetailsResponseAuthenticationConverter implements Converter<ResponseToken, Saml2Authentication> {
	private final ResponseAuthenticationConverter delegate = new ResponseAuthenticationConverter();
	private final UserDetailsService userDetailsService;

	MyUserDetailsResponseAuthenticationConverter(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	@Override
    public Saml2Authentication convert(ResponseToken responseToken) {
	    Saml2Authentication authentication = this.delegate.convert(responseToken); (1)
		UserDetails principal = this.userDetailsService.loadByUsername(username); (2)
		String saml2Response = authentication.getSaml2Response();
		Collection<GrantedAuthority> authorities = principal.getAuthorities();
		return new Saml2Authentication((AuthenticatedPrincipal) userDetails, saml2Response, authorities); (3)
    }

}
@Component
open class MyUserDetailsResponseAuthenticationConverter(val delegate: ResponseAuthenticationConverter,
        UserDetailsService userDetailsService): Converter<ResponseToken, Saml2Authentication> {

	@Override
    open fun convert(responseToken: ResponseToken): Saml2Authentication {
	    val authentication = this.delegate.convert(responseToken) (1)
		val principal = this.userDetailsService.loadByUsername(username) (2)
		val saml2Response = authentication.getSaml2Response()
		val authorities = principal.getAuthorities()
		return Saml2Authentication(userDetails as AuthenticatedPrincipal, saml2Response, authorities) (3)
    }

}
1 最初に、レスポンスから属性と権限を抽出するデフォルトのコンバーターを呼び出します
2 次に、関連情報を使用して UserDetailsService を呼び出します
33 番目に、ユーザーの詳細を含む認証を返します

UserDetailsService が AuthenticatedPrincipal も実装する値を返す場合は、カスタム認証の実装は必要ありません。

または、OpenSaml 4 を使用している場合は、次のようにして同様のことを実現できます。

  • 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 権限を返します。

プリンシパル名の設定

プリンシパル名が <saml2:NameID> 要素に含まれていない場合があります。その場合は、次のようにカスタム戦略を使用して ResponseAuthenticationConverter を設定できます。

  • Java

  • Kotlin

@Bean
ResponseAuthenticationConverter authenticationConverter() {
	ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
	authenticationConverter.setPrincipalNameConverter((assertion) -> {
		// ... work with OpenSAML's Assertion object to extract the principal
	});
	return authenticationConverter;
}
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
    val authenticationConverter: ResponseAuthenticationConverter = ResponseAuthenticationConverter()
    authenticationConverter.setPrincipalNameConverter { assertion ->
		// ... work with OpenSAML's Assertion object to extract the principal
    }
    return authenticationConverter
}

プリンシパルに付与された権限の設定

Spring Security は、OpenSamlXAuhenticationProvider を使用する際に ROLE_USER に自動的に権限を付与します。OpenSaml5AuthenticationProvider を使用する場合は、以下のように異なる権限セットを設定できます。

  • Java

  • Kotlin

@Bean
ResponseAuthenticationConverter authenticationConverter() {
	ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
	authenticationConverter.setPrincipalNameConverter((assertion) -> {
		// ... grant the needed authorities based on attributes in the assertion
	});
	return authenticationConverter;
}
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
    val authenticationConverter = ResponseAuthenticationConverter()
    authenticationConverter.setPrincipalNameConverter{ assertion ->
		// ... grant the needed authorities based on attributes in the assertion
    }
    return authenticationConverter
}

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

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;
});

OpenSaml5AuthenticationProvider を使用すると、より少ない定型文で同じことを行うことができます。

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
ResponseValidator responseValidator = ResponseValidator.withDefaults(myCustomValidator);
provider.setResponseValidator(responseValidator);

Spring Security が実行する検証手順をカスタマイズすることもできます。例: Response#InResponseTo の検証をスキップする場合は、リストから InResponseToValidator を除外して、ResponseValidator のコンストラクターを呼び出します。

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
ResponseValidator responseValidator = new ResponseValidator(new DestinationValidator(), new IssuerValidator());
provider.setResponseValidator(responseValidator);

OpenSAML は、setAssertionValidator を使用して構成可能な BearerSubjectConfirmationValidator クラスで Asssertion#InResponseTo 検証を実行します。

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

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> を自分でチェックする必要がないためです。

オープン SAML 5 を使用している場合は、OpenSaml5AuthenticationProvider.AssertionValidator を使用するより簡単な方法があります。

  • Java

  • Kotlin

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
AssertionValidator assertionValidator = AssertionValidator.builder()
        .conditionValidators((c) -> c.add(validator)).build();
provider.setAssertionValidator(assertionValidator);
val provider = OpenSaml5AuthenticationProvider()
val validator: OneTimeUseConditionValidator = ...;
val assertionValidator = AssertionValidator.builder()
        .conditionValidators { add(validator) }.build()
provider.setAssertionValidator(assertionValidator)

この同じビルダーを使用して、使用したくないバリデーターを次のように削除することもできます。

  • Java

  • Kotlin

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
AssertionValidator assertionValidator = AssertionValidator.builder()
        .conditionValidators((c) -> c.removeIf(AudienceRestrictionValidator.class::isInstance)).build();
provider.setAssertionValidator(assertionValidator);
val provider = new OpenSaml5AuthenticationProvider()
val assertionValidator = AssertionValidator.builder()
        .conditionValidators {
			c: List<ConditionValidator> -> c.removeIf { it is AudienceRestrictionValidator }
        }.build()
provider.setAssertionValidator(assertionValidator)

復号化のカスタマイズ

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 つしかないことがわかっている場合に非常に便利です。