我最近实现了一个 Spring SAML 解决方案,该解决方案允许客户向服务注册其 IdP 元数据并使用其 SAML 身份验证来访问我的 SP。
IdP 元数据作为自定义 AbstractReloadingMetadataProvider 实现的一部分存储为数据库条目,如此处所建议:
public class DbIdpMetadataProvider extends AbstractReloadingMetadataProvider {
...
@Override
protected byte[] fetchMetadata() throws MetadataProviderException {
IdpProviderData provider = null; // DAO of IdP metadata in DB
try {
log.info("Attempting to retrieve DB provider data for entity: " + entityId);
provider = dbService.getIdpByEntityId(entityId);
// verify that we have a provider
if ( null != provider) {
// get last update time recorded in DB
DateTime lastUpdate = getLastUpdate();
log.info("Performing refresh of Metadata Provider XML by reading from database");
String metadataBody = provider.getMetadataBody();
emitChangeEvent();
return metadataBody.getBytes();
}
// if no provider, throw an exception as this metadata provider instance is invalid
else {
log.error("IdP Provider could not be found for EntityId: " + entityId );
throw new MetadataProviderException("Metadata could not be found for String entity: " + entityId);
}
}
catch(Exception e) {
log.error("Failed to query database for provider entity: " + entityId);
throw new MetadataProviderException("Failed to query database for provider entity: " + entityId, e);
}
}
更新元数据时,我将元数据 XML 保存到数据库,并从 CachingMetadataManager 中删除元数据提供程序的先前实例。然后,我将证书从元数据导入到本地密钥库,并将新的元数据提供程序实例添加到 CachingMetadataManager:
@Autowired
private MetadataManager metadataManager;
...
/**
* Update our existing IdP metadata provider with new XML and other information
*/
public void updateIdpMetadata(IdPRegistrationData _data) throws RequiredDataException, NotFoundException, SystemException {
IdpProviderData provider;
XMLObject xml;
String metadataXml;
EntityDescriptor entity;
String entityId;
try {
// validation
validateIdpRegistrationData(_data);
//ensure our top level DOM element is the IDPSSO entity descriptor, removing all other metadata
xml = parseMetadataXml(_data.getMetadataXml());
entity = getIDPSSOParentEntityDescriptor(xml);
entityId = entity.getEntityID();
metadataXml = serialize(entity);
// get our provider Data
provider = getIdpByAccountSysid(_data.getAccountSysid());
if ( null == provider) {
throw new NotFoundException(NotFoundExceptionType.ACCOUNT);
}
// update our provider
provider.setEntityId(entityId);
provider.setMetadataBody(metadataXml);
// save data in database
metadataStoreDao.saveAndFlush(provider);
keystoreMgr.importMetadataCertificates(xml, provider.getUrlContext());
// remove existing provider from metadata store
removeDelegateFromManager(entityId);
// reintroduce delegate to manager
loadIdpMetadata(provider);
}
catch(NotFoundException e) {
log.error("No previous version of IDP registration metadata found to update.",e);
throw (e);
}
catch (MetadataProviderException e){
log.error("Failed to update Keystore and Signing algorithm of IdP Metadata.",e);
throw new SystemException("Failed to update certificates.");
}
catch(RequiredDataException e) {
log.error("Missing required data for update.",e);
throw(e);
}
}
/**
* Remove the metadata provider from our manager
*/
private void removeDelegateFromManager( String _entityId ) throws MetadataProviderException {
ExtendedMetadataDelegate delegate;
DbIdpMetadataProvider provider;
delegate = findMetadataDelegate(_entityId);
if( null == delegate){
log.error("Failed to find Delegate in metadata manager for Entity ID: " + _entityId );
return;
}
metadataManager.removeMetadataProvider(delegate);
provider = (DbIdpMetadataProvider)delegate.getDelegate();
provider.destroy();
metadataManager.setRefreshRequired(true);
metadataManager.refreshMetadata();
}
/**
* load our provider data to a metadata provider object
*/
private void loadIdpMetadata(IdpProviderData _providerData) throws MetadataProviderException {
DbIdpMetadataProvider idpProvider;
// initialize our IdP provider
idpProvider = new DbIdpMetadataProvider(_providerData);
idpProvider.setParserPool(parser);
addIdpToMetadataManager(idpProvider);
}
/**
* Add the metadata provider to our cache
*/
private void addIdpToMetadataManager(DbIdpMetadataProvider _provider) throws MetadataProviderException {
ExtendedMetadataDelegate delegate;
ExtendedMetadata extMeta = new ExtendedMetadata();
// initialize our provider
_provider.initialize();
extMeta = createExtendedMetadata(_provider);
delegate = new ExtendedMetadataDelegate(_provider, extMeta);
delegate.setMetadataTrustCheck(false);
delegate.initialize();
metadataManager.addMetadataProvider(delegate);
metadataManager.setRefreshRequired(true);
metadataManager.refreshMetadata();
}
问题在于刷新数据库条目时,不会应用 IdP 元数据中的
打印元数据 XML 时可以看到新证书:
<!-- new signing key -->
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="encryption">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIC4jCCAcqgAwIBAgIQafZAY7...</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIC3DCCAcSgAwIBAgIQeny6jM...</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIC3DCCAcSgAwIBAgIQRtno3W...</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
我在 SP 发起登录时收到来自 IdP 的 SUCCESS SAML 响应:
<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" Destination="https://{my-domain}:443/eas-saml/saml/SSO" ID="_0d023fb8-bf24-4b78-b690-c9b53df4db72" InResponseTo="a22h6bb0gf2e8f314e00316b198ddg1" IssueInstant="2018-10-17T18:31:47.332Z" Version="2.0">
<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://{my-domain}/adfs/services/trust</Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_317d062d-247e-4405-9dd8-0ef3d032bf3f" IssueInstant="2018-10-17T18:31:47.332Z" Version="2.0">
<Issuer>http://{my-domain}/adfs/services/trust</Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_8049000b-6e76-416c-84aa-180d61ca359a">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>x/PKyqXDECmE2IBNiZ0pqet3HqQYgDwlbeo1Vb3gXD8=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>UvhpsDE7XT1uvqGbA+IZ2sC9t8x0i42/P7tdNXO...</ds:SignatureValue>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIC3DCCAcSgAwIBAgIQRtno3W...</ds:X509Certificate>
</ds:X509Data>
</KeyInfo>
</ds:Signature>
<Subject>
<NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">userId</NameID>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<SubjectConfirmationData InResponseTo="a22h6bb0gf2e8f314e00316b198ddg1" NotOnOrAfter="2018-10-17T18:36:47.332Z" Recipient="https://{my-domain}:443/eas-saml/saml/SSO"/>
</SubjectConfirmation>
</Subject>
<Conditions NotBefore="2018-10-17T18:31:47.327Z" NotOnOrAfter="2018-10-17T18:33:47.327Z">
<AudienceRestriction>
<Audience>https://{my-domain}/eas-saml</Audience>
</AudienceRestriction>
</Conditions>
<AttributeStatement>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn">
<AttributeValue>userId</AttributeValue>
</Attribute>
<Attribute Name="http://E-Mail-Addresses">
<AttributeValue>[email protected]</AttributeValue>
</Attribute>
</AttributeStatement>
<AuthnStatement AuthnInstant="2018-10-17T18:31:47.233Z" SessionIndex="_317d062d-247e-4405-9dd8-0ef3d032bf3f">
<AuthnContext>
<AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>
</AuthnContext>
</AuthnStatement>
</Assertion>
</samlp:Response>
----------
Where as the log output shows a failure with the following stack trace:
----------
<pre>
2018-10-17 18:31:47 INFO SAMLDefaultLogger:129 - AuthNResponse;FAILURE;172.17.0.1;https://{my-domain}/eas-saml;http://{my-domain}/adfs/services/trust;;;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:88)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:174)
at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:92)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:186)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
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:334)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
Caused by: org.opensaml.xml.validation.ValidationException: Signature is not trusted or invalid
at org.springframework.security.saml.websso.AbstractProfileBase.verifySignature(AbstractProfileBase.java:272)
at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.verifyAssertionSignature(WebSSOProfileConsumerImpl.java:419)
at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.verifyAssertion(WebSSOProfileConsumerImpl.java:292)
at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:214)
... 39 more
</pre>
----------
If I restart the service, the signature is recognized and the response is successfully processed.
Can anyone see what I may be doing wrong? Shouldn't a refresh of the metadata manager be enough to allow the new certificates be used? Or is there a step I'm missing?
由于我当前使用的是 XML 配置文件,因此我修改了安全上下文 XML 以设置凭证解析器,如下所示:
<!-- Provider of default SAML Context -->
<bean id="contextProvider" class="org.springframework.security.saml.context.SAMLContextProviderImpl">
<property name="metadataResolver">
<bean class="com.mydomain.CustomMetadataCredentialResolver">
<constructor-arg index="0" ref="metadata" />
<constructor-arg index="1" ref="keyManager" />
<property name="useXmlMetadata" value="true" />
</bean>
</property>
</bean>
然后我创建了一个扩展的类
org.springframework.security.saml.trust.MetadataCredentialResolver
现在,我只需重写cacheCredentials方法:
@Component
public class CustomMetadataCredentialResolver extends MetadataCredentialResolver {
public CustomMetadataCredentialResolver(MetadataManager metadataProvider, KeyManager keyManager) {
super(metadataProvider, keyManager);
}
@Override
protected void cacheCredentials( MetadataCacheKey cacheKey, Collection<Credential> credentials ) {
//no-op
}
}
我可能会清理它以根据命令清除缓存,而不是从不缓存凭据,但现在它可以工作。