[java-oidc-common] branch main updated: JCOMOIDC-89 - Add a new CredentialFactoryBean type which allows null objects from createInstance

Phil Smart philip.smart at jisc.ac.uk
Wed Oct 25 08:57:53 UTC 2023


This is an automated email from the git hooks/post-receive script.

philsmart pushed a commit to branch main
in repository java-oidc-common.

View the commit online:
http://git.shibboleth.net/view/?p=java-oidc-common.git;a=commit;h=b65e1b30ecb7c7794586417022967289c8c68865

The following commit(s) were added to refs/heads/main by this push:
     new b65e1b3  JCOMOIDC-89 - Add a new CredentialFactoryBean type which allows null objects from createInstance
b65e1b3 is described below

commit b65e1b30ecb7c7794586417022967289c8c68865
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Oct 25 09:57:40 2023 +0100

    JCOMOIDC-89 - Add a new CredentialFactoryBean type which allows null
    objects from createInstance
    
     - Add new factory bean classes
     - Modify existing BasicJWKCredentialFactoryBean to use new factory
    classes
     - Deprecate FailIfResourceIsNull, use throwIfBeanIsNull.
    
    https://shibboleth.atlassian.net/browse/JCOMOIDC-89
---
 .../AbstractNullableComponentAwareFactoryBean.java |  84 +++++++++
 .../AbstractNullableCredentialFactoryBean.java     | 203 +++++++++++++++++++++
 .../credential/BasicJWKCredentialFactoryBean.java  |  72 +++++---
 .../BasicJWKCredentialFactoryBeanTest.java         |  80 ++++++++
 4 files changed, 415 insertions(+), 24 deletions(-)

diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/AbstractNullableComponentAwareFactoryBean.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/AbstractNullableComponentAwareFactoryBean.java
new file mode 100644
index 0000000..2725e84
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/AbstractNullableComponentAwareFactoryBean.java
@@ -0,0 +1,84 @@
+/*
+ * 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
+ *
+ *    http://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 net.shibboleth.oidc.security.credential;
+
+import javax.annotation.Nullable;
+
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.config.AbstractFactoryBean;
+
+import net.shibboleth.shared.component.DestructableComponent;
+import net.shibboleth.shared.component.InitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * 
+ * A Factory bean which is aware of the component lifecycle interfaces.
+ *
+ * <p>Unlike other bean factories, this factory allows {@code null} beans to be returned via its 
+ * {@link #createInstance()} method. Ultimately, a FactoryBean implementation may return {@code null} objects (see 
+ * {@link FactoryBean#getObject()) and so this is compatible with Spring's Factory Bean interface even though 
+ * it is not directly compatible with the {@code non-null} contract of the {@link AbstractFactoryBean#createInstance()} 
+ * method. </p>
+ * 
+ * @param <T> The type to implement
+ */
+public abstract class AbstractNullableComponentAwareFactoryBean<T> extends AbstractFactoryBean<T> {       
+    
+    /** {@inheritDoc}. Call our destroy method if apposite. */
+    @Override protected void destroyInstance(@Nullable final T instance) throws Exception {
+        super.destroyInstance(instance);
+        if (instance instanceof final DestructableComponent destructableComponent) {
+            destructableComponent.destroy();
+        }
+    }
+
+    /**
+     *  Call the child class to create the object, then initialize it apposite.
+     * 
+     *  <p>This method is allowed to return {@code null} even though it overrides a superclass method which 
+     *  is {@code non-null}. Spring {@link FactoryBean FactoryBeans} can return {@code null} objects, 
+     *  see {@link FactoryBean#getObject()}. </p>
+     * 
+     *  {@inheritDoc}.
+     */
+    @SuppressWarnings("null")
+    @Override
+    @Nullable protected final T createInstance() throws Exception {
+        if (!isSingleton()) {
+            LoggerFactory.getLogger(AbstractNullableComponentAwareFactoryBean.class).error(
+                    "Configuration error: {} should not be used to create prototype beans."
+                            + "  Destroy is never called for prototype beans",
+                            AbstractNullableComponentAwareFactoryBean.class);
+            throw new BeanCreationException("Do not use AbstractComponentAwareFactoryBean to create prototype beans");
+        }
+        final T theBean = doCreateInstance();
+        if (theBean instanceof final InitializableComponent initComponent) {
+            initComponent.initialize();
+        }
+        return theBean;
+    }
+
+    /**
+     * Implementation method to create the bean instance.
+     * 
+     * @return the bean. Can be {@code null} if the bean was not created but no error was signalled.
+     * 
+     * @throws Exception if there was an unrecoverable error constructing the bean.
+     */
+    @Nullable protected abstract T doCreateInstance() throws Exception;
+
+}
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/AbstractNullableCredentialFactoryBean.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/AbstractNullableCredentialFactoryBean.java
new file mode 100644
index 0000000..40bbce3
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/AbstractNullableCredentialFactoryBean.java
@@ -0,0 +1,203 @@
+/*
+ * 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
+ *
+ *    http://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 net.shibboleth.oidc.security.credential;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.BeanCreationException;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A factory bean to accumulate the information pertaining to an {@link Credential}. 
+ * 
+ * <p>Note, {@link #doCreateInstance()} is allowed to return {@code null} if {@code failIfBeanIsNull} is 'false' (the
+ * default throws an exception if the bean is null).</p>
+ * 
+ * @param <T> the type of credential to create.
+ */
+public abstract class AbstractNullableCredentialFactoryBean<T extends Credential> 
+                                                extends AbstractNullableComponentAwareFactoryBean<T> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractNullableCredentialFactoryBean.class);
+
+    /** Usage type of the credential. */
+    @Nullable private String usageType;
+
+    /** Names for the key represented by the credential. */
+    @Nullable  private List<String> keyNames;
+
+    /** Identifier for the owner of the credential. */
+    @Nullable private String entityID;
+    
+    /** The privateKey Password (if any). */
+    @Nullable private char[] privateKeyPassword;
+
+    /** For logging: The description of the source of the configuration.*/
+    @Nullable private String configDescription;
+    
+    /** Should the factory throw an exception if the created bean instance is null?.*/
+    private boolean throwIfBeanIsNull;
+    
+    
+    /** Constructor. */
+    protected AbstractNullableCredentialFactoryBean() {
+        throwIfBeanIsNull = true;
+    }    
+    
+    /**
+     * Should the factory throw an exception if the bean is null?
+     *  
+     * @param flag the flag
+     */
+    public void setThrowIfBeanIsNull(final boolean flag) {
+        throwIfBeanIsNull = flag;
+    }
+    
+    /**
+     * Get the flag to determine if the factory should throw an exception if the bean is {@code null}.
+     * 
+     * @return true if the factory should throw an exception if the bean is null, false otherwise. 
+     */
+    public boolean isThrowIfBeanIsNull() {
+        return throwIfBeanIsNull;
+    }
+
+   
+    /**
+     * Gets the names for the key represented by the credential.
+     * 
+     * @return names for the key represented by the credential
+     */
+    @Nullable public List<String> getKeyNames() {
+        return keyNames;
+    }
+
+    /**
+     * Gets the usage type of the credential.
+     * 
+     * @return usage type of the credential
+     */
+    @Nullable public String getUsageType() {
+        return usageType;
+    }
+
+    /**
+     * Get the entity ID of the credential.
+     * 
+     * @return the entity ID
+     */
+    @Nullable public String getEntityID() {
+        return entityID;
+    }
+
+    /**
+     * Sets the names for the key represented by the credential.
+     * 
+     * @param names names for the key represented by the credential
+     */
+    public void setKeyNames(@Nullable final List<String> names) {
+        keyNames = names;
+    }
+
+    /**
+     * Sets the usage type of the credential.
+     * 
+     * @param type usage type of the credential
+     */
+    public void setUsageType(@Nullable final String type) {
+        if (null != type) {
+            usageType = type.toUpperCase();
+        } else {
+            usageType = type;
+        }
+    }
+
+    /**
+     * Set the entity ID of the credential.
+     * 
+     * @param newEntityID the entity ID
+     */
+    public void setEntityID(@Nullable final String newEntityID) {
+        entityID = newEntityID;
+    }
+    
+    /**
+     * Get the password for the private key.
+     * 
+     * @return Returns the privateKeyPassword.
+     */
+    @Nullable public char[] getPrivateKeyPassword() {
+        return privateKeyPassword;
+    }
+
+    /**
+     * Set the password for the private key.
+     * 
+     * @param password The password to set.
+     */
+    public void setPrivateKeyPassword(@Nullable final char[] password) {
+        if (null != password && password.length > 0) {
+            privateKeyPassword = password;
+        } else {
+            privateKeyPassword = null;
+        }
+    }
+
+    /** For logging, get the description of the resource that defined this bean.
+     * 
+     * @return Returns the description.
+     */
+    @Nullable public String getConfigDescription() {
+        return configDescription;
+    }
+
+    /** For logging, set the description of the resource that defined this bean.
+     * 
+     * @param desc what to set.
+     */
+    public void setConfigDescription(@Nullable final String desc) {
+        configDescription = desc;
+    }
+    
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected T doCreateInstance() throws Exception {
+        final T credential = doCreateCredential();
+        if (credential == null && isThrowIfBeanIsNull()) {
+            log.debug("Credential was null, must not be null (if allowable set throwIfBeanIsNull "
+                    + "to 'false')");
+            throw new BeanCreationException("Null credential");
+        }
+        return credential;
+    }
+    
+    /**
+     * Implementation method to create the {@link Credential} instance. 
+     * 
+     * @return the credential, or {@code null}.
+     * 
+     * @throws Exception if there was an unrecoverable error constructing the credential.
+     */
+    @Nullable protected abstract T doCreateCredential() throws Exception;
+    
+}
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/BasicJWKCredentialFactoryBean.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/BasicJWKCredentialFactoryBean.java
index 42b26fa..e7ce913 100644
--- a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/BasicJWKCredentialFactoryBean.java
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/security/credential/BasicJWKCredentialFactoryBean.java
@@ -16,15 +16,16 @@ package net.shibboleth.oidc.security.credential;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.text.ParseException;
 import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.crypto.SecretKey;
 
-import org.opensaml.spring.credential.AbstractCredentialFactoryBean;
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.BeanCreationException;
 import org.springframework.core.io.Resource;
 
@@ -37,23 +38,23 @@ import com.nimbusds.jose.jwk.OctetSequenceKey;
 import net.shibboleth.oidc.security.CredentialConversionUtil;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.DeprecationSupport;
+import net.shibboleth.shared.primitive.DeprecationSupport.ObjectType;
+import net.shibboleth.shared.primitive.LoggerFactory;
 
 /** 
  * Factory bean for Basic JSON Web Keys (JWK).
  * 
  * @since 2.2.0
  */
-public class BasicJWKCredentialFactoryBean extends AbstractCredentialFactoryBean<BasicJWKCredential> {
+public class BasicJWKCredentialFactoryBean extends AbstractNullableCredentialFactoryBean<BasicJWKCredential> {
 
     /** Class logger. */
-    private final Logger log = LoggerFactory.getLogger(BasicJWKCredentialFactoryBean.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(BasicJWKCredentialFactoryBean.class);
 
     /** Where the private key is to be found. */
     @Nullable private Resource jwkResource;
     
-    /** Should the factory throw an exception if the resource is null?.*/
-    private boolean failIfResourceIsNull;
-    
     /**
      * The JCA algorithm name to use if the key to be converted is a symmetric key.
      * Default is AES.
@@ -62,7 +63,7 @@ public class BasicJWKCredentialFactoryBean extends AbstractCredentialFactoryBean
     
     /** Constructor.*/
     public BasicJWKCredentialFactoryBean() {
-        failIfResourceIsNull = true;
+        super();
         symmetricKeyAlgorithm = "AES";
     }
     
@@ -77,12 +78,15 @@ public class BasicJWKCredentialFactoryBean extends AbstractCredentialFactoryBean
     }
     
     /**
-     * Should the factory throw an exeception if the resource is null?
+     * Should the factory throw an exception if the resource is null?
      *  
      * @param flag the flag
      */
+    @Deprecated(since="3.1.0", forRemoval=true)
     public void setFailIfResourceIsNull(final boolean flag) {
-        failIfResourceIsNull = flag;
+        DeprecationSupport.warn(ObjectType.METHOD, "setFailIfResourceIsNull",
+                "oidc-credentials.xml", "setFailIfBeanIsNull");
+        setThrowIfBeanIsNull(flag);
     }
 
     /**
@@ -96,34 +100,54 @@ public class BasicJWKCredentialFactoryBean extends AbstractCredentialFactoryBean
 
     /** {@inheritDoc} */
     @Override
-    protected BasicJWKCredential doCreateInstance() throws Exception {
-
-        if (jwkResource == null && failIfResourceIsNull) {
-            log.error("{}: No JWK credential provided", getConfigDescription());
-            throw new BeanCreationException("No JWK credential provided");
-        } else if (jwkResource == null) {
+    @Nullable protected BasicJWKCredential doCreateCredential() throws Exception {
+        
+        final Resource jwkResourceLocal = jwkResource;
+        if (jwkResourceLocal == null) {
             return null;
         }
+        
+        assert jwkResourceLocal != null;
         JWK jwk = null;
         BasicJWKCredential jwkCredential = null;
-        try (InputStream is = jwkResource.getInputStream()) {
+        
+        try (InputStream is = jwkResourceLocal.getInputStream()) {
+            
             jwk = JWK.parse(new String(ByteStreams.toByteArray(is)));
             jwkCredential = new BasicJWKCredential();
+            
             if (jwk.getKeyType() == KeyType.EC || jwk.getKeyType() == KeyType.RSA) {
                 if (jwk.isPrivate()) {
-                    jwkCredential.setPrivateKey(((AsymmetricJWK) jwk).toPrivateKey());
+                    final PrivateKey privateKey = ((AsymmetricJWK) jwk).toPrivateKey();
+                    if (privateKey != null) {
+                        jwkCredential.setPrivateKey(privateKey);
+                    } else {
+                        throw new BeanCreationException("Private key type found, but no key supplied");
+                    }                    
                 }
-                jwkCredential.setPublicKey(((AsymmetricJWK) jwk).toPublicKey());
+                final PublicKey publicKey = ((AsymmetricJWK) jwk).toPublicKey();
+                if (publicKey != null) {
+                    jwkCredential.setPublicKey(publicKey);
+                } else {
+                    throw new BeanCreationException("Public key type found, but no key supplied");
+                }                
             } else if (jwk.getKeyType() == KeyType.OCT) {
-                jwkCredential.setSecretKey(((OctetSequenceKey) jwk).toSecretKey(symmetricKeyAlgorithm));
+                final SecretKey secretKey = ((OctetSequenceKey) jwk).toSecretKey(symmetricKeyAlgorithm);
+                if (secretKey != null) {
+                    jwkCredential.setSecretKey(secretKey);
+                } else {
+                    throw new BeanCreationException("Secret key type found, but no key supplied");
+                }
             } else {
-                throw new BeanCreationException("Unsupported KeyFile at " + jwkResource.getDescription());
+                throw new BeanCreationException("Unsupported KeyFile at " + jwkResourceLocal.getDescription());
             }
         } catch (final IOException | ParseException e) {
-            log.error("{}: Could not decode KeyFile at {}: {}", getConfigDescription(), jwkResource.getDescription(),
-                    e);
-            throw new BeanCreationException("Could not decode provided KeyFile " + jwkResource.getDescription(), e);
+            log.error("{}: Could not decode KeyFile at {}: {}", getConfigDescription(), 
+                    jwkResourceLocal.getDescription(), e);
+            throw new BeanCreationException("Could not decode provided KeyFile " + 
+                    jwkResourceLocal.getDescription(), e);
         }
+        
         jwkCredential.setUsageType(CredentialConversionUtil.getUsageType(jwk));
         jwkCredential.setEntityId(getEntityID());
         jwkCredential.setAlgorithm(jwk.getAlgorithm());
diff --git a/oidc-common-crypto-api/src/test/java/net/shibboleth/oidc/security/credential/BasicJWKCredentialFactoryBeanTest.java b/oidc-common-crypto-api/src/test/java/net/shibboleth/oidc/security/credential/BasicJWKCredentialFactoryBeanTest.java
new file mode 100644
index 0000000..30730ef
--- /dev/null
+++ b/oidc-common-crypto-api/src/test/java/net/shibboleth/oidc/security/credential/BasicJWKCredentialFactoryBeanTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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
+ *
+ *    http://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 net.shibboleth.oidc.security.credential;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.nio.charset.StandardCharsets;
+
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.core.io.ByteArrayResource;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for the {@link BasicJWKCredentialFactoryBean}.
+ */
+public class BasicJWKCredentialFactoryBeanTest {
+
+    /** Factory to test.*/
+    private BasicJWKCredentialFactoryBean factory;
+
+    /** Example EC JWK taken from JWK spec.*/
+    private final String keyResource = 
+                         """
+                         {
+                             "alg":"EC",
+                             "crv":"P-256",
+                             "kty": "EC",
+                             "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
+                             "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
+                             "use":"enc",
+                             "kid":"1"
+                          }
+                          """;
+
+    @BeforeMethod
+    public void setup() throws Exception {
+        factory = new BasicJWKCredentialFactoryBean();
+    }
+
+    @Test(expectedExceptions = BeanCreationException.class)
+    public void testNullJWKResource_Fail() throws Exception {
+        factory.setThrowIfBeanIsNull(true);
+        factory.setResource(null);
+        factory.afterPropertiesSet();
+        factory.getObject();
+    }
+    
+    @Test
+    public void testNullJWKResource_Success() throws Exception {
+        factory.setThrowIfBeanIsNull(false);
+        factory.setResource(null);
+        factory.afterPropertiesSet();
+        final var cred = factory.getObject();
+        assertNull(cred);
+    }
+
+    @Test
+    public void testBeanCreated() throws Exception {
+        factory.setThrowIfBeanIsNull(true);
+        factory.setResource(new ByteArrayResource(keyResource.getBytes(StandardCharsets.UTF_8)));
+        factory.afterPropertiesSet();
+        final var credential = factory.getObject();
+        assertNotNull(credential);
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list