驗證 <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 和相關聯的 metadata 中變更此設定,如下所示

  • 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 binding。在這些情況下,您可以透過自訂 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 的預設斷言驗證器設定一些容 tolerance

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidator(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
                .createDefaultAssertionValidator(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
3 第三,傳回包含使用者詳細資訊的自訂身份驗證
不需要呼叫 OpenSaml4AuthenticationProvider 的預設身份驗證轉換器。它會傳回一個 Saml2AuthenticatedPrincipal,其中包含從 AttributeStatement 提取的屬性以及單一 ROLE_USER 授權。

執行額外的回應驗證

OpenSaml4AuthenticationProvider 在解密 Response 後立即驗證 IssuerDestination 值。您可以透過擴展預設驗證器並與您自己的回應驗證器串聯,來自訂驗證,或者您可以完全使用您的驗證器來取代它。

例如,您可以拋出自訂例外,其中包含 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 會使用在 Saml2X509Credential 實例 中註冊的解密 RelyingPartyRegistration,自動解密 <saml2:EncryptedAssertion><saml2:EncryptedAttribute><saml2:EncryptedID> 元素。

OpenSaml4AuthenticationProvider 公開了 兩種解密策略。回應解密器用於解密 <saml2:Response> 的加密元素,例如 <saml2:EncryptedAssertion>。斷言解密器用於解密 <saml2:Assertion> 的加密元素,例如 <saml2:EncryptedAttribute><saml2:EncryptedID>

您可以將 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) }
有兩個單獨的解密器,因為斷言可以與回應分開簽署。在簽章驗證之前嘗試解密已簽署的斷言元素可能會使簽章失效。如果您的聲明方僅簽署回應,則僅使用回應解密器解密所有元素是安全的。

使用自訂驗證管理器

當然,authenticationManager DSL 方法也可以用於執行完全自訂的 SAML 2.0 身份驗證。此身份驗證管理器應預期一個 Saml2AuthenticationToken 物件,其中包含 SAML 2.0 回應 XML 資料。

  • 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

在為給定的聲明方正確設定依賴方後,它就可以接受斷言了。一旦依賴方驗證了斷言,結果將是一個具有 Saml2AuthenticatedPrincipalSaml2Authentication

這表示您可以在您的控制器中像這樣存取 principal

  • 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 非常方便。