mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-23 20:42:14 +00:00
Add ResponseAuthenticationConverter
Aside from simplifying configuration, this commit also makes it possible to provide a response authentication converter that doesn't need the NameID element to be present. Closes gh-12136
This commit is contained in:
parent
3e686abf50
commit
3869b13e68
@ -58,3 +58,108 @@ Xml::
|
||||
<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)
|
||||
----
|
||||
======
|
||||
|
@ -250,12 +250,135 @@ class SecurityConfig {
|
||||
----
|
||||
======
|
||||
|
||||
== Converting an `Assertion` into an `Authentication`
|
||||
|
||||
`OpenSamlXAuthenticationProvider#setResponseAuthenticationConverter` provides a way for you to change how it converts your assertion into an `Authentication` instance.
|
||||
|
||||
You can set a custom converter in the following way:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@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()
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
The ensuing examples all build off of this common construct to show you different ways this converter comes in handy.
|
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
|
||||
== Coordinating with a `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:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@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>
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@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> First, call the default converter, which extracts attributes and authorities from the response
|
||||
<2> Second, call the xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`] using the relevant information
|
||||
<3> Third, return an authentication that includes the user details
|
||||
|
||||
[TIP]
|
||||
====
|
||||
If your `UserDetailsService` returns a value that also implements `AuthenticatedPrincipal`, then you don't need a custom authentication implementation.
|
||||
====
|
||||
|
||||
Or, if you are using OpenSaml 4, then you can achieve something similar as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
@ -336,6 +459,78 @@ open class SecurityConfig {
|
||||
It's not required to call ``OpenSaml4AuthenticationProvider``'s default authentication converter.
|
||||
It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from ``AttributeStatement``s as well as the single `ROLE_USER` authority.
|
||||
|
||||
=== Configuring the Principal Name
|
||||
|
||||
Sometimes, the principal name is not in the `<saml2:NameID>` element.
|
||||
In that case, you can configure the `ResponseAuthenticationConverter` with a custom strategy like so:
|
||||
|
||||
[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 = ResponseAuthenticationConverter()
|
||||
authenticationConverter.setPrincipalNameConverter { assertion ->
|
||||
// ... work with OpenSAML's Assertion object to extract the principal
|
||||
}
|
||||
return authenticationConverter
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
=== Configuring a Principal's Granted Authorities
|
||||
|
||||
Spring Security automatically grants `ROLE_USER` when using `OpenSamlXAuhenticationProvider`.
|
||||
With `OpenSaml5AuthenticationProvider`, you can configure a different set of granted authorities like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
ResponseAuthenticationConverter authenticationConverter() {
|
||||
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
|
||||
authenticationConverter.setPrincipalNameConverter((assertion) -> {
|
||||
// ... grant the needed authorities based on attributes in the assertion
|
||||
});
|
||||
return authenticationConverter;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun authenticationConverter(): ResponseAuthenticationConverter {
|
||||
val authenticationConverter = ResponseAuthenticationConverter()
|
||||
authenticationConverter.setPrincipalNameConverter{ assertion ->
|
||||
// ... grant the needed authorities based on attributes in the assertion
|
||||
}
|
||||
return authenticationConverter
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
|
||||
== Performing Additional Response Validation
|
||||
|
||||
|
@ -339,7 +339,7 @@ It's common to need to set other values in the `<saml2:LogoutRequest>` than the
|
||||
|
||||
By default, Spring Security will issue a `<saml2:LogoutRequest>` and supply:
|
||||
|
||||
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation`
|
||||
* The `DestinationValidator` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation`
|
||||
* The `ID` attribute - a GUID
|
||||
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
|
||||
* The `<NameID>` element - from `Authentication#getName`
|
||||
@ -424,7 +424,7 @@ It's common to need to set other values in the `<saml2:LogoutResponse>` than the
|
||||
|
||||
By default, Spring Security will issue a `<saml2:LogoutResponse>` and supply:
|
||||
|
||||
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation`
|
||||
* The `DestinationValidator` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation`
|
||||
* The `ID` attribute - a GUID
|
||||
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
|
||||
* The `<Status>` element - `SUCCESS`
|
||||
|
@ -110,6 +110,8 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private Converter<ResponseToken, ? extends AbstractAuthenticationToken> responseAuthenticationConverter = createDefaultResponseAuthenticationConverter();
|
||||
|
||||
private boolean validateResponseAfterAssertions = false;
|
||||
|
||||
private static final Set<String> includeChildStatusCodes = new HashSet<>(
|
||||
Arrays.asList(StatusCode.REQUESTER, StatusCode.RESPONDER, StatusCode.VERSION_MISMATCH));
|
||||
|
||||
@ -143,6 +145,10 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
this.responseAuthenticationConverter = responseAuthenticationConverter;
|
||||
}
|
||||
|
||||
void setValidateResponseAfterAssertions(boolean validateResponseAfterAssertions) {
|
||||
this.validateResponseAfterAssertions = validateResponseAfterAssertions;
|
||||
}
|
||||
|
||||
static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
|
||||
return (responseToken) -> {
|
||||
Response response = responseToken.getResponse();
|
||||
@ -321,7 +327,9 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
|
||||
"Did not decrypt response [" + response.getID() + "] since it is not signed"));
|
||||
}
|
||||
if (!this.validateResponseAfterAssertions) {
|
||||
result = result.concat(this.responseValidator.convert(responseToken));
|
||||
}
|
||||
boolean allAssertionsSigned = true;
|
||||
for (Assertion assertion : response.getAssertions()) {
|
||||
AssertionToken assertionToken = new AssertionToken(assertion, token);
|
||||
@ -337,12 +345,17 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
+ "Please either sign the response or all of the assertions.";
|
||||
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, description));
|
||||
}
|
||||
if (this.validateResponseAfterAssertions) {
|
||||
result = result.concat(this.responseValidator.convert(responseToken));
|
||||
}
|
||||
else {
|
||||
Assertion firstAssertion = CollectionUtils.firstElement(response.getAssertions());
|
||||
if (firstAssertion != null && !hasName(firstAssertion)) {
|
||||
Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
|
||||
"Assertion [" + firstAssertion.getID() + "] is missing a subject");
|
||||
result = result.concat(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.hasErrors()) {
|
||||
Collection<Saml2Error> errors = result.getErrors();
|
||||
@ -422,7 +435,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private boolean hasName(Assertion assertion) {
|
||||
static boolean hasName(Assertion assertion) {
|
||||
if (assertion == null) {
|
||||
return false;
|
||||
}
|
||||
@ -435,7 +448,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
return assertion.getSubject().getNameID().getValue() != null;
|
||||
}
|
||||
|
||||
private static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
|
||||
static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
|
||||
MultiValueMap<String, Object> attributeMap = new LinkedMultiValueMap<>();
|
||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
||||
@ -452,7 +465,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
return new LinkedHashMap<>(attributeMap); // gh-11785
|
||||
}
|
||||
|
||||
private static List<String> getSessionIndexes(Assertion assertion) {
|
||||
static List<String> getSessionIndexes(Assertion assertion) {
|
||||
List<String> sessionIndexes = new ArrayList<>();
|
||||
for (AuthnStatement statement : assertion.getAuthnStatements()) {
|
||||
sessionIndexes.add(statement.getSessionIndex());
|
||||
|
@ -85,6 +85,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
|
||||
*/
|
||||
public OpenSaml4AuthenticationProvider() {
|
||||
this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml4Template());
|
||||
this.delegate.setValidateResponseAfterAssertions(false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,12 +58,15 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
|
||||
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@ -118,6 +121,7 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
|
||||
this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml5Template());
|
||||
setResponseValidator(ResponseValidator.withDefaults());
|
||||
setAssertionValidator(AssertionValidator.withDefaults());
|
||||
setResponseAuthenticationConverter(new ResponseAuthenticationConverter());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -300,6 +304,21 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
|
||||
(token) -> responseAuthenticationConverter.convert(new ResponseToken(token)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate when to validate response attributes, like {@code Destination} and
|
||||
* {@code Issuer}. By default, this value is set to false, meaning that response
|
||||
* attributes are validated first. Setting this value to {@code true} allows you to
|
||||
* use a response authentication converter that doesn't rely on the {@code NameID}
|
||||
* element in the {@link Response}'s assertion.
|
||||
* @param validateResponseAfterAssertions when to validate response attributes
|
||||
* @since 6.5
|
||||
* @see #setResponseAuthenticationConverter
|
||||
* @see ResponseAuthenticationConverter
|
||||
*/
|
||||
public void setValidateResponseAfterAssertions(boolean validateResponseAfterAssertions) {
|
||||
this.delegate.setValidateResponseAfterAssertions(validateResponseAfterAssertions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a default strategy for validating the SAML 2.0 Response
|
||||
* @return the default response validator strategy
|
||||
@ -373,12 +392,11 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
|
||||
* Construct a default strategy for converting a SAML 2.0 Response and
|
||||
* {@link Authentication} token into a {@link Saml2Authentication}
|
||||
* @return the default response authentication converter strategy
|
||||
* @deprecated please use {@link ResponseAuthenticationConverter} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public static Converter<ResponseToken, Saml2Authentication> createDefaultResponseAuthenticationConverter() {
|
||||
Converter<BaseOpenSamlAuthenticationProvider.ResponseToken, Saml2Authentication> delegate = BaseOpenSamlAuthenticationProvider
|
||||
.createDefaultResponseAuthenticationConverter();
|
||||
return (token) -> delegate
|
||||
.convert(new BaseOpenSamlAuthenticationProvider.ResponseToken(token.getResponse(), token.getToken()));
|
||||
return new ResponseAuthenticationConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -852,4 +870,81 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A default implementation of {@link OpenSaml5AuthenticationProvider}'s response
|
||||
* authentication converter. It will take the principal name from the
|
||||
* {@link org.opensaml.saml.saml2.core.NameID} element. It will also extract the
|
||||
* assertion attributes and session indexes. You can either configure the principal
|
||||
* name converter and granted authorities converter in this class or you can
|
||||
* post-process this class's result through delegation.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.5
|
||||
*/
|
||||
public static final class ResponseAuthenticationConverter implements Converter<ResponseToken, Saml2Authentication> {
|
||||
|
||||
private Converter<Assertion, String> principalNameConverter = ResponseAuthenticationConverter::authenticatedPrincipal;
|
||||
|
||||
private Converter<Assertion, Collection<GrantedAuthority>> grantedAuthoritiesConverter = ResponseAuthenticationConverter::grantedAuthorities;
|
||||
|
||||
@Override
|
||||
public Saml2Authentication convert(ResponseToken responseToken) {
|
||||
Response response = responseToken.response;
|
||||
Saml2AuthenticationToken token = responseToken.token;
|
||||
Assertion assertion = CollectionUtils.firstElement(response.getAssertions());
|
||||
String username = this.principalNameConverter.convert(assertion);
|
||||
Map<String, List<Object>> attributes = BaseOpenSamlAuthenticationProvider.getAssertionAttributes(assertion);
|
||||
List<String> sessionIndexes = BaseOpenSamlAuthenticationProvider.getSessionIndexes(assertion);
|
||||
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, attributes,
|
||||
sessionIndexes);
|
||||
String registrationId = responseToken.token.getRelyingPartyRegistration().getRegistrationId();
|
||||
principal.setRelyingPartyRegistrationId(registrationId);
|
||||
return new Saml2Authentication(principal, token.getSaml2Response(),
|
||||
this.grantedAuthoritiesConverter.convert(assertion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this strategy to extract the principal name from the {@link Assertion}. By
|
||||
* default, this will retrieve it from the
|
||||
* {@link org.opensaml.saml.saml2.core.Subject}'s
|
||||
* {@link org.opensaml.saml.saml2.core.NameID} value.
|
||||
*
|
||||
* <p>
|
||||
* Note that because of this, if there is no
|
||||
* {@link org.opensaml.saml.saml2.core.NameID} present, then the default throws an
|
||||
* exception.
|
||||
* </p>
|
||||
* @param principalNameConverter the conversion strategy to use
|
||||
*/
|
||||
public void setPrincipalNameConverter(Converter<Assertion, String> principalNameConverter) {
|
||||
Assert.notNull(principalNameConverter, "principalNameConverter cannot be null");
|
||||
this.principalNameConverter = principalNameConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this strategy to grant authorities to a principal given the first
|
||||
* {@link Assertion} in the response. By default, this will grant
|
||||
* {@code ROLE_USER}.
|
||||
* @param grantedAuthoritiesConverter the conversion strategy to use
|
||||
*/
|
||||
public void setGrantedAuthoritiesConverter(
|
||||
Converter<Assertion, Collection<GrantedAuthority>> grantedAuthoritiesConverter) {
|
||||
Assert.notNull(grantedAuthoritiesConverter, "grantedAuthoritiesConverter cannot be null");
|
||||
this.grantedAuthoritiesConverter = grantedAuthoritiesConverter;
|
||||
}
|
||||
|
||||
private static String authenticatedPrincipal(Assertion assertion) {
|
||||
if (!BaseOpenSamlAuthenticationProvider.hasName(assertion)) {
|
||||
throw new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
|
||||
"Assertion [" + assertion.getID() + "] is missing a subject"));
|
||||
}
|
||||
return assertion.getSubject().getNameID().getValue();
|
||||
}
|
||||
|
||||
private static Collection<GrantedAuthority> grantedAuthorities(Assertion assertion) {
|
||||
return AuthorityUtils.createAuthorityList("ROLE_USER");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import java.io.ObjectOutputStream;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
@ -71,12 +72,15 @@ import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.AssertionValidator;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseAuthenticationConverter;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseValidator;
|
||||
import org.springframework.security.saml2.provider.service.authentication.TestCustomOpenSaml5Objects.CustomOpenSamlObject;
|
||||
@ -92,6 +96,7 @@ import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSaml5AuthenticationProvider}
|
||||
@ -660,6 +665,47 @@ public class OpenSaml5AuthenticationProviderTests {
|
||||
verify(authenticationConverter).convert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenResponseAuthenticationConverterComponentConfiguredThenUses() {
|
||||
Converter<Assertion, Collection<GrantedAuthority>> grantedAuthoritiesConverter = mock(Converter.class);
|
||||
given(grantedAuthoritiesConverter.convert(any())).willReturn(AuthorityUtils.createAuthorityList("CUSTOM"));
|
||||
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
|
||||
authenticationConverter.setGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
|
||||
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
|
||||
provider.setResponseAuthenticationConverter(authenticationConverter);
|
||||
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
Authentication authentication = provider.authenticate(token);
|
||||
assertThat(AuthorityUtils.authorityListToSet(authentication.getAuthorities())).containsExactly("CUSTOM");
|
||||
verify(grantedAuthoritiesConverter).convert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidateResponseAfterAssertionsThenCanHaveResponseAuthenticationConverterThatDoesntNeedANameID() {
|
||||
Converter<ResponseToken, Saml2Authentication> responseAuthenticationConverter = mock(Converter.class);
|
||||
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
|
||||
provider.setValidateResponseAfterAssertions(true);
|
||||
provider.setResponseAuthenticationConverter(responseAuthenticationConverter);
|
||||
Response response = TestOpenSamlObjects
|
||||
.signedResponseWithOneAssertion((r) -> r.getAssertions().get(0).setSubject(null));
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
provider.authenticate(token);
|
||||
verify(responseAuthenticationConverter).convert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidateResponseBeforeAssertionsThenMustHaveNameID() {
|
||||
Converter<ResponseToken, Saml2Authentication> responseAuthenticationConverter = mock(Converter.class);
|
||||
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
|
||||
provider.setValidateResponseAfterAssertions(false);
|
||||
provider.setResponseAuthenticationConverter(responseAuthenticationConverter);
|
||||
Response response = TestOpenSamlObjects
|
||||
.signedResponseWithOneAssertion((r) -> r.getAssertions().get(0).setSubject(null));
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token));
|
||||
verifyNoInteractions(responseAuthenticationConverter);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setResponseAuthenticationConverterWhenNullThenIllegalArgument() {
|
||||
// @formatter:off
|
||||
|
Loading…
x
Reference in New Issue
Block a user