You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
404 lines
13 KiB
404 lines
13 KiB
= Saml 2.0 Migrations |
|
|
|
== Use OpenSAML 5 By Default |
|
|
|
OpenSAML 4.x is no longer supported by the OpenSAML team. |
|
As such, Spring Security will default to using its `OpenSaml5` components in all cases. |
|
|
|
If you want to see how well your application will respond to this, do the following: |
|
|
|
1. Update your OpenSAML dependencies to 5.x |
|
2. If you are constructing an `OpenSaml4XXX` Spring Security component, change it to `OpenSaml5`. |
|
|
|
If you cannot opt-in, then add the `opensaml-saml-api` and `opensaml-saml-impl` 4.x dependencies and exclude the 5.x dependencies from `spring-security-saml2-service-provider`. |
|
|
|
== Continue Filter Chain When No Relying Party Found |
|
|
|
In Spring Security 6, `Saml2WebSsoAuthenticationFilter` throws an exception when the request URI matches, but no relying party registration is found. |
|
|
|
There are a number of cases when an application would not consider this an error situation. |
|
For example, this filter doesn't know how the `AuthorizationFilter` will respond to a missing relying party. |
|
In some cases it may be allowable. |
|
|
|
In other cases, you may want your `AuthenticationEntryPoint` to be invoked, which would happen if this filter were to allow the request to continue to the `AuthorizationFilter`. |
|
|
|
To improve this filter's flexibility, in Spring Security 7 it will continue the filter chain when there is no relying party registration found instead of throwing an exception. |
|
|
|
For many applications, the only notable change will be that your `authenticationEntryPoint` will be invoked if the relying party registration cannot be found. |
|
When you have only one asserting party, this means by default a new authentication request will be built and sent back to the asserting party, which may cause a "Too Many Redirects" loop. |
|
|
|
To see if you are affected in this way, you can prepare for this change in 6 by setting the following property in `Saml2WebSsoAuthenticationFilter`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.saml2Login((saml2) -> saml2 |
|
.withObjectPostProcessor(new ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter>() { |
|
@Override |
|
public Saml2WebSsoAuthenticationFilter postProcess(Saml2WebSsoAuthenticationFilter filter) { |
|
filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true); |
|
return filter; |
|
} |
|
}) |
|
) |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
saml2Login { } |
|
withObjectPostProcessor( |
|
object : ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter?>() { |
|
override fun postProcess(filter: Saml2WebSsoAuthenticationFilter): Saml2WebSsoAuthenticationFilter { |
|
filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true) |
|
return filter |
|
} |
|
}) |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<b:bean id="saml2PostProcessor" class="org.example.MySaml2WebSsoAuthenticationFilterBeanPostProcessor"/> |
|
---- |
|
====== |
|
|
|
== Validate Response After Validating Assertions |
|
|
|
In Spring Security 6, the order of authenticating a `<saml2:Response>` is as follows: |
|
|
|
1. Verify the Response Signature, if any |
|
2. Decrypt the Response |
|
3. Validate Response attributes, like Destination and Issuer |
|
4. For each assertion, verify the signature, decrypt, and then validate its fields |
|
5. Check to ensure that the response has at least one assertion with a name field |
|
|
|
This ordering sometimes poses challenges since some response validation is being done in Step 3 and some in Step 5. |
|
Specifically, this poses a chellenge when an application doesn't have a name field and doesn't need it to be validated. |
|
|
|
In Spring Security 7, this is simplified by moving response validation to after assertion validation and combining the two separate validation steps 3 and 5. |
|
When this is complete, response validation will no longer check for the existence of the `NameID` attribute and rely on ``ResponseAuthenticationConverter``s to do this. |
|
|
|
This will add support ``ResponseAuthenticationConverter``s that don't use the `NameID` element in their `Authentication` instance and so don't need it validated. |
|
|
|
To opt-in to this behavior in advance, use `OpenSaml5AuthenticationProvider#setValidateResponseAfterAssertions` to `true` like so: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider(); |
|
provider.setValidateResponseAfterAssertions(true); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val provider = OpenSaml5AuthenticationProvider() |
|
provider.setValidateResponseAfterAssertions(true) |
|
---- |
|
====== |
|
|
|
This will change the authentication steps as follows: |
|
|
|
1. Verify the Response Signature, if any |
|
2. Decrypt the Response |
|
3. For each assertion, verify the signature, decrypt, and then validate its fields |
|
4. Validate Response attributes, like Destination and Issuer |
|
|
|
Note that if you have a custom response authentication converter, then you are now responsible to check if the `NameID` element exists in the event that you need it. |
|
|
|
Alternatively to updating your response authentication converter, you can specify a custom `ResponseValidator` that adds back in the check for the `NameID` element as follows: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider(); |
|
provider.setValidateResponseAfterAssertions(true); |
|
ResponseValidator responseValidator = ResponseValidator.withDefaults((responseToken) -> { |
|
Response response = responseToken.getResponse(); |
|
Assertion assertion = CollectionUtils.firstElement(response.getAssertions()); |
|
Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, |
|
"Assertion [" + firstAssertion.getID() + "] is missing a subject"); |
|
Saml2ResponseValidationResult failed = Saml2ResponseValidationResult.failure(error); |
|
if (assertion.getSubject() == null) { |
|
return failed; |
|
} |
|
if (assertion.getSubject().getNameID() == null) { |
|
return failed; |
|
} |
|
if (assertion.getSubject().getNameID().getValue() == null) { |
|
return failed; |
|
} |
|
return Saml2ResponseValidationResult.success(); |
|
}); |
|
provider.setResponseValidator(responseValidator); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val provider = OpenSaml5AuthenticationProvider() |
|
provider.setValidateResponseAfterAssertions(true) |
|
val responseValidator = ResponseValidator.withDefaults { responseToken: ResponseToken -> |
|
val response = responseToken.getResponse() |
|
val assertion = CollectionUtils.firstElement(response.getAssertions()) |
|
val error = Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, |
|
"Assertion [" + firstAssertion.getID() + "] is missing a subject") |
|
val failed = Saml2ResponseValidationResult.failure(error) |
|
if (assertion.getSubject() == null) { |
|
return@withDefaults failed |
|
} |
|
if (assertion.getSubject().getNameID() == null) { |
|
return@withDefaults failed |
|
} |
|
if (assertion.getSubject().getNameID().getValue() == null) { |
|
return@withDefaults failed |
|
} |
|
return@withDefaults Saml2ResponseValidationResult.success() |
|
} |
|
provider.setResponseValidator(responseValidator) |
|
---- |
|
====== |
|
|
|
== `RelyingPartyRegistration` Improvements |
|
|
|
`RelyingPartyRegistration` links metadata from a relying party to metadata from an asserting party. |
|
|
|
To prepare for some improvements to the API, please take the following steps: |
|
|
|
1. If you are mutating a registration by using `RelyingPartyRegistration#withRelyingPartyRegistration`, instead call `RelyingPartyRegistration#mutate` |
|
2. If you are providing or retrieving `AssertingPartyDetails`, use `getAssertingPartyMetadata` or `withAssertingPartyMetadata` instead. |
|
|
|
== `OpenSaml5AuthenticationProvider` Improvements |
|
|
|
Spring Security 7 will remove a handful of static factories from `OpenSaml5AuthenticationProvider` in favor of inner classes. |
|
These inner classes simplify customization of the response validator, the assertion validator, and the response authentication converter. |
|
|
|
=== Response Validation |
|
|
|
Instead of doing: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() { |
|
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider(); |
|
saml2.setResponseValidator((responseToken) -> OpenSamlAuthenticationProvider.createDefaultResponseValidator() |
|
.andThen((result) -> result |
|
.concat(myCustomValidator.convert(responseToken)) |
|
)); |
|
return saml2; |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider { |
|
val saml2 = OpenSaml5AuthenticationProvider() |
|
saml2.setResponseValidator { responseToken -> OpenSamlAuthenticationProvider.createDefaultResponseValidator() |
|
.andThen { result -> result |
|
.concat(myCustomValidator.convert(responseToken)) |
|
} |
|
} |
|
return saml2 |
|
} |
|
---- |
|
====== |
|
|
|
use `OpenSaml5AuthenticationProvider.ResponseValidator`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() { |
|
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider(); |
|
saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator)); |
|
return saml2; |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider { |
|
val saml2 = OpenSaml5AuthenticationProvider() |
|
saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator)) |
|
return saml2 |
|
} |
|
---- |
|
====== |
|
|
|
=== Assertion Validation |
|
|
|
Instead of doing: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() { |
|
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider(); |
|
authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider |
|
.createDefaultAssertionValidatorWithParameters(assertionToken -> { |
|
Map<String, Object> params = new HashMap<>(); |
|
params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis()); |
|
// ... other validation parameters |
|
return new ValidationContext(params); |
|
}) |
|
); |
|
return saml2; |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider { |
|
val saml2 = OpenSaml5AuthenticationProvider() |
|
authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider |
|
.createDefaultAssertionValidatorWithParameters { -> |
|
val params = HashMap<String, Object>() |
|
params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis()) |
|
// ... other validation parameters |
|
return ValidationContext(params) |
|
} |
|
) |
|
return saml2 |
|
} |
|
---- |
|
====== |
|
|
|
use `OpenSaml5AuthenticationProvider.AssertionValidator`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() { |
|
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider(); |
|
Duration tenMinutes = Duration.ofMinutes(10); |
|
authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build()); |
|
return saml2; |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider { |
|
val saml2 = OpenSaml5AuthenticationProvider() |
|
val tenMinutes = Duration.ofMinutes(10) |
|
authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build()) |
|
return saml2 |
|
} |
|
---- |
|
====== |
|
|
|
== Response Authentication Converter |
|
|
|
Instead of doing: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
Converter<ResponseToken, Saml2Authentication> authenticationConverter() { |
|
return (responseToken) -> { |
|
Saml2Authentication authentication = OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter() |
|
.convert(responseToken); |
|
// ... work with OpenSAML's Assertion object to extract the principal |
|
return new Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities()); |
|
}; |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun authenticationConverter(): Converter<ResponseToken, Saml2Authentication> { |
|
return { responseToken -> |
|
val authentication = |
|
OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter().convert(responseToken) |
|
// ... work with OpenSAML's Assertion object to extract the principal |
|
return Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities()) |
|
} |
|
} |
|
---- |
|
====== |
|
|
|
use `OpenSaml5AuthenticationProvider.ResponseAuthenticationConverter`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
ResponseAuthenticationConverter authenticationConverter() { |
|
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter(); |
|
authenticationConverter.setPrincipalNameConverter((assertion) -> { |
|
// ... work with OpenSAML's Assertion object to extract the principal |
|
}); |
|
return authenticationConverter; |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun authenticationConverter(): ResponseAuthenticationConverter { |
|
val authenticationConverter = ResponseAuthenticationConverter() |
|
authenticationConverter.setPrincipalNameConverter { assertion -> |
|
// ... work with OpenSAML's Assertion object to extract the principal |
|
} |
|
return authenticationConverter |
|
} |
|
---- |
|
======
|
|
|