Authenticating `<saml2:Response>`s
为了验证 SAML 2.0 响应,Spring Security 使用 Saml2AuthenticationTokenConverter
填充 Authentication
请求,并使用 OpenSaml4AuthenticationProvider
进行身份验证。
To verify SAML 2.0 Responses, Spring Security uses Saml2AuthenticationTokenConverter
to populate the Authentication
request and OpenSaml4AuthenticationProvider
to authenticate it.
可以通过多种方式对其进行配置,包括:
You can configure this in a number of ways including:
-
Changing the way the
RelyingPartyRegistration
is Looked Up -
Setting a clock skew to timestamp validation
-
Mapping the response to a list of
GrantedAuthority
instances -
Customizing the strategy for validating assertions
-
Customizing the strategy for decrypting response and assertion elements
要配置这些,您需要在 DSL 中使用 saml2Login#authenticationManager
方法。
To configure these, you’ll use the saml2Login#authenticationManager
method in the DSL.
Changing the SAML Response Processing Endpoint
默认端点是 /login/saml2/sso/{registrationId}
。可以在 DSL 和关联元数据中对其进行更改,如下所示:
The default endpoint is /login/saml2/sso/{registrationId}
.
You can change this in the DSL and in the associated metadata like so:
-
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()
}
及:
and:
-
Java
-
Kotlin
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
Changing RelyingPartyRegistration
lookup
默认情况下,此转换器将与任何关联的 <saml2:AuthnRequest>
或在 URL 中找到的任何 registrationId
进行匹配。或者,如果在这些情况下找不到,则尝试通过 <saml2:Response#Issuer>
元素查找它。
By default, this converter will match against any associated <saml2:AuthnRequest>
or any registrationId
it finds in the URL.
Or, if it cannot find one in either of those cases, then it attempts to look it up by the <saml2:Response#Issuer>
element.
在很多情况下,您可能需要更复杂的内容,例如在支持 ARTIFACT
绑定时。在这些情况下,可以通过自定义 AuthenticationConverter
来自定义查找,可以对其进行如下自定义:
There are a number of circumstances where you might need something more sophisticated, like if you are supporting ARTIFACT
binding.
In those cases, you can customize lookup through a custom AuthenticationConverter
, which you can customize like so:
-
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()
}
Setting a Clock Skew
声明方和依赖方的时间钟不完全同步并不少见。因此,您可以使用一定的容差来配置“OpenSaml4AuthenticationProvider”的默认断言验证器:
It’s not uncommon for the asserting and relying parties to have system clocks that aren’t perfectly synchronized. For that reason, you can configure `OpenSaml4AuthenticationProvider’s default assertion validator with some 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()
}
}
Coordinating with a UserDetailsService
或者,您可能希望包含来自旧版的 UserDetailsService
的用户信息。在这种情况下,响应身份验证转换器会派上用场,如下所示:
Or, perhaps you would like to include user details from a legacy UserDetailsService
.
In that case, the response authentication converter can come in handy, as can be seen below:
-
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 | First, call the default converter, which extracts attributes and authorities from the response |
2 | Second, call the UserDetailsService using the relevant information |
3 | Third, return a custom authentication that includes the user details |
从 |
It’s not required to call |
Performing Additional Response Validation
OpenSaml4AuthenticationProvider
在解密 Response
后立即验证 Issuer
和 Destination
值。可以通过将默认验证器与您自己的响应验证器串联起来来自定义验证,或者您可以将其完全替换为您自己的。
OpenSaml4AuthenticationProvider
validates the Issuer
and Destination
values right after decrypting the Response
.
You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.
例如,您可以使用 Response
对象中可用的任何附加信息抛出自定义异常,如下所示:
For example, you can throw a custom exception with any additional information available in the Response
object, like so:
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;
});
Performing Additional Assertion Validation
OpenSaml4AuthenticationProvider
对 SAML 2.0 断言执行最小验证。在验证签名后,它将:
OpenSaml4AuthenticationProvider
performs minimal validation on SAML 2.0 Assertions.
After verifying the signature, it will:
-
Validate
<AudienceRestriction>
and<DelegationRestriction>
conditions -
Validate `<SubjectConfirmation>`s, expect for any IP address information
为了执行附加验证,您可以配置自己的断言验证器,该断言验证器委托给“OpenSaml4AuthenticationProvider”的默认验证器,然后执行自己的验证器。
To perform additional validation, you can configure your own assertion validator that delegates to `OpenSaml4AuthenticationProvider’s default and then performs its own.
例如,您可以使用 OpenSAML 的 OneTimeUseConditionValidator
来验证 <OneTimeUse>
条件,如下所示:
For example, you can use OpenSAML’s OneTimeUseConditionValidator
to also validate a <OneTimeUse>
condition, like so:
-
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))
}
尽管建议调用 |
While recommended, it’s not necessary to call |
Customizing Decryption
Spring Security 使用在 RelyingPartyRegistration
中注册的解密 Saml2X509Credential
instances 自动解密 <saml2:EncryptedAssertion>
、<saml2:EncryptedAttribute>
和 <saml2:EncryptedID>
元素。
Spring Security decrypts <saml2:EncryptedAssertion>
, <saml2:EncryptedAttribute>
, and <saml2:EncryptedID>
elements automatically by using the decryption Saml2X509Credential
instances registered in the RelyingPartyRegistration
.
OpenSaml4AuthenticationProvider`公开 two decryption strategies。响应解密器用于解密 `<saml2:Response>`的加密元素,如 `<saml2:EncryptedAssertion>
。断言解密器用于解密 <saml2:Assertion>`的加密元素,如 `<saml2:EncryptedAttribute>`和 `<saml2:EncryptedID>
。
OpenSaml4AuthenticationProvider
exposes two decryption strategies.
The response decrypter is for decrypting encrypted elements of the <saml2:Response>
, like <saml2:EncryptedAssertion>
.
The assertion decrypter is for decrypting encrypted elements of the <saml2:Assertion>
, like <saml2:EncryptedAttribute>
and <saml2:EncryptedID>
.
可替换 OpenSaml4AuthenticationProvider’s default decryption strategy with your own.
For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>
,可像如下所示使用它:
You can replace OpenSaml4AuthenticationProvider’s default decryption strategy with your own.
For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>
, you can use it instead like so:
-
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) }
如果你还在 RFC 1901 中解密各个元素,也可以自定义断言解密器:
If you are also decrypting individual elements in a <saml2:Assertion>
, you can customize the assertion decrypter, too:
-
Java
-
Kotlin
provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
之所以有两个单独的解密器,是因为断言可以从响应中分别进行签名。在验证签名之前尝试解密已签名断言的元素可能会使签名无效。如果断言方只签署响应,则仅使用响应解密器解密所有元素是安全的。 |
There are two separate decrypters since assertions can be signed separately from responses. Trying to decrypt a signed assertion’s elements before signature verification may invalidate the signature. If your asserting party signs the response only, then it’s safe to decrypt all elements using only the response decrypter. |
Using a Custom Authentication Manager
当然,authenticationManager
DSL 方法也可用于执行完全自定义的 SAML 2.0 身份验证。此身份验证管理器应期望一个包含 SAML 2.0 响应 XML 数据的 Saml2AuthenticationToken
对象。
Of course, the authenticationManager
DSL method can be also used to perform a completely custom SAML 2.0 authentication.
This authentication manager should expect a Saml2AuthenticationToken
object containing the SAML 2.0 Response XML data.
-
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()
}
}
Using Saml2AuthenticatedPrincipal
依靠方为给定的断言方正确配置后,即可接受断言。一旦依靠方验证断言,结果就是具有 RFC 1901 的 Saml2Authentication
。
With the relying party correctly configured for a given asserting party, it’s ready to accept assertions.
Once the relying party validates an assertion, the result is a Saml2Authentication
with a Saml2AuthenticatedPrincipal
.
这意味着你可以像如下所示在控制器中访问主体:
This means that you can access the principal in your controller like so:
-
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 规范允许每个属性拥有多个值,您既可以调用 |
Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call |