Separate SAML 2.0 Login Docs

Issue gh-10367
This commit is contained in:
Josh Cummings 2021-10-29 15:06:54 -06:00
parent 6d2d3b9a69
commit 11aa02c6fb
6 changed files with 713 additions and 709 deletions

View File

@ -63,7 +63,10 @@
**** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy] **** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy]
**** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens] **** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens]
** xref:servlet/saml2/index.adoc[SAML2] ** xref:servlet/saml2/index.adoc[SAML2]
*** xref:servlet/saml2/login.adoc[SAML2 Log In] *** xref:servlet/saml2/login/index.adoc[SAML2 Log In]
**** xref:servlet/saml2/login/overview.adoc[SAML2 Log In Overview]
**** xref:servlet/saml2/login/authentication-requests.adoc[SAML2 Authentication Requests]
**** xref:servlet/saml2/login/authentication.adoc[SAML2 Authentication Responses]
*** xref:servlet/saml2/logout.adoc[SAML2 Logout] *** xref:servlet/saml2/logout.adoc[SAML2 Logout]
*** xref:servlet/saml2/metadata.adoc[SAML2 Metadata] *** xref:servlet/saml2/metadata.adoc[SAML2 Metadata]
** xref:servlet/exploits/index.adoc[Protection Against Exploits] ** xref:servlet/exploits/index.adoc[Protection Against Exploits]

View File

@ -0,0 +1,293 @@
[[servlet-saml2login-sp-initiated-factory]]
= Producing ``<saml2:AuthnRequest>``s
As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party.
Spring Security achieves this in part by registering the `Saml2WebSsoAuthenticationRequestFilter` in the filter chain.
This filter by default responds to endpoint `+/saml2/authenticate/{registrationId}+`.
For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to:
`https://rp.example.org/saml2/authenticate/ping`
and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
[[servlet-saml2login-store-authn-request]]
== Changing How the `<saml2:AuthnRequest>` Gets Stored
`Saml2WebSsoAuthenticationRequestFilter` uses an `Saml2AuthenticationRequestRepository` to persist an `AbstractSaml2AuthenticationRequest` instance before xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[sending the `<saml2:AuthnRequest>`] to the asserting party.
Additionally, `Saml2WebSsoAuthenticationFilter` and `Saml2AuthenticationTokenConverter` use an `Saml2AuthenticationRequestRepository` to load any `AbstractSaml2AuthenticationRequest` as part of xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticating the `<saml2:Response>`].
By default, Spring Security uses an `HttpSessionSaml2AuthenticationRequestRepository`, which stores the `AbstractSaml2AuthenticationRequest` in the `HttpSession`.
If you have a custom implementation of `Saml2AuthenticationRequestRepository`, you may configure it by exposing it as a `@Bean` as shown in the following example:
====
.Java
[source,java,role="primary"]
----
@Bean
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() {
return new CustomSaml2AuthenticationRequestRepository();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
return CustomSaml2AuthenticationRequestRepository()
}
----
====
[[servlet-saml2login-sp-initiated-factory-signing]]
== Changing How the `<saml2:AuthnRequest>` Gets Sent
By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party.
Many asserting parties don't require a signed `<saml2:AuthnRequest>`.
This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
.Not Requiring Signed AuthnRequests
====
.Boot
[source,yaml,role="primary"]
----
spring:
security:
saml2:
relyingparty:
okta:
identityprovider:
entity-id: ...
singlesignon.sign-request: false
----
.Java
[source,java,role="secondary"]
----
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails(party -> party
// ...
.wantAuthnRequestsSigned(false)
)
.build();
----
.Kotlin
[source,java,role="secondary"]
----
var relyingPartyRegistration: RelyingPartyRegistration =
RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
// ...
.wantAuthnRequestsSigned(false)
}
.build();
----
====
Otherwise, you will need to specify a private key to `RelyingPartyRegistration#signingX509Credentials` so that Spring Security can sign the `<saml2:AuthnRequest>` before sending.
[[servlet-saml2login-sp-initiated-factory-algorithm]]
By default, Spring Security will sign the `<saml2:AuthnRequest>` using `rsa-sha256`, though some asserting parties will require a different algorithm, as indicated in their metadata.
You can configure the algorithm based on the asserting party's xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistrationrepository[metadata using `RelyingPartyRegistrations`].
Or, you can provide it manually:
====
.Java
[source,java,role="primary"]
----
String metadataLocation = "classpath:asserting-party-metadata.xml";
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
// ...
.assertingPartyDetails((party) -> party
// ...
.signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
)
.build();
----
.Kotlin
[source,kotlin,role="secondary"]
----
var metadataLocation = "classpath:asserting-party-metadata.xml"
var relyingPartyRegistration: RelyingPartyRegistration =
RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
// ...
.signingAlgorithms { sign: MutableList<String?> ->
sign.add(
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512
)
}
}
.build();
----
====
NOTE: The snippet above uses the OpenSAML `SignatureConstants` class to supply the algorithm name.
But, that's just for convenience.
Since the datatype is `String`, you can supply the name of the algorithm directly.
[[servlet-saml2login-sp-initiated-factory-binding]]
Some asserting parties require that the `<saml2:AuthnRequest>` be POSTed.
This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
====
.Java
[source,java,role="primary"]
----
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails(party -> party
// ...
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
)
.build();
----
.Kotlin
[source,kotlin,role="secondary"]
----
var relyingPartyRegistration: RelyingPartyRegistration? =
RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
// ...
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
}
.build()
----
====
[[servlet-saml2login-sp-initiated-factory-custom-authnrequest]]
== Customizing OpenSAML's `AuthnRequest` Instance
There are a number of reasons that you may want to adjust an `AuthnRequest`.
For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
If you don't need information from the `HttpServletRequest` to make your decision, then the easiest way is to xref:servlet/saml2/login/overview.adoc#servlet-saml2login-opensaml-customization[register a custom `AuthnRequestMarshaller` with OpenSAML].
This will give you access to post-process the `AuthnRequest` instance before it's serialized.
But, if you do need something from the request, then you can use create a custom `Saml2AuthenticationRequestContext` implementation and then a `Converter<Saml2AuthenticationRequestContext, AuthnRequest>` to build an `AuthnRequest` yourself, like so:
====
.Java
[source,java,role="primary"]
----
@Component
public class AuthnRequestConverter implements
Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
private final AuthnRequestBuilder authnRequestBuilder;
private final IssuerBuilder issuerBuilder;
// ... constructor
public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context;
Issuer issuer = issuerBuilder.buildObject();
issuer.setValue(myContext.getIssuer());
AuthnRequest authnRequest = authnRequestBuilder.buildObject();
authnRequest.setIssuer(issuer);
authnRequest.setDestination(myContext.getDestination());
authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl());
// ... additional settings
authRequest.setForceAuthn(myContext.getForceAuthn());
return authnRequest;
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Component
class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
private val authnRequestBuilder: AuthnRequestBuilder? = null
private val issuerBuilder: IssuerBuilder? = null
// ... constructor
override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
val myContext: MySaml2AuthenticationRequestContext = context
val issuer: Issuer = issuerBuilder.buildObject()
issuer.value = myContext.getIssuer()
val authnRequest: AuthnRequest = authnRequestBuilder.buildObject()
authnRequest.issuer = issuer
authnRequest.destination = myContext.getDestination()
authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl()
// ... additional settings
authRequest.setForceAuthn(myContext.getForceAuthn())
return authnRequest
}
}
----
====
Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as ``@Bean``s:
====
.Java
[source,java,role="primary"]
----
@Bean
Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() {
Saml2AuthenticationRequestContextResolver resolver =
new DefaultSaml2AuthenticationRequestContextResolver();
return request -> {
Saml2AuthenticationRequestContext context = resolver.resolve(request);
return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null);
};
}
@Bean
Saml2AuthenticationRequestFactory authenticationRequestFactory(
AuthnRequestConverter authnRequestConverter) {
OpenSaml4AuthenticationRequestFactory authenticationRequestFactory =
new OpenSaml4AuthenticationRequestFactory();
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
return authenticationRequestFactory;
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver {
val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver()
return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest ->
val context = resolver.resolve(request)
MySaml2AuthenticationRequestContext(
context,
request.getParameter("force") != null
)
}
}
@Bean
open fun authenticationRequestFactory(
authnRequestConverter: AuthnRequestConverter?
): Saml2AuthenticationRequestFactory? {
val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory()
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter)
return authenticationRequestFactory
}
----
====

View File

@ -0,0 +1,384 @@
[[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.

View File

@ -0,0 +1,18 @@
[[servlet-saml2login]]
= SAML 2.0 Login
:page-section-summary-toc: 1
The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users https://wiki.shibboleth.net/confluence/display/CONCEPT/FlowsAndConfig[log in] to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc).
NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in
https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles].
[[servlet-saml2login-spring-security-history]]
Since 2009, support for relying parties has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project].
In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper.
This process is similar to the one started in 2017 for xref:servlet/oauth2/index.adoc[Spring Security's OAuth 2.0 support].
[NOTE]
====
A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository].
====

View File

@ -1,22 +1,6 @@
[[servlet-saml2login]] = SAML 2.0 Login Overview
= SAML 2.0 Login :figures: servlet/saml2
:figures: images/servlet/saml2 :icondir: icons
:icondir: images/icons
The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users https://wiki.shibboleth.net/confluence/display/CONCEPT/FlowsAndConfig[log in] to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc).
NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in
https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles].
[[servlet-saml2login-spring-security-history]]
Since 2009, support for relying parties has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project].
In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper.
This process is similar to the one started in 2017 for xref:servlet/oauth2/index.adoc[Spring Security's OAuth 2.0 support].
[NOTE]
====
A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository].
====
Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security. Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security.
First, we see that, like xref:servlet/oauth2/oauth2-login.adoc[OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication. First, we see that, like xref:servlet/oauth2/oauth2-login.adoc[OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication.
@ -32,7 +16,7 @@ image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to
image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`.
image:{icondir}/number_3.png[] Since the user lacks authorization, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__. image:{icondir}/number_3.png[] Since the user lacks authorization, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__.
The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`] which redirects to <<servlet-saml2login-sp-initiated-factory,the `<saml2:AuthnRequest>` generating endpoint>>, `Saml2WebSsoAuthenticationRequestFilter`. The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`] which redirects to xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[the `<saml2:AuthnRequest>` generating endpoint], `Saml2WebSsoAuthenticationRequestFilter`.
Or, if you've <<servlet-saml2login-relyingpartyregistrationrepository,configured more than one asserting party>>, it will first redirect to a picker page. Or, if you've <<servlet-saml2login-relyingpartyregistrationrepository,configured more than one asserting party>>, it will first redirect to a picker page.
image:{icondir}/number_4.png[] Next, the `Saml2WebSsoAuthenticationRequestFilter` creates, signs, serializes, and encodes a `<saml2:AuthnRequest>` using its configured <<servlet-saml2login-sp-initiated-factory,`Saml2AuthenticationRequestFactory`>>. image:{icondir}/number_4.png[] Next, the `Saml2WebSsoAuthenticationRequestFilter` creates, signs, serializes, and encodes a `<saml2:AuthnRequest>` using its configured <<servlet-saml2login-sp-initiated-factory,`Saml2AuthenticationRequestFactory`>>.
@ -49,7 +33,7 @@ image::{figures}/saml2webssoauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram. The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it <<servlet-saml2login-authenticate-responses, delegates to `Saml2WebSsoAuthenticationFilter`>>. image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[delegates to `Saml2WebSsoAuthenticationFilter`].
This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`. This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`. This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
@ -135,7 +119,7 @@ Your app then redirects to the configured asserting party which then sends the `
From here, consider jumping to: From here, consider jumping to:
* <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>> * <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
* <<servlet-saml2login-authenticatedprincipal,How to Use the `Saml2AuthenticatedPrincipal`>> * xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticatedprincipal[How to Use the `Saml2AuthenticatedPrincipal`]
* <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>> * <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
[[servlet-saml2login-architecture]] [[servlet-saml2login-architecture]]
@ -172,7 +156,7 @@ image:{icondir}/number_2.png[] The xref:servlet/authentication/architecture.adoc
image:{icondir}/number_3.png[] The authentication provider deserializes the response into an OpenSAML `Response` and checks its signature. image:{icondir}/number_3.png[] The authentication provider deserializes the response into an OpenSAML `Response` and checks its signature.
If the signature is invalid, authentication fails. If the signature is invalid, authentication fails.
image:{icondir}/number_4.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedAssertion` elements>>. image:{icondir}/number_4.png[] Then, the provider xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-opensamlauthenticationprovider-decryption[decrypts any `EncryptedAssertion` elements].
If any decryptions fail, authentication fails. If any decryptions fail, authentication fails.
image:{icondir}/number_5.png[] Next, the provider validates the response's `Issuer` and `Destination` values. image:{icondir}/number_5.png[] Next, the provider validates the response's `Issuer` and `Destination` values.
@ -183,7 +167,7 @@ If any signature is invalid, authentication fails.
Also, if neither the response nor the assertions have signatures, authentication fails. Also, if neither the response nor the assertions have signatures, authentication fails.
Either the response or all the assertions must have signatures. Either the response or all the assertions must have signatures.
image:{icondir}/number_7.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedID` or `EncryptedAttribute` elements>>. image:{icondir}/number_7.png[] Then, the provider xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-opensamlauthenticationprovider-decryption[,]decrypts any `EncryptedID` or `EncryptedAttribute` elements].
If any decryptions fail, authentication fails. If any decryptions fail, authentication fails.
image:{icondir}/number_8.png[] Next, the provider validates each assertion's `ExpiresAt` and `NotBefore` timestamps, the `<Subject>` and any `<AudienceRestriction>` conditions. image:{icondir}/number_8.png[] Next, the provider validates each assertion's `ExpiresAt` and `NotBefore` timestamps, the `<Subject>` and any `<AudienceRestriction>` conditions.
@ -761,7 +745,7 @@ class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationR
---- ----
==== ====
Then, you can provide this resolver to the appropriate filters that <<servlet-saml2login-sp-initiated-factory, produce ``<saml2:AuthnRequest>``s>>, <<servlet-saml2login-authenticate-responses, authenticate ``<saml2:Response>``s>>, and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata]. Then, you can provide this resolver to the appropriate filters that xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[produce ``<saml2:AuthnRequest>``s], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate ``<saml2:Response>``s], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
[NOTE] [NOTE]
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them. Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
@ -860,681 +844,3 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
} }
---- ----
==== ====
[[servlet-saml2login-sp-initiated-factory]]
== Producing ``<saml2:AuthnRequest>``s
As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party.
Spring Security achieves this in part by registering the `Saml2WebSsoAuthenticationRequestFilter` in the filter chain.
This filter by default responds to endpoint `+/saml2/authenticate/{registrationId}+`.
For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to:
`https://rp.example.org/saml2/authenticate/ping`
and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
[[servlet-saml2login-store-authn-request]]
=== Changing How the `<saml2:AuthnRequest>` Gets Stored
`Saml2WebSsoAuthenticationRequestFilter` uses an `Saml2AuthenticationRequestRepository` to persist an `AbstractSaml2AuthenticationRequest` instance before <<servlet-saml2login-sp-initiated-factory,sending the `<saml2:AuthnRequest>`>> to the asserting party.
Additionally, `Saml2WebSsoAuthenticationFilter` and `Saml2AuthenticationTokenConverter` use an `Saml2AuthenticationRequestRepository` to load any `AbstractSaml2AuthenticationRequest` as part of <<servlet-saml2login-authenticate-responses,authenticating the `<saml2:Response>`>>.
By default, Spring Security uses an `HttpSessionSaml2AuthenticationRequestRepository`, which stores the `AbstractSaml2AuthenticationRequest` in the `HttpSession`.
If you have a custom implementation of `Saml2AuthenticationRequestRepository`, you may configure it by exposing it as a `@Bean` as shown in the following example:
====
.Java
[source,java,role="primary"]
----
@Bean
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() {
return new CustomSaml2AuthenticationRequestRepository();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
return CustomSaml2AuthenticationRequestRepository()
}
----
====
[[servlet-saml2login-sp-initiated-factory-signing]]
=== Changing How the `<saml2:AuthnRequest>` Gets Sent
By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party.
Many asserting parties don't require a signed `<saml2:AuthnRequest>`.
This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
.Not Requiring Signed AuthnRequests
====
.Boot
[source,yaml,role="primary"]
----
spring:
security:
saml2:
relyingparty:
okta:
identityprovider:
entity-id: ...
singlesignon.sign-request: false
----
.Java
[source,java,role="secondary"]
----
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails(party -> party
// ...
.wantAuthnRequestsSigned(false)
)
.build();
----
.Kotlin
[source,java,role="secondary"]
----
var relyingPartyRegistration: RelyingPartyRegistration =
RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
// ...
.wantAuthnRequestsSigned(false)
}
.build();
----
====
Otherwise, you will need to specify a private key to `RelyingPartyRegistration#signingX509Credentials` so that Spring Security can sign the `<saml2:AuthnRequest>` before sending.
[[servlet-saml2login-sp-initiated-factory-algorithm]]
By default, Spring Security will sign the `<saml2:AuthnRequest>` using `rsa-sha256`, though some asserting parties will require a different algorithm, as indicated in their metadata.
You can configure the algorithm based on the asserting party's <<servlet-saml2login-relyingpartyregistrationrepository,metadata using `RelyingPartyRegistrations`>>.
Or, you can provide it manually:
====
.Java
[source,java,role="primary"]
----
String metadataLocation = "classpath:asserting-party-metadata.xml";
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
// ...
.assertingPartyDetails((party) -> party
// ...
.signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
)
.build();
----
.Kotlin
[source,kotlin,role="secondary"]
----
var metadataLocation = "classpath:asserting-party-metadata.xml"
var relyingPartyRegistration: RelyingPartyRegistration =
RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
// ...
.signingAlgorithms { sign: MutableList<String?> ->
sign.add(
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512
)
}
}
.build();
----
====
NOTE: The snippet above uses the OpenSAML `SignatureConstants` class to supply the algorithm name.
But, that's just for convenience.
Since the datatype is `String`, you can supply the name of the algorithm directly.
[[servlet-saml2login-sp-initiated-factory-binding]]
Some asserting parties require that the `<saml2:AuthnRequest>` be POSTed.
This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
====
.Java
[source,java,role="primary"]
----
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails(party -> party
// ...
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
)
.build();
----
.Kotlin
[source,kotlin,role="secondary"]
----
var relyingPartyRegistration: RelyingPartyRegistration? =
RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
// ...
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
}
.build()
----
====
[[servlet-saml2login-sp-initiated-factory-custom-authnrequest]]
=== Customizing OpenSAML's `AuthnRequest` Instance
There are a number of reasons that you may want to adjust an `AuthnRequest`.
For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
If you don't need information from the `HttpServletRequest` to make your decision, then the easiest way is to <<servlet-saml2login-opensaml-customization,register a custom `AuthnRequestMarshaller` with OpenSAML>>.
This will give you access to post-process the `AuthnRequest` instance before it's serialized.
But, if you do need something from the request, then you can use create a custom `Saml2AuthenticationRequestContext` implementation and then a `Converter<Saml2AuthenticationRequestContext, AuthnRequest>` to build an `AuthnRequest` yourself, like so:
====
.Java
[source,java,role="primary"]
----
@Component
public class AuthnRequestConverter implements
Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
private final AuthnRequestBuilder authnRequestBuilder;
private final IssuerBuilder issuerBuilder;
// ... constructor
public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context;
Issuer issuer = issuerBuilder.buildObject();
issuer.setValue(myContext.getIssuer());
AuthnRequest authnRequest = authnRequestBuilder.buildObject();
authnRequest.setIssuer(issuer);
authnRequest.setDestination(myContext.getDestination());
authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl());
// ... additional settings
authRequest.setForceAuthn(myContext.getForceAuthn());
return authnRequest;
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Component
class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
private val authnRequestBuilder: AuthnRequestBuilder? = null
private val issuerBuilder: IssuerBuilder? = null
// ... constructor
override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
val myContext: MySaml2AuthenticationRequestContext = context
val issuer: Issuer = issuerBuilder.buildObject()
issuer.value = myContext.getIssuer()
val authnRequest: AuthnRequest = authnRequestBuilder.buildObject()
authnRequest.issuer = issuer
authnRequest.destination = myContext.getDestination()
authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl()
// ... additional settings
authRequest.setForceAuthn(myContext.getForceAuthn())
return authnRequest
}
}
----
====
Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as ``@Bean``s:
====
.Java
[source,java,role="primary"]
----
@Bean
Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() {
Saml2AuthenticationRequestContextResolver resolver =
new DefaultSaml2AuthenticationRequestContextResolver();
return request -> {
Saml2AuthenticationRequestContext context = resolver.resolve(request);
return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null);
};
}
@Bean
Saml2AuthenticationRequestFactory authenticationRequestFactory(
AuthnRequestConverter authnRequestConverter) {
OpenSaml4AuthenticationRequestFactory authenticationRequestFactory =
new OpenSaml4AuthenticationRequestFactory();
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
return authenticationRequestFactory;
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver {
val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver()
return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest ->
val context = resolver.resolve(request)
MySaml2AuthenticationRequestContext(
context,
request.getParameter("force") != null
)
}
}
@Bean
open fun authenticationRequestFactory(
authnRequestConverter: AuthnRequestConverter?
): Saml2AuthenticationRequestFactory? {
val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory()
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter)
return authenticationRequestFactory
}
----
====
[[servlet-saml2login-authenticate-responses]]
== Authenticating ``<saml2:Response>``s
To verify SAML 2.0 Responses, Spring Security uses <<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 <<servlet-saml2login-rpr-credentials,`Saml2X509Credential` instances>> registered in the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>.
`OpenSaml4AuthenticationProvider` exposes <<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.

View File

@ -52,7 +52,7 @@ SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository re
return http.build(); return http.build();
} }
---- ----
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to xref:servlet/saml2/login.adoc#servlet-saml2login-rpr-duplicated[multiple instances] <1> - First, add your signing key to the `RelyingPartyRegistration` instance or to xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-duplicated[multiple instances]
<2> - Second, indicate that your application wants to use SAML SLO to logout the end user <2> - Second, indicate that your application wants to use SAML SLO to logout the end user
=== Runtime Expectations === Runtime Expectations
@ -61,8 +61,8 @@ Given the above configuration any logged in user can send a `POST /logout` to yo
Your application will then do the following: Your application will then do the following:
1. Logout the user and invalidate the session 1. Logout the user and invalidate the session
2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `<saml2:LogoutRequest>` based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the currently logged-in user. 2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `<saml2:LogoutRequest>` based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the currently logged-in user.
3. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] 3. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`]
4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party 4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
5. Redirect to any configured successful logout endpoint 5. Redirect to any configured successful logout endpoint
@ -70,8 +70,8 @@ Also, your application can participate in an AP-initiated logout when the assert
1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party 1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
2. Logout the user and invalidate the session 2. Logout the user and invalidate the session
3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the just logged-out user 3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the just logged-out user
4. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] 4. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`]
== Configuring Logout Endpoints == Configuring Logout Endpoints