package org.pac4j.saml.client;
import org.bouncycastle.jce.X509Principal;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.xmlsec.config.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.pac4j.core.context.HttpConstants;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.util.CommonHelper;
import org.pac4j.core.util.InitializableObject;
import org.pac4j.saml.exceptions.SAMLException;
import org.pac4j.saml.storage.EmptyStorageFactory;
import org.pac4j.saml.storage.SAMLMessageStorageFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.*;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* The {@link SAML2ClientConfiguration} is responsible for capturing client settings and passing them around.
*
* @author Misagh Moayyed
* @since 1.7
*/
public class SAML2ClientConfiguration extends InitializableObject {
private static final Logger LOGGER = LoggerFactory.getLogger(SAML2ClientConfiguration.class);
protected static final String RESOURCE_PREFIX = "resource:";
protected static final String CLASSPATH_PREFIX = "classpath:";
protected static final String FILE_PREFIX = "file:";
private Resource keystoreResource;
private String keystorePassword;
private String privateKeyPassword;
private Resource identityProviderMetadataResource;
private String identityProviderEntityId;
private String serviceProviderEntityId;
private int maximumAuthenticationLifetime;
private boolean forceAuth = false;
private boolean forceSignRedirectBindingAuthnRequest;
private String comparisonType = null;
private String destinationBindingType = SAMLConstants.SAML2_POST_BINDING_URI;
private String authnContextClassRef = null;
private String nameIdPolicyFormat = null;
private WritableResource serviceProviderMetadataResource;
private boolean forceServiceProviderMetadataGeneration;
private SAMLMessageStorageFactory samlMessageStorageFactory = new EmptyStorageFactory();
private boolean authnRequestSigned = true;
private Collection<String> blackListedSignatureSigningAlgorithms;
private List<String> signatureAlgorithms;
private List<String> signatureReferenceDigestMethods;
private String signatureCanonicalizationAlgorithm;
private boolean wantsAssertionsSigned = true;
private String keyStoreAlias;
private String keyStoreType;
public SAML2ClientConfiguration() {}
public SAML2ClientConfiguration(final String keystorePath, final String keystorePassword, final String privateKeyPassword,
final String identityProviderMetadataPath) {
this(null, null, mapPathToResource(keystorePath), keystorePassword, privateKeyPassword,
mapPathToResource(identityProviderMetadataPath), null, null);
}
public SAML2ClientConfiguration(final Resource keystoreResource, final String keystorePassword, final String privateKeyPassword,
final Resource identityProviderMetadataResource) {
this(null, null, keystoreResource, keystorePassword, privateKeyPassword,
identityProviderMetadataResource, null, null);
}
public SAML2ClientConfiguration(final Resource keystoreResource, final String keyStoreAlias,
final String keyStoreType, final String keystorePassword, final String privateKeyPassword,
final Resource identityProviderMetadataResource) {
this(keyStoreAlias, keyStoreType, keystoreResource, keystorePassword, privateKeyPassword,
identityProviderMetadataResource, null, null);
}
private SAML2ClientConfiguration(final String keyStoreAlias, final String keyStoreType,
final Resource keystoreResource, final String keystorePassword,
final String privateKeyPassword, final Resource identityProviderMetadataResource,
final String identityProviderEntityId, final String serviceProviderEntityId) {
this.keyStoreAlias = keyStoreAlias;
this.keyStoreType = keyStoreType;
this.keystoreResource = keystoreResource;
this.keystorePassword = keystorePassword;
this.privateKeyPassword = privateKeyPassword;
this.identityProviderMetadataResource = identityProviderMetadataResource;
this.identityProviderEntityId = identityProviderEntityId;
this.serviceProviderEntityId = serviceProviderEntityId;
}
@Override
protected void internalInit() {
CommonHelper.assertNotNull("keystoreResource", this.keystoreResource);
CommonHelper.assertNotBlank("keystorePassword", this.keystorePassword);
CommonHelper.assertNotBlank("privateKeyPassword", this.privateKeyPassword);
CommonHelper.assertNotNull("identityProviderMetadataResource", this.identityProviderMetadataResource);
if (!this.keystoreResource.exists()) {
if (this.keystoreResource instanceof WritableResource) {
LOGGER.warn("Provided keystoreResource does not exist. Creating one for: {}", this.keystoreResource);
createKeystore();
} else {
throw new TechnicalException("Provided keystoreResource does not exist and cannot be created");
}
}
final BasicSignatureSigningConfiguration config = DefaultSecurityConfigurationBootstrap.buildDefaultSignatureSigningConfiguration();
this.blackListedSignatureSigningAlgorithms = new ArrayList<>(config.getBlacklistedAlgorithms());
this.signatureAlgorithms = new ArrayList<>(config.getSignatureAlgorithms());
this.signatureReferenceDigestMethods = new ArrayList<>(config.getSignatureReferenceDigestMethods());
this.signatureReferenceDigestMethods.remove("http://www.w3.org/2001/04/xmlenc#sha512");
this.signatureCanonicalizationAlgorithm = config.getSignatureCanonicalizationAlgorithm();
}
public void setIdentityProviderMetadataResource(final Resource identityProviderMetadataResource) {
this.identityProviderMetadataResource = identityProviderMetadataResource;
}
public void setIdentityProviderMetadataResourceFilepath(final String path) {
this.identityProviderMetadataResource = new FileSystemResource(path);
}
public void setIdentityProviderMetadataResourceClasspath(final String path) {
this.identityProviderMetadataResource = new ClassPathResource(path);
}
public void setIdentityProviderMetadataResourceUrl(final String url) {
this.identityProviderMetadataResource = newUrlResource(url);
}
public void setIdentityProviderMetadataPath(final String path) {
this.identityProviderMetadataResource = mapPathToResource(path);
}
protected static UrlResource newUrlResource(final String url) {
try {
return new UrlResource(url);
} catch (final MalformedURLException e) {
throw new TechnicalException(e);
}
}
protected static Resource mapPathToResource(final String path) {
CommonHelper.assertNotBlank("path", path);
if (path.startsWith(RESOURCE_PREFIX)) {
return new ClassPathResource(path.substring(RESOURCE_PREFIX.length()));
} else if (path.startsWith(CLASSPATH_PREFIX)) {
return new ClassPathResource(path.substring(CLASSPATH_PREFIX.length()));
} else if (path.startsWith(HttpConstants.SCHEME_HTTP) || path.startsWith(HttpConstants.SCHEME_HTTPS)) {
return newUrlResource(path);
} else if (path.startsWith(FILE_PREFIX)) {
return new FileSystemResource(path.substring(FILE_PREFIX.length()));
} else {
return new FileSystemResource(path);
}
}
public Resource getIdentityProviderMetadataResource() {
return this.identityProviderMetadataResource;
}
public void setIdentityProviderEntityId(final String identityProviderEntityId) {
this.identityProviderEntityId = identityProviderEntityId;
}
public String getIdentityProviderEntityId() {
return identityProviderEntityId;
}
public void setKeystoreAlias(final String keyStoreAlias) {
this.keyStoreAlias = keyStoreAlias;
}
public void setKeystoreType(final String keyStoreType) {
this.keyStoreType = keyStoreType;
}
public void setKeystoreResource(final Resource keystoreResource) {
this.keystoreResource = keystoreResource;
}
public void setKeystoreResourceFilepath(final String path) {
this.keystoreResource = new FileSystemResource(path);
}
public void setKeystoreResourceClasspath(final String path) {
this.keystoreResource = new ClassPathResource(path);
}
public void setKeystoreResourceUrl(final String url) {
this.keystoreResource = newUrlResource(url);
}
public void setKeystorePath(final String path) {
this.keystoreResource = mapPathToResource(path);
}
public void setKeystorePassword(final String keystorePassword) {
this.keystorePassword = keystorePassword;
}
public void setPrivateKeyPassword(final String privateKeyPassword) {
this.privateKeyPassword = privateKeyPassword;
}
public String getKeyStoreAlias() {
return keyStoreAlias;
}
public String getKeyStoreType() {
return this.keyStoreType;
}
public Resource getKeystoreResource() {
return keystoreResource;
}
public String getKeystorePassword() {
return keystorePassword;
}
public String getPrivateKeyPassword() {
return privateKeyPassword;
}
public void setServiceProviderMetadataResource(final WritableResource serviceProviderMetadataResource) {
this.serviceProviderMetadataResource = serviceProviderMetadataResource;
}
public void setServiceProviderMetadataResourceFilepath(final String path) {
this.serviceProviderMetadataResource = new FileSystemResource(path);
}
public void setServiceProviderMetadataPath(final String path) {
final Resource resource = mapPathToResource(path);
if (!(resource instanceof WritableResource)) {
throw new TechnicalException(path + " must be a writable resource");
} else {
this.serviceProviderMetadataResource = (WritableResource) resource;
}
}
public void setForceServiceProviderMetadataGeneration(final boolean forceServiceProviderMetadataGeneration) {
this.forceServiceProviderMetadataGeneration = forceServiceProviderMetadataGeneration;
}
public WritableResource getServiceProviderMetadataResource() {
return serviceProviderMetadataResource;
}
public void setServiceProviderEntityId(final String serviceProviderEntityId) {
this.serviceProviderEntityId = serviceProviderEntityId;
}
public String getServiceProviderEntityId() {
return serviceProviderEntityId;
}
public boolean isForceAuth() {
return forceAuth;
}
public void setForceAuth(final boolean forceAuth) {
this.forceAuth = forceAuth;
}
public String getComparisonType() {
return comparisonType;
}
public void setComparisonType(final String comparisonType) {
this.comparisonType = comparisonType;
}
public String getDestinationBindingType() {
return destinationBindingType;
}
public void setDestinationBindingType(final String destinationBindingType) {
this.destinationBindingType = destinationBindingType;
}
public String getAuthnContextClassRef() {
return authnContextClassRef;
}
public void setAuthnContextClassRef(final String authnContextClassRef) {
this.authnContextClassRef = authnContextClassRef;
}
public String getNameIdPolicyFormat() {
return nameIdPolicyFormat;
}
public void setNameIdPolicyFormat(final String nameIdPolicyFormat) {
this.nameIdPolicyFormat = nameIdPolicyFormat;
}
public int getMaximumAuthenticationLifetime() {
return maximumAuthenticationLifetime;
}
public void setMaximumAuthenticationLifetime(final int maximumAuthenticationLifetime) {
this.maximumAuthenticationLifetime = maximumAuthenticationLifetime;
}
public boolean isForceServiceProviderMetadataGeneration() {
return forceServiceProviderMetadataGeneration;
}
public SAMLMessageStorageFactory getSamlMessageStorageFactory() {
return samlMessageStorageFactory;
}
public void setSamlMessageStorageFactory(final SAMLMessageStorageFactory samlMessageStorageFactory) {
this.samlMessageStorageFactory = samlMessageStorageFactory;
}
public Collection<String> getBlackListedSignatureSigningAlgorithms() {
return blackListedSignatureSigningAlgorithms;
}
public void setBlackListedSignatureSigningAlgorithms(final Collection<String> blackListedSignatureSigningAlgorithms) {
this.blackListedSignatureSigningAlgorithms = blackListedSignatureSigningAlgorithms;
}
public List<String> getSignatureAlgorithms() {
return signatureAlgorithms;
}
public void setSignatureAlgorithms(final List<String> signatureAlgorithms) {
this.signatureAlgorithms = signatureAlgorithms;
}
public List<String> getSignatureReferenceDigestMethods() {
return signatureReferenceDigestMethods;
}
public void setSignatureReferenceDigestMethods(final List<String> signatureReferenceDigestMethods) {
this.signatureReferenceDigestMethods = signatureReferenceDigestMethods;
}
public String getSignatureCanonicalizationAlgorithm() {
return signatureCanonicalizationAlgorithm;
}
public void setSignatureCanonicalizationAlgorithm(final String signatureCanonicalizationAlgorithm) {
this.signatureCanonicalizationAlgorithm = signatureCanonicalizationAlgorithm;
}
public boolean getWantsAssertionsSigned() {
return this.wantsAssertionsSigned;
}
public void setWantsAssertionsSigned(boolean wantsAssertionsSigned) {
this.wantsAssertionsSigned = wantsAssertionsSigned;
}
public boolean isForceSignRedirectBindingAuthnRequest() {
return forceSignRedirectBindingAuthnRequest;
}
public void setForceSignRedirectBindingAuthnRequest(final boolean forceSignRedirectBindingAuthnRequest) {
this.forceSignRedirectBindingAuthnRequest = forceSignRedirectBindingAuthnRequest;
}
public boolean isAuthnRequestSigned() {
return authnRequestSigned;
}
/**
* Initializes the configuration for a particular client.
*
* @param clientName
* Name of the client. The configuration can use the value or not.
* @param context
* Web context to transport additional information to the configuration.
*/
protected void init(final String clientName, final WebContext context) {
init();
}
private void createKeystore() {
try {
Security.addProvider(new BouncyCastleProvider());
if (CommonHelper.isBlank(this.keyStoreAlias)) {
this.keyStoreAlias = getClass().getSimpleName();
LOGGER.warn("Using keystore alias {}", this.keyStoreAlias);
}
if (CommonHelper.isBlank(this.keyStoreType)) {
this.keyStoreType = KeyStore.getDefaultType();
LOGGER.warn("Using keystore type {}", this.keyStoreType);
}
final KeyStore ks = KeyStore.getInstance(this.keyStoreType);
final char[] password = this.keystorePassword.toCharArray();
ks.load(null, password);
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
final KeyPair kp = kpg.genKeyPair();
final X509V3CertificateGenerator cert = new X509V3CertificateGenerator();
cert.setSerialNumber(BigInteger.valueOf(1));
final String dn = InetAddress.getLocalHost().getHostName();
cert.setSubjectDN(new X509Principal("CN=" + dn));
cert.setIssuerDN(new X509Principal("CN=" + dn));
cert.setPublicKey(kp.getPublic());
cert.setNotBefore(new Date());
final Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.YEAR, 1);
cert.setNotAfter(c.getTime());
cert.setSignatureAlgorithm("SHA1WithRSA");
final PrivateKey signingKey = kp.getPrivate();
final X509Certificate certificate = cert.generate(signingKey, "BC");
ks.setKeyEntry(this.keyStoreAlias, signingKey, password, new Certificate[]{certificate});
try (final FileOutputStream fos = new FileOutputStream(this.keystoreResource.getFile().getCanonicalPath())) {
ks.store(fos, password);
fos.flush();
}
LOGGER.info("Created keystore {} with key alias {} ",
keystoreResource.getFile().getCanonicalPath(),
ks.aliases().nextElement());
} catch (final Exception e) {
throw new SAMLException("Could not create keystore", e);
}
}
}