Browse Source
Implements minimal SAML 2.0 login/authentication functionality with the
following feature set:
- Supports IDP initiated login at the default url of /login/saml2/sso/{registrationId}
- Supports SP initiated login at the default url of /saml2/authenticate/{registrationId}
- Supports basic java-configuration via DSL
- Provides an integration sample using Spring Boot
Not implemented with this MVP
- Single Logout
- Dynamic Service Provider Metadata
Fixes gh-6019
pull/7260/head
39 changed files with 4260 additions and 19 deletions
@ -0,0 +1,314 @@
@@ -0,0 +1,314 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.config.annotation.web.configurers.saml2; |
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.security.authentication.AuthenticationProvider; |
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder; |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; |
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; |
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; |
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider; |
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory; |
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory; |
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; |
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; |
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; |
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter; |
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; |
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; |
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
import javax.servlet.Filter; |
||||
|
||||
import static org.springframework.util.StringUtils.hasText; |
||||
|
||||
/** |
||||
* An {@link AbstractHttpConfigurer} for SAML 2.0 Login, |
||||
* which leverages the SAML 2.0 Web Browser Single Sign On (WebSSO) Flow. |
||||
* |
||||
* <p> |
||||
* SAML 2.0 Login provides an application with the capability to have users log in |
||||
* by using their existing account at an SAML 2.0 Identity Provider. |
||||
* |
||||
* <p> |
||||
* Defaults are provided for all configuration options with the only required configuration |
||||
* being {@link #relyingPartyRegistrationRepository(RelyingPartyRegistrationRepository)} . |
||||
* Alternatively, a {@link RelyingPartyRegistrationRepository} {@code @Bean} may be registered instead. |
||||
* |
||||
* <h2>Security Filters</h2> |
||||
* |
||||
* The following {@code Filter}'s are populated: |
||||
* |
||||
* <ul> |
||||
* <li>{@link Saml2WebSsoAuthenticationFilter}</li> |
||||
* <li>{@link Saml2WebSsoAuthenticationRequestFilter}</li> |
||||
* </ul> |
||||
* |
||||
* <h2>Shared Objects Created</h2> |
||||
* |
||||
* The following shared objects are populated: |
||||
* |
||||
* <ul> |
||||
* <li>{@link RelyingPartyRegistrationRepository} (required)</li> |
||||
* <li>{@link Saml2AuthenticationRequestFactory} (optional)</li> |
||||
* </ul> |
||||
* |
||||
* <h2>Shared Objects Used</h2> |
||||
* |
||||
* The following shared objects are used: |
||||
* |
||||
* <ul> |
||||
* <li>{@link RelyingPartyRegistrationRepository} (required)</li> |
||||
* <li>{@link Saml2AuthenticationRequestFactory} (optional)</li> |
||||
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not configured |
||||
* and {@code DefaultLoginPageGeneratingFilter} is available, than a default login page will be made available</li> |
||||
* </ul> |
||||
* |
||||
* @since 5.2 |
||||
* @see HttpSecurity#saml2Login() |
||||
* @see Saml2WebSsoAuthenticationFilter |
||||
* @see Saml2WebSsoAuthenticationRequestFilter |
||||
* @see RelyingPartyRegistrationRepository |
||||
* @see AbstractAuthenticationFilterConfigurer |
||||
*/ |
||||
public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>> extends |
||||
AbstractAuthenticationFilterConfigurer<B, Saml2LoginConfigurer<B>, Saml2WebSsoAuthenticationFilter> { |
||||
|
||||
private String loginPage; |
||||
|
||||
private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; |
||||
|
||||
private AuthenticationRequestEndpointConfig authenticationRequestEndpoint = new AuthenticationRequestEndpointConfig(); |
||||
|
||||
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; |
||||
|
||||
/** |
||||
* Sets the {@code RelyingPartyRegistrationRepository} of relying parties, each party representing a |
||||
* service provider, SP and this host, and identity provider, IDP pair that communicate with each other. |
||||
* @param repo the repository of relying parties |
||||
* @return the {@link Saml2LoginConfigurer} for further configuration |
||||
*/ |
||||
public Saml2LoginConfigurer relyingPartyRegistrationRepository(RelyingPartyRegistrationRepository repo) { |
||||
this.relyingPartyRegistrationRepository = repo; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
@Override |
||||
public Saml2LoginConfigurer<B> loginPage(String loginPage) { |
||||
Assert.hasText(loginPage, "loginPage cannot be empty"); |
||||
this.loginPage = loginPage; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
@Override |
||||
public Saml2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) { |
||||
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty"); |
||||
Assert.state(loginProcessingUrl.contains("{registrationId}"), "{registrationId} path variable is required"); |
||||
this.loginProcessingUrl = loginProcessingUrl; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
@Override |
||||
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) { |
||||
return new AntPathRequestMatcher(loginProcessingUrl); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
* |
||||
* Initializes this filter chain for SAML 2 Login. |
||||
* The following actions are taken: |
||||
* <ul> |
||||
* <li>The WebSSO endpoint has CSRF disabled, typically {@code /login/saml2/sso}</li> |
||||
* <li>A {@link Saml2WebSsoAuthenticationFilter is configured}</li> |
||||
* <li>The {@code loginProcessingUrl} is set</li> |
||||
* <li>A custom login page is configured, <b>or</b></li> |
||||
* <li>A default login page with all SAML 2.0 Identity Providers is configured</li> |
||||
* <li>An {@link OpenSamlAuthenticationProvider} is configured</li> |
||||
* </ul> |
||||
*/ |
||||
@Override |
||||
public void init(B http) throws Exception { |
||||
registerDefaultCsrfOverride(http); |
||||
if (this.relyingPartyRegistrationRepository == null) { |
||||
this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class); |
||||
} |
||||
|
||||
Saml2WebSsoAuthenticationFilter webSsoFilter = new Saml2WebSsoAuthenticationFilter(this.relyingPartyRegistrationRepository); |
||||
setAuthenticationFilter(webSsoFilter); |
||||
super.loginProcessingUrl(this.loginProcessingUrl); |
||||
|
||||
if (hasText(this.loginPage)) { |
||||
// Set custom login page
|
||||
super.loginPage(this.loginPage); |
||||
super.init(http); |
||||
} else { |
||||
final Map<String, String> providerUrlMap = |
||||
getIdentityProviderUrlMap( |
||||
this.authenticationRequestEndpoint.filterProcessingUrl, |
||||
this.relyingPartyRegistrationRepository |
||||
); |
||||
|
||||
boolean singleProvider = providerUrlMap.size() == 1; |
||||
if (singleProvider) { |
||||
// Setup auto-redirect to provider login page
|
||||
// when only 1 IDP is configured
|
||||
this.updateAuthenticationDefaults(); |
||||
this.updateAccessDefaults(http); |
||||
|
||||
String loginUrl = providerUrlMap.entrySet().iterator().next().getKey(); |
||||
final LoginUrlAuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint(loginUrl); |
||||
registerAuthenticationEntryPoint(http, entryPoint); |
||||
} |
||||
else { |
||||
super.init(http); |
||||
} |
||||
} |
||||
http.authenticationProvider(getAuthenticationProvider()); |
||||
this.initDefaultLoginFilter(http); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
* |
||||
* During the {@code configure} phase, a {@link Saml2WebSsoAuthenticationRequestFilter} |
||||
* is added to handle SAML 2.0 AuthNRequest redirects |
||||
*/ |
||||
@Override |
||||
public void configure(B http) throws Exception { |
||||
http.addFilter(this.authenticationRequestEndpoint.build(http)); |
||||
super.configure(http); |
||||
} |
||||
|
||||
private AuthenticationProvider getAuthenticationProvider() { |
||||
AuthenticationProvider provider = new OpenSamlAuthenticationProvider(); |
||||
return postProcess(provider); |
||||
} |
||||
|
||||
private void registerDefaultCsrfOverride(B http) { |
||||
CsrfConfigurer<B> csrf = http.getConfigurer(CsrfConfigurer.class); |
||||
if (csrf == null) { |
||||
return; |
||||
} |
||||
|
||||
csrf.ignoringRequestMatchers( |
||||
new AntPathRequestMatcher(loginProcessingUrl) |
||||
); |
||||
} |
||||
|
||||
private void initDefaultLoginFilter(B http) { |
||||
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageGeneratingFilter.class); |
||||
if (loginPageGeneratingFilter == null || this.isCustomLoginPage()) { |
||||
return; |
||||
} |
||||
|
||||
loginPageGeneratingFilter.setSaml2LoginEnabled(true); |
||||
loginPageGeneratingFilter.setSaml2AuthenticationUrlToProviderName( |
||||
this.getIdentityProviderUrlMap( |
||||
this.authenticationRequestEndpoint.filterProcessingUrl, |
||||
this.relyingPartyRegistrationRepository |
||||
) |
||||
); |
||||
loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage()); |
||||
loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl()); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private Map<String, String> getIdentityProviderUrlMap( |
||||
String authRequestPrefixUrl, |
||||
RelyingPartyRegistrationRepository idpRepo |
||||
) { |
||||
Map<String, String> idps = new LinkedHashMap<>(); |
||||
if (idpRepo instanceof Iterable) { |
||||
Iterable<RelyingPartyRegistration> repo = (Iterable<RelyingPartyRegistration>) idpRepo; |
||||
repo.forEach( |
||||
p -> |
||||
idps.put( |
||||
authRequestPrefixUrl.replace("{registrationId}", p.getRegistrationId()), |
||||
p.getRegistrationId() |
||||
) |
||||
); |
||||
} |
||||
return idps; |
||||
} |
||||
|
||||
private <C> C getSharedOrBean(B http, Class<C> clazz) { |
||||
C shared = http.getSharedObject(clazz); |
||||
if (shared != null) { |
||||
return shared; |
||||
} |
||||
return getBeanOrNull(http, clazz); |
||||
} |
||||
|
||||
private <C> C getBeanOrNull(B http, Class<C> clazz) { |
||||
ApplicationContext context = http.getSharedObject(ApplicationContext.class); |
||||
if (context == null) { |
||||
return null; |
||||
} |
||||
try { |
||||
return context.getBean(clazz); |
||||
} catch (NoSuchBeanDefinitionException e) {} |
||||
return null; |
||||
} |
||||
|
||||
private <C> void setSharedObject(B http, Class<C> clazz, C object) { |
||||
if (http.getSharedObject(clazz) == null) { |
||||
http.setSharedObject(clazz, object); |
||||
} |
||||
} |
||||
|
||||
private final class AuthenticationRequestEndpointConfig { |
||||
private String filterProcessingUrl = "/saml2/authenticate/{registrationId}"; |
||||
private AuthenticationRequestEndpointConfig() { |
||||
} |
||||
|
||||
private Filter build(B http) { |
||||
Saml2AuthenticationRequestFactory authenticationRequestResolver = getResolver(http); |
||||
|
||||
Saml2WebSsoAuthenticationRequestFilter authenticationRequestFilter = |
||||
new Saml2WebSsoAuthenticationRequestFilter(Saml2LoginConfigurer.this.relyingPartyRegistrationRepository); |
||||
authenticationRequestFilter.setAuthenticationRequestFactory(authenticationRequestResolver); |
||||
return authenticationRequestFilter; |
||||
} |
||||
|
||||
private Saml2AuthenticationRequestFactory getResolver(B http) { |
||||
Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class); |
||||
if (resolver == null ) { |
||||
resolver = new OpenSamlAuthenticationRequestFactory(); |
||||
} |
||||
return resolver; |
||||
} |
||||
} |
||||
|
||||
|
||||
} |
||||
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'io.spring.convention.spring-module' |
||||
|
||||
dependencies { |
||||
compile project(':spring-security-core') |
||||
compile project(':spring-security-web') |
||||
|
||||
compile("org.opensaml:opensaml-core:3.3.0") |
||||
compile("org.opensaml:opensaml-saml-api:3.3.0") |
||||
compile("org.opensaml:opensaml-saml-impl:3.3.0") |
||||
|
||||
provided 'javax.servlet:javax.servlet-api' |
||||
} |
||||
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2; |
||||
|
||||
/** |
||||
* @since 5.2 |
||||
*/ |
||||
public class Saml2Exception extends RuntimeException { |
||||
|
||||
public Saml2Exception(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
public Saml2Exception(String message, Throwable cause) { |
||||
super(message, cause); |
||||
} |
||||
|
||||
public Saml2Exception(Throwable cause) { |
||||
super(cause); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,162 @@
@@ -0,0 +1,162 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.security.saml2.credentials; |
||||
|
||||
import java.security.PrivateKey; |
||||
import java.security.cert.X509Certificate; |
||||
import java.util.LinkedHashSet; |
||||
import java.util.Set; |
||||
|
||||
import static java.util.Arrays.asList; |
||||
import static org.springframework.util.Assert.notEmpty; |
||||
import static org.springframework.util.Assert.notNull; |
||||
import static org.springframework.util.Assert.state; |
||||
|
||||
/** |
||||
* Saml2X509Credential is meant to hold an X509 certificate, or an X509 certificate and a |
||||
* private key. Per: |
||||
* https://www.oasis-open.org/committees/download.php/8958/sstc-saml-implementation-guidelines-draft-01.pdf
|
||||
* Line: 584, Section 4.3 Credentials Used for both signing, signature verification and encryption/decryption |
||||
* |
||||
* @since 5.2 |
||||
*/ |
||||
public class Saml2X509Credential { |
||||
public enum Saml2X509CredentialType { |
||||
VERIFICATION, |
||||
ENCRYPTION, |
||||
SIGNING, |
||||
DECRYPTION, |
||||
} |
||||
|
||||
private final PrivateKey privateKey; |
||||
private final X509Certificate certificate; |
||||
private final Set<Saml2X509CredentialType> credentialTypes; |
||||
|
||||
/** |
||||
* Creates a Saml2X509Credentials representing Identity Provider credentials for |
||||
* verification, encryption or both. |
||||
* @param certificate an IDP X509Certificate, cannot be null |
||||
* @param types credential types, must be one of {@link Saml2X509CredentialType#VERIFICATION} or |
||||
* {@link Saml2X509CredentialType#ENCRYPTION} or both. |
||||
*/ |
||||
public Saml2X509Credential(X509Certificate certificate, Saml2X509CredentialType... types) { |
||||
this(null, false, certificate, types); |
||||
validateUsages(types, Saml2X509CredentialType.VERIFICATION, Saml2X509CredentialType.ENCRYPTION); |
||||
} |
||||
|
||||
/** |
||||
* Creates a Saml2X509Credentials representing Service Provider credentials for |
||||
* signing, decryption or both. |
||||
* @param privateKey a private key used for signing or decryption, cannot be null |
||||
* @param certificate an SP X509Certificate shared with identity providers, cannot be null |
||||
* @param types credential types, must be one of {@link Saml2X509CredentialType#SIGNING} or |
||||
* {@link Saml2X509CredentialType#DECRYPTION} or both. |
||||
*/ |
||||
public Saml2X509Credential(PrivateKey privateKey, X509Certificate certificate, Saml2X509CredentialType... types) { |
||||
this(privateKey, true, certificate, types); |
||||
validateUsages(types, Saml2X509CredentialType.SIGNING, Saml2X509CredentialType.DECRYPTION); |
||||
} |
||||
|
||||
private Saml2X509Credential( |
||||
PrivateKey privateKey, |
||||
boolean keyRequired, |
||||
X509Certificate certificate, |
||||
Saml2X509CredentialType... types) { |
||||
notNull(certificate, "certificate cannot be null"); |
||||
notEmpty(types, "credentials types cannot be empty"); |
||||
if (keyRequired) { |
||||
notNull(privateKey, "privateKey cannot be null"); |
||||
} |
||||
this.privateKey = privateKey; |
||||
this.certificate = certificate; |
||||
this.credentialTypes = new LinkedHashSet<>(asList(types)); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Returns true if the credential has a private key and can be used for signing, the types will contain |
||||
* {@link Saml2X509CredentialType#SIGNING}. |
||||
* @return true if the credential is a {@link Saml2X509CredentialType#SIGNING} type |
||||
*/ |
||||
public boolean isSigningCredential() { |
||||
return getCredentialTypes().contains(Saml2X509CredentialType.SIGNING); |
||||
} |
||||
|
||||
/** |
||||
* Returns true if the credential has a private key and can be used for decryption, the types will contain |
||||
* {@link Saml2X509CredentialType#DECRYPTION}. |
||||
* @return true if the credential is a {@link Saml2X509CredentialType#DECRYPTION} type |
||||
*/ |
||||
public boolean isDecryptionCredential() { |
||||
return getCredentialTypes().contains(Saml2X509CredentialType.DECRYPTION); |
||||
} |
||||
|
||||
/** |
||||
* Returns true if the credential has a certificate and can be used for signature verification, the types will contain |
||||
* {@link Saml2X509CredentialType#VERIFICATION}. |
||||
* @return true if the credential is a {@link Saml2X509CredentialType#VERIFICATION} type |
||||
*/ |
||||
public boolean isSignatureVerficationCredential() { |
||||
return getCredentialTypes().contains(Saml2X509CredentialType.VERIFICATION); |
||||
} |
||||
|
||||
/** |
||||
* Returns true if the credential has a certificate and can be used for signature verification, the types will contain |
||||
* {@link Saml2X509CredentialType#VERIFICATION}. |
||||
* @return true if the credential is a {@link Saml2X509CredentialType#VERIFICATION} type |
||||
*/ |
||||
public boolean isEncryptionCredential() { |
||||
return getCredentialTypes().contains(Saml2X509CredentialType.ENCRYPTION); |
||||
} |
||||
|
||||
/** |
||||
* Returns the credential types for this credential. |
||||
* @return a set of credential types/usages that this credential can be used for |
||||
*/ |
||||
protected Set<Saml2X509CredentialType> getCredentialTypes() { |
||||
return this.credentialTypes; |
||||
} |
||||
|
||||
/** |
||||
* Returns the private key, or null if this credential type doesn't require one. |
||||
* @return the private key, or null |
||||
* @see {@link #Saml2X509Credential(PrivateKey, X509Certificate, Saml2X509CredentialType...)} |
||||
*/ |
||||
public PrivateKey getPrivateKey() { |
||||
return this.privateKey; |
||||
} |
||||
|
||||
/** |
||||
* Returns the X509 certificate for ths credential. Cannot be null |
||||
* @return the X509 certificate |
||||
*/ |
||||
public X509Certificate getCertificate() { |
||||
return this.certificate; |
||||
} |
||||
|
||||
private void validateUsages(Saml2X509CredentialType[] usages, Saml2X509CredentialType... validUsages) { |
||||
for (Saml2X509CredentialType usage : usages) { |
||||
boolean valid = false; |
||||
for (Saml2X509CredentialType validUsage : validUsages) { |
||||
if (usage == validUsage) { |
||||
valid = true; |
||||
break; |
||||
} |
||||
} |
||||
state(valid, () -> usage +" is not a valid usage for this credential"); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,403 @@
@@ -0,0 +1,403 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.security.saml2.provider.service.authentication; |
||||
|
||||
import org.springframework.core.convert.converter.Converter; |
||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; |
||||
import org.springframework.security.authentication.AuthenticationProvider; |
||||
import org.springframework.security.authentication.AuthenticationServiceException; |
||||
import org.springframework.security.authentication.InsufficientAuthenticationException; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; |
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
||||
import org.springframework.security.saml2.Saml2Exception; |
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import org.opensaml.saml.common.SignableSAMLObject; |
||||
import org.opensaml.saml.common.assertion.AssertionValidationException; |
||||
import org.opensaml.saml.common.assertion.ValidationContext; |
||||
import org.opensaml.saml.common.assertion.ValidationResult; |
||||
import org.opensaml.saml.saml2.assertion.ConditionValidator; |
||||
import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator; |
||||
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; |
||||
import org.opensaml.saml.saml2.assertion.StatementValidator; |
||||
import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator; |
||||
import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator; |
||||
import org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator; |
||||
import org.opensaml.saml.saml2.core.Assertion; |
||||
import org.opensaml.saml.saml2.core.EncryptedAssertion; |
||||
import org.opensaml.saml.saml2.core.EncryptedID; |
||||
import org.opensaml.saml.saml2.core.NameID; |
||||
import org.opensaml.saml.saml2.core.Response; |
||||
import org.opensaml.saml.saml2.core.Subject; |
||||
import org.opensaml.saml.saml2.encryption.Decrypter; |
||||
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; |
||||
import org.opensaml.security.credential.Credential; |
||||
import org.opensaml.security.credential.CredentialResolver; |
||||
import org.opensaml.security.credential.CredentialSupport; |
||||
import org.opensaml.security.credential.impl.CollectionCredentialResolver; |
||||
import org.opensaml.xmlsec.config.DefaultSecurityConfigurationBootstrap; |
||||
import org.opensaml.xmlsec.encryption.support.DecryptionException; |
||||
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; |
||||
import org.opensaml.xmlsec.keyinfo.impl.StaticKeyInfoCredentialResolver; |
||||
import org.opensaml.xmlsec.signature.support.SignatureException; |
||||
import org.opensaml.xmlsec.signature.support.SignaturePrevalidator; |
||||
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; |
||||
import org.opensaml.xmlsec.signature.support.SignatureValidator; |
||||
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; |
||||
|
||||
import java.security.cert.X509Certificate; |
||||
import java.time.Duration; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
import static java.lang.String.format; |
||||
import static java.util.Collections.singleton; |
||||
import static java.util.Collections.singletonList; |
||||
import static org.springframework.util.Assert.notNull; |
||||
import static org.springframework.util.StringUtils.hasText; |
||||
|
||||
/** |
||||
* @since 5.2 |
||||
*/ |
||||
public final class OpenSamlAuthenticationProvider implements AuthenticationProvider { |
||||
|
||||
private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class); |
||||
|
||||
private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance(); |
||||
private Converter<Assertion, Collection<? extends GrantedAuthority>> authoritiesExtractor = (a -> singletonList(new SimpleGrantedAuthority("ROLE_USER"))); |
||||
private GrantedAuthoritiesMapper authoritiesMapper = (a -> a); |
||||
private Duration responseTimeValidationSkew = Duration.ofMinutes(5); |
||||
|
||||
/** |
||||
* Sets the {@link Converter} used for extracting assertion attributes that |
||||
* can be mapped to authorities. |
||||
* @param authoritiesExtractor the {@code Converter} used for mapping the |
||||
* assertion attributes to authorities |
||||
*/ |
||||
public void setAuthoritiesExtractor(Converter<Assertion, Collection<? extends GrantedAuthority>> authoritiesExtractor) { |
||||
Assert.notNull(authoritiesExtractor, "authoritiesExtractor cannot be null"); |
||||
this.authoritiesExtractor = authoritiesExtractor; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link GrantedAuthoritiesMapper} used for mapping assertion attributes |
||||
* to a new set of authorities which will be associated to the {@link Saml2Authentication}. |
||||
* Note: This implementation is only retrieving |
||||
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the user's authorities |
||||
*/ |
||||
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { |
||||
notNull(authoritiesMapper, "authoritiesMapper cannot be null"); |
||||
this.authoritiesMapper = authoritiesMapper; |
||||
} |
||||
|
||||
/** |
||||
* Sets the duration for how much time skew an assertion may tolerate during |
||||
* timestamp, NotOnOrBefore and NotOnOrAfter, validation. |
||||
* @param responseTimeValidationSkew duration for skew tolerance |
||||
*/ |
||||
public void setResponseTimeValidationSkew(Duration responseTimeValidationSkew) { |
||||
this.responseTimeValidationSkew = responseTimeValidationSkew; |
||||
} |
||||
|
||||
/** |
||||
* @param authentication the authentication request object, must be of type |
||||
* {@link Saml2AuthenticationToken} |
||||
* |
||||
* @return {@link Saml2Authentication} if the assertion is valid |
||||
* @throws AuthenticationException if a validation exception occurs |
||||
*/ |
||||
@Override |
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
||||
try { |
||||
Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication; |
||||
String xml = token.getSaml2Response(); |
||||
Response samlResponse = getSaml2Response(xml); |
||||
|
||||
Assertion assertion = validateSaml2Response(token, token.getRecipientUri(), samlResponse); |
||||
final String username = getUsername(token, assertion); |
||||
if (username == null) { |
||||
throw new UsernameNotFoundException("Assertion [" + |
||||
assertion.getID() + |
||||
"] is missing a user identifier"); |
||||
} |
||||
return new Saml2Authentication( |
||||
() -> username, token.getSaml2Response(), |
||||
this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion)) |
||||
); |
||||
}catch (Saml2Exception | IllegalArgumentException e) { |
||||
throw new AuthenticationServiceException(e.getMessage(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
@Override |
||||
public boolean supports(Class<?> authentication) { |
||||
return authentication != null && Saml2AuthenticationToken.class.isAssignableFrom(authentication); |
||||
} |
||||
|
||||
private Collection<? extends GrantedAuthority> getAssertionAuthorities(Assertion assertion) { |
||||
return this.authoritiesExtractor.convert(assertion); |
||||
} |
||||
|
||||
private String getUsername(Saml2AuthenticationToken token, Assertion assertion) { |
||||
final Subject subject = assertion.getSubject(); |
||||
if (subject == null) { |
||||
return null; |
||||
} |
||||
if (subject.getNameID() != null) { |
||||
return subject.getNameID().getValue(); |
||||
} |
||||
if (subject.getEncryptedID() != null) { |
||||
NameID nameId = decrypt(token, subject.getEncryptedID()); |
||||
return nameId.getValue(); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private Assertion validateSaml2Response(Saml2AuthenticationToken token, |
||||
String recipient, |
||||
Response samlResponse) throws AuthenticationException { |
||||
if (hasText(samlResponse.getDestination()) && !recipient.equals(samlResponse.getDestination())) { |
||||
throw new Saml2Exception("Invalid SAML response destination: " + samlResponse.getDestination()); |
||||
} |
||||
|
||||
final String issuer = samlResponse.getIssuer().getValue(); |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Processing SAML response from " + issuer); |
||||
} |
||||
if (token == null) { |
||||
throw new Saml2Exception(format("SAML 2 Provider for %s was not found.", issuer)); |
||||
} |
||||
boolean responseSigned = hasValidSignature(samlResponse, token); |
||||
for (Assertion a : samlResponse.getAssertions()) { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Checking plain assertion validity " + a); |
||||
} |
||||
if (isValidAssertion(recipient, a, token, !responseSigned)) { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Found valid assertion. Skipping potential others."); |
||||
} |
||||
return a; |
||||
} |
||||
} |
||||
for (EncryptedAssertion ea : samlResponse.getEncryptedAssertions()) { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Checking encrypted assertion validity " + ea); |
||||
} |
||||
|
||||
Assertion a = decrypt(token, ea); |
||||
if (isValidAssertion(recipient, a, token, false)) { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Found valid encrypted assertion. Skipping potential others."); |
||||
} |
||||
return a; |
||||
} |
||||
} |
||||
throw new InsufficientAuthenticationException("Unable to find a valid assertion"); |
||||
} |
||||
|
||||
private boolean hasValidSignature(SignableSAMLObject samlResponse, Saml2AuthenticationToken token) { |
||||
if (!samlResponse.isSigned()) { |
||||
return false; |
||||
} |
||||
|
||||
final List<X509Certificate> verificationKeys = getVerificationKeys(token); |
||||
if (verificationKeys.isEmpty()) { |
||||
return false; |
||||
} |
||||
|
||||
for (X509Certificate key : verificationKeys) { |
||||
final Credential credential = getVerificationCredential(key); |
||||
try { |
||||
SignatureValidator.validate(samlResponse.getSignature(), credential); |
||||
return true; |
||||
} |
||||
catch (SignatureException ignored) { |
||||
logger.debug("Signature validation failed", ignored); |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private boolean isValidAssertion(String recipient, Assertion a, Saml2AuthenticationToken token, boolean signatureRequired) { |
||||
final SAML20AssertionValidator validator = getAssertionValidator(token); |
||||
Map<String, Object> validationParams = new HashMap<>(); |
||||
validationParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false); |
||||
validationParams.put( |
||||
SAML2AssertionValidationParameters.CLOCK_SKEW, |
||||
this.responseTimeValidationSkew |
||||
); |
||||
validationParams.put( |
||||
SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, |
||||
singleton(token.getLocalSpEntityId()) |
||||
); |
||||
if (hasText(recipient)) { |
||||
validationParams.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, singleton(recipient)); |
||||
} |
||||
|
||||
if (signatureRequired && !hasValidSignature(a, token)) { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug(format("Assertion [%s] does not a valid signature.", a.getID())); |
||||
} |
||||
return false; |
||||
} |
||||
a.setSignature(null); |
||||
|
||||
// validation for recipient
|
||||
ValidationContext vctx = new ValidationContext(validationParams); |
||||
try { |
||||
final ValidationResult result = validator.validate(a, vctx); |
||||
final boolean valid = result.equals(ValidationResult.VALID); |
||||
if (!valid) { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug(format("Failed to validate assertion from %s with user %s", token.getIdpEntityId(), |
||||
getUsername(token, a) |
||||
)); |
||||
} |
||||
} |
||||
return valid; |
||||
} |
||||
catch (AssertionValidationException e) { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Failed to validate assertion:", e); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
} |
||||
|
||||
private Response getSaml2Response(String xml) throws Saml2Exception, AuthenticationException { |
||||
final Object result = this.saml.resolve(xml); |
||||
if (result == null) { |
||||
throw new AuthenticationCredentialsNotFoundException("SAMLResponse returned null object"); |
||||
} |
||||
else if (result instanceof Response) { |
||||
return (Response) result; |
||||
} |
||||
throw new IllegalArgumentException("Invalid response class:"+result.getClass().getName()); |
||||
} |
||||
|
||||
private SAML20AssertionValidator getAssertionValidator(Saml2AuthenticationToken provider) { |
||||
List<ConditionValidator> conditions = Collections.singletonList(new AudienceRestrictionConditionValidator()); |
||||
final BearerSubjectConfirmationValidator subjectConfirmationValidator = |
||||
new BearerSubjectConfirmationValidator(); |
||||
|
||||
List<SubjectConfirmationValidator> subjects = Collections.singletonList(subjectConfirmationValidator); |
||||
List<StatementValidator> statements = Collections.emptyList(); |
||||
|
||||
Set<Credential> credentials = new HashSet<>(); |
||||
for (X509Certificate key : getVerificationKeys(provider)) { |
||||
final Credential cred = getVerificationCredential(key); |
||||
credentials.add(cred); |
||||
} |
||||
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); |
||||
SignatureTrustEngine signatureTrustEngine = new ExplicitKeySignatureTrustEngine( |
||||
credentialsResolver, |
||||
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver() |
||||
); |
||||
SignaturePrevalidator signaturePrevalidator = new SAMLSignatureProfileValidator(); |
||||
return new SAML20AssertionValidator( |
||||
conditions, |
||||
subjects, |
||||
statements, |
||||
signatureTrustEngine, |
||||
signaturePrevalidator |
||||
); |
||||
} |
||||
|
||||
private Credential getVerificationCredential(X509Certificate certificate) { |
||||
return CredentialSupport.getSimpleCredential(certificate, null); |
||||
} |
||||
|
||||
private Decrypter getDecrypter(Saml2X509Credential key) { |
||||
Credential credential = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); |
||||
KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(credential); |
||||
Decrypter decrypter = new Decrypter(null, resolver, this.saml.getEncryptedKeyResolver()); |
||||
decrypter.setRootInNewDocument(true); |
||||
return decrypter; |
||||
} |
||||
|
||||
private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion) { |
||||
Saml2Exception last = null; |
||||
List<Saml2X509Credential> decryptionCredentials = getDecryptionCredentials(token); |
||||
if (decryptionCredentials.isEmpty()) { |
||||
throw new Saml2Exception("No valid decryption credentials found."); |
||||
} |
||||
for (Saml2X509Credential key : decryptionCredentials) { |
||||
final Decrypter decrypter = getDecrypter(key); |
||||
try { |
||||
return decrypter.decrypt(assertion); |
||||
} |
||||
catch (DecryptionException e) { |
||||
last = new Saml2Exception(e); |
||||
} |
||||
} |
||||
throw last; |
||||
} |
||||
|
||||
private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) { |
||||
Saml2Exception last = null; |
||||
List<Saml2X509Credential> decryptionCredentials = getDecryptionCredentials(token); |
||||
if (decryptionCredentials.isEmpty()) { |
||||
throw new Saml2Exception("No valid decryption credentials found."); |
||||
} |
||||
for (Saml2X509Credential key : decryptionCredentials) { |
||||
final Decrypter decrypter = getDecrypter(key); |
||||
try { |
||||
return (NameID) decrypter.decrypt(assertion); |
||||
} |
||||
catch (DecryptionException e) { |
||||
last = new Saml2Exception(e); |
||||
} |
||||
} |
||||
throw last; |
||||
} |
||||
|
||||
private List<Saml2X509Credential> getDecryptionCredentials(Saml2AuthenticationToken token) { |
||||
List<Saml2X509Credential> result = new LinkedList<>(); |
||||
for (Saml2X509Credential c : token.getX509Credentials()) { |
||||
if (c.isDecryptionCredential()) { |
||||
result.add(c); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
private List<X509Certificate> getVerificationKeys(Saml2AuthenticationToken token) { |
||||
List<X509Certificate> result = new LinkedList<>(); |
||||
for (Saml2X509Credential c : token.getX509Credentials()) { |
||||
if (c.isSignatureVerficationCredential()) { |
||||
result.add(c.getCertificate()); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
import org.joda.time.DateTime; |
||||
import org.opensaml.core.xml.io.MarshallingException; |
||||
import org.opensaml.saml.saml2.core.AuthnRequest; |
||||
import org.opensaml.saml.saml2.core.Issuer; |
||||
import org.opensaml.security.SecurityException; |
||||
import org.opensaml.xmlsec.signature.support.SignatureException; |
||||
|
||||
import java.time.Clock; |
||||
import java.time.Instant; |
||||
import java.util.UUID; |
||||
|
||||
/** |
||||
* @since 5.2 |
||||
*/ |
||||
public class OpenSamlAuthenticationRequestFactory implements Saml2AuthenticationRequestFactory { |
||||
private Clock clock = Clock.systemUTC(); |
||||
private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance(); |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
@Override |
||||
public String createAuthenticationRequest(Saml2AuthenticationRequest request) { |
||||
AuthnRequest auth = this.saml.buildSAMLObject(AuthnRequest.class); |
||||
auth.setID("ARQ" + UUID.randomUUID().toString().substring(1)); |
||||
auth.setIssueInstant(new DateTime(this.clock.millis())); |
||||
auth.setForceAuthn(Boolean.FALSE); |
||||
auth.setIsPassive(Boolean.FALSE); |
||||
auth.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"); |
||||
Issuer issuer = this.saml.buildSAMLObject(Issuer.class); |
||||
issuer.setValue(request.getLocalSpEntityId()); |
||||
auth.setIssuer(issuer); |
||||
auth.setDestination(request.getWebSsoUri()); |
||||
try { |
||||
return this.saml.toXml( |
||||
auth, |
||||
request.getCredentials(), |
||||
request.getLocalSpEntityId() |
||||
); |
||||
} |
||||
catch (MarshallingException | SignatureException | SecurityException e) { |
||||
throw new IllegalStateException(e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* ' |
||||
* Use this {@link Clock} with {@link Instant#now()} for generating |
||||
* timestamps |
||||
* |
||||
* @param clock |
||||
*/ |
||||
public void setClock(Clock clock) { |
||||
Assert.notNull(clock, "clock cannot be null"); |
||||
this.clock = clock; |
||||
} |
||||
} |
||||
@ -0,0 +1,255 @@
@@ -0,0 +1,255 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.security.saml2.provider.service.authentication; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import javax.xml.XMLConstants; |
||||
import javax.xml.namespace.QName; |
||||
|
||||
import org.springframework.security.saml2.Saml2Exception; |
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential; |
||||
|
||||
import net.shibboleth.utilities.java.support.component.ComponentInitializationException; |
||||
import net.shibboleth.utilities.java.support.xml.BasicParserPool; |
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport; |
||||
import net.shibboleth.utilities.java.support.xml.XMLParserException; |
||||
import org.opensaml.core.config.ConfigurationService; |
||||
import org.opensaml.core.config.InitializationException; |
||||
import org.opensaml.core.config.InitializationService; |
||||
import org.opensaml.core.xml.XMLObject; |
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistry; |
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; |
||||
import org.opensaml.core.xml.io.MarshallerFactory; |
||||
import org.opensaml.core.xml.io.MarshallingException; |
||||
import org.opensaml.core.xml.io.UnmarshallerFactory; |
||||
import org.opensaml.core.xml.io.UnmarshallingException; |
||||
import org.opensaml.saml.common.SignableSAMLObject; |
||||
import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; |
||||
import org.opensaml.security.SecurityException; |
||||
import org.opensaml.security.credential.BasicCredential; |
||||
import org.opensaml.security.credential.Credential; |
||||
import org.opensaml.security.credential.CredentialSupport; |
||||
import org.opensaml.security.credential.UsageType; |
||||
import org.opensaml.security.x509.BasicX509Credential; |
||||
import org.opensaml.xmlsec.SignatureSigningParameters; |
||||
import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; |
||||
import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; |
||||
import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; |
||||
import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; |
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants; |
||||
import org.opensaml.xmlsec.signature.support.SignatureException; |
||||
import org.opensaml.xmlsec.signature.support.SignatureSupport; |
||||
import org.w3c.dom.Document; |
||||
import org.w3c.dom.Element; |
||||
|
||||
import static java.lang.Boolean.FALSE; |
||||
import static java.lang.Boolean.TRUE; |
||||
import static java.util.Arrays.asList; |
||||
import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getBuilderFactory; |
||||
|
||||
/** |
||||
* @since 5.2 |
||||
*/ |
||||
final class OpenSamlImplementation { |
||||
private static OpenSamlImplementation instance = new OpenSamlImplementation(); |
||||
|
||||
private final BasicParserPool parserPool = new BasicParserPool(); |
||||
private final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( |
||||
asList( |
||||
new InlineEncryptedKeyResolver(), |
||||
new EncryptedElementTypeEncryptedKeyResolver(), |
||||
new SimpleRetrievalMethodEncryptedKeyResolver() |
||||
) |
||||
); |
||||
|
||||
private OpenSamlImplementation() { |
||||
bootstrap(); |
||||
} |
||||
|
||||
/* |
||||
* ============================================================== |
||||
* PRIVATE METHODS |
||||
* ============================================================== |
||||
*/ |
||||
private void bootstrap() { |
||||
// configure default values
|
||||
// maxPoolSize = 5;
|
||||
this.parserPool.setMaxPoolSize(50); |
||||
// coalescing = true;
|
||||
this.parserPool.setCoalescing(true); |
||||
// expandEntityReferences = false;
|
||||
this.parserPool.setExpandEntityReferences(false); |
||||
// ignoreComments = true;
|
||||
this.parserPool.setIgnoreComments(true); |
||||
// ignoreElementContentWhitespace = true;
|
||||
this.parserPool.setIgnoreElementContentWhitespace(true); |
||||
// namespaceAware = true;
|
||||
this.parserPool.setNamespaceAware(true); |
||||
// schema = null;
|
||||
this.parserPool.setSchema(null); |
||||
// dtdValidating = false;
|
||||
this.parserPool.setDTDValidating(false); |
||||
// xincludeAware = false;
|
||||
this.parserPool.setXincludeAware(false); |
||||
|
||||
Map<String, Object> builderAttributes = new HashMap<>(); |
||||
this.parserPool.setBuilderAttributes(builderAttributes); |
||||
|
||||
Map<String, Boolean> parserBuilderFeatures = new HashMap<>(); |
||||
parserBuilderFeatures.put("http://apache.org/xml/features/disallow-doctype-decl", TRUE); |
||||
parserBuilderFeatures.put(XMLConstants.FEATURE_SECURE_PROCESSING, TRUE); |
||||
parserBuilderFeatures.put("http://xml.org/sax/features/external-general-entities", FALSE); |
||||
parserBuilderFeatures.put("http://apache.org/xml/features/validation/schema/normalized-value", FALSE); |
||||
parserBuilderFeatures.put("http://xml.org/sax/features/external-parameter-entities", FALSE); |
||||
parserBuilderFeatures.put("http://apache.org/xml/features/dom/defer-node-expansion", FALSE); |
||||
this.parserPool.setBuilderFeatures(parserBuilderFeatures); |
||||
|
||||
try { |
||||
this.parserPool.initialize(); |
||||
} |
||||
catch (ComponentInitializationException x) { |
||||
throw new Saml2Exception("Unable to initialize OpenSaml v3 ParserPool", x); |
||||
} |
||||
|
||||
try { |
||||
InitializationService.initialize(); |
||||
} |
||||
catch (InitializationException e) { |
||||
throw new Saml2Exception("Unable to initialize OpenSaml v3", e); |
||||
} |
||||
|
||||
XMLObjectProviderRegistry registry; |
||||
synchronized (ConfigurationService.class) { |
||||
registry = ConfigurationService.get(XMLObjectProviderRegistry.class); |
||||
if (registry == null) { |
||||
registry = new XMLObjectProviderRegistry(); |
||||
ConfigurationService.register(XMLObjectProviderRegistry.class, registry); |
||||
} |
||||
} |
||||
|
||||
registry.setParserPool(this.parserPool); |
||||
} |
||||
|
||||
/* |
||||
* ============================================================== |
||||
* PUBLIC METHODS |
||||
* ============================================================== |
||||
*/ |
||||
static OpenSamlImplementation getInstance() { |
||||
return instance; |
||||
} |
||||
|
||||
EncryptedKeyResolver getEncryptedKeyResolver() { |
||||
return this.encryptedKeyResolver; |
||||
} |
||||
|
||||
<T> T buildSAMLObject(final Class<T> clazz) { |
||||
try { |
||||
QName defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); |
||||
return (T) getBuilderFactory().getBuilder(defaultElementName).buildObject(defaultElementName); |
||||
} |
||||
catch (IllegalAccessException e) { |
||||
throw new Saml2Exception("Could not create SAML object", e); |
||||
} |
||||
catch (NoSuchFieldException e) { |
||||
throw new Saml2Exception("Could not create SAML object", e); |
||||
} |
||||
} |
||||
|
||||
XMLObject resolve(String xml) { |
||||
return resolve(xml.getBytes(StandardCharsets.UTF_8)); |
||||
} |
||||
|
||||
private XMLObject resolve(byte[] xml) { |
||||
XMLObject parsed = parse(xml); |
||||
if (parsed != null) { |
||||
return parsed; |
||||
} |
||||
throw new Saml2Exception("Deserialization not supported for given data set"); |
||||
} |
||||
|
||||
private XMLObject parse(byte[] xml) { |
||||
try { |
||||
Document document = this.parserPool.parse(new ByteArrayInputStream(xml)); |
||||
Element element = document.getDocumentElement(); |
||||
return getUnmarshallerFactory().getUnmarshaller(element).unmarshall(element); |
||||
} |
||||
catch (UnmarshallingException | XMLParserException e) { |
||||
throw new Saml2Exception(e); |
||||
} |
||||
} |
||||
|
||||
private UnmarshallerFactory getUnmarshallerFactory() { |
||||
return XMLObjectProviderRegistrySupport.getUnmarshallerFactory(); |
||||
} |
||||
|
||||
String toXml(XMLObject object, List<Saml2X509Credential> signingCredentials, String localSpEntityId) |
||||
throws MarshallingException, SignatureException, SecurityException { |
||||
if (object instanceof SignableSAMLObject && null != hasSigningCredential(signingCredentials)) { |
||||
signXmlObject( |
||||
(SignableSAMLObject) object, |
||||
getSigningCredential(signingCredentials, localSpEntityId) |
||||
); |
||||
} |
||||
final MarshallerFactory marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory(); |
||||
Element element = marshallerFactory.getMarshaller(object).marshall(object); |
||||
return SerializeSupport.nodeToString(element); |
||||
} |
||||
|
||||
private Saml2X509Credential hasSigningCredential(List<Saml2X509Credential> credentials) { |
||||
for (Saml2X509Credential c : credentials) { |
||||
if (c.isSigningCredential()) { |
||||
return c; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private void signXmlObject(SignableSAMLObject object, Credential credential) |
||||
throws MarshallingException, SecurityException, SignatureException { |
||||
SignatureSigningParameters parameters = new SignatureSigningParameters(); |
||||
parameters.setSigningCredential(credential); |
||||
parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); |
||||
parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); |
||||
parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); |
||||
SignatureSupport.signObject(object, parameters); |
||||
} |
||||
|
||||
private Credential getSigningCredential(List<Saml2X509Credential> signingCredential, |
||||
String localSpEntityId |
||||
) { |
||||
Saml2X509Credential credential = hasSigningCredential(signingCredential); |
||||
if (credential == null) { |
||||
throw new IllegalArgumentException("no signing credential configured"); |
||||
} |
||||
BasicCredential cred = getBasicCredential(credential); |
||||
cred.setEntityId(localSpEntityId); |
||||
cred.setUsageType(UsageType.SIGNING); |
||||
return cred; |
||||
} |
||||
|
||||
private BasicX509Credential getBasicCredential(Saml2X509Credential credential) { |
||||
return CredentialSupport.getSimpleCredential( |
||||
credential.getCertificate(), |
||||
credential.getPrivateKey() |
||||
); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication; |
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
import org.springframework.security.core.AuthenticatedPrincipal; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import java.util.Collection; |
||||
|
||||
/** |
||||
* An implementation of an {@link AbstractAuthenticationToken} |
||||
* that represents an authenticated SAML 2.0 {@link Authentication}. |
||||
* <p> |
||||
* The {@link Authentication} associates valid SAML assertion |
||||
* data with a Spring Security authentication object |
||||
* The complete assertion is contained in the object in String format, |
||||
* {@link Saml2Authentication#getSaml2Response()} |
||||
* @since 5.2 |
||||
* @see AbstractAuthenticationToken |
||||
*/ |
||||
public class Saml2Authentication extends AbstractAuthenticationToken { |
||||
|
||||
private final AuthenticatedPrincipal principal; |
||||
private final String saml2Response; |
||||
|
||||
public Saml2Authentication(AuthenticatedPrincipal principal, |
||||
String saml2Response, |
||||
Collection<? extends GrantedAuthority> authorities) { |
||||
super(authorities); |
||||
Assert.notNull(principal, "principal cannot be null"); |
||||
Assert.hasText(saml2Response, "saml2Response cannot be null"); |
||||
this.principal = principal; |
||||
this.saml2Response = saml2Response; |
||||
setAuthenticated(true); |
||||
} |
||||
|
||||
@Override |
||||
public Object getPrincipal() { |
||||
return this.principal; |
||||
} |
||||
|
||||
/** |
||||
* Returns the SAML response object, as decoded XML. May contain encrypted elements |
||||
* @return string representation of the SAML Response XML object |
||||
*/ |
||||
public String getSaml2Response() { |
||||
return this.saml2Response; |
||||
} |
||||
|
||||
@Override |
||||
public Object getCredentials() { |
||||
return getSaml2Response(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication; |
||||
|
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* Data holder for information required to send an {@code AuthNRequest} |
||||
* from the service provider to the identity provider |
||||
* |
||||
* @see {@link Saml2AuthenticationRequestFactory} |
||||
* @see https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf (line 2031)
|
||||
* @since 5.2 |
||||
*/ |
||||
public class Saml2AuthenticationRequest { |
||||
private final String localSpEntityId; |
||||
private final List<Saml2X509Credential> credentials; |
||||
private String webSsoUri; |
||||
|
||||
public Saml2AuthenticationRequest(String localSpEntityId, String webSsoUri, List<Saml2X509Credential> credentials) { |
||||
Assert.hasText(localSpEntityId, "localSpEntityId cannot be null"); |
||||
Assert.hasText(localSpEntityId, "webSsoUri cannot be null"); |
||||
this.localSpEntityId = localSpEntityId; |
||||
this.webSsoUri = webSsoUri; |
||||
this.credentials = new LinkedList<>(); |
||||
for (Saml2X509Credential c : credentials) { |
||||
if (c.isSigningCredential()) { |
||||
this.credentials.add(c); |
||||
} |
||||
} |
||||
Assert.notEmpty(this.credentials, "at least one SIGNING credential must be present"); |
||||
} |
||||
|
||||
|
||||
public String getLocalSpEntityId() { |
||||
return this.localSpEntityId; |
||||
} |
||||
|
||||
public String getWebSsoUri() { |
||||
return this.webSsoUri; |
||||
} |
||||
|
||||
public List<Saml2X509Credential> getCredentials() { |
||||
return this.credentials; |
||||
} |
||||
} |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication; |
||||
|
||||
/** |
||||
* Component that generates an AuthenticationRequest, <code>samlp:AuthnRequestType</code> as defined by |
||||
* https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf
|
||||
* Page 50, Line 2147 |
||||
* |
||||
* @since 5.2 |
||||
*/ |
||||
public interface Saml2AuthenticationRequestFactory { |
||||
/** |
||||
* Creates an authentication request from the Service Provider, sp, |
||||
* to the Identity Provider, idp. |
||||
* The authentication result is an XML string that may be signed, encrypted, both or neither. |
||||
* |
||||
* @param request - information about the identity provider, the recipient of this authentication request and |
||||
* accompanying data |
||||
* @return XML data in the format of a String. This data may be signed, encrypted, both signed and encrypted or |
||||
* neither signed and encrypted |
||||
*/ |
||||
String createAuthenticationRequest(Saml2AuthenticationRequest request); |
||||
} |
||||
@ -0,0 +1,133 @@
@@ -0,0 +1,133 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication; |
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* Represents an incoming SAML 2.0 response containing an assertion that has not been validated. |
||||
* {@link Saml2AuthenticationToken#isAuthenticated()} will always return false. |
||||
* @since 5.2 |
||||
*/ |
||||
public class Saml2AuthenticationToken extends AbstractAuthenticationToken { |
||||
|
||||
private final String saml2Response; |
||||
private final String recipientUri; |
||||
private String idpEntityId; |
||||
private String localSpEntityId; |
||||
private List<Saml2X509Credential> credentials; |
||||
|
||||
/** |
||||
* Creates an authentication token from an incoming SAML 2 Response object |
||||
* @param saml2Response inflated and decoded XML representation of the SAML 2 Response |
||||
* @param recipientUri the URL that the SAML 2 Response was received at. Used for validation |
||||
* @param idpEntityId the entity ID of the asserting entity |
||||
* @param localSpEntityId the configured local SP, the relying party, entity ID |
||||
* @param credentials the credentials configured for signature verification and decryption |
||||
*/ |
||||
public Saml2AuthenticationToken(String saml2Response, |
||||
String recipientUri, |
||||
String idpEntityId, |
||||
String localSpEntityId, |
||||
List<Saml2X509Credential> credentials) { |
||||
super(null); |
||||
this.saml2Response = saml2Response; |
||||
this.recipientUri = recipientUri; |
||||
this.idpEntityId = idpEntityId; |
||||
this.localSpEntityId = localSpEntityId; |
||||
this.credentials = credentials; |
||||
} |
||||
|
||||
/** |
||||
* Returns the decoded and inflated SAML 2.0 Response XML object as a string |
||||
* @return decoded and inflated XML data as a {@link String} |
||||
*/ |
||||
@Override |
||||
public Object getCredentials() { |
||||
return getSaml2Response(); |
||||
} |
||||
|
||||
/** |
||||
* Always returns null. |
||||
* @return null |
||||
*/ |
||||
@Override |
||||
public Object getPrincipal() { |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Returns inflated and decoded XML representation of the SAML 2 Response |
||||
* @return inflated and decoded XML representation of the SAML 2 Response |
||||
*/ |
||||
public String getSaml2Response() { |
||||
return this.saml2Response; |
||||
} |
||||
|
||||
/** |
||||
* Returns the URI that the SAML 2 Response object came in on |
||||
* @return URI as a string |
||||
*/ |
||||
public String getRecipientUri() { |
||||
return this.recipientUri; |
||||
} |
||||
|
||||
/** |
||||
* Returns the configured entity ID of the receiving relying party, SP |
||||
* @return an entityID for the configured local relying party |
||||
*/ |
||||
public String getLocalSpEntityId() { |
||||
return this.localSpEntityId; |
||||
} |
||||
|
||||
/** |
||||
* Returns all the credentials associated with the relying party configuraiton |
||||
* @return |
||||
*/ |
||||
public List<Saml2X509Credential> getX509Credentials() { |
||||
return this.credentials; |
||||
} |
||||
|
||||
/** |
||||
* @return false |
||||
*/ |
||||
@Override |
||||
public boolean isAuthenticated() { |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* The state of this object cannot be changed. Will always throw an exception |
||||
* @param authenticated ignored |
||||
* @throws {@link IllegalArgumentException} |
||||
*/ |
||||
@Override |
||||
public void setAuthenticated(boolean authenticated) { |
||||
throw new IllegalArgumentException(); |
||||
} |
||||
|
||||
/** |
||||
* Returns the configured IDP, asserting party, entity ID |
||||
* @return a string representing the entity ID |
||||
*/ |
||||
public String getIdpEntityId() { |
||||
return this.idpEntityId; |
||||
} |
||||
} |
||||
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.registration; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.Iterator; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
import static java.util.Arrays.asList; |
||||
import static org.springframework.util.Assert.notEmpty; |
||||
import static org.springframework.util.Assert.notNull; |
||||
|
||||
/** |
||||
* @since 5.2 |
||||
*/ |
||||
public class InMemoryRelyingPartyRegistrationRepository |
||||
implements RelyingPartyRegistrationRepository, Iterable<RelyingPartyRegistration> { |
||||
|
||||
private final Map<String, RelyingPartyRegistration> byRegistrationId; |
||||
|
||||
public InMemoryRelyingPartyRegistrationRepository(RelyingPartyRegistration... registrations) { |
||||
this(asList(registrations)); |
||||
} |
||||
|
||||
public InMemoryRelyingPartyRegistrationRepository(Collection<RelyingPartyRegistration> registrations) { |
||||
notEmpty(registrations, "registrations cannot be empty"); |
||||
this.byRegistrationId = createMappingToIdentityProvider(registrations); |
||||
} |
||||
|
||||
private static Map<String, RelyingPartyRegistration> createMappingToIdentityProvider( |
||||
Collection<RelyingPartyRegistration> rps |
||||
) { |
||||
LinkedHashMap<String, RelyingPartyRegistration> result = new LinkedHashMap<>(); |
||||
for (RelyingPartyRegistration rp : rps) { |
||||
notNull(rp, "relying party collection cannot contain null values"); |
||||
String key = rp.getRegistrationId(); |
||||
notNull(rp, "relying party identifier cannot be null"); |
||||
Assert.isNull(result.get(key), () -> "relying party duplicate identifier '" + key+"' detected."); |
||||
result.put(key, rp); |
||||
} |
||||
return Collections.unmodifiableMap(result); |
||||
} |
||||
|
||||
@Override |
||||
public RelyingPartyRegistration findByRegistrationId(String id) { |
||||
return this.byRegistrationId.get(id); |
||||
} |
||||
|
||||
@Override |
||||
public Iterator<RelyingPartyRegistration> iterator() { |
||||
return this.byRegistrationId.values().iterator(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,304 @@
@@ -0,0 +1,304 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.registration; |
||||
|
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential; |
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
import java.util.function.Consumer; |
||||
import java.util.function.Function; |
||||
|
||||
import static java.util.Collections.unmodifiableList; |
||||
import static org.springframework.util.Assert.hasText; |
||||
import static org.springframework.util.Assert.notEmpty; |
||||
import static org.springframework.util.Assert.notNull; |
||||
|
||||
/** |
||||
* Represents a configured service provider, SP, and a remote identity provider, IDP, pair. |
||||
* Each SP/IDP pair is uniquely identified using a <code>registrationId</code>, an arbitrary string. |
||||
* A fully configured registration may look like |
||||
* <pre> |
||||
* //remote IDP entity ID
|
||||
* String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"; |
||||
* //remote WebSSO Endpoint - Where to Send AuthNRequests to
|
||||
* String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php"; |
||||
* //local registration ID
|
||||
* String registrationId = "simplesamlphp"; |
||||
* //local entity ID - autogenerated based on URL
|
||||
* String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; |
||||
* //local SSO URL - autogenerated, endpoint to receive SAML Response objects
|
||||
* String acsUrlTemplate = "{baseUrl}/login/saml2/sso/{registrationId}"; |
||||
* //local signing (and local decryption key and remote encryption certificate)
|
||||
* Saml2X509Credential signingCredential = getSigningCredential(); |
||||
* //IDP certificate for verification of incoming messages
|
||||
* Saml2X509Credential idpVerificationCertificate = getVerificationCertificate(); |
||||
* RelyingPartyRegistration rp = RelyingPartyRegistration.withRegistrationId(registrationId) |
||||
* .remoteIdpEntityId(idpEntityId) |
||||
* .idpWebSsoUrl(webSsoEndpoint) |
||||
* .credentials(c -> c.add(signingCredential)) |
||||
* .credentials(c -> c.add(idpVerificationCertificate)) |
||||
* .localEntityIdTemplate(localEntityIdTemplate) |
||||
* .assertionConsumerServiceUrlTemplate(acsTemplate) |
||||
* .build(); |
||||
* </pre> |
||||
* @since 5.2 |
||||
*/ |
||||
public class RelyingPartyRegistration { |
||||
|
||||
private final String registrationId; |
||||
private final String remoteIdpEntityId; |
||||
private final String assertionConsumerServiceUrlTemplate; |
||||
private final String idpWebSsoUrl; |
||||
private final List<Saml2X509Credential> credentials; |
||||
private final String localEntityIdTemplate; |
||||
|
||||
private RelyingPartyRegistration(String idpEntityId, String registrationId, String assertionConsumerServiceUrlTemplate, |
||||
String idpWebSsoUri, List<Saml2X509Credential> credentials, String localEntityIdTemplate) { |
||||
hasText(idpEntityId, "idpEntityId cannot be empty"); |
||||
hasText(registrationId, "registrationId cannot be empty"); |
||||
hasText(assertionConsumerServiceUrlTemplate, "assertionConsumerServiceUrlTemplate cannot be empty"); |
||||
hasText(localEntityIdTemplate, "localEntityIdTemplate cannot be empty"); |
||||
notEmpty(credentials, "credentials cannot be empty"); |
||||
notNull(idpWebSsoUri, "idpWebSsoUri cannot be empty"); |
||||
for (Saml2X509Credential c : credentials) { |
||||
notNull(c, "credentials cannot contain null elements"); |
||||
} |
||||
this.registrationId = registrationId; |
||||
this.remoteIdpEntityId = idpEntityId; |
||||
this.assertionConsumerServiceUrlTemplate = assertionConsumerServiceUrlTemplate; |
||||
this.credentials = unmodifiableList(new LinkedList<>(credentials)); |
||||
this.idpWebSsoUrl = idpWebSsoUri; |
||||
this.localEntityIdTemplate = localEntityIdTemplate; |
||||
} |
||||
|
||||
/** |
||||
* Returns the entity ID of the IDP, the asserting party. |
||||
* @return entity ID of the asserting party |
||||
*/ |
||||
public String getRemoteIdpEntityId() { |
||||
return this.remoteIdpEntityId; |
||||
} |
||||
|
||||
/** |
||||
* Returns the unique relying party registration ID |
||||
* @return registrationId |
||||
*/ |
||||
public String getRegistrationId() { |
||||
return this.registrationId; |
||||
} |
||||
|
||||
/** |
||||
* returns the URL template for which ACS URL authentication requests should contain |
||||
* Possible variables are {@code baseUrl}, {@code registrationId}, |
||||
* {@code baseScheme}, {@code baseHost}, and {@code basePort}. |
||||
* @return string containing the ACS URL template, with or without variables present |
||||
*/ |
||||
public String getAssertionConsumerServiceUrlTemplate() { |
||||
return this.assertionConsumerServiceUrlTemplate; |
||||
} |
||||
|
||||
/** |
||||
* Contains the URL for which to send the SAML 2 Authentication Request to initiate |
||||
* a single sign on flow. |
||||
* @return a IDP URL that accepts REDIRECT or POST binding for authentication requests |
||||
*/ |
||||
public String getIdpWebSsoUrl() { |
||||
return this.idpWebSsoUrl; |
||||
} |
||||
|
||||
/** |
||||
* The local relying party, or Service Provider, can generate it's entity ID based on |
||||
* possible variables of {@code baseUrl}, {@code registrationId}, |
||||
* {@code baseScheme}, {@code baseHost}, and {@code basePort}, for example |
||||
* {@code {baseUrl}/saml2/service-provider-metadata/{registrationId}} |
||||
* @return a string containing the entity ID or entity ID template |
||||
*/ |
||||
public String getLocalEntityIdTemplate() { |
||||
return this.localEntityIdTemplate; |
||||
} |
||||
|
||||
/** |
||||
* Returns a list of configured credentials to be used in message exchanges between relying party, SP, and |
||||
* asserting party, IDP. |
||||
* @return a list of credentials |
||||
*/ |
||||
public List<Saml2X509Credential> getCredentials() { |
||||
return this.credentials; |
||||
} |
||||
|
||||
/** |
||||
* @return a filtered list containing only credentials of type |
||||
* {@link Saml2X509CredentialType#VERIFICATION}. |
||||
* Returns an empty list of credentials are not found |
||||
*/ |
||||
public List<Saml2X509Credential> getVerificationCredentials() { |
||||
return filterCredentials(c -> c.isSignatureVerficationCredential()); |
||||
} |
||||
|
||||
/** |
||||
* @return a filtered list containing only credentials of type |
||||
* {@link Saml2X509CredentialType#SIGNING}. |
||||
* Returns an empty list of credentials are not found |
||||
*/ |
||||
public List<Saml2X509Credential> getSigningCredentials() { |
||||
return filterCredentials(c -> c.isSigningCredential()); |
||||
} |
||||
|
||||
/** |
||||
* @return a filtered list containing only credentials of type |
||||
* {@link Saml2X509CredentialType#ENCRYPTION}. |
||||
* Returns an empty list of credentials are not found |
||||
*/ |
||||
public List<Saml2X509Credential> getEncryptionCredentials() { |
||||
return filterCredentials(c -> c.isEncryptionCredential()); |
||||
} |
||||
|
||||
/** |
||||
* @return a filtered list containing only credentials of type |
||||
* {@link Saml2X509CredentialType#DECRYPTION}. |
||||
* Returns an empty list of credentials are not found |
||||
*/ |
||||
public List<Saml2X509Credential> getDecryptionCredentials() { |
||||
return filterCredentials(c -> c.isDecryptionCredential()); |
||||
} |
||||
|
||||
private List<Saml2X509Credential> filterCredentials(Function<Saml2X509Credential, Boolean> filter) { |
||||
List<Saml2X509Credential> result = new LinkedList<>(); |
||||
for (Saml2X509Credential c : getCredentials()) { |
||||
if (filter.apply(c)) { |
||||
result.add(c); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Creates a {@code RelyingPartyRegistration} {@link Builder} with a known {@code registrationId} |
||||
* @param registrationId a string identifier for the {@code RelyingPartyRegistration} |
||||
* @return {@code Builder} to create a {@code RelyingPartyRegistration} object |
||||
*/ |
||||
public static Builder withRegistrationId(String registrationId) { |
||||
Assert.hasText(registrationId, "registrationId cannot be empty"); |
||||
return new Builder(registrationId); |
||||
} |
||||
|
||||
public static class Builder { |
||||
private String registrationId; |
||||
private String remoteIdpEntityId; |
||||
private String idpWebSsoUrl; |
||||
private String assertionConsumerServiceUrlTemplate; |
||||
private List<Saml2X509Credential> credentials = new LinkedList<>(); |
||||
private String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; |
||||
|
||||
private Builder(String registrationId) { |
||||
this.registrationId = registrationId; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Sets the {@code registrationId} template. Often be used in URL paths |
||||
* @param id registrationId for this object, should be unique |
||||
* @return this object |
||||
*/ |
||||
public Builder registrationId(String id) { |
||||
this.registrationId = id; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@code entityId} for the remote asserting party, the Identity Provider. |
||||
* @param entityId the IDP entityId |
||||
* @return this object |
||||
*/ |
||||
public Builder remoteIdpEntityId(String entityId) { |
||||
this.remoteIdpEntityId = entityId; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/AssertionConsumerService">Assertion Consumer |
||||
* Service</a> URL template. It can contain variables {@code baseUrl}, {@code registrationId}, |
||||
* {@code baseScheme}, {@code baseHost}, and {@code basePort}. |
||||
* @param assertionConsumerServiceUrlTemplate the Assertion Consumer Service URL template (i.e. |
||||
* "{baseUrl}/login/saml2/sso/{registrationId}". |
||||
* @return this object |
||||
*/ |
||||
public Builder assertionConsumerServiceUrlTemplate(String assertionConsumerServiceUrlTemplate) { |
||||
this.assertionConsumerServiceUrlTemplate = assertionConsumerServiceUrlTemplate; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@code SSO URL} for the remote asserting party, the Identity Provider. |
||||
* @param url - a URL that accepts authentication requests via REDIRECT or POST bindings |
||||
* @return this object |
||||
*/ |
||||
public Builder idpWebSsoUrl(String url) { |
||||
this.idpWebSsoUrl = url; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Modifies the collection of {@link Saml2X509Credential} objects |
||||
* used in communication between IDP and SP |
||||
* For example: |
||||
* <code> |
||||
* Saml2X509Credential credential = ...; |
||||
* return RelyingPartyRegistration.withRegistrationId("id") |
||||
* .credentials(c -> c.add(credential)) |
||||
* ... |
||||
* .build(); |
||||
* </code> |
||||
* @param credentials - a consumer that can modify the collection of credentials |
||||
* @return this object |
||||
*/ |
||||
public Builder credentials(Consumer<Collection<Saml2X509Credential>> credentials) { |
||||
credentials.accept(this.credentials); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the local relying party, or Service Provider, entity Id template. |
||||
* can generate it's entity ID based on possible variables of {@code baseUrl}, {@code registrationId}, |
||||
* {@code baseScheme}, {@code baseHost}, and {@code basePort}, for example |
||||
* {@code {baseUrl}/saml2/service-provider-metadata/{registrationId}} |
||||
* @return a string containing the entity ID or entity ID template |
||||
*/ |
||||
|
||||
public Builder localEntityIdTemplate(String template) { |
||||
this.localEntityIdTemplate = template; |
||||
return this; |
||||
} |
||||
|
||||
public RelyingPartyRegistration build() { |
||||
return new RelyingPartyRegistration( |
||||
remoteIdpEntityId, |
||||
registrationId, |
||||
assertionConsumerServiceUrlTemplate, |
||||
idpWebSsoUrl, |
||||
credentials, |
||||
localEntityIdTemplate |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
} |
||||
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.registration; |
||||
|
||||
/** |
||||
* Resolves a {@link RelyingPartyRegistration}, a configured service provider and remote identity provider pair, |
||||
* by entityId or registrationId |
||||
* @since 5.2 |
||||
*/ |
||||
public interface RelyingPartyRegistrationRepository { |
||||
|
||||
/** |
||||
* Resolves an {@link RelyingPartyRegistration} by registrationId, or returns the default provider |
||||
* if no registrationId is provided |
||||
* |
||||
* @param registrationId - a provided registrationId, may be be null or empty |
||||
* @return {@link RelyingPartyRegistration} if found, {@code null} if an registrationId is provided and |
||||
* no registration is found. Returns a default, implementation specific, |
||||
* {@link RelyingPartyRegistration} if no registrationId is provided |
||||
*/ |
||||
RelyingPartyRegistration findByRegistrationId(String registrationId); |
||||
|
||||
} |
||||
@ -0,0 +1,134 @@
@@ -0,0 +1,134 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.servlet.filter; |
||||
|
||||
import org.springframework.security.saml2.Saml2Exception; |
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; |
||||
import org.springframework.util.StringUtils; |
||||
import org.springframework.web.util.UriComponents; |
||||
import org.springframework.web.util.UriComponentsBuilder; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
import java.util.Base64; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.zip.Deflater; |
||||
import java.util.zip.DeflaterOutputStream; |
||||
import java.util.zip.Inflater; |
||||
import java.util.zip.InflaterOutputStream; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8; |
||||
import static java.util.zip.Deflater.DEFLATED; |
||||
import static org.springframework.security.web.util.UrlUtils.buildFullRequestUrl; |
||||
import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl; |
||||
|
||||
/** |
||||
* @since 5.2 |
||||
*/ |
||||
final class Saml2Utils { |
||||
|
||||
private static final char PATH_DELIMITER = '/'; |
||||
private static Base64.Encoder ENCODER = Base64.getEncoder(); |
||||
private static Base64.Decoder DECODER = Base64.getDecoder(); |
||||
|
||||
static String encode(byte[] b) { |
||||
return ENCODER.encodeToString(b); |
||||
} |
||||
|
||||
static byte[] decode(String s) { |
||||
return DECODER.decode(s); |
||||
} |
||||
|
||||
static byte[] deflate(String s) { |
||||
try { |
||||
ByteArrayOutputStream b = new ByteArrayOutputStream(); |
||||
DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(DEFLATED, true)); |
||||
deflater.write(s.getBytes(UTF_8)); |
||||
deflater.finish(); |
||||
return b.toByteArray(); |
||||
} |
||||
catch (IOException e) { |
||||
throw new Saml2Exception("Unable to deflate string", e); |
||||
} |
||||
} |
||||
|
||||
static String inflate(byte[] b) { |
||||
try { |
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(); |
||||
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true)); |
||||
iout.write(b); |
||||
iout.finish(); |
||||
return new String(out.toByteArray(), UTF_8); |
||||
} |
||||
catch (IOException e) { |
||||
throw new Saml2Exception("Unable to inflate string", e); |
||||
} |
||||
} |
||||
|
||||
static String getServiceProviderEntityId(RelyingPartyRegistration rp, HttpServletRequest request) { |
||||
return resolveUrlTemplate( |
||||
rp.getLocalEntityIdTemplate(), |
||||
getApplicationUri(request), |
||||
rp.getRemoteIdpEntityId(), |
||||
rp.getRegistrationId() |
||||
); |
||||
} |
||||
|
||||
static String resolveUrlTemplate(String template, String baseUrl, String entityId, String registrationId) { |
||||
if (!StringUtils.hasText(template)) { |
||||
return baseUrl; |
||||
} |
||||
|
||||
Map<String, String> uriVariables = new HashMap<>(); |
||||
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(baseUrl) |
||||
.replaceQuery(null) |
||||
.fragment(null) |
||||
.build(); |
||||
String scheme = uriComponents.getScheme(); |
||||
uriVariables.put("baseScheme", scheme == null ? "" : scheme); |
||||
String host = uriComponents.getHost(); |
||||
uriVariables.put("baseHost", host == null ? "" : host); |
||||
// following logic is based on HierarchicalUriComponents#toUriString()
|
||||
int port = uriComponents.getPort(); |
||||
uriVariables.put("basePort", port == -1 ? "" : ":" + port); |
||||
String path = uriComponents.getPath(); |
||||
if (StringUtils.hasLength(path)) { |
||||
if (path.charAt(0) != PATH_DELIMITER) { |
||||
path = PATH_DELIMITER + path; |
||||
} |
||||
} |
||||
uriVariables.put("basePath", path == null ? "" : path); |
||||
uriVariables.put("baseUrl", uriComponents.toUriString()); |
||||
uriVariables.put("entityId", StringUtils.hasText(entityId) ? entityId : ""); |
||||
uriVariables.put("registrationId", StringUtils.hasText(registrationId) ? registrationId : ""); |
||||
|
||||
return UriComponentsBuilder.fromUriString(template) |
||||
.buildAndExpand(uriVariables) |
||||
.toUriString(); |
||||
} |
||||
|
||||
static String getApplicationUri(HttpServletRequest request) { |
||||
UriComponents uriComponents = fromHttpUrl(buildFullRequestUrl(request)) |
||||
.replacePath(request.getContextPath()) |
||||
.replaceQuery(null) |
||||
.fragment(null) |
||||
.build(); |
||||
return uriComponents.toUriString(); |
||||
} |
||||
} |
||||
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.servlet.filter; |
||||
|
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; |
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; |
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; |
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; |
||||
import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy; |
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8; |
||||
import static org.springframework.util.StringUtils.hasText; |
||||
|
||||
/** |
||||
* @since 5.2 |
||||
*/ |
||||
public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProcessingFilter { |
||||
|
||||
public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/saml2/sso/{registrationId}"; |
||||
private final RequestMatcher matcher; |
||||
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; |
||||
|
||||
public Saml2WebSsoAuthenticationFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { |
||||
super(DEFAULT_FILTER_PROCESSES_URI); |
||||
Assert.notNull(relyingPartyRegistrationRepository, "relyingPartyRegistrationRepository cannot be null"); |
||||
this.matcher = new AntPathRequestMatcher(DEFAULT_FILTER_PROCESSES_URI); |
||||
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository; |
||||
setAllowSessionCreation(true); |
||||
setSessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy()); |
||||
} |
||||
|
||||
@Override |
||||
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { |
||||
return (super.requiresAuthentication(request, response) && hasText(request.getParameter("SAMLResponse"))); |
||||
} |
||||
|
||||
@Override |
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) |
||||
throws AuthenticationException { |
||||
String saml2Response = request.getParameter("SAMLResponse"); |
||||
byte[] b = Saml2Utils.decode(saml2Response); |
||||
|
||||
String responseXml = inflateIfRequired(request, b); |
||||
RelyingPartyRegistration rp = |
||||
this.relyingPartyRegistrationRepository.findByRegistrationId(this.matcher.matcher(request).getVariables().get("registrationId")); |
||||
String localSpEntityId = Saml2Utils.getServiceProviderEntityId(rp, request); |
||||
final Saml2AuthenticationToken authentication = new Saml2AuthenticationToken( |
||||
responseXml, |
||||
request.getRequestURL().toString(), |
||||
rp.getRemoteIdpEntityId(), |
||||
localSpEntityId, |
||||
rp.getCredentials() |
||||
); |
||||
return getAuthenticationManager().authenticate(authentication); |
||||
} |
||||
|
||||
private String inflateIfRequired(HttpServletRequest request, byte[] b) { |
||||
if (HttpMethod.GET.matches(request.getMethod())) { |
||||
return Saml2Utils.inflate(b); |
||||
} |
||||
else { |
||||
return new String(b, UTF_8); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.servlet.filter; |
||||
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory; |
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequest; |
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory; |
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; |
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; |
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.filter.OncePerRequestFilter; |
||||
import org.springframework.web.util.UriComponentsBuilder; |
||||
import org.springframework.web.util.UriUtils; |
||||
|
||||
import java.io.IOException; |
||||
import java.nio.charset.StandardCharsets; |
||||
import javax.servlet.FilterChain; |
||||
import javax.servlet.ServletException; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import static java.lang.String.format; |
||||
import static org.springframework.security.saml2.provider.service.servlet.filter.Saml2Utils.deflate; |
||||
import static org.springframework.security.saml2.provider.service.servlet.filter.Saml2Utils.encode; |
||||
|
||||
/** |
||||
* @since 5.2 |
||||
*/ |
||||
public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter { |
||||
|
||||
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; |
||||
|
||||
private RequestMatcher redirectMatcher = new AntPathRequestMatcher("/saml2/authenticate/{registrationId}"); |
||||
|
||||
private Saml2AuthenticationRequestFactory authenticationRequestFactory = new OpenSamlAuthenticationRequestFactory(); |
||||
|
||||
public Saml2WebSsoAuthenticationRequestFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { |
||||
Assert.notNull(relyingPartyRegistrationRepository, "relyingPartyRegistrationRepository cannot be null"); |
||||
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository; |
||||
} |
||||
|
||||
public void setAuthenticationRequestFactory(Saml2AuthenticationRequestFactory authenticationRequestFactory) { |
||||
Assert.notNull(authenticationRequestFactory, "authenticationRequestFactory cannot be null"); |
||||
this.authenticationRequestFactory = authenticationRequestFactory; |
||||
} |
||||
|
||||
public void setRedirectMatcher(RequestMatcher redirectMatcher) { |
||||
Assert.notNull(redirectMatcher, "redirectMatcher cannot be null"); |
||||
this.redirectMatcher = redirectMatcher; |
||||
} |
||||
|
||||
@Override |
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
||||
throws ServletException, IOException { |
||||
MatchResult matcher = this.redirectMatcher.matcher(request); |
||||
if (!matcher.isMatch()) { |
||||
filterChain.doFilter(request, response); |
||||
return; |
||||
} |
||||
|
||||
String registrationId = matcher.getVariables().get("registrationId"); |
||||
sendRedirect(request, response, registrationId); |
||||
} |
||||
|
||||
private void sendRedirect(HttpServletRequest request, HttpServletResponse response, String registrationId) |
||||
throws IOException { |
||||
if (this.logger.isDebugEnabled()) { |
||||
this.logger.debug(format("Creating SAML2 SP Authentication Request for IDP[%s]", registrationId)); |
||||
} |
||||
RelyingPartyRegistration relyingParty = this.relyingPartyRegistrationRepository.findByRegistrationId(registrationId); |
||||
String redirectUrl = createSamlRequestRedirectUrl(request, relyingParty); |
||||
response.sendRedirect(redirectUrl); |
||||
} |
||||
|
||||
private String createSamlRequestRedirectUrl(HttpServletRequest request, RelyingPartyRegistration relyingParty) { |
||||
Saml2AuthenticationRequest authNRequest = createAuthenticationRequest(relyingParty, request); |
||||
String xml = this.authenticationRequestFactory.createAuthenticationRequest(authNRequest); |
||||
String encoded = encode(deflate(xml)); |
||||
String relayState = request.getParameter("RelayState"); |
||||
String redirect = UriComponentsBuilder |
||||
.fromUriString(relyingParty.getIdpWebSsoUrl()) |
||||
.queryParam("SAMLRequest", UriUtils.encode(encoded, StandardCharsets.ISO_8859_1)) |
||||
.queryParam("RelayState", UriUtils.encode(relayState, StandardCharsets.ISO_8859_1)) |
||||
.build(true) |
||||
.toUriString(); |
||||
return redirect; |
||||
} |
||||
|
||||
private Saml2AuthenticationRequest createAuthenticationRequest(RelyingPartyRegistration relyingParty, HttpServletRequest request) { |
||||
String localSpEntityId = Saml2Utils.getServiceProviderEntityId(relyingParty, request); |
||||
return new Saml2AuthenticationRequest( |
||||
localSpEntityId, |
||||
Saml2Utils.resolveUrlTemplate( |
||||
relyingParty.getAssertionConsumerServiceUrlTemplate(), |
||||
Saml2Utils.getApplicationUri(request), |
||||
relyingParty.getRemoteIdpEntityId(), |
||||
relyingParty.getRegistrationId() |
||||
), |
||||
relyingParty.getSigningCredentials() |
||||
); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
public class OpenSamlImplementationTests { |
||||
|
||||
@Test |
||||
public void getInstance() { |
||||
OpenSamlImplementation.getInstance(); |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
<configuration> |
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
||||
<encoder> |
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> |
||||
</encoder> |
||||
</appender> |
||||
|
||||
<logger name="org.springframework.security" level="${sec.log.level:-WARN}"/> |
||||
|
||||
<root level="${root.level:-WARN}"> |
||||
<appender-ref ref="STDOUT"/> |
||||
</root> |
||||
|
||||
</configuration> |
||||
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
= OAuth 2.0 Login Sample |
||||
|
||||
This guide provides instructions on setting up the sample application with SAML 2.0 Login using |
||||
Spring Security's `saml2Login()` feature. |
||||
|
||||
The sample application uses Spring Boot 2.2.0.M5 and the `spring-security-saml2-service-provider` |
||||
module which is new in Spring Security 5.2. |
||||
|
||||
== Goals |
||||
|
||||
`saml2Login()` provides a very simple, basic, implementation of a Service Provider |
||||
that can receive a SAML 2 Response XML object via the HTTP-POST and HTTP-REDIRECT bindings |
||||
against a known SAML reference implementation by SimpleSAMLPhp. |
||||
|
||||
|
||||
The following features are implemented in the MVP |
||||
|
||||
1. Receive and validate a SAML 2.0 Response object containing an assertion |
||||
and create a valid authentication in Spring Security |
||||
2. Send a SAML 2 AuthNRequest object to an Identity Provider |
||||
3. Provide a framework for components used in SAML 2.0 authentication that can |
||||
be swapped by configuration |
||||
4. Sample working against the SimpleSAMLPhP reference implementation |
||||
|
||||
== Run the Sample |
||||
|
||||
=== Start up the Sample Boot Application |
||||
``` |
||||
./gradlew :spring-security-samples-boot-saml2login:bootRun |
||||
``` |
||||
|
||||
=== Open a Browser |
||||
|
||||
http://localhost:8080/ |
||||
|
||||
You will be redirect to the SimpleSAMLPhp IDP |
||||
|
||||
=== Type in your credentials |
||||
|
||||
``` |
||||
User: user |
||||
Password: password |
||||
``` |
||||
|
||||
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot' |
||||
|
||||
dependencies { |
||||
compile project(':spring-security-config') |
||||
compile project(':spring-security-saml2-service-provider') |
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf' |
||||
compile 'org.springframework.boot:spring-boot-starter-web' |
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' |
||||
|
||||
testCompile project(':spring-security-test') |
||||
testCompile 'net.sourceforge.htmlunit:htmlunit' |
||||
testCompile 'org.springframework.boot:spring-boot-starter-test' |
||||
} |
||||
@ -0,0 +1,513 @@
@@ -0,0 +1,513 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.samples; |
||||
|
||||
import org.springframework.security.saml2.Saml2Exception; |
||||
|
||||
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty; |
||||
import org.apache.commons.codec.binary.Base64; |
||||
import org.apache.xml.security.algorithms.JCEMapper; |
||||
import org.apache.xml.security.encryption.XMLCipherParameters; |
||||
import org.joda.time.DateTime; |
||||
import org.joda.time.Duration; |
||||
import org.junit.Assert; |
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; |
||||
import org.opensaml.profile.action.EventIds; |
||||
import org.opensaml.profile.context.EventContext; |
||||
import org.opensaml.profile.context.ProfileRequestContext; |
||||
import org.opensaml.saml.common.SAMLObjectBuilder; |
||||
import org.opensaml.saml.common.SAMLVersion; |
||||
import org.opensaml.saml.saml2.core.Artifact; |
||||
import org.opensaml.saml.saml2.core.ArtifactResolve; |
||||
import org.opensaml.saml.saml2.core.ArtifactResponse; |
||||
import org.opensaml.saml.saml2.core.Assertion; |
||||
import org.opensaml.saml.saml2.core.AttributeQuery; |
||||
import org.opensaml.saml.saml2.core.AttributeStatement; |
||||
import org.opensaml.saml.saml2.core.AuthnRequest; |
||||
import org.opensaml.saml.saml2.core.AuthnStatement; |
||||
import org.opensaml.saml.saml2.core.Conditions; |
||||
import org.opensaml.saml.saml2.core.EncryptedAssertion; |
||||
import org.opensaml.saml.saml2.core.EncryptedID; |
||||
import org.opensaml.saml.saml2.core.Issuer; |
||||
import org.opensaml.saml.saml2.core.LogoutRequest; |
||||
import org.opensaml.saml.saml2.core.LogoutResponse; |
||||
import org.opensaml.saml.saml2.core.NameID; |
||||
import org.opensaml.saml.saml2.core.Response; |
||||
import org.opensaml.saml.saml2.core.Subject; |
||||
import org.opensaml.saml.saml2.core.SubjectConfirmation; |
||||
import org.opensaml.saml.saml2.core.SubjectConfirmationData; |
||||
import org.opensaml.saml.saml2.encryption.Encrypter; |
||||
import org.opensaml.security.credential.BasicCredential; |
||||
import org.opensaml.security.credential.Credential; |
||||
import org.opensaml.security.credential.CredentialSupport; |
||||
import org.opensaml.xmlsec.encryption.support.DataEncryptionParameters; |
||||
import org.opensaml.xmlsec.encryption.support.EncryptionException; |
||||
import org.opensaml.xmlsec.encryption.support.KeyEncryptionParameters; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
import java.security.NoSuchAlgorithmException; |
||||
import java.security.NoSuchProviderException; |
||||
import java.security.cert.X509Certificate; |
||||
import java.util.zip.Deflater; |
||||
import java.util.zip.DeflaterOutputStream; |
||||
import java.util.zip.Inflater; |
||||
import java.util.zip.InflaterOutputStream; |
||||
import javax.annotation.Nonnull; |
||||
import javax.annotation.Nullable; |
||||
import javax.crypto.SecretKey; |
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8; |
||||
import static java.util.Arrays.asList; |
||||
import static java.util.zip.Deflater.DEFLATED; |
||||
import static org.opensaml.security.crypto.KeySupport.generateKey; |
||||
|
||||
/** |
||||
* <b>Copied from OpenSAML Source Code</b> Helper methods for creating/testing SAML 2 |
||||
* objects within profile action tests. When methods herein refer to mock objects they are |
||||
* always objects that have been created via Mockito unless otherwise noted. |
||||
*/ |
||||
public class OpenSamlActionTestingSupport { |
||||
|
||||
static Base64 UNCHUNKED_ENCODER = new Base64(0, new byte[] { '\n' }); |
||||
|
||||
/** ID used for all generated {@link Response} objects. */ |
||||
final static String REQUEST_ID = "request"; |
||||
|
||||
/** ID used for all generated {@link Response} objects. */ |
||||
final static String RESPONSE_ID = "response"; |
||||
|
||||
/** ID used for all generated {@link Assertion} objects. */ |
||||
final static String ASSERTION_ID = "assertion"; |
||||
|
||||
static String encode(byte[] b) { |
||||
return UNCHUNKED_ENCODER.encodeToString(b); |
||||
} |
||||
|
||||
static byte[] decode(String s) { |
||||
return UNCHUNKED_ENCODER.decode(s); |
||||
} |
||||
|
||||
static byte[] deflate(String s) { |
||||
try { |
||||
ByteArrayOutputStream b = new ByteArrayOutputStream(); |
||||
DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(DEFLATED, true)); |
||||
deflater.write(s.getBytes(UTF_8)); |
||||
deflater.finish(); |
||||
return b.toByteArray(); |
||||
} |
||||
catch (IOException e) { |
||||
throw new Saml2Exception("Unable to deflate string", e); |
||||
} |
||||
} |
||||
|
||||
static String inflate(byte[] b) { |
||||
try { |
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(); |
||||
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true)); |
||||
iout.write(b); |
||||
iout.finish(); |
||||
return new String(out.toByteArray(), UTF_8); |
||||
} |
||||
catch (IOException e) { |
||||
throw new Saml2Exception("Unable to inflate string", e); |
||||
} |
||||
} |
||||
|
||||
static EncryptedAssertion encryptAssertion(Assertion assertion, X509Certificate certificate) { |
||||
Encrypter encrypter = getEncrypter(certificate); |
||||
try { |
||||
Encrypter.KeyPlacement keyPlacement = Encrypter.KeyPlacement.valueOf("PEER"); |
||||
encrypter.setKeyPlacement(keyPlacement); |
||||
return encrypter.encrypt(assertion); |
||||
} |
||||
catch (EncryptionException e) { |
||||
throw new Saml2Exception("Unable to encrypt assertion.", e); |
||||
} |
||||
} |
||||
|
||||
static EncryptedID encryptNameId(NameID nameID, X509Certificate certificate) { |
||||
Encrypter encrypter = getEncrypter(certificate); |
||||
try { |
||||
Encrypter.KeyPlacement keyPlacement = Encrypter.KeyPlacement.valueOf("PEER"); |
||||
encrypter.setKeyPlacement(keyPlacement); |
||||
return encrypter.encrypt(nameID); |
||||
} |
||||
catch (EncryptionException e) { |
||||
throw new Saml2Exception("Unable to encrypt nameID.", e); |
||||
} |
||||
} |
||||
|
||||
static Encrypter getEncrypter(X509Certificate certificate) { |
||||
Credential credential = CredentialSupport.getSimpleCredential(certificate, null); |
||||
final String dataAlgorithm = XMLCipherParameters.AES_256; |
||||
final String keyAlgorithm = XMLCipherParameters.RSA_1_5; |
||||
SecretKey secretKey = generateKeyFromURI(dataAlgorithm); |
||||
BasicCredential dataCredential = new BasicCredential(secretKey); |
||||
DataEncryptionParameters dataEncryptionParameters = new DataEncryptionParameters(); |
||||
dataEncryptionParameters.setEncryptionCredential(dataCredential); |
||||
dataEncryptionParameters.setAlgorithm(dataAlgorithm); |
||||
|
||||
KeyEncryptionParameters keyEncryptionParameters = new KeyEncryptionParameters(); |
||||
keyEncryptionParameters.setEncryptionCredential(credential); |
||||
keyEncryptionParameters.setAlgorithm(keyAlgorithm); |
||||
|
||||
Encrypter encrypter = new Encrypter(dataEncryptionParameters, asList(keyEncryptionParameters)); |
||||
|
||||
return encrypter; |
||||
} |
||||
|
||||
static SecretKey generateKeyFromURI(String algoURI) { |
||||
try { |
||||
String jceAlgorithmName = JCEMapper.getJCEKeyAlgorithmFromURI(algoURI); |
||||
int keyLength = JCEMapper.getKeyLengthFromURI(algoURI); |
||||
return generateKey(jceAlgorithmName, keyLength, null); |
||||
} |
||||
catch (NoSuchAlgorithmException | NoSuchProviderException e) { |
||||
throw new Saml2Exception(e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Builds an empty response. The ID of the message is {@link #OUTBOUND_MSG_ID}, the |
||||
* issue instant is 1970-01-01T00:00:00Z and the SAML version is |
||||
* {@link SAMLVersion#VERSION_11}. |
||||
* @return the constructed response |
||||
*/ |
||||
@Nonnull |
||||
static Response buildResponse() { |
||||
final SAMLObjectBuilder<Response> responseBuilder = (SAMLObjectBuilder<Response>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<Response>getBuilderOrThrow(Response.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final Response response = responseBuilder.buildObject(); |
||||
response.setID(OUTBOUND_MSG_ID); |
||||
response.setIssueInstant(DateTime.now()); |
||||
response.setVersion(SAMLVersion.VERSION_20); |
||||
|
||||
return response; |
||||
} |
||||
|
||||
/** |
||||
* Builds an empty artifact response. The ID of the message is |
||||
* {@link #OUTBOUND_MSG_ID}, the issue instant is 1970-01-01T00:00:00Z and the SAML |
||||
* version is {@link SAMLVersion#VERSION_11}. |
||||
* @return the constructed response |
||||
*/ |
||||
@Nonnull |
||||
static ArtifactResponse buildArtifactResponse() { |
||||
final SAMLObjectBuilder<ArtifactResponse> responseBuilder = (SAMLObjectBuilder<ArtifactResponse>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<ArtifactResponse>getBuilderOrThrow(ArtifactResponse.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final ArtifactResponse response = responseBuilder.buildObject(); |
||||
response.setID(OUTBOUND_MSG_ID); |
||||
response.setIssueInstant(DateTime.now()); |
||||
response.setVersion(SAMLVersion.VERSION_20); |
||||
|
||||
return response; |
||||
} |
||||
|
||||
/** |
||||
* Builds an {@link LogoutRequest}. If a {@link NameID} is given, it will be added to |
||||
* the constructed {@link LogoutRequest}. |
||||
* @param name the NameID to add to the request |
||||
* @return the built request |
||||
*/ |
||||
@Nonnull |
||||
static LogoutRequest buildLogoutRequest(final @Nullable NameID name) { |
||||
final SAMLObjectBuilder<Issuer> issuerBuilder = (SAMLObjectBuilder<Issuer>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<Issuer>getBuilderOrThrow(Issuer.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final SAMLObjectBuilder<LogoutRequest> reqBuilder = (SAMLObjectBuilder<LogoutRequest>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<LogoutRequest>getBuilderOrThrow(LogoutRequest.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final Issuer issuer = issuerBuilder.buildObject(); |
||||
issuer.setValue(INBOUND_MSG_ISSUER); |
||||
|
||||
final LogoutRequest req = reqBuilder.buildObject(); |
||||
req.setID(REQUEST_ID); |
||||
req.setIssueInstant(DateTime.now()); |
||||
req.setIssuer(issuer); |
||||
req.setVersion(SAMLVersion.VERSION_20); |
||||
|
||||
if (name != null) { |
||||
req.setNameID(name); |
||||
} |
||||
|
||||
return req; |
||||
} |
||||
|
||||
/** |
||||
* Builds an empty logout response. The ID of the message is {@link #OUTBOUND_MSG_ID}, |
||||
* the issue instant is 1970-01-01T00:00:00Z and the SAML version is |
||||
* {@link SAMLVersion#VERSION_11}. |
||||
* @return the constructed response |
||||
*/ |
||||
@Nonnull |
||||
static LogoutResponse buildLogoutResponse() { |
||||
final SAMLObjectBuilder<LogoutResponse> responseBuilder = (SAMLObjectBuilder<LogoutResponse>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<LogoutResponse>getBuilderOrThrow(LogoutResponse.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final LogoutResponse response = responseBuilder.buildObject(); |
||||
response.setID(OUTBOUND_MSG_ID); |
||||
response.setIssueInstant(DateTime.now()); |
||||
response.setVersion(SAMLVersion.VERSION_20); |
||||
|
||||
return response; |
||||
} |
||||
|
||||
/** |
||||
* Builds an empty assertion. The ID of the message is {@link #ASSERTION_ID}, the |
||||
* issue instant is 1970-01-01T00:00:00Z and the SAML version is |
||||
* {@link SAMLVersion#VERSION_11}. |
||||
* @return the constructed assertion |
||||
*/ |
||||
@Nonnull |
||||
static Assertion buildAssertion() { |
||||
final SAMLObjectBuilder<Assertion> assertionBuilder = (SAMLObjectBuilder<Assertion>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<Assertion>getBuilderOrThrow(Assertion.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final Assertion assertion = assertionBuilder.buildObject(); |
||||
assertion.setID(ASSERTION_ID); |
||||
assertion.setIssueInstant(DateTime.now()); |
||||
assertion.setVersion(SAMLVersion.VERSION_20); |
||||
|
||||
return assertion; |
||||
} |
||||
|
||||
@Nonnull |
||||
static SubjectConfirmation buildSubjectConfirmation() { |
||||
final SAMLObjectBuilder<SubjectConfirmation> subjectConfirmation = (SAMLObjectBuilder<SubjectConfirmation>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<SubjectConfirmation>getBuilderOrThrow(SubjectConfirmation.DEFAULT_ELEMENT_NAME); |
||||
|
||||
return subjectConfirmation.buildObject(); |
||||
} |
||||
|
||||
/** |
||||
* Builds an authentication statement. The authn instant is set to |
||||
* 1970-01-01T00:00:00Z. |
||||
* @return the constructed statement |
||||
*/ |
||||
@Nonnull |
||||
static AuthnStatement buildAuthnStatement() { |
||||
final SAMLObjectBuilder<AuthnStatement> statementBuilder = (SAMLObjectBuilder<AuthnStatement>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<AuthnStatement>getBuilderOrThrow(AuthnStatement.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final AuthnStatement statement = statementBuilder.buildObject(); |
||||
statement.setAuthnInstant(DateTime.now()); |
||||
|
||||
return statement; |
||||
} |
||||
|
||||
/** |
||||
* Builds an empty attribute statement. |
||||
* @return the constructed statement |
||||
*/ |
||||
@Nonnull |
||||
static AttributeStatement buildAttributeStatement() { |
||||
final SAMLObjectBuilder<AttributeStatement> statementBuilder = (SAMLObjectBuilder<AttributeStatement>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<AttributeStatement>getBuilderOrThrow(AttributeStatement.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final AttributeStatement statement = statementBuilder.buildObject(); |
||||
|
||||
return statement; |
||||
} |
||||
|
||||
/** |
||||
* Builds a {@link Subject}. If a principal name is given a {@link NameID}, whose |
||||
* value is the given principal name, will be created and added to the |
||||
* {@link Subject}. |
||||
* @param principalName the principal name to add to the subject |
||||
* @return the built subject |
||||
*/ |
||||
@Nonnull |
||||
static Subject buildSubject(final @Nullable String principalName) { |
||||
final SAMLObjectBuilder<Subject> subjectBuilder = (SAMLObjectBuilder<Subject>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<Subject>getBuilderOrThrow(Subject.DEFAULT_ELEMENT_NAME); |
||||
final Subject subject = subjectBuilder.buildObject(); |
||||
|
||||
if (principalName != null) { |
||||
subject.setNameID(buildNameID(principalName)); |
||||
} |
||||
|
||||
return subject; |
||||
} |
||||
|
||||
@Nonnull |
||||
static SubjectConfirmationData buildSubjectConfirmationData(String localSpEntityId) { |
||||
final SAMLObjectBuilder<SubjectConfirmationData> subjectBuilder = (SAMLObjectBuilder<SubjectConfirmationData>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory() |
||||
.<SubjectConfirmationData>getBuilderOrThrow(SubjectConfirmationData.DEFAULT_ELEMENT_NAME); |
||||
final SubjectConfirmationData subject = subjectBuilder.buildObject(); |
||||
subject.setRecipient(localSpEntityId); |
||||
subject.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); |
||||
subject.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); |
||||
return subject; |
||||
} |
||||
|
||||
@Nonnull |
||||
static Conditions buildConditions() { |
||||
final SAMLObjectBuilder<Conditions> subjectBuilder = (SAMLObjectBuilder<Conditions>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<Conditions>getBuilderOrThrow(Conditions.DEFAULT_ELEMENT_NAME); |
||||
final Conditions conditions = subjectBuilder.buildObject(); |
||||
conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); |
||||
conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); |
||||
return conditions; |
||||
} |
||||
|
||||
/** |
||||
* Builds a {@link NameID}. |
||||
* @param principalName the principal name to use in the NameID |
||||
* @return the built NameID |
||||
*/ |
||||
@Nonnull |
||||
static NameID buildNameID(final @Nonnull @NotEmpty String principalName) { |
||||
final SAMLObjectBuilder<NameID> nameIdBuilder = (SAMLObjectBuilder<NameID>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<NameID>getBuilderOrThrow(NameID.DEFAULT_ELEMENT_NAME); |
||||
final NameID nameId = nameIdBuilder.buildObject(); |
||||
nameId.setValue(principalName); |
||||
return nameId; |
||||
} |
||||
|
||||
/** |
||||
* Builds a {@link Issuer}. |
||||
* @param entityID the entity ID to use in the Issuer |
||||
* @return the built Issuer |
||||
*/ |
||||
@Nonnull |
||||
static Issuer buildIssuer(final @Nonnull @NotEmpty String entityID) { |
||||
final SAMLObjectBuilder<Issuer> issuerBuilder = (SAMLObjectBuilder<Issuer>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<Issuer>getBuilderOrThrow(Issuer.DEFAULT_ELEMENT_NAME); |
||||
final Issuer issuer = issuerBuilder.buildObject(); |
||||
issuer.setValue(entityID); |
||||
return issuer; |
||||
} |
||||
|
||||
/** |
||||
* Builds an {@link AttributeQuery}. If a {@link Subject} is given, it will be added |
||||
* to the constructed {@link AttributeQuery}. |
||||
* @param subject the subject to add to the query |
||||
* @return the built query |
||||
*/ |
||||
@Nonnull |
||||
static AttributeQuery buildAttributeQueryRequest(final @Nullable Subject subject) { |
||||
final SAMLObjectBuilder<Issuer> issuerBuilder = (SAMLObjectBuilder<Issuer>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<Issuer>getBuilderOrThrow(Issuer.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final SAMLObjectBuilder<AttributeQuery> queryBuilder = (SAMLObjectBuilder<AttributeQuery>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<AttributeQuery>getBuilderOrThrow(AttributeQuery.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final Issuer issuer = issuerBuilder.buildObject(); |
||||
issuer.setValue(INBOUND_MSG_ISSUER); |
||||
|
||||
final AttributeQuery query = queryBuilder.buildObject(); |
||||
query.setID(REQUEST_ID); |
||||
query.setIssueInstant(DateTime.now()); |
||||
query.setIssuer(issuer); |
||||
query.setVersion(SAMLVersion.VERSION_20); |
||||
|
||||
if (subject != null) { |
||||
query.setSubject(subject); |
||||
} |
||||
|
||||
return query; |
||||
} |
||||
|
||||
/** |
||||
* Builds an {@link AuthnRequest}. |
||||
* @return the built request |
||||
*/ |
||||
@Nonnull |
||||
static AuthnRequest buildAuthnRequest() { |
||||
final SAMLObjectBuilder<Issuer> issuerBuilder = (SAMLObjectBuilder<Issuer>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<Issuer>getBuilderOrThrow(Issuer.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final SAMLObjectBuilder<AuthnRequest> requestBuilder = (SAMLObjectBuilder<AuthnRequest>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<AuthnRequest>getBuilderOrThrow(AuthnRequest.DEFAULT_ELEMENT_NAME); |
||||
|
||||
final Issuer issuer = issuerBuilder.buildObject(); |
||||
issuer.setValue(INBOUND_MSG_ISSUER); |
||||
|
||||
final AuthnRequest request = requestBuilder.buildObject(); |
||||
request.setID(REQUEST_ID); |
||||
request.setIssueInstant(DateTime.now()); |
||||
request.setIssuer(issuer); |
||||
request.setVersion(SAMLVersion.VERSION_20); |
||||
|
||||
return request; |
||||
} |
||||
|
||||
/** |
||||
* Builds a {@link ArtifactResolve}. |
||||
* @param artifact the artifact to add to the request |
||||
* @return the built request |
||||
*/ |
||||
@Nonnull |
||||
static ArtifactResolve buildArtifactResolve(final @Nullable String artifact) { |
||||
final SAMLObjectBuilder<ArtifactResolve> requestBuilder = (SAMLObjectBuilder<ArtifactResolve>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<ArtifactResolve>getBuilderOrThrow(ArtifactResolve.DEFAULT_ELEMENT_NAME); |
||||
final ArtifactResolve request = requestBuilder.buildObject(); |
||||
request.setID(REQUEST_ID); |
||||
request.setIssueInstant(DateTime.now()); |
||||
request.setVersion(SAMLVersion.VERSION_11); |
||||
|
||||
if (artifact != null) { |
||||
final SAMLObjectBuilder<Artifact> artifactBuilder = (SAMLObjectBuilder<Artifact>) XMLObjectProviderRegistrySupport |
||||
.getBuilderFactory().<Artifact>getBuilderOrThrow(Artifact.DEFAULT_ELEMENT_NAME); |
||||
final Artifact art = artifactBuilder.buildObject(); |
||||
art.setArtifact(artifact); |
||||
request.setArtifact(art); |
||||
} |
||||
|
||||
return request; |
||||
} |
||||
|
||||
/** ID of the inbound message. */ |
||||
public final static String INBOUND_MSG_ID = "inbound"; |
||||
|
||||
/** Issuer of the inbound message. */ |
||||
public final static String INBOUND_MSG_ISSUER = "http://sp.example.org"; |
||||
|
||||
/** ID of the outbound message. */ |
||||
public final static String OUTBOUND_MSG_ID = "outbound"; |
||||
|
||||
/** Issuer of the outbound message. */ |
||||
public final static String OUTBOUND_MSG_ISSUER = "http://idp.example.org"; |
||||
|
||||
/** |
||||
* Checks that the request context contains an EventContext, and that the event |
||||
* content is as given. |
||||
* @param profileRequestContext the context to check |
||||
* @param event event to check |
||||
*/ |
||||
static void assertEvent(@Nonnull final ProfileRequestContext profileRequestContext, |
||||
@Nonnull final Object event) { |
||||
EventContext ctx = profileRequestContext.getSubcontext(EventContext.class); |
||||
Assert.assertNotNull(ctx); |
||||
Assert.assertEquals(ctx.getEvent(), event); |
||||
} |
||||
|
||||
/** |
||||
* Checks that the given request context does not contain an EventContext (thus |
||||
* signaling a "proceed" event). |
||||
* @param profileRequestContext the context to check |
||||
*/ |
||||
static void assertProceedEvent(@Nonnull final ProfileRequestContext profileRequestContext) { |
||||
EventContext<String> ctx = profileRequestContext.getSubcontext(EventContext.class); |
||||
Assert.assertTrue(ctx == null || ctx.getEvent().equals(EventIds.PROCEED_EVENT_ID)); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,362 @@
@@ -0,0 +1,362 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.security.samples; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.SpringBootConfiguration; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.context.annotation.ComponentScan; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.test.context.junit4.SpringRunner; |
||||
import org.springframework.test.web.servlet.MockMvc; |
||||
import org.springframework.test.web.servlet.ResultActions; |
||||
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport; |
||||
import org.joda.time.DateTime; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.opensaml.core.xml.XMLObject; |
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; |
||||
import org.opensaml.core.xml.io.MarshallerFactory; |
||||
import org.opensaml.core.xml.io.MarshallingException; |
||||
import org.opensaml.saml.common.SignableSAMLObject; |
||||
import org.opensaml.saml.saml2.core.Assertion; |
||||
import org.opensaml.saml.saml2.core.EncryptedAssertion; |
||||
import org.opensaml.saml.saml2.core.EncryptedID; |
||||
import org.opensaml.saml.saml2.core.Response; |
||||
import org.opensaml.saml.saml2.core.SubjectConfirmation; |
||||
import org.opensaml.saml.saml2.core.SubjectConfirmationData; |
||||
import org.opensaml.security.SecurityException; |
||||
import org.opensaml.security.credential.BasicCredential; |
||||
import org.opensaml.security.credential.Credential; |
||||
import org.opensaml.security.credential.CredentialSupport; |
||||
import org.opensaml.security.credential.UsageType; |
||||
import org.opensaml.security.crypto.KeySupport; |
||||
import org.opensaml.xmlsec.SignatureSigningParameters; |
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants; |
||||
import org.opensaml.xmlsec.signature.support.SignatureException; |
||||
import org.opensaml.xmlsec.signature.support.SignatureSupport; |
||||
import org.w3c.dom.Element; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.security.KeyException; |
||||
import java.security.PrivateKey; |
||||
import java.security.PublicKey; |
||||
import java.security.cert.CertificateException; |
||||
import java.security.cert.CertificateFactory; |
||||
import java.security.cert.X509Certificate; |
||||
import java.util.UUID; |
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8; |
||||
import static org.hamcrest.Matchers.containsString; |
||||
import static org.hamcrest.Matchers.startsWith; |
||||
import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildConditions; |
||||
import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildIssuer; |
||||
import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildSubject; |
||||
import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildSubjectConfirmation; |
||||
import static org.springframework.security.samples.OpenSamlActionTestingSupport.buildSubjectConfirmationData; |
||||
import static org.springframework.security.samples.OpenSamlActionTestingSupport.encryptNameId; |
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; |
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; |
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
||||
@RunWith(SpringRunner.class) |
||||
@SpringBootTest |
||||
@AutoConfigureMockMvc |
||||
public class Saml2LoginIntegrationTests { |
||||
|
||||
static final String LOCAL_SP_ENTITY_ID = "http://localhost:8080/saml2/service-provider-metadata/simplesamlphp"; |
||||
|
||||
@Autowired |
||||
MockMvc mockMvc; |
||||
|
||||
@SpringBootConfiguration |
||||
@EnableAutoConfiguration |
||||
@ComponentScan(basePackages = "sample") |
||||
public static class SpringBootApplicationTestConfig { |
||||
} |
||||
|
||||
@Test |
||||
public void redirectToLoginPageSingleProvider() throws Exception { |
||||
mockMvc.perform(get("http://localhost:8080/some/url")) |
||||
.andExpect(status().is3xxRedirection()) |
||||
.andExpect(redirectedUrl("http://localhost:8080/saml2/authenticate/simplesamlphp")); |
||||
} |
||||
|
||||
@Test |
||||
public void testAuthNRequest() throws Exception { |
||||
mockMvc.perform(get("http://localhost:8080/saml2/authenticate/simplesamlphp")) |
||||
.andExpect(status().is3xxRedirection()) |
||||
.andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php?SAMLRequest="))); |
||||
} |
||||
|
||||
@Test |
||||
public void testRelayState() throws Exception { |
||||
mockMvc.perform( |
||||
get("http://localhost:8080/saml2/authenticate/simplesamlphp") |
||||
.param("RelayState", "relay state value with spaces") |
||||
) |
||||
.andExpect(status().is3xxRedirection()) |
||||
.andExpect(header().string("Location", startsWith("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php?SAMLRequest="))) |
||||
.andExpect(header().string("Location", containsString("RelayState=relay%20state%20value%20with%20spaces"))); |
||||
} |
||||
|
||||
@Test |
||||
public void signedResponse() throws Exception { |
||||
final String username = "testuser@spring.security.saml"; |
||||
Assertion assertion = buildAssertion(username); |
||||
Response response = buildResponse(assertion); |
||||
signXmlObject(response, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); |
||||
String xml = toXml(response); |
||||
mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") |
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED) |
||||
.param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) |
||||
.andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) |
||||
.andExpect(authenticated().withUsername(username)); |
||||
} |
||||
|
||||
@Test |
||||
public void signedAssertion() throws Exception { |
||||
final String username = "testuser@spring.security.saml"; |
||||
Assertion assertion = buildAssertion(username); |
||||
Response response = buildResponse(assertion); |
||||
signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); |
||||
String xml = toXml(response); |
||||
final ResultActions actions = mockMvc |
||||
.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") |
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED) |
||||
.param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) |
||||
.andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) |
||||
.andExpect(authenticated().withUsername(username)); |
||||
} |
||||
|
||||
@Test |
||||
public void unsigned() throws Exception { |
||||
Assertion assertion = buildAssertion("testuser@spring.security.saml"); |
||||
Response response = buildResponse(assertion); |
||||
String xml = toXml(response); |
||||
mockMvc.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") |
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED) |
||||
.param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) |
||||
.andExpect(status().is3xxRedirection()) |
||||
.andExpect(redirectedUrl("/login?error")) |
||||
.andExpect(unauthenticated()); |
||||
} |
||||
|
||||
@Test |
||||
public void signedResponseEncryptedAssertion() throws Exception { |
||||
final String username = "testuser@spring.security.saml"; |
||||
Assertion assertion = buildAssertion(username); |
||||
EncryptedAssertion encryptedAssertion = |
||||
OpenSamlActionTestingSupport.encryptAssertion(assertion, decodeCertificate(spCertificate)); |
||||
Response response = buildResponse(encryptedAssertion); |
||||
signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); |
||||
String xml = toXml(response); |
||||
final ResultActions actions = mockMvc |
||||
.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") |
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED) |
||||
.param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) |
||||
.andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) |
||||
.andExpect(authenticated().withUsername(username)); |
||||
} |
||||
|
||||
@Test |
||||
public void unsignedResponseEncryptedAssertion() throws Exception { |
||||
final String username = "testuser@spring.security.saml"; |
||||
Assertion assertion = buildAssertion(username); |
||||
EncryptedAssertion encryptedAssertion = |
||||
OpenSamlActionTestingSupport.encryptAssertion(assertion, decodeCertificate(spCertificate)); |
||||
Response response = buildResponse(encryptedAssertion); |
||||
String xml = toXml(response); |
||||
final ResultActions actions = mockMvc |
||||
.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") |
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED) |
||||
.param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) |
||||
.andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) |
||||
.andExpect(authenticated().withUsername(username)); |
||||
} |
||||
|
||||
@Test |
||||
public void signedResponseEncryptedNameId() throws Exception { |
||||
final String username = "testuser@spring.security.saml"; |
||||
Assertion assertion = buildAssertion(username); |
||||
final EncryptedID nameId = encryptNameId(assertion.getSubject().getNameID(), decodeCertificate(spCertificate)); |
||||
assertion.getSubject().setEncryptedID(nameId); |
||||
assertion.getSubject().setNameID(null); |
||||
Response response = buildResponse(assertion); |
||||
signXmlObject(assertion, getSigningCredential(idpCertificate, idpPrivateKey, UsageType.SIGNING)); |
||||
String xml = toXml(response); |
||||
final ResultActions actions = mockMvc |
||||
.perform(post("http://localhost:8080/login/saml2/sso/simplesamlphp") |
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED) |
||||
.param("SAMLResponse", OpenSamlActionTestingSupport.encode(xml.getBytes(UTF_8)))) |
||||
.andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")) |
||||
.andExpect(authenticated().withUsername(username)); |
||||
} |
||||
|
||||
private Response buildResponse(Assertion assertion) { |
||||
Response response = buildResponse(); |
||||
response.getAssertions().add(assertion); |
||||
return response; |
||||
} |
||||
|
||||
private Response buildResponse(EncryptedAssertion assertion) { |
||||
Response response = buildResponse(); |
||||
response.getEncryptedAssertions().add(assertion); |
||||
return response; |
||||
} |
||||
|
||||
private Response buildResponse() { |
||||
Response response = OpenSamlActionTestingSupport.buildResponse(); |
||||
response.setID("_" + UUID.randomUUID().toString()); |
||||
response.setDestination("http://localhost:8080/login/saml2/sso/simplesamlphp"); |
||||
response.setIssuer(buildIssuer("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php")); |
||||
return response; |
||||
} |
||||
|
||||
private Assertion buildAssertion(String username) { |
||||
Assertion assertion = OpenSamlActionTestingSupport.buildAssertion(); |
||||
assertion.setIssueInstant(DateTime.now()); |
||||
assertion.setIssuer(buildIssuer("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php")); |
||||
assertion.setSubject(buildSubject(username)); |
||||
assertion.setConditions(buildConditions()); |
||||
|
||||
SubjectConfirmation subjectConfirmation = buildSubjectConfirmation(); |
||||
|
||||
// Default to bearer with basic valid confirmation data, but the test can change
|
||||
// as appropriate
|
||||
subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); |
||||
final SubjectConfirmationData confirmationData = buildSubjectConfirmationData(LOCAL_SP_ENTITY_ID); |
||||
confirmationData.setRecipient("http://localhost:8080/login/saml2/sso/simplesamlphp"); |
||||
subjectConfirmation.setSubjectConfirmationData(confirmationData); |
||||
assertion.getSubject().getSubjectConfirmations().add(subjectConfirmation); |
||||
return assertion; |
||||
} |
||||
|
||||
protected Credential getSigningCredential(String certificate, String key, UsageType usageType) |
||||
throws CertificateException, KeyException { |
||||
PublicKey publicKey = decodeCertificate(certificate).getPublicKey(); |
||||
final PrivateKey privateKey = KeySupport.decodePrivateKey(key.getBytes(UTF_8), new char[0]); |
||||
BasicCredential cred = CredentialSupport.getSimpleCredential(publicKey, privateKey); |
||||
cred.setUsageType(usageType); |
||||
cred.setEntityId("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"); |
||||
return cred; |
||||
} |
||||
|
||||
private void signXmlObject(SignableSAMLObject object, Credential credential) |
||||
throws MarshallingException, SecurityException, SignatureException { |
||||
SignatureSigningParameters parameters = new SignatureSigningParameters(); |
||||
parameters.setSigningCredential(credential); |
||||
parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); |
||||
parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); |
||||
parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); |
||||
SignatureSupport.signObject(object, parameters); |
||||
} |
||||
|
||||
private String toXml(XMLObject object) throws MarshallingException { |
||||
final MarshallerFactory marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory(); |
||||
Element element = marshallerFactory.getMarshaller(object).marshall(object); |
||||
return SerializeSupport.nodeToString(element); |
||||
} |
||||
|
||||
private X509Certificate decodeCertificate(String source) { |
||||
try { |
||||
final CertificateFactory factory = CertificateFactory.getInstance("X.509"); |
||||
return (X509Certificate) factory.generateCertificate( |
||||
new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)) |
||||
); |
||||
} catch (Exception e) { |
||||
throw new IllegalArgumentException(e); |
||||
} |
||||
} |
||||
|
||||
private String idpCertificate = "-----BEGIN CERTIFICATE-----\n" |
||||
+ "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD\n" |
||||
+ "VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD\n" |
||||
+ "VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX\n" |
||||
+ "c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw\n" |
||||
+ "aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ\n" |
||||
+ "BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa\n" |
||||
+ "BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD\n" |
||||
+ "DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr\n" |
||||
+ "QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62\n" |
||||
+ "E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz\n" |
||||
+ "2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW\n" |
||||
+ "RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ\n" |
||||
+ "nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5\n" |
||||
+ "cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph\n" |
||||
+ "iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5\n" |
||||
+ "ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD\n" |
||||
+ "AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO\n" |
||||
+ "nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v\n" |
||||
+ "ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu\n" |
||||
+ "xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z\n" |
||||
+ "V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3\n" |
||||
+ "lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + "-----END CERTIFICATE-----\n"; |
||||
|
||||
private String idpPrivateKey = "-----BEGIN PRIVATE KEY-----\n" |
||||
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4cn62E1xLqpN3\n" |
||||
+ "4PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZX\n" |
||||
+ "W+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHE\n" |
||||
+ "fDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7h\n" |
||||
+ "Z6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/T\n" |
||||
+ "Xy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7\n" |
||||
+ "I+J5lS8VAgMBAAECggEBAKyxBlIS7mcp3chvq0RF7B3PHFJMMzkwE+t3pLJcs4cZ\n" |
||||
+ "nezh/KbREfP70QjXzk/llnZCvxeIs5vRu24vbdBm79qLHqBuHp8XfHHtuo2AfoAQ\n" |
||||
+ "l4h047Xc/+TKMivnPQ0jX9qqndKDLqZDf5wnbslDmlskvF0a/MjsLU0TxtOfo+dB\n" |
||||
+ "t55FW11cGqxZwhS5Gnr+cbw3OkHz23b9gEOt9qfwPVepeysbmm9FjU+k4yVa7rAN\n" |
||||
+ "xcbzVb6Y7GCITe2tgvvEHmjB9BLmWrH3mZ3Af17YU/iN6TrpPd6Sj3QoS+2wGtAe\n" |
||||
+ "HbUs3CKJu7bIHcj4poal6Kh8519S+erJTtqQ8M0ZiEECgYEA43hLYAPaUueFkdfh\n" |
||||
+ "9K/7ClH6436CUH3VdizwUXi26fdhhV/I/ot6zLfU2mgEHU22LBECWQGtAFm8kv0P\n" |
||||
+ "zPn+qjaR3e62l5PIlSYbnkIidzoDZ2ztu4jF5LgStlTJQPteFEGgZVl5o9DaSZOq\n" |
||||
+ "Yd7G3XqXuQ1VGMW58G5FYJPtA1cCgYEAz5TPUtK+R2KXHMjUwlGY9AefQYRYmyX2\n" |
||||
+ "Tn/OFgKvY8lpAkMrhPKONq7SMYc8E9v9G7A0dIOXvW7QOYSapNhKU+np3lUafR5F\n" |
||||
+ "4ZN0bxZ9qjHbn3AMYeraKjeutHvlLtbHdIc1j3sxe/EzltRsYmiqLdEBW0p6hwWg\n" |
||||
+ "tyGhYWVyaXMCgYAfDOKtHpmEy5nOCLwNXKBWDk7DExfSyPqEgSnk1SeS1HP5ctPK\n" |
||||
+ "+1st6sIhdiVpopwFc+TwJWxqKdW18tlfT5jVv1E2DEnccw3kXilS9xAhWkfwrEvf\n" |
||||
+ "V5I74GydewFl32o+NZ8hdo9GL1I8zO1rIq/et8dSOWGuWf9BtKu/vTGTTQKBgFxU\n" |
||||
+ "VjsCnbvmsEwPUAL2hE/WrBFaKocnxXx5AFNt8lEyHtDwy4Sg1nygGcIJ4sD6koQk\n" |
||||
+ "RdClT3LkvR04TAiSY80bN/i6ZcPNGUwSaDGZEWAIOSWbkwZijZNFnSGOEgxZX/IG\n" |
||||
+ "yd39766vREEMTwEeiMNEOZQ/dmxkJm4OOVe25cLdAoGACOtPnq1Fxay80UYBf4rQ\n" |
||||
+ "+bJ9yX1ulB8WIree1hD7OHSB2lRHxrVYWrglrTvkh63Lgx+EcsTV788OsvAVfPPz\n" |
||||
+ "BZrn8SdDlQqalMxUBYEFwnsYD3cQ8yOUnijFVC4xNcdDv8OIqVgSk4KKxU5AshaA\n" + "xk6Mox+u8Cc2eAK12H13i+8=\n" |
||||
+ "-----END PRIVATE KEY-----\n"; |
||||
|
||||
private String spCertificate = "-----BEGIN CERTIFICATE-----\n" + |
||||
"MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\n" + |
||||
"VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG\n" + |
||||
"A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD\n" + |
||||
"DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1\n" + |
||||
"MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES\n" + |
||||
"MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN\n" + |
||||
"TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s\n" + |
||||
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos\n" + |
||||
"vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM\n" + |
||||
"+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG\n" + |
||||
"y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi\n" + |
||||
"XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+\n" + |
||||
"qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD\n" + |
||||
"RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" + |
||||
"-----END CERTIFICATE-----"; |
||||
|
||||
} |
||||
@ -0,0 +1,183 @@
@@ -0,0 +1,183 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package boot.saml2.config; |
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential; |
||||
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.RelyingPartyRegistrationRepository; |
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import java.security.cert.X509Certificate; |
||||
import java.security.interfaces.RSAPrivateKey; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import static java.util.Collections.emptyList; |
||||
import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION; |
||||
import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION; |
||||
import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING; |
||||
import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION; |
||||
|
||||
@Configuration |
||||
@ConfigurationProperties(prefix = "spring.security.saml2.login") |
||||
@Import(X509CredentialsConverters.class) |
||||
public class Saml2LoginBootConfiguration { |
||||
|
||||
private List<SampleRelyingParty> relyingParties; |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean |
||||
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { |
||||
return new InMemoryRelyingPartyRegistrationRepository(getRelyingParties(relyingParties)); |
||||
} |
||||
|
||||
public void setRelyingParties(List<SampleRelyingParty> providers) { |
||||
this.relyingParties = providers; |
||||
} |
||||
|
||||
private List<RelyingPartyRegistration> getRelyingParties(List<SampleRelyingParty> sampleRelyingParties) { |
||||
String acsUrlTemplate = "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; |
||||
return sampleRelyingParties.stream() |
||||
.map( |
||||
p -> StringUtils.hasText(p.getLocalSpEntityIdTemplate()) ? |
||||
RelyingPartyRegistration.withRegistrationId(p.getRegistrationId()) |
||||
.assertionConsumerServiceUrlTemplate(acsUrlTemplate) |
||||
.remoteIdpEntityId(p.getEntityId()) |
||||
.idpWebSsoUrl(p.getWebSsoUrl()) |
||||
.credentials(c -> c.addAll(p.getProviderCredentials())) |
||||
.localEntityIdTemplate(p.getLocalSpEntityIdTemplate()) |
||||
.build() : |
||||
RelyingPartyRegistration.withRegistrationId(p.getRegistrationId()) |
||||
.assertionConsumerServiceUrlTemplate(acsUrlTemplate) |
||||
.remoteIdpEntityId(p.getEntityId()) |
||||
.idpWebSsoUrl(p.getWebSsoUrl()) |
||||
.credentials(c -> c.addAll(p.getProviderCredentials())) |
||||
.build() |
||||
) |
||||
.collect(Collectors.toList()); |
||||
} |
||||
|
||||
public static class SampleRelyingParty { |
||||
|
||||
private String entityId; |
||||
private List<Saml2X509Credential> signingCredentials = emptyList(); |
||||
private List<X509Certificate> verificationCredentials = emptyList(); |
||||
private String registrationId; |
||||
private String webSsoUrl; |
||||
private String localSpEntityIdTemplate; |
||||
|
||||
public String getEntityId() { |
||||
return entityId; |
||||
} |
||||
|
||||
public String getLocalSpEntityIdTemplate() { |
||||
return localSpEntityIdTemplate; |
||||
} |
||||
|
||||
public void setEntityId(String entityId) { |
||||
this.entityId = entityId; |
||||
} |
||||
|
||||
public List<Saml2X509Credential> getSigningCredentials() { |
||||
return signingCredentials; |
||||
} |
||||
|
||||
public void setSigningCredentials(List<X509KeyCertificatePair> credentials) { |
||||
this.signingCredentials = credentials |
||||
.stream() |
||||
.map(c -> |
||||
new Saml2X509Credential( |
||||
c.getPrivateKey(), |
||||
c.getCertificate(), |
||||
SIGNING, |
||||
DECRYPTION |
||||
) |
||||
) |
||||
.collect(Collectors.toList()); |
||||
} |
||||
|
||||
public void setVerificationCredentials(List<X509Certificate> credentials) { |
||||
this.verificationCredentials = new LinkedList<>(credentials); |
||||
} |
||||
|
||||
public List<X509Certificate> getVerificationCredentials() { |
||||
return verificationCredentials; |
||||
} |
||||
|
||||
public List<Saml2X509Credential> getProviderCredentials() { |
||||
LinkedList<Saml2X509Credential> result = new LinkedList<>(getSigningCredentials()); |
||||
for (X509Certificate c : getVerificationCredentials()) { |
||||
result.add(new Saml2X509Credential(c, ENCRYPTION, VERIFICATION)); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
public String getRegistrationId() { |
||||
return registrationId; |
||||
} |
||||
|
||||
public SampleRelyingParty setRegistrationId(String registrationId) { |
||||
this.registrationId = registrationId; |
||||
return this; |
||||
} |
||||
|
||||
public String getWebSsoUrl() { |
||||
return webSsoUrl; |
||||
} |
||||
|
||||
public SampleRelyingParty setWebSsoUrl(String webSsoUrl) { |
||||
this.webSsoUrl = webSsoUrl; |
||||
return this; |
||||
} |
||||
|
||||
public void setLocalSpEntityIdTemplate(String localSpEntityIdTemplate) { |
||||
this.localSpEntityIdTemplate = localSpEntityIdTemplate; |
||||
} |
||||
} |
||||
|
||||
public static class X509KeyCertificatePair { |
||||
|
||||
private RSAPrivateKey privateKey; |
||||
private X509Certificate certificate; |
||||
|
||||
public RSAPrivateKey getPrivateKey() { |
||||
return this.privateKey; |
||||
} |
||||
|
||||
public void setPrivateKey(RSAPrivateKey privateKey) { |
||||
this.privateKey = privateKey; |
||||
} |
||||
|
||||
public X509Certificate getCertificate() { |
||||
return certificate; |
||||
} |
||||
|
||||
public void setCertificate(X509Certificate certificate) { |
||||
this.certificate = certificate; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package boot.saml2.config; |
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.core.convert.converter.Converter; |
||||
import org.springframework.security.converter.RsaKeyConverters; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.security.cert.CertificateFactory; |
||||
import java.security.cert.X509Certificate; |
||||
import java.security.interfaces.RSAPrivateKey; |
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8; |
||||
|
||||
@Configuration |
||||
public class X509CredentialsConverters { |
||||
|
||||
@Component |
||||
@ConfigurationPropertiesBinding |
||||
public static class X509CertificateConverter implements Converter<String, X509Certificate> { |
||||
@Override |
||||
public X509Certificate convert (String source){ |
||||
try { |
||||
final CertificateFactory factory = CertificateFactory.getInstance("X.509"); |
||||
return (X509Certificate) factory.generateCertificate( |
||||
new ByteArrayInputStream(source.getBytes(UTF_8)) |
||||
); |
||||
} |
||||
catch (Exception e) { |
||||
throw new IllegalArgumentException(e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Component |
||||
@ConfigurationPropertiesBinding |
||||
public static class RSAPrivateKeyConverter implements Converter<String, RSAPrivateKey> { |
||||
@Override |
||||
public RSAPrivateKey convert (String source){ |
||||
return RsaKeyConverters.pkcs8().convert(new ByteArrayInputStream(source.getBytes(UTF_8))); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package sample; |
||||
|
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import sample.Saml2LoginApplication; |
||||
|
||||
@Controller |
||||
public class IndexController { |
||||
|
||||
private static final Log logger = LogFactory.getLog(Saml2LoginApplication.class); |
||||
|
||||
@GetMapping("/") |
||||
public String index() { |
||||
return "index"; |
||||
} |
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package sample; |
||||
|
||||
import org.springframework.boot.SpringApplication; |
||||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||
import org.springframework.context.annotation.Import; |
||||
|
||||
import boot.saml2.config.Saml2LoginBootConfiguration; |
||||
|
||||
@SpringBootApplication |
||||
@Import(Saml2LoginBootConfiguration.class) |
||||
public class Saml2LoginApplication { |
||||
|
||||
public static void main(String[] args) { |
||||
SpringApplication.run(Saml2LoginApplication.class, args); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package sample; |
||||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; |
||||
|
||||
@EnableWebSecurity |
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter { |
||||
|
||||
@Override |
||||
protected void configure(HttpSecurity http) throws Exception { |
||||
//@formatter:off
|
||||
http |
||||
.authorizeRequests() |
||||
.anyRequest().authenticated() |
||||
.and() |
||||
.saml2Login() |
||||
; |
||||
//@formatter:on
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
spring: |
||||
security: |
||||
saml2: |
||||
login: |
||||
relying-parties: |
||||
- entity-id: https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php |
||||
registration-id: simplesamlphp |
||||
web-sso-url: https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php |
||||
signing-credentials: |
||||
- private-key: | |
||||
-----BEGIN PRIVATE KEY----- |
||||
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE |
||||
VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK |
||||
cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6 |
||||
Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn |
||||
x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5 |
||||
wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd |
||||
vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY |
||||
8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX |
||||
oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx |
||||
EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0 |
||||
KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt |
||||
YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr |
||||
9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM |
||||
INrtuLp4YHbgk1mi |
||||
-----END PRIVATE KEY----- |
||||
certificate: | |
||||
-----BEGIN CERTIFICATE----- |
||||
MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC |
||||
VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG |
||||
A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD |
||||
DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1 |
||||
MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES |
||||
MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN |
||||
TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s |
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos |
||||
vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM |
||||
+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG |
||||
y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi |
||||
XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+ |
||||
qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD |
||||
RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B |
||||
-----END CERTIFICATE----- |
||||
verification-credentials: |
||||
- | |
||||
-----BEGIN CERTIFICATE----- |
||||
MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD |
||||
VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD |
||||
VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX |
||||
c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw |
||||
aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ |
||||
BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa |
||||
BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD |
||||
DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr |
||||
QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62 |
||||
E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz |
||||
2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW |
||||
RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ |
||||
nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5 |
||||
cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph |
||||
iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5 |
||||
ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD |
||||
AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO |
||||
nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v |
||||
ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu |
||||
xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z |
||||
V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3 |
||||
lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk |
||||
-----END CERTIFICATE----- |
||||
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
<!-- |
||||
~ Copyright 2002-2019 the original author or authors. |
||||
~ |
||||
~ Licensed under the Apache License, Version 2.0 (the "License"); |
||||
~ you may not use this file except in compliance with the License. |
||||
~ You may obtain a copy of the License at |
||||
~ |
||||
~ https://www.apache.org/licenses/LICENSE-2.0 |
||||
~ |
||||
~ Unless required by applicable law or agreed to in writing, software |
||||
~ distributed under the License is distributed on an "AS IS" BASIS, |
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
~ See the License for the specific language governing permissions and |
||||
~ limitations under the License. |
||||
--> |
||||
|
||||
<!doctype html> |
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> |
||||
<head> |
||||
<title>Spring Security - SAML 2 Log In</title> |
||||
<meta charset="utf-8" /> |
||||
</head> |
||||
<body> |
||||
<h1>Success</h1> |
||||
<div>You are authenticated as <span sec:authentication="name"></span></div> |
||||
<ul> |
||||
<li> |
||||
<a th:href="@{/logout}">Log Out</a> |
||||
</li> |
||||
<li> |
||||
<a href="https://simplesaml-for-spring-saml.cfapps.io/module.php/core/authenticate.php?as=example-userpass&logout"> |
||||
Log out of SimpleSAMLPhp |
||||
</a> |
||||
</li> |
||||
</ul> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-war' |
||||
|
||||
dependencies { |
||||
compile project(':spring-security-saml2-service-provider') |
||||
compile project(':spring-security-config') |
||||
compile "org.bouncycastle:bcprov-jdk15on" |
||||
compile "org.bouncycastle:bcpkix-jdk15on" |
||||
|
||||
testCompile project(':spring-security-test') |
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
/* |
||||
* Copyright 2002-2013 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.security.samples.config; |
||||
|
||||
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; |
||||
import org.springframework.security.web.session.HttpSessionEventPublisher; |
||||
|
||||
/** |
||||
* We customize {@link AbstractSecurityWebApplicationInitializer} to enable the |
||||
* {@link HttpSessionEventPublisher}. |
||||
* |
||||
* @author Rob Winch |
||||
*/ |
||||
public class MessageSecurityWebApplicationInitializer extends |
||||
AbstractSecurityWebApplicationInitializer { |
||||
|
||||
@Override |
||||
protected boolean enableHttpSessionEventPublisher() { |
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,162 @@
@@ -0,0 +1,162 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.security.samples.config; |
||||
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; |
||||
import org.springframework.security.converter.RsaKeyConverters; |
||||
import org.springframework.security.saml2.credentials.Saml2X509Credential; |
||||
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.servlet.filter.Saml2WebSsoAuthenticationFilter; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.security.PrivateKey; |
||||
import java.security.cert.CertificateFactory; |
||||
import java.security.cert.X509Certificate; |
||||
|
||||
import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION; |
||||
import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING; |
||||
import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION; |
||||
|
||||
@EnableWebSecurity |
||||
@EnableGlobalMethodSecurity(prePostEnabled = true) |
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter { |
||||
|
||||
RelyingPartyRegistration getSaml2AuthenticationConfiguration() throws Exception { |
||||
//remote IDP entity ID
|
||||
String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"; |
||||
//remote WebSSO Endpoint - Where to Send AuthNRequests to
|
||||
String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php"; |
||||
//local registration ID
|
||||
String registrationId = "simplesamlphp"; |
||||
//local entity ID - autogenerated based on URL
|
||||
String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; |
||||
//local signing (and decryption key)
|
||||
Saml2X509Credential signingCredential = getSigningCredential(); |
||||
//IDP certificate for verification of incoming messages
|
||||
Saml2X509Credential idpVerificationCertificate = getVerificationCertificate(); |
||||
String acsUrlTemplate = "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; |
||||
return RelyingPartyRegistration.withRegistrationId(registrationId) |
||||
.remoteIdpEntityId(idpEntityId) |
||||
.idpWebSsoUrl(webSsoEndpoint) |
||||
.credentials(c -> c.add(signingCredential)) |
||||
.credentials(c -> c.add(idpVerificationCertificate)) |
||||
.localEntityIdTemplate(localEntityIdTemplate) |
||||
.assertionConsumerServiceUrlTemplate(acsUrlTemplate) |
||||
.build(); |
||||
} |
||||
|
||||
@Override |
||||
protected void configure(HttpSecurity http) throws Exception { |
||||
// @formatter:off
|
||||
http |
||||
.authorizeRequests() |
||||
.anyRequest().authenticated() |
||||
.and() |
||||
.saml2Login() |
||||
.relyingPartyRegistrationRepository( |
||||
new InMemoryRelyingPartyRegistrationRepository( |
||||
getSaml2AuthenticationConfiguration() |
||||
) |
||||
) |
||||
; |
||||
// @formatter:on
|
||||
} |
||||
|
||||
private Saml2X509Credential getVerificationCertificate() { |
||||
String certificate = "-----BEGIN CERTIFICATE-----\n" + |
||||
"MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD\n" + |
||||
"VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD\n" + |
||||
"VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX\n" + |
||||
"c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw\n" + |
||||
"aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ\n" + |
||||
"BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa\n" + |
||||
"BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD\n" + |
||||
"DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr\n" + |
||||
"QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62\n" + |
||||
"E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz\n" + |
||||
"2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW\n" + |
||||
"RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ\n" + |
||||
"nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5\n" + |
||||
"cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph\n" + |
||||
"iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5\n" + |
||||
"ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD\n" + |
||||
"AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO\n" + |
||||
"nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v\n" + |
||||
"ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu\n" + |
||||
"xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z\n" + |
||||
"V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3\n" + |
||||
"lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + |
||||
"-----END CERTIFICATE-----"; |
||||
return new Saml2X509Credential( |
||||
x509Certificate(certificate), |
||||
VERIFICATION |
||||
); |
||||
} |
||||
|
||||
private X509Certificate x509Certificate(String source) { |
||||
try { |
||||
final CertificateFactory factory = CertificateFactory.getInstance("X.509"); |
||||
return (X509Certificate) factory.generateCertificate( |
||||
new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)) |
||||
); |
||||
} catch (Exception e) { |
||||
throw new IllegalArgumentException(e); |
||||
} |
||||
} |
||||
|
||||
private Saml2X509Credential getSigningCredential() { |
||||
String key = "-----BEGIN PRIVATE KEY-----\n" + |
||||
"MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" + |
||||
"VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" + |
||||
"cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" + |
||||
"Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" + |
||||
"x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" + |
||||
"wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" + |
||||
"vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" + |
||||
"8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" + |
||||
"oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" + |
||||
"EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" + |
||||
"KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" + |
||||
"YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" + |
||||
"9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" + |
||||
"INrtuLp4YHbgk1mi\n" + |
||||
"-----END PRIVATE KEY-----"; |
||||
String certificate = "-----BEGIN CERTIFICATE-----\n" + |
||||
"MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\n" + |
||||
"VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG\n" + |
||||
"A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD\n" + |
||||
"DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1\n" + |
||||
"MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES\n" + |
||||
"MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN\n" + |
||||
"TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s\n" + |
||||
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos\n" + |
||||
"vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM\n" + |
||||
"+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG\n" + |
||||
"y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi\n" + |
||||
"XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+\n" + |
||||
"qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD\n" + |
||||
"RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" + |
||||
"-----END CERTIFICATE-----"; |
||||
PrivateKey pk = RsaKeyConverters.pkcs8().convert(new ByteArrayInputStream(key.getBytes())); |
||||
X509Certificate cert = x509Certificate(certificate); |
||||
return new Saml2X509Credential(pk, cert, SIGNING, DECRYPTION); |
||||
} |
||||
} |
||||
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.security.samples.config; |
||||
|
||||
import org.springframework.test.context.ContextConfiguration; |
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; |
||||
|
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class) |
||||
@ContextConfiguration(classes = SecurityConfig.class) |
||||
public class SecurityConfigTests { |
||||
|
||||
@Test |
||||
public void securityConfigurationLoads() { |
||||
} |
||||
} |
||||
Loading…
Reference in new issue