I tried to use SAML Holder-of-key profile of Spring Security SAML extension, but I didn't succeed, I even googled it but did not find any related documents or samples.
I added the following tag to the tomcat config (server.xml file) to enable HTTPS scheme and client authentication:
<Connector port="8443" SSLEnabled="true" scheme="https" secure="true" clientAuth="want" sslProtocol="TLS" keystoreFile="ks.keystore" keystorePass="password" truststoreFile="ts.keystore" truststorePass="password"/>
This config seems to work, since when I access my application website, it request for client certificate and sends the AuthN Request to the IdP. The IdP validates the request but does not ask for client certificate and in response, it returns an assertion without HoK subject confirmation and I get the following error:
org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:95)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.access.channel.ChannelProcessingFilter.doFilter(ChannelProcessingFilter.java:152)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.saml.metadata.MetadataGeneratorFilter.doFilter(MetadataGeneratorFilter.java:87)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
at org.springframework.security.web.debug.DebugFilter.invokeWithWrappedRequest(DebugFilter.java:75)
at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:62)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:221)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:107)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:504)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:155)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:76)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:934)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:90)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:515)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1012)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:642)
at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:223)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1555)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
Caused by: org.opensaml.common.SAMLException: Response doesn't have any valid assertion which would pass subject validation
at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:229)
at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:84)
... 43 more
Caused by: org.opensaml.common.SAMLException: Assertion invalidated by subject confirmation - can't be confirmed by holder-of-key method
at org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl.verifySubject(WebSSOProfileConsumerHoKImpl.java:150)
at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.verifyAssertion(WebSSOProfileConsumerImpl.java:296)
at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:214)
... 44 more
This is my Spring Security configuration:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<context:annotation-config/>
<security:debug/>
<context:component-scan base-package="org.springframework.security.saml" />
<security:http entry-point-ref="samlEntryPoint" use-expressions="false">
<security:csrf disabled="true" />
<security:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" requires-channel="https" />
<security:custom-filter before="FIRST" ref="metadataGeneratorFilter" />
<security:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter" />
</security:http>
<!-- Filter automatically generates default SP metadata -->
<bean id="metadataGeneratorFilter" class="org.springframework.security.saml.metadata.MetadataGeneratorFilter">
<constructor-arg>
<bean class="org.springframework.security.saml.metadata.MetadataGenerator">
<property name="bindingsHoKSSO">
<list>
<value>post</value>
<value>artifact</value>
</list>
</property>
<property name="entityBaseURL" value="https://localhost:8443" />
<property name="extendedMetadata">
<bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
<property name="signMetadata" value="true"/>
<property name="idpDiscoveryEnabled" value="true"/>
</bean>
</property>
</bean>
</constructor-arg>
</bean>
<bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
<security:filter-chain-map request-matcher="ant">
<security:filter-chain pattern="/saml/login/**" filters="samlEntryPoint" />
<security:filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter" />
<security:filter-chain pattern="/saml/HoKSSO" filters="samlWebSSOHoKProcessingFilter" />
<security:filter-chain pattern="/saml/metadata/**" filters="metadataDisplayFilter"/>
<security:filter-chain pattern="/saml/discovery/**" filters="samlIDPDiscovery"/>
<security:filter-chain pattern="/saml/logout/**" filters="samlLogoutFilter" />
<security:filter-chain pattern="/saml/single_logout/**" filters="samlLogoutProcessingFilter" />
</security:filter-chain-map>
</bean>
<!-- LOGOUT -->
<!-- (Local Logout) Override default logout processing filter with the one processing SAML messages -->
<bean id="samlLogoutFilter" class="org.springframework.security.saml.SAMLLogoutFilter">
<constructor-arg index="0" ref="successLogoutHandler"/>
<constructor-arg index="1" ref="logoutHandler"/>
<constructor-arg index="2" ref="logoutHandler"/>
</bean>
<!-- (Single Logout) Filter processing incoming logout messages -->
<!-- First argument determines URL user will be redirected to after successful global logout -->
<bean id="samlLogoutProcessingFilter" class="org.springframework.security.saml.SAMLLogoutProcessingFilter">
<constructor-arg index="0" ref="successLogoutHandler"/>
<constructor-arg index="1" ref="logoutHandler"/>
</bean>
<!-- Handler for successful logout -->
<bean id="successLogoutHandler" class="org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler">
<property name="defaultTargetUrl" value="/logout.jsp"/>
</bean>
<!-- Logout handler terminating local session -->
<bean id="logoutHandler" class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler">
<property name="invalidateHttpSession" value="false"/>
</bean>
<!-- SAML 2.0 Logout Profile -->
<bean id="logoutprofile" class="org.springframework.security.saml.websso.SingleLogoutProfileImpl"/>
<!-- IDP Discovery Service -->
<bean id="samlIDPDiscovery" class="org.springframework.security.saml.SAMLDiscovery">
<property name="idpSelectionPath" value="/idpSelection.jsp"/>
</bean>
<bean id="metadataDisplayFilter" class="org.springframework.security.saml.metadata.MetadataDisplayFilter" />
<!-- Processing filter for WebSSO Holder-of-Key profile -->
<bean id="samlWebSSOHoKProcessingFilter" class="org.springframework.security.saml.SAMLWebSSOHoKProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationSuccessHandler" ref="successRedirectHandler"/>
<property name="authenticationFailureHandler" ref="failureRedirectHandler"/>
</bean>
<bean id="samlWebSSOProcessingFilter" class="org.springframework.security.saml.SAMLProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationSuccessHandler" ref="successRedirectHandler" />
<property name="authenticationFailureHandler" ref="failureRedirectHandler" />
</bean>
<bean id="successRedirectHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/" />
</bean>
<bean id="failureRedirectHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="useForward" value="true" />
<property name="defaultFailureUrl" value="/error.jsp" />
</bean>
<bean class="org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer">
<property name="sslHostnameVerification" value="default"/>
</bean>
<bean id="samlEntryPoint" class="org.springframework.security.saml.SAMLEntryPoint">
<property name="defaultProfileOptions">
<bean class="org.springframework.security.saml.websso.WebSSOProfileOptions">
<property name="assertionConsumerIndex" value="2" />
<property name="binding" value="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" />
<property name="includeScoping" value="false" />
</bean>
</property>
</bean>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="samlAuthenticationProvider" />
</security:authentication-manager>
<bean id="metadata" class="org.springframework.security.saml.metadata.CachingMetadataManager">
<constructor-arg>
<list>
<bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
<constructor-arg>
<bean class="org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider">
<constructor-arg>
<value type="java.io.File">classpath:IS_metadata.xml</value>
</constructor-arg>
<property name="parserPool" ref="parserPool"/>
</bean>
</constructor-arg>
<constructor-arg>
<bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
</bean>
</constructor-arg>
</bean>
</list>
</constructor-arg>
<!--<property name="defaultIDP" value="https://localhost:9443/samlsso" />-->
</bean>
<bean class="org.springframework.security.saml.SAMLBootstrap" />
<bean id="samlAuthenticationProvider" class="org.springframework.security.saml.SAMLAuthenticationProvider" />
<!-- SAML 2.0 WebSSO Assertion Consumer -->
<bean id="webSSOprofileConsumer" class="org.springframework.security.saml.websso.WebSSOProfileConsumerImpl" />
<!-- SAML 2.0 Web SSO profile -->
<bean id="webSSOprofile" class="org.springframework.security.saml.websso.WebSSOProfileImpl" />
<!-- SAML 2.0 Holder-of-Key WebSSO Assertion Consumer -->
<bean id="hokWebSSOprofileConsumer" class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl" />
<!-- SAML 2.0 Holder-of-Key Web SSO profile -->
<bean id="hokWebSSOProfile" class="org.springframework.security.saml.websso.WebSSOProfileHoKImpl" />
<!-- SAML 2.0 ECP profile -->
<bean id="ecpprofile" class="org.springframework.security.saml.websso.WebSSOProfileECPImpl"/>
<!-- Provider of default SAML Context -->
<bean id="contextProvider" class="org.springframework.security.saml.context.SAMLContextProviderImpl">
<!--<property name="metadataResolver">
<bean class="org.springframework.security.saml.trust.MetadataCredentialResolver">
<constructor-arg index="0" ref="metadata" />
<constructor-arg name="keyManager" index="1" ref="keyManager" />
<property name="useXmlMetadata" value="false" />
</bean>
</property>-->
<property name="storageFactory">
<bean class="org.springframework.security.saml.storage.EmptyStorageFactory" />
</property>
</bean>
<bean id="samlLogger" class="org.springframework.security.saml.log.SAMLDefaultLogger" />
<bean id="keyManager" class="org.springframework.security.saml.key.JKSKeyManager">
<constructor-arg value="classpath:samlKeystore.jks" />
<constructor-arg type="java.lang.String" value="nalle123" />
<constructor-arg>
<map>
<entry key="spalias" value="nalle123" />
</map>
</constructor-arg>
<constructor-arg type="java.lang.String" value="spalias" />
</bean>
<!-- Class loading incoming SAML messages from httpRequest stream -->
<bean id="processor" class="org.springframework.security.saml.processor.SAMLProcessorImpl">
<constructor-arg>
<list>
<ref bean="redirectBinding" />
<ref bean="postBinding" />
<ref bean="artifactBinding" />
<ref bean="soapBinding" />
<ref bean="paosBinding" />
</list>
</constructor-arg>
</bean>
<!-- Bindings, encoders and decoders used for creating and parsing messages -->
<bean id="postBinding" class="org.springframework.security.saml.processor.HTTPPostBinding">
<constructor-arg ref="parserPool" />
<constructor-arg ref="velocityEngine" />
</bean>
<bean id="redirectBinding" class="org.springframework.security.saml.processor.HTTPRedirectDeflateBinding">
<constructor-arg ref="parserPool" />
</bean>
<bean id="artifactBinding" class="org.springframework.security.saml.processor.HTTPArtifactBinding">
<constructor-arg ref="parserPool" />
<constructor-arg ref="velocityEngine" />
<constructor-arg>
<bean class="org.springframework.security.saml.websso.ArtifactResolutionProfileImpl">
<constructor-arg>
<bean class="org.apache.commons.httpclient.HttpClient">
<constructor-arg>
<bean class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager" />
</constructor-arg>
</bean>
</constructor-arg>
<property name="processor">
<bean class="org.springframework.security.saml.processor.SAMLProcessorImpl">
<constructor-arg ref="soapBinding" />
</bean>
</property>
</bean>
</constructor-arg>
</bean>
<bean id="soapBinding" class="org.springframework.security.saml.processor.HTTPSOAP11Binding">
<constructor-arg ref="parserPool" />
</bean>
<bean id="paosBinding" class="org.springframework.security.saml.processor.HTTPPAOS11Binding">
<constructor-arg ref="parserPool" />
</bean>
<!-- XML parser pool needed for OpenSAML parsing -->
<bean id="parserPool" class="org.opensaml.xml.parse.StaticBasicParserPool" init-method="initialize">
<property name="builderFeatures">
<map>
<entry key="http://apache.org/xml/features/dom/defer-node-expansion" value="false" />
</map>
</property>
</bean>
<bean id="parserPoolHolder" class="org.springframework.security.saml.parser.ParserPoolHolder" />
<!-- Initialization of the velocity engine -->
<bean id="velocityEngine" class="org.springframework.security.saml.util.VelocityFactory" factory-method="getEngine" />
</beans>
I am using WSO2 Identity Server v4.5.0 as IdP in this case, this is WSO2 IdP Metadata:
<?xml version="1.0" encoding="UTF-8"?><md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://localhost:9443/samlsso" validUntil="2023-09-23T06:57:15.396Z">
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
MIIDXTCCAkWgAwIBAgIETYI6hjANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJJ
UjELMAkGA1UECBMCTkExDzANBgNVBAcTBlRlaHJhbjENMAsGA1UEChMEV1NPMjEQ
MA4GA1UECxMHV1NPMiBJUzERMA8GA1UEAxMIaG9zdG5hbWUwHhcNMTUwOTI3MDYy
NDE2WhcNMTUxMjI2MDYyNDE2WjBfMQswCQYDVQQGEwJJUjELMAkGA1UECBMCTkEx
DzANBgNVBAcTBlRlaHJhbjENMAsGA1UEChMEV1NPMjEQMA4GA1UECxMHV1NPMiBJ
UzERMA8GA1UEAxMIaG9zdG5hbWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQCOD3I08VYC+auzqP6sZbnejD40jtuDuX/IooDxzphysqeTaPVOq0Fquv9i
uIb6XfxGk1hbFiThriCynXAkxeNbmK2ByurRVoJBgdFcCB9JfbGNapVodAbVl9cR
5kXmMJAdqXFDOrMCluira/7HzR0SpoG6A41M/cOkJHq7qtdQlCBaC+L0C5KK+P6/
g4X1zKIt5+vmn1lnDDxdOlCUsv5xVgEYLai+2ArPCZzMxKwlGQ/yWoDky2HXRrnx
ja/vV0J1VeV86tVwyxMb4Bm4XKohrH5sVtzE296JoiPl3rLfGeWpYEO4DXfJYLbi
4+kUvQ4MXTcsSDwQI9aBwVhia8uVAgMBAAGjITAfMB0GA1UdDgQWBBT5CS2/DmR3
lWx35Pmf1jZwbYJpcTANBgkqhkiG9w0BAQsFAAOCAQEANU/dEo7hWpQEDaYvaZmN
IJbck5fqKw4bgbPE2D6ifaYdb4SxxbNL3eBHg1Hbr5hCwLuX4zeqS9D8mrGzWnap
ZgG88VtDl/Y75t9Q3/y8PaRzHKaijo6ydyewLRtumhxFf/nXVKod9kNSPrOorM+o
T/1ht+yfaxCeeK32aV/SLs42raPI3LAT2ZyPimiVhsou72jXD7st8aRhm21qliZq
Qezbz8MccO1peASUikh8ksXNzb6Uh/3o/ks03NJibBZXwgzXR/62KXq8+FRNKEy+
Ec0yudq7bSaBGalQ3SqLXycKP9/Ct58eI2qRVGa0RwqcieDZIspFCgbAlZ2+qIDU
rQ==
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://localhost:9443/samlsso"
ResponseLocation="https://localhost:9443/samlsso"/>
<md:SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://localhost:9443/samlsso"/>
<md:SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://localhost:9443/samlsso"/>
<md:SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser"
hoksso:ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
xmlns:hoksso="urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser"
Location="https://localhost:9443/samlsso"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
Can anyone help me configure Spring Security SAML to enable Holder-of-key profile correctly?
OK, my bad. Since IdP does not ask for client certificate, it seems that this version of "WSO2 IS" I'm using (v4.5.0), does not support issuing SAML HoK subject confirmation assertions (I updated the question accordingly). I will try with the latest release of WSO2 Identity Server (v5.0.0) and share the results.
I'm still not sure whether my spring configuration is correct or not.
Thanks
The above Spring SAML configuration is enough for enabling Holder-of-key profile at SP-side. I tried it with SimpleSAMLphp as the IdP and it worked.
It seems WSO2 does not support HoK web browser SSO profile (Please correct me if I am wrong), instead, it supports obtaining/issuing HoK messages through Security Token Service (STS).