Spring 安全 SAML:让 <Signature> 块出现在 <AuthnRequest> 中

Spring Security SAML: Getting <Signature> block to appear in <AuthnRequest>

我很难让 Spring 安全 SAML 与 ADFS 2.0 一起工作。

根据我当前的配置,生成的 AuthnRequest 如下所示:-

<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest
    AssertionConsumerServiceURL="https://localhost:8443/helix/saml/SSO"
    Destination="https://server/adfs/ls/"
    ForceAuthn="false" ID="a14edaf38ih92bi8acji5a1664a80e"
    IsPassive="false" IssueInstant="2016-02-15T21:05:57.980Z"
    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    Version="2.0" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
    <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://localhost:8443/helix/saml/metadata</saml2:Issuer>
    <saml2p:Scoping ProxyCount="2"/>
</saml2p:AuthnRequest>

但是,它会导致 ADFS 端出错:-

Microsoft.IdentityModel.Protocols.XmlSignature.SignatureVerificationFailedException: MSIS0038: SAML Message has wrong signature. Issuer: 'https://localhost:8443/helix/saml/metadata'.
   at Microsoft.IdentityServer.Protocols.Saml.Contract.SamlContractUtility.CreateSamlMessage(MSISSamlBindingMessage message)
   at Microsoft.IdentityServer.Service.SamlProtocol.SamlProtocolService.CreateErrorMessage(CreateErrorMessageRequest createErrorMessageRequest)
   at Microsoft.IdentityServer.Service.SamlProtocol.SamlProtocolService.ProcessRequest(Message requestMessage)

我的安全团队告诉我,我的 AuthnRequest 应该看起来像这样:-

<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="https://localhost:8443/helix/saml/SSO" Destination="https://server/adfs/ls/" ID="_e082771303738e4e6872e8d5711446d4" IssueInstant="2016-02-15T19:51:50.627Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0">
    <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://localhost:8443/helix/saml/metadata</saml2:Issuer>
    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
            <Reference URI="#_e082771303738e4e6872e8d5711446d4">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                <DigestValue>S8r/XbIhlFGFSMfLoSt/7IlksiI=</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>TT4n3==...</SignatureValue>
        <KeyInfo>
            <X509Data>
                <X509Certificate>MIIC8z...</X509Certificate>
            </X509Data>
        </KeyInfo>
    </Signature>
    <saml2p:NameIDPolicy AllowCreate="true" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"/>
    <saml2p:RequestedAuthnContext Comparison="exact">
        <saml2:AuthnContextClassRef xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml2:AuthnContextClassRef>
    </saml2p:RequestedAuthnContext>
</saml2p:AuthnRequest>

...但是,我无法尝试让 <Signature> 块出现在我的 AuthnRequest.

我当前的 Spring 安全 SAML 配置如下所示...我很抱歉它相当冗长,但我不确定除了整个配置之外还应该在此处粘贴什么。

@Configuration
@EnableWebSecurity
public abstract class SecuritySAMLConfig extends WebSecurityConfigurerAdapter {
    private static final String METADATA_URL = "https://server/federationmetadata/2007-06/federationmetadata.xml";
    private static final String ALIAS = "apollo";
    private static final String STORE_PASS = "secret";

    @Autowired
    private SAMLUserDetailsServiceImpl samlUserDetailsServiceImpl;

    @Autowired
    private SAMLAuthenticationProvider samlAuthenticationProvider;

    @Bean
    public static SAMLBootstrap SAMLBootstrap() {
        return new CustomSamlBootstrap();
    }

    @Bean
    public VelocityEngine velocityEngine() {
        return VelocityFactory.getEngine();
    }

    @Bean(initMethod = "initialize")
    public StaticBasicParserPool parserPool() {
        return new StaticBasicParserPool();
    }

    @Bean(name = "parserPoolHolder")
    public ParserPoolHolder parserPoolHolder() {
        return new ParserPoolHolder();
    }

    @Bean
    public MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager() {
        return new MultiThreadedHttpConnectionManager();
    }

    @Bean
    public HttpClient httpClient(MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager) {
        return new HttpClient(multiThreadedHttpConnectionManager);
    }

    @Bean
    public SAMLAuthenticationProvider samlAuthenticationProvider() {
        SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
        samlAuthenticationProvider.setUserDetails(samlUserDetailsServiceImpl);
        samlAuthenticationProvider.setForcePrincipalAsString(false);
        return samlAuthenticationProvider;
    }

    @Bean
    public SAMLContextProviderImpl contextProvider() {
        return new SAMLContextProviderImpl();
    }

    @Bean
    public SAMLDefaultLogger samlLogger() {
        return new SAMLDefaultLogger();
    }

    @Bean
    public WebSSOProfileConsumer webSSOprofileConsumer() {
        return new WebSSOProfileConsumerImpl();
    }

    @Bean
    public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
        return new WebSSOProfileConsumerHoKImpl();
    }

    @Bean
    public WebSSOProfile webSSOprofile() {
        return new WebSSOProfileImpl();
    }

    @Bean
    public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
        return new WebSSOProfileConsumerHoKImpl();
    }

    @Bean
    public WebSSOProfileECPImpl ecpprofile() {
        return new WebSSOProfileECPImpl();
    }

    @Bean
    public SingleLogoutProfile logoutprofile() {
        return new SingleLogoutProfileImpl();
    }

    @Bean
    public KeyManager keyManager() {
        DefaultResourceLoader loader = new DefaultResourceLoader();
        Resource storeFile = loader.getResource("classpath:keystore.jks");
        Map<String, String> passwords = new HashMap<>();
        passwords.put(ALIAS, STORE_PASS);
        return new JKSKeyManager(storeFile, STORE_PASS, passwords, ALIAS);
    }

    @Bean
    public WebSSOProfileOptions webSSOProfileOptions() {
        return new WebSSOProfileOptions();
    }

    @Bean
    public SAMLEntryPoint samlEntryPoint(WebSSOProfileOptions webSSOProfileOptions) {
        SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
        samlEntryPoint.setDefaultProfileOptions(webSSOProfileOptions);
        return samlEntryPoint;
    }

    @Bean
    public ExtendedMetadata extendedMetadata() {
        ExtendedMetadata extendedMetadata = new ExtendedMetadata();
        extendedMetadata.setIdpDiscoveryEnabled(false);

        // #######
        // ####### In theory, by setting these keys, the signature block should appear,
        // ####### but, that didn't work for me
        // #######
        extendedMetadata.setSignMetadata(true);
        extendedMetadata.setSigningKey(ALIAS);
        extendedMetadata.setEncryptionKey(ALIAS);
        // #######

        return extendedMetadata;
    }

    @Bean
    public ExtendedMetadataDelegate extendedMetadataDelegate(HttpClient httpClient,
                                                             ParserPool parserPool,
                                                             ExtendedMetadata extendedMetadata) throws MetadataProviderException {
        Timer backgroundTaskTimer = new Timer(true);
        HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(backgroundTaskTimer,
                                                                             httpClient,
                                                                             METADATA_URL);
        httpMetadataProvider.setParserPool(parserPool);
        ExtendedMetadataDelegate extendedMetadataDelegate = new ExtendedMetadataDelegate(httpMetadataProvider,
                                                                                         extendedMetadata);
        extendedMetadataDelegate.setMetadataTrustCheck(false);

        return extendedMetadataDelegate;
    }

    @Bean
    public CachingMetadataManager metadata(ExtendedMetadataDelegate extendedMetadataDelegate) throws MetadataProviderException {
        List<MetadataProvider> providers = new ArrayList<>();
        providers.add(extendedMetadataDelegate);
        return new CachingMetadataManager(providers);
    }

    @Bean
    public MetadataGenerator metadataGenerator(ExtendedMetadata extendedMetadata, KeyManager keyManager) {
        MetadataGenerator metadataGenerator = new MetadataGenerator();

        metadataGenerator.setExtendedMetadata(extendedMetadata);
        metadataGenerator.setIncludeDiscoveryExtension(false);
        metadataGenerator.setKeyManager(keyManager);

        return metadataGenerator;
    }

    @Bean
    public MetadataDisplayFilter metadataDisplayFilter() {
        return new MetadataDisplayFilter();
    }

    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler() {
        SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        successRedirectHandler.setDefaultTargetUrl("/landing");
        return successRedirectHandler;
    }

    @Bean
    public SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() {
        SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
        failureHandler.setUseForward(true);
        failureHandler.setDefaultFailureUrl("/error");
        return failureHandler;
    }

    @Bean
    public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter(SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler,
                                                                       AuthenticationManager authenticationManager,
                                                                       SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler) throws Exception {
        SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter();
        samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(savedRequestAwareAuthenticationSuccessHandler);
        samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManager);
        samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(simpleUrlAuthenticationFailureHandler);
        return samlWebSSOHoKProcessingFilter;
    }

    @Bean
    public SAMLProcessingFilter samlProcessingFilter(AuthenticationManager authenticationManager,
                                                     SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler,
                                                     SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler) throws Exception {
        SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
        samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager);
        samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(savedRequestAwareAuthenticationSuccessHandler);
        samlWebSSOProcessingFilter.setAuthenticationFailureHandler(simpleUrlAuthenticationFailureHandler);
        return samlWebSSOProcessingFilter;
    }

    @Bean
    public MetadataGeneratorFilter metadataGeneratorFilter(MetadataGenerator metadataGenerator) {
        return new MetadataGeneratorFilter(metadataGenerator);
    }

    @Bean
    public SimpleUrlLogoutSuccessHandler simpleUrlLogoutSuccessHandler() {
        SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
        successLogoutHandler.setDefaultTargetUrl("/");
        return successLogoutHandler;
    }

    @Bean
    public SecurityContextLogoutHandler securityContextLogoutHandler() {
        SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
        logoutHandler.setInvalidateHttpSession(true);
        logoutHandler.setClearAuthentication(true);
        return logoutHandler;
    }

    @Bean
    public SAMLLogoutProcessingFilter samlLogoutProcessingFilter(SimpleUrlLogoutSuccessHandler simpleUrlLogoutSuccessHandler,
                                                                 SecurityContextLogoutHandler securityContextLogoutHandler) {
        return new SAMLLogoutProcessingFilter(simpleUrlLogoutSuccessHandler, securityContextLogoutHandler);
    }

    @Bean
    public SAMLLogoutFilter samlLogoutFilter(SimpleUrlLogoutSuccessHandler simpleUrlLogoutSuccessHandler,
                                             SecurityContextLogoutHandler securityContextLogoutHandler) {
        return new SAMLLogoutFilter(simpleUrlLogoutSuccessHandler,
                                    new LogoutHandler[]{securityContextLogoutHandler},
                                    new LogoutHandler[]{securityContextLogoutHandler});
    }

    @Bean
    public HTTPArtifactBinding artifactBinding(HTTPSOAP11Binding httpSOAP11Binding,
                                               HttpClient httpClient,
                                               ParserPool parserPool,
                                               VelocityEngine velocityEngine) {
        ArtifactResolutionProfileImpl artifactResolutionProfile = new ArtifactResolutionProfileImpl(httpClient);
        artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(httpSOAP11Binding));

        return new HTTPArtifactBinding(parserPool, velocityEngine, artifactResolutionProfile);
    }

    @Bean
    public HTTPSOAP11Binding httpSOAP11Binding(ParserPool parserPool) {
        return new HTTPSOAP11Binding(parserPool);
    }

    @Bean
    public HTTPPostBinding httpPostBinding(ParserPool parserPool, VelocityEngine velocityEngine) {
        return new HTTPPostBinding(parserPool, velocityEngine);
    }

    @Bean
    public HTTPRedirectDeflateBinding httpRedirectDeflateBinding(ParserPool parserPool) {
        return new HTTPRedirectDeflateBinding(parserPool);
    }

    @Bean
    public HTTPPAOS11Binding httpPAOS11Binding(ParserPool parserPool) {
        return new HTTPPAOS11Binding(parserPool);
    }

    @Bean
    public SAMLProcessorImpl processor(HTTPRedirectDeflateBinding httpRedirectDeflateBinding,
                                       HTTPPostBinding httpPostBinding,
                                       HTTPArtifactBinding httpArtifactBinding,
                                       HTTPSOAP11Binding httpSOAP11Binding,
                                       HTTPPAOS11Binding httpPAOS11Binding) {
        Collection<SAMLBinding> bindings = new ArrayList<>();
        bindings.add(httpRedirectDeflateBinding);
        bindings.add(httpPostBinding);
        bindings.add(httpArtifactBinding);
        bindings.add(httpSOAP11Binding);
        bindings.add(httpPAOS11Binding);
        return new SAMLProcessorImpl(bindings);
    }

    @Bean
    public FilterChainProxy filterChainProxy(SAMLEntryPoint samlEntryPoint,
                                             SAMLLogoutFilter samlLogoutFilter,
                                             MetadataDisplayFilter metadataDisplayFilter,
                                             SAMLProcessingFilter samlProcessingFilter,
                                             SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter,
                                             SAMLLogoutProcessingFilter samlLogoutProcessingFilter) throws Exception {
        List<SecurityFilterChain> chains = new ArrayList<>();
        chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"), samlEntryPoint));
        chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"), samlLogoutFilter));
        chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
                                                  metadataDisplayFilter));
        chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"), samlProcessingFilter));
        chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"),
                                                  samlWebSSOHoKProcessingFilter));
        chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
                                                  samlLogoutProcessingFilter));

        return new FilterChainProxy(chains);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(samlAuthenticationProvider);
    }
}

如果有人能将我推向正确的方向,我将不胜感激。

谢谢。

更新

MetadataGenerator 生成的我的 SP 元数据如下所示:-

<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="https___localhost_8443_helix_saml_metadata" entityID="https://localhost:8443/helix/saml/metadata">
    <md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
        <md:KeyDescriptor use="signing">
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>MIICx....</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </md:KeyDescriptor>
        <md:KeyDescriptor use="encryption">
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>MIICxz....</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </md:KeyDescriptor>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:8443/helix/saml/SingleLogout"/>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:8443/helix/saml/SingleLogout"/>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName</md:NameIDFormat>
        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:8443/helix/saml/SSO" index="0" isDefault="true"/>
        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://localhost:8443/helix/saml/SSO" index="1"/>
    </md:SPSSODescriptor>
</md:EntityDescriptor>

我还想指出,我使用的是 SHA256withRSA 而不是 SHA1withRSA。因此,我将默认的 SAMLBootstrap bean 替换为以下内容:-

public final class CustomSamlBootstrap extends SAMLBootstrap {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        super.postProcessBeanFactory(beanFactory);
        BasicSecurityConfiguration config = (BasicSecurityConfiguration) Configuration.getGlobalSecurityConfiguration();
        config.registerSignatureAlgorithmURI("RSA", SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
        config.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256);
    }
}

更新 2

我通过修改 WebSSOProfileOptions.

让它工作
@Bean
public WebSSOProfileOptions webSSOProfileOptions() {
    WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
    webSSOProfileOptions.setIncludeScoping(false);

    // Added this line
    webSSOProfileOptions.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);

    return webSSOProfileOptions;
}

您使用什么绑定向 IDP 发送请求?如果是 HTTP 请求,SAML 标准要求在传递消息之前删除签名。然后对序列化请求执行签名并作为 GET 参数发送。

我相信你应该设置:

@Bean
public MetadataGenerator metadataGenerator(ExtendedMetadata extendedMetadata, KeyManager keyManager) {
    MetadataGenerator metadataGenerator = new MetadataGenerator();
    // ... other configuration here
    metadataGenerator.setRequestSigned ( true );

    return metadataGenerator;
}

在 ExtendedMetadata 中,您只设置了要签名的元数据文件。这不会影响 AuthnRequest 的签名。