Skip to content

Commit

Permalink
Test security.saml-callback-handler WS-Security configuration option q…
Browse files Browse the repository at this point in the history
  • Loading branch information
ppalaga committed Nov 5, 2023
1 parent 95e989d commit 29e4cf0
Show file tree
Hide file tree
Showing 18 changed files with 624 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,6 @@ a| [[quarkus-cxf-rt-ws-security_quarkus.cxf.client.-clients-.security.saml-callb
--
A link:../../user-guide/configuration.html#beanRefs[reference] to a `javax.security.auth.callback.CallbackHandler` implementation used to construct SAML Assertions.

This option is experimental, because it is link:https://github.com/quarkiverse/quarkus-cxf/issues/1052[not covered by tests] yet.

ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_CXF_CLIENT__CLIENTS__SECURITY_SAML_CALLBACK_HANDLER+++[]
endif::add-copy-button-to-env-var[]
Expand Down Expand Up @@ -1296,8 +1294,6 @@ a| [[quarkus-cxf-rt-ws-security_quarkus.cxf.endpoint.-endpoints-.security.saml-c
--
A link:../../user-guide/configuration.html#beanRefs[reference] to a `javax.security.auth.callback.CallbackHandler` implementation used to construct SAML Assertions.

This option is experimental, because it is link:https://github.com/quarkiverse/quarkus-cxf/issues/1052[not covered by tests] yet.

ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_CXF_ENDPOINT__ENDPOINTS__SECURITY_SAML_CALLBACK_HANDLER+++[]
endif::add-copy-button-to-env-var[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,41 @@ void indexDependencies(BuildProducer<IndexDependencyBuildItem> indexDependencies
@BuildStep
NativeImageResourceBuildItem nativeImageResource() {
return new NativeImageResourceBuildItem(
"opensaml-config.properties",

// from org.apache.wss4j.common.saml.OpenSAMLBootstrap
"default-config.xml",
"schema-config.xml",
"saml1-assertion-config.xml",
"saml1-metadata-config.xml",
"saml1-protocol-config.xml",
"saml2-assertion-config.xml",
"saml2-assertion-delegation-restriction-config.xml",
"saml2-ecp-config.xml",
"saml2-metadata-algorithm-config.xml",
"saml2-metadata-attr-config.xml",
"saml2-metadata-config.xml",
"saml2-metadata-idp-discovery-config.xml",
"saml2-metadata-query-config.xml",
"saml2-metadata-reqinit-config.xml",
"saml2-metadata-ui-config.xml",
"saml2-metadata-rpi-config.xml",
"saml2-protocol-config.xml",
"saml2-protocol-thirdparty-config.xml",
"saml2-protocol-aslo-config.xml",
"saml2-channel-binding-config.xml",
"saml-ec-gss-config.xml",
"signature-config.xml",
"wss4j-signature-config.xml",
"encryption-config.xml",
"xacml20-context-config.xml",
"xacml20-policy-config.xml",
"xacml10-saml2-profile-config.xml",
"xacml11-saml2-profile-config.xml",
"xacml2-saml2-profile-config.xml",
"xacml3-saml2-profile-config.xml",
"saml2-xacml2-profile.xml",

"schema/xmltooling-config.xsd",
"schema/datatypes.dtd",
"schema/xml.xsd",
Expand Down Expand Up @@ -79,6 +114,8 @@ void reflectiveClasses(
@BuildStep
void registerServices(BuildProducer<ServiceProviderBuildItem> serviceProvider) {
Stream.of(
"org.opensaml.core.config.Configuration",
"org.opensaml.core.config.ConfigurationPropertiesSource",
"org.opensaml.core.config.Initializer",
"org.opensaml.xmlsec.signature.support.SignerProvider",
"org.opensaml.xmlsec.algorithm.AlgorithmDescriptor",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,6 @@ public interface SecurityConfig {
/**
* A <a href="../../user-guide/configuration.html#beanRefs">reference</a> to a
* {@code javax.security.auth.callback.CallbackHandler} implementation used to construct SAML Assertions.
* <p>
* This option is experimental, because it is
* <a href="https://github.com/quarkiverse/quarkus-cxf/issues/1052">not covered by tests</a> yet.
*
* @since 2.5.0
*/
Expand Down Expand Up @@ -275,7 +272,7 @@ public interface SecurityConfig {
boolean scFromJaasSubject();

/**
* If {@code}, then if the SAML Token contains Audience Restriction URIs, one of them must match one of the
* If {@code true}, then if the SAML Token contains Audience Restriction URIs, one of them must match one of the
* values in {@code audience.restrictions}; otherwise the SAML AudienceRestriction validation is disabled.
* <p>
* This option is experimental, because it is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

public class CryptoProducers {

private final PasswordEncryptor dummyPasswordEncryptor = new PasswordEncryptor() {
private static final PasswordEncryptor dummyPasswordEncryptor = new PasswordEncryptor() {

@Override
public String encrypt(String password) {
Expand All @@ -30,29 +30,23 @@ public String decrypt(String encryptedPassword) {
@ApplicationScoped
@Named
public Crypto bobCrypto() {
Properties props = new Properties();
props.put("org.apache.ws.security.crypto.provider", "org.apache.ws.security.components.crypto.Merlin");
props.put("org.apache.ws.security.crypto.merlin.keystore.type", "jks");
props.put("org.apache.ws.security.crypto.merlin.keystore.password", "password");
props.put("org.apache.ws.security.crypto.merlin.keystore.alias", "bob");
props.put("org.apache.ws.security.crypto.merlin.file", "bob.jks");
try {
return CryptoFactory.getInstance(props, CryptoFactory.class.getClassLoader(), dummyPasswordEncryptor);
} catch (WSSecurityException e) {
throw new RuntimeException(e);
}
return createCrypto("jks", "bob", "password", "bob.jks");
}

@Produces
@ApplicationScoped
@Named
public Crypto aliceCrypto() {
return createCrypto("jks", "alice", "password", "alice.jks");
}

public static Crypto createCrypto(String type, String alias, String password, String keyStoreFile) {
Properties props = new Properties();
props.put("org.apache.ws.security.crypto.provider", "org.apache.ws.security.components.crypto.Merlin");
props.put("org.apache.ws.security.crypto.merlin.keystore.type", "jks");
props.put("org.apache.ws.security.crypto.merlin.keystore.password", "password");
props.put("org.apache.ws.security.crypto.merlin.keystore.alias", "alice");
props.put("org.apache.ws.security.crypto.merlin.file", "alice.jks");
props.put("org.apache.ws.security.crypto.merlin.keystore.type", type);
props.put("org.apache.ws.security.crypto.merlin.keystore.password", password);
props.put("org.apache.ws.security.crypto.merlin.keystore.alias", alias);
props.put("org.apache.ws.security.crypto.merlin.file", keyStoreFile);
try {
return CryptoFactory.getInstance(props, CryptoFactory.class.getClassLoader(), dummyPasswordEncryptor);
} catch (WSSecurityException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkiverse.cxf.it.security.policy;

import jakarta.jws.WebMethod;
import jakarta.jws.WebService;

import org.apache.cxf.annotations.Policy;

/**
*/
@WebService(serviceName = "Saml1PolicyHelloService")
@Policy(placement = Policy.Placement.BINDING, uri = "saml1-policy.xml")
public interface Saml1PolicyHelloService extends AbstractHelloService {
@WebMethod
@Override
String hello(String text);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkiverse.cxf.it.security.policy;

import jakarta.jws.WebMethod;
import jakarta.jws.WebService;

@WebService
public class Saml1PolicyHelloServiceImpl implements Saml1PolicyHelloService {

@WebMethod
@Override
public String hello(String text) {
return "Hello " + text + " from helloSaml1!";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkiverse.cxf.it.security.policy;

import jakarta.jws.WebMethod;
import jakarta.jws.WebService;

import org.apache.cxf.annotations.Policy;

/**
*/
@WebService(serviceName = "Saml2PolicyHelloService")
@Policy(placement = Policy.Placement.BINDING, uri = "saml2-policy.xml")
public interface Saml2PolicyHelloService extends AbstractHelloService {
@WebMethod
@Override
String hello(String text);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkiverse.cxf.it.security.policy;

import jakarta.jws.WebMethod;
import jakarta.jws.WebService;

@WebService
public class Saml2PolicyHelloServiceImpl implements Saml2PolicyHelloService {

@WebMethod
@Override
public String hello(String text) {
return "Hello " + text + " from helloSaml2!";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package io.quarkiverse.cxf.it.security.policy;

import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Collections;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Named;

import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.crypto.CryptoType;
import org.apache.wss4j.common.saml.SAMLCallback;
import org.apache.wss4j.common.saml.bean.AttributeBean;
import org.apache.wss4j.common.saml.bean.AttributeStatementBean;
import org.apache.wss4j.common.saml.bean.ConditionsBean;
import org.apache.wss4j.common.saml.bean.KeyInfoBean;
import org.apache.wss4j.common.saml.bean.KeyInfoBean.CERT_IDENTIFIER;
import org.apache.wss4j.common.saml.bean.SubjectBean;
import org.apache.wss4j.common.saml.bean.Version;
import org.apache.wss4j.common.saml.builder.SAML1Constants;
import org.apache.wss4j.common.saml.builder.SAML2Constants;
import org.apache.wss4j.dom.WSConstants;

public class SamlBeanProducers {

@Produces
@ApplicationScoped
@Named
public CallbackHandler saml2CallbackHandler() {
return new SamlCallbackHandler(Version.SAML_20);
}

@Produces
@ApplicationScoped
@Named
public CallbackHandler saml1CallbackHandler() {
return new SamlCallbackHandler(Version.SAML_11);
}

/**
* A CallbackHandler instance that is used by the STS to mock up a SAML Attribute Assertion.
* <p>
* Adapted from <a href=
* "https://github.com/apache/cxf/blob/cxf-4.0.3/systests/ws-security/src/test/java/org/apache/cxf/systest/ws/saml/client/SamlCallbackHandler.java">https://github.com/apache/cxf/blob/cxf-4.0.3/systests/ws-security/src/test/java/org/apache/cxf/systest/ws/saml/client/SamlCallbackHandler.java</a>
*/
public static class SamlCallbackHandler implements CallbackHandler {
private final Version samlVersion;
private String confirmationMethod;
private CERT_IDENTIFIER keyInfoIdentifier = CERT_IDENTIFIER.X509_CERT;
private final boolean signAssertion = false;
private ConditionsBean conditions;
private String cryptoAlias = "alice";
private String cryptoPassword = "password";
private String cryptoKeystoreFile = "alice.jks";
private String signatureAlgorithm = WSConstants.RSA_SHA1;
private String digestAlgorithm = WSConstants.SHA1;

public SamlCallbackHandler(Version samlVersion) {
this.samlVersion = samlVersion;
switch (samlVersion) {
case SAML_20:
this.confirmationMethod = SAML2Constants.CONF_SENDER_VOUCHES;
break;
case SAML_11:
case SAML_10:
this.confirmationMethod = SAML1Constants.CONF_SENDER_VOUCHES;
break;
default:
throw new IllegalStateException("Unexpected " + Version.class.getName() + ": " + samlVersion);
}
}

public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof SAMLCallback) {
SAMLCallback callback = (SAMLCallback) callbacks[i];
callback.setSamlVersion(samlVersion);
if (conditions != null) {
callback.setConditions(conditions);
}

callback.setIssuer("sts");
String subjectName = "uid=sts-client,o=mock-sts.com";
String subjectQualifier = "www.mock-sts.com";
SubjectBean subjectBean = new SubjectBean(
subjectName, subjectQualifier, confirmationMethod);
if (SAML2Constants.CONF_HOLDER_KEY.equals(confirmationMethod)
|| SAML1Constants.CONF_HOLDER_KEY.equals(confirmationMethod)) {
try {
KeyInfoBean keyInfo = createKeyInfo();
subjectBean.setKeyInfo(keyInfo);
} catch (Exception ex) {
throw new IOException("Problem creating KeyInfo: " + ex.getMessage());
}
}
callback.setSubject(subjectBean);

AttributeStatementBean attrBean = new AttributeStatementBean();
attrBean.setSubject(subjectBean);

AttributeBean attributeBean = new AttributeBean();
switch (samlVersion) {
case SAML_20:
attributeBean.setQualifiedName("subject-role");
break;
case SAML_11:
case SAML_10:
attributeBean.setSimpleName("subject-role");
attributeBean.setQualifiedName("http://custom-ns");
break;
default:
throw new IllegalStateException("Unexpected " + Version.class.getName() + ": " + samlVersion);
}
attributeBean.addAttributeValue("system-user");
attrBean.setSamlAttributes(Collections.singletonList(attributeBean));
callback.setAttributeStatementData(Collections.singletonList(attrBean));
callback.setSignatureAlgorithm(signatureAlgorithm);
callback.setSignatureDigestAlgorithm(digestAlgorithm);

Crypto crypto = io.quarkiverse.cxf.it.security.policy.CryptoProducers.createCrypto("jks", cryptoAlias,
cryptoPassword, cryptoKeystoreFile);
callback.setIssuerCrypto(crypto);
callback.setIssuerKeyName(cryptoAlias);
callback.setIssuerKeyPassword(cryptoPassword);
callback.setSignAssertion(signAssertion);
}
}
}

protected KeyInfoBean createKeyInfo() throws Exception {
Crypto crypto = io.quarkiverse.cxf.it.security.policy.CryptoProducers.createCrypto("jks", cryptoAlias,
cryptoPassword, cryptoKeystoreFile);
CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
cryptoType.setAlias(cryptoAlias);
X509Certificate[] certs = crypto.getX509Certificates(cryptoType);

KeyInfoBean keyInfo = new KeyInfoBean();
keyInfo.setCertIdentifer(keyInfoIdentifier);
if (keyInfoIdentifier == CERT_IDENTIFIER.X509_CERT) {
keyInfo.setCertificate(certs[0]);
} else if (keyInfoIdentifier == CERT_IDENTIFIER.KEY_VALUE) {
keyInfo.setPublicKey(certs[0].getPublicKey());
}

return keyInfo;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ public class SecurityPolicyResource {
@CXFClient("helloEncryptSignCrypto")
EncryptSignPolicyHelloService helloEncryptSignCrypto;

@Inject
@CXFClient("helloSaml1")
Saml1PolicyHelloService helloSaml1;

@Inject
@CXFClient("helloSaml2")
Saml2PolicyHelloService helloSaml2;

@Inject
@Named("messageCollector")
MessageCollector messageCollector;
Expand Down Expand Up @@ -140,6 +148,12 @@ public Response hello(@PathParam("client") String client, String body) {
case "helloEncryptSignCrypto":
service = helloEncryptSignCrypto;
break;
case "helloSaml1":
service = helloSaml1;
break;
case "helloSaml2":
service = helloSaml2;
break;
default:
throw new IllegalStateException("Unexpected client " + client);
}
Expand Down
Loading

0 comments on commit 29e4cf0

Please sign in to comment.