Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.springframework.core.io.ResourceLoader;
import org.springframework.security.converter.RsaKeyConverters;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
Expand Down Expand Up @@ -153,7 +154,7 @@ private static Map<String, Map<String, Object>> getAssertingParties(Element elem
}

private static void addVerificationCredentials(Map<String, Object> assertingParty,
RelyingPartyRegistration.AssertingPartyDetails.Builder builder) {
AssertingPartyMetadata.Builder<?> builder) {
List<String> verificationCertificateLocations = (List<String>) assertingParty.get(ELT_VERIFICATION_CREDENTIAL);
List<Saml2X509Credential> verificationCredentials = new ArrayList<>();
for (String certificateLocation : verificationCertificateLocations) {
Expand All @@ -163,7 +164,7 @@ private static void addVerificationCredentials(Map<String, Object> assertingPart
}

private static void addEncryptionCredentials(Map<String, Object> assertingParty,
RelyingPartyRegistration.AssertingPartyDetails.Builder builder) {
AssertingPartyMetadata.Builder<?> builder) {
List<String> encryptionCertificateLocations = (List<String>) assertingParty.get(ELT_ENCRYPTION_CREDENTIAL);
List<Saml2X509Credential> encryptionCredentials = new ArrayList<>();
for (String certificateLocation : encryptionCertificateLocations) {
Expand Down Expand Up @@ -220,8 +221,8 @@ private static RelyingPartyRegistration.Builder getBuilderFromMetadataLocationIf
}
else {
builder = RelyingPartyRegistration.withRegistrationId(registrationId)
.assertingPartyDetails((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties,
apBuilder, parserContext));
.assertingPartyMetadata((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt,
assertingParties, apBuilder, parserContext));
}
addRemainingProperties(relyingPartyRegistrationElt, builder);
return builder;
Expand Down Expand Up @@ -260,7 +261,7 @@ private static void addRemainingProperties(Element relyingPartyRegistrationElt,
}

private static void buildAssertingParty(Element relyingPartyElt, Map<String, Map<String, Object>> assertingParties,
RelyingPartyRegistration.AssertingPartyDetails.Builder builder, ParserContext parserContext) {
AssertingPartyMetadata.Builder<?> builder, ParserContext parserContext) {
String assertingPartyId = relyingPartyElt.getAttribute(ATT_ASSERTING_PARTY_ID);
if (!assertingParties.containsKey(assertingPartyId)) {
Object source = parserContext.extractSource(relyingPartyElt);
Expand Down Expand Up @@ -293,7 +294,7 @@ private static void buildAssertingParty(Element relyingPartyElt, Map<String, Map
}

private static void addSigningAlgorithms(Map<String, Object> assertingParty,
RelyingPartyRegistration.AssertingPartyDetails.Builder builder) {
AssertingPartyMetadata.Builder<?> builder) {
String signingAlgorithmsAttr = getAsString(assertingParty, ATT_SIGNING_ALGORITHMS);
if (StringUtils.hasText(signingAlgorithmsAttr)) {
List<String> signingAlgorithms = Arrays.asList(signingAlgorithmsAttr.split(","));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Java::
----
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails(party -> party
.assertingPartyMetadata(party -> party
// ...
.wantAuthnRequestsSigned(false)
)
Expand All @@ -128,7 +128,7 @@ Kotlin::
var relyingPartyRegistration: RelyingPartyRegistration =
RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
.assertingPartyMetadata { party: AssertingPartyMetadata.Builder -> party
// ...
.wantAuthnRequestsSigned(false)
}
Expand All @@ -154,7 +154,7 @@ Java::
String metadataLocation = "classpath:asserting-party-metadata.xml";
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
// ...
.assertingPartyDetails((party) -> party
.assertingPartyMetadata((party) -> party
// ...
.signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
)
Expand All @@ -169,7 +169,7 @@ var metadataLocation = "classpath:asserting-party-metadata.xml"
var relyingPartyRegistration: RelyingPartyRegistration =
RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
.assertingPartyMetadata { party: AssertingPartyMetadata.Builder -> party
// ...
.signingAlgorithms { sign: MutableList<String?> ->
sign.add(
Expand Down Expand Up @@ -197,7 +197,7 @@ Java::
----
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails(party -> party
.assertingPartyMetadata(party -> party
// ...
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
)
Expand All @@ -211,7 +211,7 @@ Kotlin::
var relyingPartyRegistration: RelyingPartyRegistration? =
RelyingPartyRegistration.withRegistrationId("okta")
// ...
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
.assertingPartyMetadata { party: AssertingPartyMetadata.Builder -> party
// ...
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
}
Expand Down
10 changes: 5 additions & 5 deletions docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exc
Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
RelyingPartyRegistration registration = RelyingPartyRegistration
.withRegistrationId("example")
.assertingPartyDetails(party -> party
.assertingPartyMetadata(party -> party
.entityId("https://idp.example.com/issuer")
.singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
.wantAuthnRequestsSigned(false)
Expand All @@ -508,7 +508,7 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
val registration = RelyingPartyRegistration
.withRegistrationId("example")
.assertingPartyDetails { party: AssertingPartyDetails.Builder ->
.assertingPartyMetadata { party: AssertingPartyMetadata.Builder ->
party
.entityId("https://idp.example.com/issuer")
.singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
Expand Down Expand Up @@ -699,7 +699,7 @@ RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.wit
.entityId("{baseUrl}/{registrationId}")
.decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
.assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
.assertingPartyDetails(party -> party
.assertingPartyMetadata(party -> party
.entityId("https://ap.example.org")
.verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
.singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
Expand All @@ -718,7 +718,7 @@ val relyingPartyRegistration =
c.add(relyingPartyDecryptingCredential())
}
.assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
.assertingPartyDetails { party -> party
.assertingPartyMetadata { party -> party
.entityId("https://ap.example.org")
.verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
.singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
Expand All @@ -730,7 +730,7 @@ val relyingPartyRegistration =
[TIP]
====
The top-level metadata methods are details about the relying party.
The methods inside `assertingPartyDetails` are details about the asserting party.
The methods inside `AssertingPartyMetadata` are details about the asserting party.
====

[NOTE]
Expand Down
4 changes: 2 additions & 2 deletions docs/modules/ROOT/pages/servlet/saml2/logout.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ It's common to need to set other values in the `<saml2:LogoutRequest>` than the

By default, Spring Security will issue a `<saml2:LogoutRequest>` and supply:

* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation`
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation`
* The `ID` attribute - a GUID
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
* The `<NameID>` element - from `Authentication#getName`
Expand Down Expand Up @@ -424,7 +424,7 @@ It's common to need to set other values in the `<saml2:LogoutResponse>` than the

By default, Spring Security will issue a `<saml2:LogoutResponse>` and supply:

* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation`
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation`
* The `ID` attribute - a GUID
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
* The `<Status>` element - `SUCCESS`
Expand Down
128 changes: 123 additions & 5 deletions docs/modules/ROOT/pages/servlet/saml2/metadata.adoc
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[[servlet-saml2login-metadata]]
= Saml 2.0 Metadata

Spring Security can <<parsing-asserting-party-metadata,parse asserting party metadata>> to produce an `AssertingPartyDetails` instance as well as <<publishing-relying-party-metadata,publish relying party metadata>> from a `RelyingPartyRegistration` instance.
Spring Security can <<parsing-asserting-party-metadata,parse asserting party metadata>> to produce an `AssertingPartyMetadata` instance as well as <<publishing-relying-party-metadata,publish relying party metadata>> from a `RelyingPartyRegistration` instance.

[[parsing-asserting-party-metadata]]
== Parsing `<saml2:IDPSSODescriptor>` metadata

You can parse an asserting party's metadata xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistrationrepository[using `RelyingPartyRegistrations`].

When using the OpenSAML vendor support, the resulting `AssertingPartyDetails` will be of type `OpenSamlAssertingPartyDetails`.
When using the OpenSAML vendor support, the resulting `AssertingPartyMetadata` will be of type `OpenSamlAssertingPartyDetails`.
This means you'll be able to do get the underlying OpenSAML XMLObject by doing the following:

[tabs]
Expand All @@ -18,7 +18,7 @@ Java::
[source,java,role="primary"]
----
OpenSamlAssertingPartyDetails details = (OpenSamlAssertingPartyDetails)
registration.getAssertingPartyDetails();
registration.getAssertingPartyMetadata();
EntityDescriptor openSamlEntityDescriptor = details.getEntityDescriptor();
----

Expand All @@ -27,11 +27,129 @@ Kotlin::
[source,kotlin,role="secondary"]
----
val details: OpenSamlAssertingPartyDetails =
registration.getAssertingPartyDetails() as OpenSamlAssertingPartyDetails;
val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor();
registration.getAssertingPartyMetadata() as OpenSamlAssertingPartyDetails
val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor()
----
======

=== Using `AssertingPartyMetadataRepository`

You can also be more targeted than `RelyingPartyRegistrations` by using `AssertingPartyMetadataRepository`, an interface that allows for only retrieving the asserting party metadata.

This allows three valuable features:

* Implementations can refresh asserting party metadata in an expiry-aware fashion
* Implementations of `RelyingPartyRegistrationRepository` can more easily articulate a relationship between a relying party and its one or many corresponding asserting parties
* Implementations can verify metadata signatures

For example, `OpenSamlAssertingPartyMetadataRepository` uses OpenSAML's `MetadataResolver`, and API whose implementations regularly refresh the underlying metadata in an expiry-aware fashion.

This means that you can now create a refreshable `RelyingPartyRegistrationRepository` in just a few lines of code:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Component
public class RefreshableRelyingPartyRegistrationRepository
implements IterableRelyingPartyRegistrationRepository {

private final AssertingPartyMetadataRepository metadata =
OpenSamlAssertingPartyMetadataRepository
.fromTrustedMetadataLocation("https://idp.example.org/metadata").build();

@Override
public RelyingPartyRegistration findByRegistrationId(String registrationId) {
AssertingPartyMetadata metadata = this.metadata.findByEntityId(registrationId);
if (metadata == null) {
return null;
}
return applyRelyingParty(metadata);
}

@Override
public Iterator<RelyingPartyRegistration> iterator() {
return StreamSupport.stream(this.metadata.spliterator(), false)
.map(this::applyRelyingParty).iterator();
}

private RelyingPartyRegistration applyRelyingParty(AssertingPartyMetadata metadata) {
return RelyingPartyRegistration.withAssertingPartyMetadata(metadata)
// apply any relying party configuration
.build();
}

}
----

Kotlin::
+
[source,kotlin,role="secondary"]
----
@Component
class RefreshableRelyingPartyRegistrationRepository : IterableRelyingPartyRegistrationRepository {

private val metadata: AssertingPartyMetadataRepository =
OpenSamlAssertingPartyMetadataRepository.fromTrustedMetadataLocation(
"https://idp.example.org/metadata").build()

fun findByRegistrationId(registrationId:String?): RelyingPartyRegistration {
val metadata = this.metadata.findByEntityId(registrationId)
if (metadata == null) {
return null
}
return applyRelyingParty(metadata)
}

fun iterator(): Iterator<RelyingPartyRegistration> {
return StreamSupport.stream(this.metadata.spliterator(), false)
.map(this::applyRelyingParty).iterator()
}

private fun applyRelyingParty(metadata: AssertingPartyMetadata): RelyingPartyRegistration {
val details: AssertingPartyMetadata = metadata as AssertingPartyMetadata
return RelyingPartyRegistration.withAssertingPartyMetadata(details)
// apply any relying party configuration
.build()
}
}
----
======

[TIP]
`OpenSamlAssertingPartyMetadataRepository` also ships with a constructor so you can provide a custom `MetadataResolver`. Since the underlying `MetadataResolver` is doing the expirying and refreshing, if you use the constructor directly, you will only get these features by providing an implementation that does so.

=== Verifying Metadata Signatures

You can also verify metadata signatures using `OpenSamlAssertingPartyMetadataRepository` by providing the appropriate set of ``Saml2X509Credential``s as follows:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
OpenSamlAssertingPartyMetadataRepository.withMetadataLocation("https://idp.example.org/metadata")
.verificationCredentials((c) -> c.add(myVerificationCredential))
.build();
----

Kotlin::
+
[source,kotlin,role="secondary"]
----
OpenSamlAssertingPartyMetadataRepository.withMetadataLocation("https://idp.example.org/metadata")
.verificationCredentials({ c : Collection<Saml2X509Credential> ->
c.add(myVerificationCredential) })
.build()
----
======

[NOTE]
If no credentials are provided, the component will not perform signature validation.

[[publishing-relying-party-metadata]]
== Producing `<saml2:SPSSODescriptor>` Metadata

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ public static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefau
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, message));
}
String assertingPartyEntityId = token.getRelyingPartyRegistration()
.getAssertingPartyDetails()
.getAssertingPartyMetadata()
.getEntityId();
if (!StringUtils.hasText(issuer) || !issuer.equals(assertingPartyEntityId)) {
String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID());
Expand Down Expand Up @@ -775,7 +775,7 @@ private static ValidationContext createValidationContext(AssertionToken assertio
RelyingPartyRegistration relyingPartyRegistration = token.getRelyingPartyRegistration();
String audience = relyingPartyRegistration.getEntityId();
String recipient = relyingPartyRegistration.getAssertionConsumerServiceLocation();
String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyDetails().getEntityId();
String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyMetadata().getEntityId();
Map<String, Object> params = new HashMap<>();
Assertion assertion = assertionToken.getAssertion();
if (assertionContainsInResponseTo(assertion)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ static QueryParametersPartial sign(RelyingPartyRegistration registration) {
private static SignatureSigningParameters resolveSigningParameters(
RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms();
List<String> algorithms = relyingPartyRegistration.getAssertingPartyMetadata().getSigningAlgorithms();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,12 @@ static VerifierPartial verifySignature(RequestAbstractType object, RelyingPartyR

static SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) {
Set<Credential> credentials = new HashSet<>();
Collection<Saml2X509Credential> keys = registration.getAssertingPartyDetails().getVerificationX509Credentials();
Collection<Saml2X509Credential> keys = registration.getAssertingPartyMetadata()
.getVerificationX509Credentials();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(registration.getAssertingPartyDetails().getEntityId());
cred.setEntityId(registration.getAssertingPartyMetadata().getEntityId());
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public Saml2MessageBinding getBinding() {
* @since 5.7
*/
public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) {
String location = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation();
String location = registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation();
return new Builder(registration).authenticationRequestUri(location);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public Saml2MessageBinding getBinding() {
* @since 5.7
*/
public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) {
String location = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation();
String location = registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation();
return new Builder(registration).authenticationRequestUri(location);
}

Expand Down
Loading