385 lines
15 KiB
Plaintext
385 lines
15 KiB
Plaintext
[[servlet-saml2login-authenticate-responses]]
|
|
= Authenticating ``<saml2:Response>``s
|
|
|
|
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] by default.
|
|
|
|
You can configure this in a number of ways including:
|
|
|
|
1. Setting a clock skew to timestamp validation
|
|
2. Mapping the response to a list of `GrantedAuthority` instances
|
|
3. Customizing the strategy for validating assertions
|
|
4. Customizing the strategy for decrypting response and assertion elements
|
|
|
|
To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
|
|
|
|
[[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
|
|
== Setting a Clock Skew
|
|
|
|
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
|
|
[source,java,role="primary"]
|
|
----
|
|
@EnableWebSecurity
|
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
|
@Override
|
|
protected void configure(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
|
|
.authorizeRequests(authz -> authz
|
|
.anyRequest().authenticated()
|
|
)
|
|
.saml2Login(saml2 -> saml2
|
|
.authenticationManager(new ProviderManager(authenticationProvider))
|
|
);
|
|
}
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@EnableWebSecurity
|
|
open class SecurityConfig : WebSecurityConfigurerAdapter() {
|
|
override fun configure(http: HttpSecurity) {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
[[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:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@EnableWebSecurity
|
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
@Autowired
|
|
UserDetailsService userDetailsService;
|
|
|
|
@Override
|
|
protected void configure(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
|
|
.authorizeRequests(authz -> authz
|
|
.anyRequest().authenticated()
|
|
)
|
|
.saml2Login(saml2 -> saml2
|
|
.authenticationManager(new ProviderManager(authenticationProvider))
|
|
);
|
|
}
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@EnableWebSecurity
|
|
open class SecurityConfig : WebSecurityConfigurerAdapter() {
|
|
@Autowired
|
|
var userDetailsService: UserDetailsService? = null
|
|
|
|
override fun configure(http: HttpSecurity) {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
----
|
|
====
|
|
<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 a custom authentication that includes the user details
|
|
|
|
[NOTE]
|
|
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.
|
|
|
|
[[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
|
|
== Performing Additional Response Validation
|
|
|
|
`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.
|
|
|
|
For example, you can throw a custom exception with any additional information available in the `Response` object, like so:
|
|
[source,java]
|
|
----
|
|
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` performs minimal validation on SAML 2.0 Assertions.
|
|
After verifying the signature, it will:
|
|
|
|
1. Validate `<AudienceRestriction>` and `<DelegationRestriction>` conditions
|
|
2. Validate ``<SubjectConfirmation>``s, expect for any IP address information
|
|
|
|
To perform additional validation, you can configure your own assertion validator that delegates to `OpenSaml4AuthenticationProvider` 's default and then performs its own.
|
|
|
|
[[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]]
|
|
For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
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()));
|
|
});
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
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))
|
|
}
|
|
----
|
|
====
|
|
|
|
[NOTE]
|
|
While recommended, it's not necessary to call `OpenSaml4AuthenticationProvider` 's default assertion validator.
|
|
A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself.
|
|
|
|
[[servlet-saml2login-opensamlauthenticationprovider-decryption]]
|
|
== Customizing Decryption
|
|
|
|
Spring Security decrypts `<saml2:EncryptedAssertion>`, `<saml2:EncryptedAttribute>`, and `<saml2:EncryptedID>` elements automatically by using the decryption xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-credentials[`Saml2X509Credential` instances] registered in the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`].
|
|
|
|
`OpenSaml4AuthenticationProvider` exposes xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[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>`.
|
|
|
|
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
|
|
[source,java,role="primary"]
|
|
----
|
|
MyDecryptionService decryptionService = ...;
|
|
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
|
|
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
val decryptionService: MyDecryptionService = ...
|
|
val provider = OpenSaml4AuthenticationProvider()
|
|
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
|
|
----
|
|
====
|
|
|
|
If you are also decrypting individual elements in a `<saml2:Assertion>`, you can customize the assertion decrypter, too:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
|
|
----
|
|
====
|
|
|
|
NOTE: 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.
|
|
|
|
[[servlet-saml2login-authenticationmanager-custom]]
|
|
== Using a Custom Authentication Manager
|
|
|
|
[[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]]
|
|
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
|
|
[source,java,role="primary"]
|
|
----
|
|
@EnableWebSecurity
|
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
|
@Override
|
|
protected void configure(HttpSecurity http) throws Exception {
|
|
AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
|
|
http
|
|
.authorizeRequests(authorize -> authorize
|
|
.anyRequest().authenticated()
|
|
)
|
|
.saml2Login(saml2 -> saml2
|
|
.authenticationManager(authenticationManager)
|
|
)
|
|
;
|
|
}
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@EnableWebSecurity
|
|
open class SecurityConfig : WebSecurityConfigurerAdapter() {
|
|
override fun configure(http: HttpSecurity) {
|
|
val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
|
|
http {
|
|
authorizeRequests {
|
|
authorize(anyRequest, authenticated)
|
|
}
|
|
saml2Login {
|
|
authenticationManager = customAuthenticationManager
|
|
}
|
|
}
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
[[servlet-saml2login-authenticatedprincipal]]
|
|
== Using `Saml2AuthenticatedPrincipal`
|
|
|
|
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
|
|
[source,java,role="primary"]
|
|
----
|
|
@Controller
|
|
public class MainController {
|
|
@GetMapping("/")
|
|
public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
|
|
String email = principal.getFirstAttribute("email");
|
|
model.setAttribute("email", email);
|
|
return "index";
|
|
}
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@Controller
|
|
class MainController {
|
|
@GetMapping("/")
|
|
fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
|
|
val email = principal.getFirstAttribute<String>("email")
|
|
model.setAttribute("email", email)
|
|
return "index"
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
[TIP]
|
|
Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call `getAttribute` to get the list of attributes or `getFirstAttribute` to get the first in the list.
|
|
`getFirstAttribute` is quite handy when you know that there is only one value.
|