= SAML Migrations The following steps relate to changes around how to configure SAML 2.0. == Use OpenSAML 4 OpenSAML 3 has reached its end-of-life. As such, Spring Security 6 drops support for it, bumping up its OpenSAML baseline to 4. To prepare for the upgrade, update your pom to depend on OpenSAML 4 instead of 3: ==== .Maven [source,maven,role="primary"] ---- org.opensaml opensaml-core 4.2.1 org.opensaml opensaml-saml-api 4.2.1 org.opensaml opensaml-saml-impl 4.2.1 ---- .Gradle [source,gradle,role="secondary"] ---- dependencies { constraints { api "org.opensaml:opensaml-core:4.2.1" api "org.opensaml:opensaml-saml-api:4.2.1" api "org.opensaml:opensaml-saml-impl:4.2.1" } } ---- ==== You must use at least OpenSAML 4.1.1 to update to Spring Security 6's SAML support. == Use `OpenSaml4AuthenticationProvider` In order to support both OpenSAML 3 and 4 at the same time, Spring Security released `OpenSamlAuthenticationProvider` and `OpenSaml4AuthenticationProvider`. In 6.0, because OpenSAML3 support is removed, `OpenSamlAuthenticationProvider` is removed as well. Not all methods in `OpenSamlAuthenticationProvider` were ported 1-to-1 to `OpenSaml4AuthenticationProvider`. As such, some adjustment will be required to make the challenge. Consider the following representative usage of `OpenSamlAuthenticationProvider`: ==== .Java [source,java,role="primary"] ---- OpenSamlAuthenticationProvider versionThree = new OpenSamlAuthenticationProvider(); versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor); versionThree.setResponseTimeValidationSkew(myDuration); ---- .Kotlin [source,kotlin,role="secondary"] ---- val versionThree: OpenSamlAuthenticationProvider = OpenSamlAuthenticationProvider() versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor) versionThree.setResponseTimeValidationSkew(myDuration) ---- ==== This should change to: ==== .Java [source,java,role="primary"] ---- Converter delegate = OpenSaml4AuthenticationProvider .createDefaultResponseAuthenticationConverter(); OpenSaml4AuthenticationProvider versionFour = new OpenSaml4AuthenticationProvider(); versionFour.setResponseAuthenticationConverter((responseToken) -> { Saml2Authentication authentication = delegate.convert(responseToken); Assertion assertion = responseToken.getResponse().getAssertions().get(0); AuthenticatedPrincipal principal = (AuthenticatedPrincipal) authentication.getPrincipal(); Collection authorities = myAuthoritiesExtractor.convert(assertion); return new Saml2Authentication(principal, authentication.getSaml2Response(), authorities); }); Converter validator = OpenSaml4AuthenticationProvider .createDefaultAssertionValidatorWithParameters((p) -> p.put(CLOCK_SKEW, myDuration)); versionFour.setAssertionValidator(validator); ---- .Kotlin [source,kotlin,role="secondary"] ---- val delegate = OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter() val versionFour = OpenSaml4AuthenticationProvider() versionFour.setResponseAuthenticationConverter({ responseToken -> { val authentication = delegate.convert(responseToken) val assertion = responseToken.getResponse().getAssertions().get(0) val principal = (AuthenticatedPrincipal) authentication.getPrincipal() val authorities = myAuthoritiesExtractor.convert(assertion) return Saml2Authentication(principal, authentication.getSaml2Response(), authorities) } }) val validator = OpenSaml4AuthenticationProvider .createDefaultAssertionValidatorWithParameters({ p -> p.put(CLOCK_SKEW, myDuration) }) versionFour.setAssertionValidator(validator) ---- ==== == Stop Using SAML 2.0 `Converter` constructors In an early release of Spring Security's SAML 2.0 support, `Saml2MetadataFilter` and `Saml2AuthenticationTokenConverter` shipped with constructors of type `Converter`. This level of abstraction made it tricky to evolve the class and so a dedicated interface `RelyingPartyRegistrationResolver` was introduced in a later release. In 6.0, the `Converter` constructors are removed. To prepare for this in 5.8, change classes that implement `Converter` to instead implement `RelyingPartyRegistrationResolver`. == Change to Using `Saml2AuthenticationRequestResolver` `Saml2AuthenticationContextResolver` and `Saml2AuthenticationRequestFactory` are removed in 6.0 as is the `Saml2WebSsoAuthenticationRequestFilter` that requires them. They are replaced by `Saml2AuthenticationRequestResolver` and a new constructor in `Saml2WebSsoAuthenticationRequestFilter`. The new interface removes an unnecessary transport object between the two classes. Most applications need do nothing; however, if you use or configure `Saml2AuthenticationRequestContextResolver` or `Saml2AuthenticationRequestFactory`, try the following steps to convert instead use `Saml2AuthenticationRequestResolver`. === Use `setAuthnRequestCustomizer` instead of `setAuthenticationRequestContextConverter` If you are calling `OpenSaml4AuthenticationReqeustFactory#setAuthenticationRequestContextConverter`, for example, like so: ==== .Java [source,java,role="primary"] ---- @Bean Saml2AuthenticationRequestFactory authenticationRequestFactory() { OpenSaml4AuthenticationRequestFactory factory = new OpenSaml4AuthenticationRequestFactory(); factory.setAuthenticationRequestContextConverter((context) -> { AuthnRequestBuilder authnRequestBuilder = ConfigurationService.get(XMLObjectProviderRegistry.class) .getBuilderFactory().getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); IssuerBuilder issuerBuilder = ConfigurationService.get(XMLObjectProviderRegistry.class) .getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); tring issuer = context.getIssuer(); String destination = context.getDestination(); String assertionConsumerServiceUrl = context.getAssertionConsumerServiceUrl(); String protocolBinding = context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding().getUrn(); AuthnRequest auth = authnRequestBuilder.buildObject(); auth.setID("ARQ" + UUID.randomUUID().toString().substring(1)); auth.setIssueInstant(Instant.now()); auth.setForceAuthn(Boolean.TRUE); auth.setIsPassive(Boolean.FALSE); auth.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); Issuer iss = issuerBuilder.buildObject(); iss.setValue(issuer); auth.setIssuer(iss); auth.setDestination(destination); auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl); }); return factory; } ---- ==== to ensure that ForceAuthn is set to `true`, you can instead do: ==== .Java [source,java,role="primary"] ---- @Bean Saml2AuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistrationResolver registrations) { OpenSaml4AuthenticationRequestResolver reaolver = new OpenSaml4AuthenticationRequestResolver(registrations); resolver.setAuthnRequestCustomizer((context) -> context.getAuthnRequest().setForceAuthn(Boolean.TRUE)); return resolver; } ---- ==== Also, since `setAuthnRequestCustomizer` has direct access to the `HttpServletRequest`, there is no need for a `Saml2AuthenticationRequestContextResolver`. Simply use `setAuthnRequestCustomizer` to read directly from `HttpServletRequest` this information you need. === Use `setAuthnRequestCustomizer` instead of `setProtocolBinding` Instead of doing: ==== .Java [source,java,role="primary"] ---- @Bean Saml2AuthenticationRequestFactory authenticationRequestFactory() { OpenSaml4AuthenticationRequestFactory factory = new OpenSaml4AuthenticationRequestFactory(); factory.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") return factory; } ---- ==== you can do: ==== .Java [source,java,role="primary"] ---- @Bean Saml2AuthenticationRequestResolver authenticationRequestResolver() { OpenSaml4AuthenticationRequestResolver reaolver = new OpenSaml4AuthenticationRequestResolver(registrations); resolver.setAuthnRequestCustomizer((context) -> context.getAuthnRequest() .setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")); return resolver; } ---- ==== [NOTE] ==== Since Spring Security only supports the `POST` binding for authentication, there is not very much value in overriding the protocol binding at this point in time. ==== == Use the latest `Saml2AuthenticationToken` constructor In an early release, `Saml2AuthenticationToken` took several individual settings as constructor parameters. This created a challenge each time a new parameter needed to be added. Since most of these settings were part of `RelyingPartyRegistration`, a new constructor was added where a `RelyingPartyRegistration` could be provided, making the constructor more stable. It also is valuable in that it more closely aligns with the design of `OAuth2LoginAuthenticationToken`. Most applications do not construct this class directly since `Saml2WebSsoAuthenticationFilter` does. However, in the event that your application constructs one, please change from: ==== .Java [source,java,role="primary"] ---- new Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(), registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials()) ---- .Kotlin [source,kotlin,role="secondary"] ---- Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(), registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials()) ---- ==== to: ==== .Java [source,java,role="primary"] ---- new Saml2AuthenticationToken(saml2Response, registration) ---- .Kotlin [source,kotlin,role="secondary"] ---- Saml2AuthenticationToken(saml2Response, registration) ---- ==== == Use `RelyingPartyRegistration` updated methods In an early release of Spring Security's SAML support, there was some ambiguity on the meaning of certain `RelyingPartyRegistration` methods and their function. As more capabilities were added to `RelyingPartyRegistration`, it became necessary to clarify this ambiguity by changing method names to ones that aligned with spec language. The deprecated methods in `RelyingPartyRegstration` are removed. To prepare for that, consider the following representative usage of `RelyingPartyRegistration`: ==== .Java [source,java,role="primary"] ---- String idpEntityId = registration.getRemoteIdpEntityId(); String assertionConsumerServiceUrl = registration.getAssertionConsumerServiceUrlTemplate(); String idpWebSsoUrl = registration.getIdpWebSsoUrl(); String localEntityId = registration.getLocalEntityIdTemplate(); List verifying = registration.getCredentials().stream() .filter(Saml2X509Credential::isSignatureVerficationCredential) .collect(Collectors.toList()); ---- .Kotlin [source,kotlin,role="secondary"] ---- val idpEntityId: String = registration.getRemoteIdpEntityId() val assertionConsumerServiceUrl: String = registration.getAssertionConsumerServiceUrlTemplate() val idpWebSsoUrl: String = registration.getIdpWebSsoUrl() val localEntityId: String = registration.getLocalEntityIdTemplate() val verifying: List = registration.getCredentials() .filter(Saml2X509Credential::isSignatureVerficationCredential) ---- ==== This should change to: ==== .Java [source,java,role="primary"] ---- String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId(); String assertionConsumerServiceLocation = registration.getAssertionConsumerServiceLocation(); String singleSignOnServiceLocation = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation(); String entityId = registration.getEntityId(); List verifying = registration.getAssertingPartyDetails().getVerificationX509Credentials(); ---- .Kotlin [source,kotlin,role="secondary"] ---- val assertingPartyEntityId: String = registration.getAssertingPartyDetails().getEntityId() val assertionConsumerServiceLocation: String = registration.getAssertionConsumerServiceLocation() val singleSignOnServiceLocation: String = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation() val entityId: String = registration.getEntityId() val verifying: List = registration.getAssertingPartyDetails().getVerificationX509Credentials() ---- ==== For a complete listing of all changed methods, please see {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[``RelyingPartyRegistration``'s JavaDoc].