Skip to content

Commit 1c885cf

Browse files
committed
Document Federation Usecase
Closes gh-12764
1 parent 1c3ce1e commit 1c885cf

File tree

3 files changed

+309
-55
lines changed

3 files changed

+309
-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: 182 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`.
@@ -712,56 +713,6 @@ resource.inputStream.use {
712713
[TIP]
713714
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
714715

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-
765716
[[servlet-saml2login-rpr-duplicated]]
766717
=== Duplicated Relying Party Configurations
767718

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

docs/modules/ROOT/pages/servlet/saml2/metadata.adoc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,45 @@ filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
103103
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
104104
----
105105
====
106+
107+
== Changing the Way a `RelyingPartyRegistration` Is Looked Up
108+
109+
To apply a custom `RelyingPartyRegistrationResolver` to the metadata endpoint, you can provide it directly in the filter constructor like so:
110+
111+
====
112+
.Java
113+
[source,java,role="primary"]
114+
----
115+
RelyingPartyRegistrationResolver myRegistrationResolver = ...;
116+
Saml2MetadataFilter metadata = new Saml2MetadataFilter(myRegistrationResolver, new OpenSamlMetadataResolver());
117+
118+
// ...
119+
120+
http.addFilterBefore(metadata, BasicAuthenticationFilter.class);
121+
----
122+
123+
.Kotlin
124+
----
125+
val myRegistrationResolver: RelyingPartyRegistrationResolver = ...;
126+
val metadata = new Saml2MetadataFilter(myRegistrationResolver, OpenSamlMetadataResolver());
127+
128+
// ...
129+
130+
http.addFilterBefore(metadata, BasicAuthenticationFilter::class.java);
131+
----
132+
====
133+
134+
In the event that you are applying a `RelyingPartyRegistrationResolver` to remove the `registrationId` from the URI, you must also change the URI in the filter like so:
135+
136+
====
137+
.Java
138+
[source,java,role="primary"]
139+
----
140+
metadata.setRequestMatcher("/saml2/metadata")
141+
----
142+
143+
.Kotlin
144+
----
145+
metadata.setRequestMatcher("/saml2/metadata")
146+
----
147+
====

0 commit comments

Comments
 (0)