Skip to content

Commit 383e0c2

Browse files
committed
Merge branch '5.7.x' into 5.8.x
2 parents 97ba596 + 0421e25 commit 383e0c2

File tree

3 files changed

+319
-55
lines changed

3 files changed

+319
-55
lines changed

docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,97 @@
11
[[servlet-saml2login-authenticate-responses]]
22
= Authenticating ``<saml2:Response>``s
33

4-
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] by default.
4+
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-authentication-saml2authenticationtokenconverter[`Saml2AuthenticationTokenConverter`] to populate the `Authentication` request and xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] to authenticate it.
55

66
You can configure this in a number of ways including:
77

8-
1. Setting a clock skew to timestamp validation
9-
2. Mapping the response to a list of `GrantedAuthority` instances
10-
3. Customizing the strategy for validating assertions
11-
4. Customizing the strategy for decrypting response and assertion elements
8+
1. Changing the way the `RelyingPartyRegistration` is Looked Up
9+
2. Setting a clock skew to timestamp validation
10+
3. Mapping the response to a list of `GrantedAuthority` instances
11+
4. Customizing the strategy for validating assertions
12+
5. Customizing the strategy for decrypting response and assertion elements
1213

1314
To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
1415

16+
[[relyingpartyregistrationresolver-apply]]
17+
== Changing `RelyingPartyRegistration` Lookup
18+
19+
`RelyingPartyRegistration` lookup is customized xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[in a `RelyingPartyRegistrationResolver`].
20+
21+
To apply a `RelyingPartyRegistrationResolver` when processing `<saml2:Response>` payloads, you should first publish a `Saml2AuthenticationTokenConverter` bean like so:
22+
23+
====
24+
.Java
25+
[source,java,role="primary"]
26+
----
27+
@Bean
28+
Saml2AuthenticationTokenConverter authenticationConverter(InMemoryRelyingPartyRegistrationRepository registrations) {
29+
return new Saml2AuthenticationTokenConverter(new MyRelyingPartyRegistrationResolver(registrations));
30+
}
31+
----
32+
33+
.Kotlin
34+
[source,kotlin,role="secondary"]
35+
----
36+
@Bean
37+
fun authenticationConverter(val registrations: InMemoryRelyingPartyRegistrationRepository): Saml2AuthenticationTokenConverter {
38+
return Saml2AuthenticationTokenConverter(MyRelyingPartyRegistrationResolver(registrations));
39+
}
40+
----
41+
====
42+
43+
Recall that the Assertion Consumer Service URL is `+/saml2/login/sso/{registrationId}+` by default.
44+
If you are no longer wanting the `registrationId` in the URL, change it in the filter chain and in your relying party metadata:
45+
46+
====
47+
.Java
48+
[source,java,role="primary"]
49+
----
50+
@Bean
51+
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
52+
http
53+
// ...
54+
.saml2Login((saml2) -> saml2.filterProcessingUrl("/saml2/login/sso"))
55+
// ...
56+
57+
return http.build();
58+
}
59+
----
60+
61+
.Kotlin
62+
[source,kotlin,role="secondary"]
63+
----
64+
@Bean
65+
fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
66+
http {
67+
// ...
68+
.saml2Login {
69+
filterProcessingUrl = "/saml2/login/sso"
70+
}
71+
// ...
72+
}
73+
74+
return http.build()
75+
}
76+
----
77+
====
78+
79+
and:
80+
81+
====
82+
.Java
83+
[source,java,role="primary"]
84+
----
85+
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
86+
----
87+
88+
.Kotlin
89+
[source,kotlin,role="secondary"]
90+
----
91+
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
92+
----
93+
====
94+
1595
[[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
1696
== Setting a Clock Skew
1797

docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc

Lines changed: 192 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ image::{figures}/saml2webssoauthenticationfilter.png[]
3333

3434
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
3535

36+
[[servlet-saml2login-authentication-saml2authenticationtokenconverter]]
3637
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`].
3738
This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
3839
This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
@@ -640,6 +641,16 @@ which in a deployed application would translate to
640641

641642
`+https://rp.example.com/adfs+`
642643

644+
The prevailing URI patterns are as follows:
645+
646+
* `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a `<saml2:AuthnRequest>`] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party
647+
* `+/saml2/login/sso/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's `<saml2:Response>`] based on the configurations for that `RelyingPartyRegistration`
648+
* `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `<saml2:LogoutRequest>` and `<saml2:LogoutResponse>` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state
649+
* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration`
650+
651+
Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios.
652+
If you wish to remove the `registrationId` from the URL for any reason, you can <<servlet-saml2login-rpr-relyingpartyregistrationresolver,specify a `RelyingPartyRegistrationResolver`>> to tell Spring Security how to look up the `registrationId`.
653+
643654
[[servlet-saml2login-rpr-credentials]]
644655
=== Credentials
645656

@@ -712,56 +723,6 @@ resource.inputStream.use {
712723
[TIP]
713724
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
714725

715-
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
716-
=== Resolving the Relying Party from the Request
717-
718-
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
719-
720-
There are a number of reasons you may want to customize. Among them:
721-
722-
* You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme
723-
* You may identify tenants in a way other than by the URI path
724-
725-
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
726-
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
727-
728-
You can provide a simpler resolver that, for example, always returns the same relying party:
729-
730-
====
731-
.Java
732-
[source,java,role="primary"]
733-
----
734-
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
735-
736-
private final RelyingPartyRegistrationResolver delegate;
737-
738-
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
739-
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
740-
}
741-
742-
@Override
743-
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
744-
return this.delegate.resolve(request, "single");
745-
}
746-
}
747-
----
748-
749-
.Kotlin
750-
[source,kotlin,role="secondary"]
751-
----
752-
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
753-
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
754-
return this.delegate.resolve(request, "single")
755-
}
756-
}
757-
----
758-
====
759-
760-
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].
761-
762-
[NOTE]
763-
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
764-
765726
[[servlet-saml2login-rpr-duplicated]]
766727
=== Duplicated Relying Party Configurations
767728

@@ -856,3 +817,184 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
856817
}
857818
----
858819
====
820+
821+
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
822+
=== Resolving the `RelyingPartyRegistration` from the Request
823+
824+
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
825+
826+
There are a number of reasons you may want to customize that. Among them:
827+
828+
* You may already <<relyingpartyregistrationresolver-single, know which `RelyingPartyRegistration` you need>>
829+
* You may be <<relyingpartyregistrationresolver-entityid, federating many asserting parties>>
830+
831+
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
832+
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
833+
834+
[NOTE]
835+
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
836+
837+
[[relyingpartyregistrationresolver-single]]
838+
==== Resolving to a Single Consistent `RelyingPartyRegistration`
839+
840+
You can provide a resolver that, for example, always returns the same `RelyingPartyRegistration`:
841+
842+
====
843+
.Java
844+
[source,java,role="primary"]
845+
----
846+
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
847+
848+
private final RelyingPartyRegistrationResolver delegate;
849+
850+
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
851+
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
852+
}
853+
854+
@Override
855+
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
856+
return this.delegate.resolve(request, "single");
857+
}
858+
}
859+
----
860+
861+
.Kotlin
862+
[source,kotlin,role="secondary"]
863+
----
864+
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
865+
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
866+
return this.delegate.resolve(request, "single")
867+
}
868+
}
869+
----
870+
====
871+
872+
[TIP]
873+
You might next take a look at how to use this resolver to customize xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[`<saml2:SPSSODescriptor>` metadata production].
874+
875+
[[relyingpartyregistrationresolver-entityid]]
876+
==== Resolving Based on the `<saml2:Response#Issuer>`
877+
878+
When you have one relying party that can accept assertions from multiple asserting parties, you will have as many ``RelyingPartyRegistration``s as asserting parties, with the <<servlet-saml2login-rpr-duplicated, relying party information duplicated across each instance>>.
879+
880+
This carries the implication that the assertion consumer service endpoint will be different for each asserting party, which may not be desirable.
881+
882+
You can instead resolve the `registrationId` via the `Issuer`.
883+
A custom implementation of `RelyingPartyRegistrationResolver` that does this may look like:
884+
885+
====
886+
.Java
887+
[source,java,role="primary"]
888+
----
889+
public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
890+
private final InMemoryRelyingPartyRegistrationRepository registrations;
891+
892+
// ... constructor
893+
894+
@Override
895+
RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
896+
if (registrationId != null) {
897+
return this.registrations.findByRegistrationId(registrationId);
898+
}
899+
String entityId = resolveEntityIdFromSamlResponse(request);
900+
for (RelyingPartyRegistration registration : this.registrations) {
901+
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
902+
return registration;
903+
}
904+
}
905+
return null;
906+
}
907+
908+
private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
909+
// ...
910+
}
911+
}
912+
----
913+
914+
.Kotlin
915+
[source,kotlin,role="secondary"]
916+
----
917+
class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
918+
RelyingPartyRegistrationResolver {
919+
@Override
920+
fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
921+
if (registrationId != null) {
922+
return this.registrations.findByRegistrationId(registrationId)
923+
}
924+
String entityId = resolveEntityIdFromSamlResponse(request)
925+
for (val registration : this.registrations) {
926+
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
927+
return registration
928+
}
929+
}
930+
return null
931+
}
932+
933+
private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
934+
// ...
935+
}
936+
}
937+
----
938+
====
939+
940+
[TIP]
941+
You might next take a look at how to use this resolver to customize xref:servlet/saml2/login/authentication.adoc#relyingpartyregistrationresolver-apply[`<saml2:Response>` authentication].
942+
943+
[[federating-saml2-login]]
944+
=== Federating Login
945+
946+
One common arrangement with SAML 2.0 is an identity provider that has multiple asserting parties.
947+
In this case, the identity provider's metadata endpoint returns multiple `<md:IDPSSODescriptor>` elements.
948+
949+
These multiple asserting parties can be accessed in a single call to `RelyingPartyRegistrations` like so:
950+
951+
====
952+
.Java
953+
[source,java,role="primary"]
954+
----
955+
Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
956+
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
957+
.stream().map((builder) -> builder
958+
.registrationId(UUID.randomUUID().toString())
959+
.entityId("https://example.org/saml2/sp")
960+
.build()
961+
)
962+
.collect(Collectors.toList()));
963+
----
964+
965+
.Kotlin
966+
[source,java,role="secondary"]
967+
----
968+
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
969+
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
970+
.stream().map { builder : RelyingPartyRegistration.Builder -> builder
971+
.registrationId(UUID.randomUUID().toString())
972+
.entityId("https://example.org/saml2/sp")
973+
.build()
974+
}
975+
.collect(Collectors.toList()));
976+
----
977+
====
978+
979+
Note that because the registration id is set to a random value, this will change certain SAML 2.0 endpoints to be unpredictable.
980+
There are several ways to address this; let's focus on a way that suits the specific use case of federation.
981+
982+
In many federation cases, all the asserting parties share service provider configuration.
983+
Given that Spring Security will by default include the `registrationId` in all many of its SAML 2.0 URIs, the next step is often to change these URIs to exclude the `registrationId`.
984+
985+
There are two main URIs you will want to change along those lines:
986+
987+
* <<relyingpartyregistrationresolver-entityid,Resolve by `<saml2:Response#Issuer>`>>
988+
* <<relyingpartyregistrationresolver-single,Resolve with a default `RelyingPartyRegistration`>>
989+
990+
[NOTE]
991+
Optionally, you may also want to change the Authentication Request location, but since this is a URI internal to the app and not published to asserting parties, the benefit is often minimal.
992+
993+
You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
994+
995+
[[using-spring-security-saml-extension-uris]]
996+
=== Using Spring Security SAML Extension URIs
997+
998+
In the event that you are migrating from the Spring Security SAML Extension, there may be some benefit to configuring your application to use the SAML Extension URI defaults.
999+
1000+
For more information on this, please see {gh-samples-url}/servlet/spring-boot/java/saml2/custom-urls[our `custom-urls` sample] and {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].

0 commit comments

Comments
 (0)