Skip to content

Commit aedabf5

Browse files
committed
Merge branch '6.0.x'
2 parents c506303 + ddad623 commit aedabf5

File tree

3 files changed

+319
-57
lines changed

3 files changed

+319
-57
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 & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ image::{figures}/saml2webssoauthenticationfilter.png[]
4141
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
4242
====
4343

44+
[[servlet-saml2login-authentication-saml2authenticationtokenconverter]]
4445
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`].
4546
This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
4647
This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
@@ -662,6 +663,16 @@ In a deployed application, that translates to:
662663

663664
`+https://rp.example.com/adfs+`
664665

666+
The prevailing URI patterns are as follows:
667+
668+
* `+/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
669+
* `+/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`
670+
* `+/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
671+
* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration`
672+
673+
Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios.
674+
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`.
675+
665676
[[servlet-saml2login-rpr-credentials]]
666677
=== Credentials
667678

@@ -736,58 +747,6 @@ resource.inputStream.use {
736747
When you specify the locations of these files as the appropriate Spring Boot properties, Spring Boot performs these conversions for you.
737748
====
738749

739-
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
740-
=== Resolving the Relying Party from the Request
741-
742-
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration ID in the URI path.
743-
744-
You may want to customize for a number of reasons, including:
745-
746-
* You may know that your application is never going to be a multi-tenant application and, as a result, want a simpler URL scheme.
747-
* You may identify tenants in a way other than by the URI path.
748-
749-
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
750-
The default looks up the registration ID from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
751-
752-
You can provide a simpler resolver that, for example, always returns the same relying party:
753-
754-
====
755-
.Java
756-
[source,java,role="primary"]
757-
----
758-
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
759-
760-
private final RelyingPartyRegistrationResolver delegate;
761-
762-
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
763-
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
764-
}
765-
766-
@Override
767-
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
768-
return this.delegate.resolve(request, "single");
769-
}
770-
}
771-
----
772-
773-
.Kotlin
774-
[source,kotlin,role="secondary"]
775-
----
776-
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
777-
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
778-
return this.delegate.resolve(request, "single")
779-
}
780-
}
781-
----
782-
====
783-
784-
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>` instances], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate `<saml2:Response>` instances], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
785-
786-
[NOTE]
787-
====
788-
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
789-
====
790-
791750
[[servlet-saml2login-rpr-duplicated]]
792751
=== Duplicated Relying Party Configurations
793752

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