parent
6d2d3b9a69
commit
11aa02c6fb
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
----
|
||||||
|
====
|
||||||
|
|
|
@ -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.
|
|
@ -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].
|
||||||
|
====
|
|
@ -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.
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue