[java-plugin-shibd-oidc] 12/13: WIP: JSHIBDOIDC-35 - Add Storage Service ClientSecret Credential Resolver

Codeberg noreply at shibboleth.net
Fri Oct 16 14:48:52 UTC 2026


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

codeberg pushed a commit to branch dev/JSHIBDOIDC-31-cred-resolver
in repository java-plugin-shibd-oidc.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-oidc/commit/4dd7e860f08f5a35806356b758ee3e59162df0f8

commit 4dd7e860f08f5a35806356b758ee3e59162df0f8
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Oct 15 17:47:20 2026 +0100

    WIP: JSHIBDOIDC-35 - Add Storage Service ClientSecret Credential
    Resolver
    
     - migrate OIDC client secret resolution to credential storage service
     - replace default client secret resolver with storage-backed
    implementation
     - wip: work to cleanup places the specific client-secret resolver to
    new credential service is being used. Is it not specific to
    client_secrets it resolves anything from the resolver service.
     - wip: add tests that exerccise the content encryption from
    client_secret
     - wip: where things will be filtered about by filtering criterion
    resolver
    
    https://shibboleth.atlassian.net/browse/JSHIBDOIDC-35
---
 ...earerTokenForClientAuthenticationPredicate.java |   2 +-
 .../shibboleth/sp/oidc/testing/TestConstants.java  |   3 +
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   1 +
 .../net/shibboleth/sp/service/agent/postconfig.xml |   3 +-
 .../sp/service/credentials/postconfig.xml          |  59 ++--
 .../idp/module/conf/sp/oidc-credentials.xml        |  63 ----
 .../shibboleth/idp/module/conf/sp/oidc.properties  |   3 -
 .../net/shibboleth/sp/oidc/conf/module.properties  |  33 +-
 .../sp/oidc/flows/OIDCTokenConsumerFlowTest.java   |  36 +++
 .../net/shibboleth/idp/module/conf/sp/agents.xml   |   2 -
 .../idp/module/conf/sp/oidc-test-agents.xml        |  33 +-
 .../idp/module/credentials/sp/client-secrets.txt   |   1 +
 .../impl/ClientSecretCredentialResolver.java       |  42 +--
 .../impl/ClientSecretStorageServiceResolver.java   | 234 ++++++++++++++
 .../ClientSecretStorageServiceResolverTest.java    | 337 +++++++++++++++++++++
 .../JWKCredentialStorageServiceResolverTest.java   |   4 +-
 sp-oidc-impl/src/test/resources/logback-test.xml   |  16 +
 17 files changed, 707 insertions(+), 165 deletions(-)

diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/JWTBearerTokenForClientAuthenticationPredicate.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/JWTBearerTokenForClientAuthenticationPredicate.java
index 2303d78..4ea6d73 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/JWTBearerTokenForClientAuthenticationPredicate.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/JWTBearerTokenForClientAuthenticationPredicate.java
@@ -53,7 +53,7 @@ public class JWTBearerTokenForClientAuthenticationPredicate extends AbstractRely
                 final ClientAuthenticationMethod method = new ClientAuthenticationMethod(authMethod);
                 if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT) || 
                         method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
-                    log.trace("Client authentication type requires key lookup, is '{}", method);
+                    log.trace("Client authentication type requires key lookup, is '{}'", method);
                     return true;
                 } else {
                     log.trace("Configured client authentication method '{}' does not require a signed JWT bearer token"
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java
index 9e9c60d..7d6aeb9 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java
@@ -59,6 +59,9 @@ public final class TestConstants {
     /** Application ID used when private_key_jwt has been configured.*/
     public static final String APPLICATION_ID_PRIVATE_KEY_JWT = "test-oidc-application-with-ro-private-key-jwt";
     
+    /** Application ID used when private_key_jwt has been configured.*/
+    public static final String APPLICATION_ID_CLIENT_SECRET_JWT = "test-oidc-application-with-ro-client-secret-jwt";
+    
     /** Application ID used when the form_post response mode has been configured.*/
     public static final String APPLICATION_ID_POST_RESPONSE_MODE = "test-oidc-application-with-post-responsemode";
 
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 6ff8ab1..9a245a2 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -165,6 +165,7 @@
     </bean>   
     
     <!-- Global abstract bean for client secret resolvers which are specified in a number of different bean files -->
+    <!-- TODO this is not just a client secret resolver, it resolves everything from the credential service! CHANGE THE NAME AND THING WHERE USED -->
     <bean id="shibboleth.sp.oidc.ClientSecretCredentialResolver" class="net.shibboleth.sp.jose.config.impl.ClientSecretCredentialResolver"
        c:resolver-ref="shibboleth.sp.CredentialResolverService" scope="prototype" abstract="true"
        c:requestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
index b122fa4..26052b9 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
@@ -321,7 +321,8 @@
 						    </bean>
                     </constructor-arg>
                 </bean>
-                <bean id="ClientSecretCredentialResolver" parent="shibboleth.sp.oidc.ClientSecretCredentialResolver"/>
+                <!-- TODO why do we need this seperate one if we use the credential resolver service above? -->
+               <!--  <bean id="ClientSecretCredentialResolver" parent="shibboleth.sp.oidc.ClientSecretCredentialResolver"/> -->
             </list>
         </constructor-arg>
     </bean>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/credentials/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/credentials/postconfig.xml
index 00f8ea3..410cdb3 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/credentials/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/credentials/postconfig.xml
@@ -12,9 +12,12 @@
        default-init-method="initialize"
        default-destroy-method="destroy">
        
-    <!--  Credential Resolvers that are auto-wired into the credential resolver service (shibboleth.sp.CredentialResolverService) -->
+    <!--  
+        Credential Resolvers that are auto-wired into the credential resolver service 
+        (shibboleth.sp.CredentialResolverService). 
+    -->
 
-    <!-- WIP: Additional resolver parent beans. -->
+    <!-- Resolver parent beans. -->
 
     <bean id="shibboleth.sp.oidc.CredentialResolver.MapBased"
             parent="shibboleth.sp.CredentialResolver.MapBased" abstract="true">
@@ -23,7 +26,6 @@
         </property>
     </bean>
     
-    <!-- Abstract JOSE JWK Credential Beans TODO: maybe move to credentials-system.xml -->
     <bean id="shibboleth.sp.CredentialResolver.StorageBased.JWK"
         class="net.shibboleth.sp.jose.credential.impl.JWKCredentialStorageService" abstract="true"
         p:storageService-ref="shibboleth.sp.CredentialStorage.ReadOnly"
@@ -34,8 +36,10 @@
         class="net.shibboleth.sp.jose.credential.impl.JWKCredentialStorageService" abstract="true"
         p:storageService-ref="shibboleth.sp.CredentialStorage.ReadOnly"
         p:velocityEngine-ref="shibboleth.VelocityEngine"
-        p:contextTemplate="${protocol}" />
+        p:contextTemplate="${protocol}" />        
+
     
+    <!-- OIDC specific resolver parent beans. -->
     
     <bean id="shibboleth.sp.oidc.CredentialResolver.StorageBased.JWK"
                     parent="shibboleth.sp.CredentialResolver.StorageBased.JWK" abstract="true">
@@ -71,15 +75,10 @@
         </property>
     </bean>
     
-    <!-- END -->
-    
-    
-    
-
-    <!-- Installs a "default" set of OIDC credentials into the resolver service. -->
-    
-    <bean id="shibboleth.sp.oidc.DefaultCredentialResolver" class="net.shibboleth.sp.credential.impl.StaticCredentialResolver"
-            c:_0="#{getObject('shibboleth.sp.oidc.DefaultCredentials') ?: getObject('DefaultCredentials')}">
+    <bean id="shibboleth.sp.oidc.CredentialResolver.StorageBased.ClientSecret"
+        class="net.shibboleth.sp.jose.credential.impl.ClientSecretStorageServiceResolver" abstract="true"
+        p:velocityEngine-ref="shibboleth.VelocityEngine"
+        p:contextTemplate="${agentID}">
         <property name="order">
             <util:constant static-field="org.springframework.core.Ordered.LOWEST_PRECEDENCE" />
         </property>
@@ -87,25 +86,29 @@
             <util:constant static-field="net.shibboleth.oidc.saml.xmlobject.Constants.OIDC_PROTOCOL_URI" />
         </property>
     </bean>
-    
-    <bean id="shibboleth.sp.oidc.DefaultClientSecretCredentialResolver" class="net.shibboleth.sp.jose.config.impl.StaticClientSecretCredentialResolver"
-            c:_0="#{getObject('shibboleth.sp.oidc.DefaultClientSecretCredentials') ?: getObject('DefaultClientSecretCredentials')}">
-        <property name="protocols">
-            <util:constant static-field="net.shibboleth.oidc.saml.xmlobject.Constants.OIDC_PROTOCOL_URI" />
+
+
+    <!-- Installs a "default" set of OIDC credentials into the resolver service. -->
+   
+    <bean id="shibboleth.sp.oidc.DefaultStorageServiceClientSecretResolver" 
+        parent="shibboleth.sp.oidc.CredentialResolver.StorageBased.ClientSecret"
+        p:clientSecretTemplateString="${transformedRelyingPartyID}">
+        <property name="storageService">
+            <bean parent="shibboleth.ResourceBackedStorageService"
+                p:resource="%{sp.oidc.service.clientSecret.resource:%{idp.home}/credentials/sp/client-secrets.txt}"
+                p:delimiter="%{sp.oidc.service.clientSecret.delimiter::}"/>
         </property>
     </bean>
     
-   
-    
-    <bean id="DefaultClientSecretCredentials" class="org.springframework.beans.factory.config.ListFactoryBean" lazy-init="true">
-        <property name="sourceList">
-            <list>
-                <!-- TODO: This is a temporary definition, eventually these need to come from an OP and Agent (issuer) lookup resolver -->
-                <bean id="shibboleth.oidc.DefaultClientSecretCredential" parent="shibboleth.oidc.ClientSecretCredential"
-                         p:secret="%{sp.oidc.defaultClientSecret:#{null}}"/>
-            </list>
+    <bean id="shibboleth.sp.oidc.DefaultCredentialResolver" class="net.shibboleth.sp.credential.impl.StaticCredentialResolver"
+            c:_0="#{getObject('shibboleth.sp.oidc.DefaultCredentials') ?: getObject('DefaultCredentials')}">
+        <property name="order">
+            <util:constant static-field="org.springframework.core.Ordered.LOWEST_PRECEDENCE" />
         </property>
-    </bean>
+        <property name="protocols">
+            <util:constant static-field="net.shibboleth.oidc.saml.xmlobject.Constants.OIDC_PROTOCOL_URI" />
+        </property>
+    </bean>  
     
     <bean id="DefaultCredentials" class="org.springframework.beans.factory.config.ListFactoryBean" lazy-init="true">
         <property name="sourceList">
diff --git a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc-credentials.xml b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc-credentials.xml
deleted file mode 100644
index 8ac72ff..0000000
--- a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc-credentials.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<beans xmlns="http://www.springframework.org/schema/beans"
-       xmlns:context="http://www.springframework.org/schema/context"
-       xmlns:util="http://www.springframework.org/schema/util"
-       xmlns:p="http://www.springframework.org/schema/p"
-       xmlns:c="http://www.springframework.org/schema/c"
-       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
-                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
-                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
-                           
-       default-init-method="initialize"
-       default-destroy-method="destroy">
-    
-     <!--
-    This file contains default OIDC signing and encryption credentials. This file should be imported into agents.xml.
-    -->
-    
-    <!-- Uncomment these credentials for advanced signing and decryption operations -->
-    
-    <!-- 
-    <bean id="shibboleth.oidc.DefaultRSSigningCredential" parent="shibboleth.oidc.JWKCredential"
-        p:resource="%{sp.oidc.signing.rs.key:#{null}}" p:throwIfNull="false" />
-
-    <bean id="shibboleth.oidc.DefaultESSigningCredential" parent="shibboleth.oidc.JWKCredential"
-        p:resource="%{sp.oidc.signing.es.key:#{null}}" p:throwIfNull="false" />
-
-    <bean id="shibboleth.oidc.DefaultRSAEncryptionCredential" parent="shibboleth.oidc.JWKCredential"
-        p:resource="%{sp.oidc.encryption.rsa.key:#{null}}" p:throwIfNull="false" />
-    -->
-
-    <!--
-    Lists ALL of your RP's signing credentials for the default security configuration. 
-    If you define additional signing credentials make sure to include them within this list.
-    -->
-    <util:list id="shibboleth.oidc.SigningCredentials">
-     <!-- 
-          <ref bean="shibboleth.oidc.DefaultRSSigningCredential" />
-          <ref bean="shibboleth.oidc.DefaultESSigningCredential" />
-      -->
-    </util:list>
-
-    <!--
-    Lists ALL of your RP's token decryption credentials for the default security configuration.
-    If you  define additional decryption credentials make sure to include them within this list.
-    -->
-    <util:list id="shibboleth.oidc.EncryptionCredentials">
-        <!--  
-            <ref bean="shibboleth.oidc.DefaultRSAEncryptionCredential" /> 
-         -->
-    </util:list>
-    
-    <!--
-        The default credential based on the client_secret, which will be the only used credential in 
-        most cases.
-    -->
-
-    <!--  
-    <bean id="shibboleth.oidc.DefaultCredential" parent="shibboleth.oidc.ClientSecretCredential"
-        p:secret="%{sp.oidc.defaultClientSecret:#{null}}"/>
-    -->
-
-</beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
index b2ee9bc..bc934a5 100644
--- a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
+++ b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
@@ -1,8 +1,5 @@
 # OIDC-specific RP settings
 
-# A client_secret to use by default, disable if you are using more than one authenticatingAuthority
-#sp.oidc.defaultClientSecret =
-
 ## Signing and encryption/decryption keys for request objects, id tokens, client authentication, and userinfo response
 #sp.oidc.signing.rs.key = %{idp.home}/credentials/sp/sp-signing-rs.jwk
 #sp.oidc.signing.es.key = %{idp.home}/credentials/sp/sp-signing-es.jwk
diff --git a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/sp/oidc/conf/module.properties b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/sp/oidc/conf/module.properties
index d34b40a..b5b1035 100644
--- a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/sp/oidc/conf/module.properties
+++ b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/sp/oidc/conf/module.properties
@@ -13,27 +13,24 @@ sp.OIDC.url = /SP/OIDC
 sp.OIDC.1.src = /net/shibboleth/idp/module/conf/sp/oidc.properties
 sp.OIDC.1.dest = conf/sp/oidc.properties
 
-sp.OIDC.2.src = /net/shibboleth/idp/module/conf/sp/oidc-credentials.xml
-sp.OIDC.2.dest = conf/sp/oidc-credentials.xml
+sp.OIDC.2.src =  /net/shibboleth/idp/module/conf/sp/oidc-metadata-providers.xml
+sp.OIDC.2.dest = conf/sp/oidc-metadata-providers.xml
+sp.OIDC.2.replace = false
 
-sp.OIDC.3.src =  /net/shibboleth/idp/module/conf/sp/oidc-metadata-providers.xml
-sp.OIDC.3.dest = conf/sp/oidc-metadata-providers.xml
+sp.OIDC.3.src =  /net/shibboleth/idp/module/conf/attributes/oidc-claim-rules.xml
+sp.OIDC.3.dest = conf/attributes/oidc-claim-rules.xml
 sp.OIDC.3.replace = false
 
-sp.OIDC.4.src =  /net/shibboleth/idp/module/conf/attributes/oidc-claim-rules.xml
-sp.OIDC.4.dest = conf/attributes/oidc-claim-rules.xml
-sp.OIDC.4.replace = false
+sp.OIDC.4.src = /net/shibboleth/idp/module/bin/jwtgen.sh
+sp.OIDC.4.dest = bin/jwtgen-sp.sh
+sp.OIDC.4.exec = true
+sp.OIDC.4.replace = true
+sp.OIDC.4.windows = false
 
-sp.OIDC.5.src = /net/shibboleth/idp/module/bin/jwtgen.sh
-sp.OIDC.5.dest = bin/jwtgen-sp.sh
-sp.OIDC.5.exec = true
+sp.OIDC.5.src = /net/shibboleth/idp/module/bin/jwtgen.bat
+sp.OIDC.5.dest = bin/jwtgen-sp.bat
 sp.OIDC.5.replace = true
-sp.OIDC.5.windows = false
+sp.OIDC.5.nonwindows = false
 
-sp.OIDC.6.src = /net/shibboleth/idp/module/bin/jwtgen.bat
-sp.OIDC.6.dest = bin/jwtgen-sp.bat
-sp.OIDC.6.replace = true
-sp.OIDC.6.nonwindows = false
-
-sp.OIDC.7.src = /net/shibboleth/idp/module/bin/lib/json-web-key-generator-0.8.2-jar-with-dependencies.jar
-sp.OIDC.7.dest = bin/net.shibboleth.sp.oidc/lib/json-web-key-generator-0.8.2-jar-with-dependencies.jar
+sp.OIDC.6.src = /net/shibboleth/idp/module/bin/lib/json-web-key-generator-0.8.2-jar-with-dependencies.jar
+sp.OIDC.6.dest = bin/net.shibboleth.sp.oidc/lib/json-web-key-generator-0.8.2-jar-with-dependencies.jar
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
index b1cf13a..5b9e3ab 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
@@ -194,6 +194,42 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 TestConstants.RESOURCE_URL);
     }
     
+    /**
+     * Test successful flow with a signed id_token and a plain user info response. Use client_secret_jwt client 
+     * authentication.
+     * 
+     * @throws IOException on error
+     */
+    @Test
+    public void testSuccess_SignedIDToken_PlainUserInfo_ClientSecretJWT() throws Exception {
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(
+                Instant.now().plusSeconds(3600), Instant.now(), null, null), 
+                constructJSONUserInfoResponse());
+
+        final AuthenticationSuccessResponse response = 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.STATE_TOKEN);         
+        final DDF input = buildRemotedQueryStringResponse(response);
+        
+        // Add cookies
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
+                TestConstants.STATE_TOKEN, 
+                TestConstants.APPLICATION_ID_CLIENT_SECRET_JWT,
+                TestHelper.buildAuthenticationState(null, false, null), true));
+        
+        // Use application which requires private_key_jwt
+        setApplicationRequest(TestConstants.APPLICATION_ID_CLIENT_SECRET_JWT, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        final DDF output = assertOutputMessageSuccess(result);
+        assert output != null;
+        System.out.println("test output: " + output.toString());
+        validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"),
+                TestConstants.RESOURCE_URL);
+    }
+    
     /**
      * Test successful flow with a signed id_token and a plain user info response with max_age set.
      * 
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/agents.xml b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/agents.xml
index 4b63d08..59a4a62 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/agents.xml
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/agents.xml
@@ -65,7 +65,5 @@
     </util:list>
     -->
 
-
-    <import resource="oidc-credentials.xml" />
     
 </beans>
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml
index 2880e10..f0d8708 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml
@@ -16,8 +16,6 @@
     This file is used in the sp-oidc-conf-impl unit tests by overriding the AgentResolver service resources.
     -->
 
-    <import resource="oidc-credentials.xml" />
-
     <bean id="testsp.example.org" parent="shibboleth.sp.Agent" p:credentials="#{ {null : 'foo'} }"
         p:issuer="testspclientid">
         <property name="applications">
@@ -45,6 +43,11 @@
                  <bean p:id="test-oidc-application-with-ro-private-key-jwt" parent="shibboleth.sp.Application"
                     p:authenticatingAuthority="https://op.example.org"
                     p:profileConfigurations-ref="test.RequestObjectProfileConfigurations.PrivateKeyJWT"/>
+                    
+                 <!-- Use clinet_secret_jwt client authentication -->
+                 <bean p:id="test-oidc-application-with-ro-client-secret-jwt" parent="shibboleth.sp.Application"
+                    p:authenticatingAuthority="https://op.example.org"
+                    p:profileConfigurations-ref="test.RequestObjectProfileConfigurations.ClientSecretJWT"/>
                  
                     
                 <bean p:id="test-oidc-application-with-ro-with-requested-attrs" parent="shibboleth.sp.Application"
@@ -59,9 +62,6 @@
     <!-- All test profile configurations use the same default secret no matter what the OP -->
     <util:list id="test.ProfileConfigurations">
         <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig" p:useRequestObject="false">
-            <property name="clientCredential">
-                  <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
-            </property>
         </bean>
     </util:list>
     
@@ -71,34 +71,28 @@
     
     <util:list id="test.WithPKCE">
         <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig" p:forcePKCE="true">
-            <property name="clientCredential">
-                  <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
-            </property>
         </bean>
     </util:list>
     
     <util:list id="test.WithPostResponseMode">
         <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig" p:responseMode="form_post">
-            <property name="clientCredential">
-                  <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
-            </property>
         </bean>
     </util:list>
     
     <util:list id="test.RequestObjectProfileConfigurations">
         <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig" p:useRequestObject="true" p:signRequestObject="true" p:encryptRequestObject="false">
-            <property name="clientCredential">
-                  <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
-            </property>
         </bean>
     </util:list>
     
     <util:list id="test.RequestObjectProfileConfigurations.PrivateKeyJWT">
         <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig" p:useRequestObject="true" p:signRequestObject="true" p:encryptRequestObject="false"
             p:tokenEndpointAuthMethod="private_key_jwt">
-            <property name="clientCredential">
-                  <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
-            </property>
+        </bean>
+    </util:list>
+    
+    <util:list id="test.RequestObjectProfileConfigurations.ClientSecretJWT">
+        <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig" p:useRequestObject="true" p:signRequestObject="true" p:encryptRequestObject="false"
+            p:tokenEndpointAuthMethod="client_secret_jwt">
         </bean>
     </util:list>
     
@@ -106,10 +100,7 @@
         <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig" p:useRequestObject="true" p:signRequestObject="true" p:encryptRequestObject="false">
             <property name="requestedClaimsLookupStrategy">
                 <bean id="basicRequestedClaims" class="net.shibboleth.sp.oidc.functions.RequestedClaimsExampleFunction"/>
-            </property> 
-            <property name="clientCredential">
-                  <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
-            </property>       
+            </property>      
         </bean>
     </util:list>
     
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/sp/client-secrets.txt b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/sp/client-secrets.txt
new file mode 100644
index 0000000..1b50a1b
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/sp/client-secrets.txt
@@ -0,0 +1 @@
+testsp.example.org:07c11a0ae31600ec5813bf9b24b0e92e5efe8051:zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ
\ No newline at end of file
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/config/impl/ClientSecretCredentialResolver.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/config/impl/ClientSecretCredentialResolver.java
index f92fd33..a590f19 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/config/impl/ClientSecretCredentialResolver.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/config/impl/ClientSecretCredentialResolver.java
@@ -17,16 +17,17 @@ package net.shibboleth.sp.jose.config.impl;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.core.criterion.EntityIdCriterion;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.criterion.ProfileRequestContextCriterion;
 import org.opensaml.security.credential.Credential;
 import org.opensaml.security.credential.CredentialResolver;
-import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
-import org.opensaml.security.credential.impl.AbstractCriteriaFilteringCredentialResolver;
+import org.opensaml.security.credential.impl.AbstractCredentialResolver;
 import org.slf4j.Logger;
 
 import jakarta.servlet.ServletRequest;
 import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.jose.SignatureSigningParametersResolver;
 import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.shared.annotation.ParameterName;
 import net.shibboleth.shared.logic.Constraint;
@@ -44,14 +45,14 @@ import net.shibboleth.sp.criteria.ApplicationIDCriterion;
 
 /**
  * A {@link JOSEObjectCredentialResolver} that extracts a client_secret credential from the {@link CredentialResolver} 
- * service. 
+ * service. This is a specialised and limited function version of a {@link SignatureSigningParametersResolver} for
+ * dealing with the {@link CredentialResolver} service and client_secrets.
  * 
- * TODO: The client secret MUST be appropriate for the relying party (OpenID Provider) and Agent in question. As a 
- * result, an {@link EvaluableEntityIDCredentialCriterion} has to be built to filter out any credentials not registered 
- * with a given OpenID Provider — this is essential so secrets are not sent to the wrong Provider.
+ * <p>The client secret returned MUST be appropriate for the relying party (OpenID Provider) and Agent in question — 
+ * this is essential so secrets are not sent to the wrong Provider. It is left to the credential resolver service to 
+ * ensure appropriate credentials are returned.</p>
  */
-//TODO: Should be in common once finished
-public class ClientSecretCredentialResolver extends AbstractCriteriaFilteringCredentialResolver 
+public class ClientSecretCredentialResolver extends AbstractCredentialResolver 
 																		implements JOSEObjectCredentialResolver {
     
     /** Class logger. */
@@ -76,10 +77,11 @@ public class ClientSecretCredentialResolver extends AbstractCriteriaFilteringCre
     	credentialResolver = Constraint.isNotNull(resolver, "CredentialResolver cannot be null");
     	servletRequestSupplier = Constraint.isNotNull(requestSupplier, "ServletRequest supplier cannot be null");
     }
+ 
     
     /** {@inheritDoc} */
     @Override
-    @Nonnull protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) 
+    @Nonnull public Iterable<Credential> resolve(@Nullable final CriteriaSet criteriaSet) 
             throws ResolverException {
     	
     	final CriteriaSet finalCriteria = buildCriteriaSet(criteriaSet);
@@ -91,13 +93,8 @@ public class ClientSecretCredentialResolver extends AbstractCriteriaFilteringCre
     }
     
     /**
-     * Add or construct final {@link CriteriaSet} to supply to resolver. 
-     * 
-     * TODO: Mandate an {@link EvaluableEntityIDCredentialCriterion} is created, as credentials resolved must be 
-     * appropriate for the OpenID Provider. Or just pass through an EntityID type, so the resolver can use it as a key
-     * for lookup.
-     * 
-     * TODO: The AgentID is already present, so credentials should be resolved for the correct client.
+     * Add or construct final {@link CriteriaSet} to supply to resolver. Importantly the agentID and relying partyID 
+     * (OP) are included in the crtieria set so appropriate credentials can be returned.
      * 
      * @param criteria existing criteria to add to, if any
      * 
@@ -118,7 +115,6 @@ public class ClientSecretCredentialResolver extends AbstractCriteriaFilteringCre
         
         // Add Agent/Application info.
         final AgentRequestContext arc = prc.getSubcontext(AgentRequestContext.class);
-        //TODO we should probably mandate this exists, so we can ensure the agentId criterion is used
         if (arc != null) {
             final Agent agent = arc.getAgent();
             if (agent != null) {
@@ -137,16 +133,9 @@ public class ClientSecretCredentialResolver extends AbstractCriteriaFilteringCre
         if (rpc != null) {
             final String rpid = rpc.getRelyingPartyId();
             if (rpid != null) {
-            	// Create an evaluable criterion, as we require this to be evaluated
-            	// TODO we need to evaluate credentials on the RPID and Issuer/Agent eventually
-                //finalCriteria.add(new EvaluableEntityIDCredentialCriterion(rpid));
-            } else {
-            	throw new ResolverException("Client secret credential resolver requires relying party ID "
-            			+ "to scope credentials for");
+                finalCriteria.add(new EntityIdCriterion(rpid));
             }
-        } else {
-        	throw new ResolverException("Client secret credential resolver requires a relying party context");
-        }
+        } 
         
         //TODO: do we need to specify a 'secret' type here. e.g. via another criterion
 
@@ -166,4 +155,5 @@ public class ClientSecretCredentialResolver extends AbstractCriteriaFilteringCre
         return null;
     }
 
+
 }
\ No newline at end of file
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/credential/impl/ClientSecretStorageServiceResolver.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/credential/impl/ClientSecretStorageServiceResolver.java
new file mode 100644
index 0000000..a0d0e85
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/credential/impl/ClientSecretStorageServiceResolver.java
@@ -0,0 +1,234 @@
+
+package net.shibboleth.sp.jose.credential.impl;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.exception.VelocityException;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+import org.opensaml.storage.StorageRecord;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+
+import net.shibboleth.oidc.security.credential.ClientSecretCredential;
+import net.shibboleth.oidc.security.credential.DefaultClientSecretCredential;
+import net.shibboleth.oidc.security.credential.impl.DataEncryptionAlgorithmCriterion;
+import net.shibboleth.oidc.security.credential.impl.KeyManagmentAlgorithmCriterion;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.velocity.Template;
+import net.shibboleth.sp.credential.AbstractStorageServiceCredentialResolver;
+
+/**
+ * A client secret credential resolver that resolves client secrets from the attached storage service into usable 
+ * cryptographic credentials.
+ * 
+ * <p>This resolver has the following behaviour:</p>
+ * <ul>
+ * <li>Missing Criterion required to derive a suitable credential returns an empty list of credentials.</li>
+ * <li>Asymmetric key requests, return an empty list if credentials.</li>
+ * <li>Failure to derive a credential, throws an exception.</li>
+ * <li>Otherwise, a suitable credential is derived for either SIGNING or ENCRYPTION use.</p>
+ * </ul>
+ */
+public class ClientSecretStorageServiceResolver extends AbstractStorageServiceCredentialResolver<Credential> {
+	
+	/** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ClientSecretStorageServiceResolver.class);
+    
+    /** Template used to construct the storage key for the client secret. */
+    @Nonnull private String clientSecretTemplateString;
+    
+    /** Velocity template for the client secret storage key. */
+    @NonnullAfterInit private Template clientSecretTemplate;
+
+
+    /** Constructor.*/
+	public ClientSecretStorageServiceResolver() {
+		super(Credential.class);
+		clientSecretTemplateString = "${transformedRelyingPartyID}";
+	}
+	
+    /**
+     * Sets the Velocity template string for the client secret storage key.
+     * 
+     * <p>Defaults to "${transformedRelyingPartyID}", which punts any other decoration to the storage context.</p>
+     * 
+     * @param template template string
+     */
+    public void setClientSecretTemplateString(@Nonnull @NotEmpty final String template) {
+        checkSetterPreconditions();
+        
+        clientSecretTemplateString = Constraint.isNotNull(StringSupport.trimOrNull(template),
+                "JSON Web Key template cannot be null or empty");
+    }
+	
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        clientSecretTemplate = Template.fromTemplate(getVelocityEngine(), clientSecretTemplateString,
+                StandardCharsets.UTF_8);
+    }
+
+	 /** {@inheritDoc} */
+	@Override
+	@Nonnull @NotLive @Unmodifiable protected Iterable<Credential> doResolve(final VelocityContext velocityContext, 
+			final CriteriaSet criteria, final String storageContext) throws ResolverException {
+		
+		if (!verifyCriteria(criteria)) {
+			log.debug("Unsuitable criteria for client secret credential resolution");
+			return CollectionSupport.emptyList();
+		}
+		
+		final StorageRecord<String> secretData;
+
+        try {        	
+            final String keyStorageKey = clientSecretTemplate.merge(velocityContext);            
+	        log.debug("CredentialResolver {}: Resolved storage key ({})", getId(), keyStorageKey);            
+	        secretData = getStorageService().read(storageContext, keyStorageKey);  
+            
+        } catch (final VelocityException e) {
+            throw new ResolverException("Error executing storage key template", e);
+        } catch (final IOException e) {
+            throw new ResolverException("I/O exception retrieving credential data", e);
+        }
+        
+        if (secretData == null) {
+            log.debug("Credential Resolver {}: No client secret resolved", getId());
+            return CollectionSupport.emptyList();
+        }
+        
+        log.debug("CredentialResolver {}: Resolved data for client secret, constructing credential", getId());
+        
+        final Credential derivedCredential = 
+        		deriveClientSecretCredential(new DefaultClientSecretCredential(secretData.getValue()), criteria);		
+		if (derivedCredential == null) {
+			log.debug("Unable to derive a suitable credential from client secret");
+			return CollectionSupport.emptyList();
+		}
+		log.debug("CredentialResolver {}: Resolved client secret", getId());
+		return CollectionSupport.listOf(derivedCredential);
+		
+	}
+	
+	 /**
+     * Verify the criteria set has the required criterion to correctly process the client secret credential.
+     * 
+     * @param criteriaSet the criteria set to check
+     * 
+     * @return true iff the criteria contain the correct criterion to derive a suitable credential, false otherwise.
+     */
+    private boolean verifyCriteria(@Nullable final CriteriaSet criteriaSet) {
+    	
+    	if (criteriaSet == null) {
+    		log.trace("No criteria set found, can not process client secrets");
+    		return false;
+    	}
+    	
+		final UsageCriterion usageTypeCriterion = criteriaSet.get(UsageCriterion.class);
+        if (usageTypeCriterion == null) {
+        	log.trace("No usage type criterion supplied, unable to derive client_secret credential");
+        	return false;
+        }
+        
+        if (usageTypeCriterion.getUsage() == UsageType.ENCRYPTION) {
+        
+	        final KeyManagmentAlgorithmCriterion alg = criteriaSet.get(KeyManagmentAlgorithmCriterion.class);
+	        if (alg == null) {
+	        	log.trace(
+	                    "Credential criteria set did not contain an instance of KeyManagmentAlgorithmCriterion");
+	        	return false;
+	        }
+	        // The encryption method is only relevant to key derivation for the Direct Encryption mode
+	        final DataEncryptionAlgorithmCriterion enc = criteriaSet.get(DataEncryptionAlgorithmCriterion.class);
+	        if (enc == null) {
+	        	log.trace(
+	                    "Credential criteria set did not contain an instance of DataEncryptionAlgorithmCriterion");
+	        	return false;
+	        }
+        }
+        return true;
+    }
+	
+	/**
+     * Use the usage type and algorithm information in the criteria to build a suitable signing or encryption 
+     * credential.
+     * 
+     * <p>Only supports symmetric key encryption algorithms. Request for asymmetric key encryption algorithms are 
+     * ignored.</p>
+     * 
+     * @param secretCred the raw client_secret credential
+     * @param criteriaSet the criteria set used to find algorithm details for encryption keys
+     * 
+     * @return a suitable credential or {@code null} if the requested key type is unsupported
+     * 
+     * @throws ResolverException if there is an error deriving the key
+     */
+    @Nullable protected Credential deriveClientSecretCredential(@Nonnull final ClientSecretCredential secretCred, 
+            @Nonnull final CriteriaSet criteriaSet) throws ResolverException {
+        
+    	// Already verified that the criterion exist
+        final UsageCriterion usageTypeCriterion = criteriaSet.get(UsageCriterion.class);
+        final UsageType usageType = usageTypeCriterion.getUsage();
+        
+        if (usageType == UsageType.SIGNING) {
+            // Create a signing credential
+            final Credential signingCred = secretCred.toSigningCredential();
+            log.debug("Derived signing credential '{}'", signingCred.getKeyNames());            
+            return signingCred;
+            
+        } else if (usageType == UsageType.ENCRYPTION) {
+            // Create an encryption credential suitable for the algorithms specified
+            final KeyManagmentAlgorithmCriterion alg = criteriaSet.get(KeyManagmentAlgorithmCriterion.class);
+
+            // Technically the encryption method is only relevant to key derivation for the Direct Encryption mode
+            final DataEncryptionAlgorithmCriterion enc = criteriaSet.get(DataEncryptionAlgorithmCriterion.class);
+            
+            // Can only derive symmetric key credentials, ignore if not
+            if (JWEAlgorithm.Family.SYMMETRIC.contains(JWEAlgorithm.parse(alg.getAlgorithm()))) {                     
+                try {
+                    final JWEAlgorithm jweAlgorithm = JWEAlgorithm.parse(alg.getAlgorithm());
+                    final EncryptionMethod encryptionMethod = EncryptionMethod.parse(enc.getEncAlgorithm());
+                    final Credential derivedCred = secretCred.toEncryptionCredential(
+                            jweAlgorithm, encryptionMethod);
+                    
+                    log.debug("Derived encryption credential '{}' from 'alg={}' and 'enc={}'", derivedCred.getKeyNames()
+                            ,alg.getAlgorithm(), enc.getEncAlgorithm());
+                    return derivedCred;
+                    
+                } catch (final JOSEException e) {
+                    log.trace("Unable to derive symmetric encryption key from client_secret using 'alg={}' "
+                    		+ "and 'enc={}'", alg.getAlgorithm(), enc.getEncAlgorithm());      
+                    throw new ResolverException("Unable to create encryption key from client_secret", e);
+                }                
+            } else {    
+                log.trace("Asymmetric key requested, client_secret not appropriate");
+                return null;
+            }
+        } else { 
+            throw new ResolverException("Unable to create key from client_secret, incompatible usage type");
+        }
+        
+    }
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/credential/impl/ClientSecretStorageServiceResolverTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/credential/impl/ClientSecretStorageServiceResolverTest.java
new file mode 100644
index 0000000..928cde9
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/credential/impl/ClientSecretStorageServiceResolverTest.java
@@ -0,0 +1,337 @@
+
+package net.shibboleth.sp.oidc.profile.credential.impl;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.time.Duration;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.crypto.SecretKey;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+import org.springframework.core.io.FileSystemResource;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JWEAlgorithm;
+
+import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.oidc.security.credential.impl.DataEncryptionAlgorithmCriterion;
+import net.shibboleth.oidc.security.credential.impl.KeyManagmentAlgorithmCriterion;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.spring.resource.ResourceHelper;
+import net.shibboleth.shared.testing.VelocityEngine;
+import net.shibboleth.sp.criteria.AgentIDCriterion;
+import net.shibboleth.sp.criteria.ApplicationIDCriterion;
+import net.shibboleth.sp.jose.credential.impl.ClientSecretStorageServiceResolver;
+import net.shibboleth.sp.storage.impl.ResourceBackedMemoryStorageService;
+
+/**
+ * Tests for the {@link ClientSecretStorageServiceResolver}.
+ */
+public class ClientSecretStorageServiceResolverTest {
+	
+	private Path testRoot;
+	
+	@BeforeMethod
+    public void setUp() throws IOException {
+        testRoot = Files.createTempDirectory("test-secret-resolver");
+    }
+ 
+    private void tearDownWorker() throws IOException {
+        if (testRoot != null) {
+            Files.walkFileTree(testRoot, new SimpleFileVisitor<Path>() {
+                @Override
+                public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs)
+                    throws IOException
+                {
+                    Files.delete(file);
+                    return FileVisitResult.CONTINUE;
+                }
+                @Override
+                public FileVisitResult postVisitDirectory(final Path dir, final IOException e)
+                    throws IOException
+                {
+                    if (e == null) {
+                        Files.delete(dir);
+                        return FileVisitResult.CONTINUE;
+                    }
+                    // directory iteration failed
+                    throw e;
+                }
+            });
+            testRoot = null;
+        }
+    }
+
+    @AfterMethod
+    public void tearDown() throws IOException, InterruptedException {
+        try {
+            tearDownWorker();
+        } catch (final DirectoryNotEmptyException ex) {
+            // We hates the Microsoft Defender.  (it pins files so directories cannot be deleted)
+            Thread.sleep(10);
+            tearDownWorker();
+        }
+    }
+    
+    @Test
+    public void testStorageByAgentContextAndRelyingPartyID() throws Exception {
+    	
+    	// Create the secrets file
+    	final Path secretsFile = testRoot.resolve(FileSystems.getDefault().getPath("secret.txt"));
+        
+        // Generate key for testing.
+    	generateClientSecretEntries(secretsFile, 
+    			List.of(List.of("localhost:e73fd7b4d5d40e19ecd14b919b23fbd8ea99b748:secret1"),
+    					List.of("sp.example.org:e73fd7b4d5d40e19ecd14b919b23fbd8ea99b748:secret2")));
+        
+        
+        final ResourceBackedMemoryStorageService storage = new ResourceBackedMemoryStorageService();
+        storage.setId("test");
+        storage.setResource(ResourceHelper.of(new FileSystemResource(secretsFile)));
+        storage.setDelimiter(":");
+        storage.initialize();
+        
+        final ClientSecretStorageServiceResolver resolver = new ClientSecretStorageServiceResolver();
+        resolver.setId("test");
+        resolver.setStorageService(storage);
+        resolver.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        resolver.setContextTemplate("${agentID}");
+        resolver.initialize();
+        
+        JWKCredential credential = (JWKCredential) resolver.resolveSingle(
+                buildCriteriaSet("localhost", "default", "https://op.example.org/", UsageType.SIGNING, null, null));
+        assertNotNull(credential);
+        
+        SecretKey secKey = credential.getSecretKey();
+        assertNotNull(secKey);
+        
+        credential = (JWKCredential) resolver.resolveSingle(
+                buildCriteriaSet("sp.example.org", "default", "https://op.example.org/", UsageType.ENCRYPTION,
+                		JWEAlgorithm.A128KW, EncryptionMethod.A128CBC_HS256));
+        assertNotNull(credential);
+        
+        secKey = credential.getSecretKey();
+        assertNotNull(secKey);
+        
+        credential = (JWKCredential) resolver.resolveSingle(
+                buildCriteriaSet("missing-agent", "default", "https://op.example.org/", UsageType.ENCRYPTION,
+        		JWEAlgorithm.A128KW, EncryptionMethod.A128CBC_HS256));
+        Assert.assertNull(credential);
+        
+        storage.destroy();
+    }
+    
+    @Test
+    public void testStorageByAgentContextAndRelyingPartyID_FailNoCriterionForEncryption() throws Exception {
+    	
+    	// Create the secrets file
+    	final Path secretsFile = testRoot.resolve(FileSystems.getDefault().getPath("secret.txt"));
+        
+        // Generate key for testing.
+    	generateClientSecretEntries(secretsFile, 
+    			List.of(List.of("localhost:e73fd7b4d5d40e19ecd14b919b23fbd8ea99b748:secret1"),
+    					List.of("sp.example.org:e73fd7b4d5d40e19ecd14b919b23fbd8ea99b748:secret2")));
+        
+        
+        final ResourceBackedMemoryStorageService storage = new ResourceBackedMemoryStorageService();
+        storage.setId("test");
+        storage.setResource(ResourceHelper.of(new FileSystemResource(secretsFile)));
+        storage.setDelimiter(":");
+        storage.initialize();
+        
+        final ClientSecretStorageServiceResolver resolver = new ClientSecretStorageServiceResolver();
+        resolver.setId("test");
+        resolver.setStorageService(storage);
+        resolver.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        resolver.setContextTemplate("${agentID}");
+        resolver.initialize();
+        
+        
+        final JWKCredential credential = (JWKCredential) resolver.resolveSingle(
+                buildCriteriaSet("sp.example.org", "default", "https://op.example.org/", UsageType.ENCRYPTION,
+                		null, null));
+        assertNull(credential);
+        
+        storage.destroy();
+    }
+    
+    /**
+     * Test that the derived credentials are cached by the resolver. This is reliant on tight timing between the
+     * storage service detecting an empty file to effectively wipe the credentials from the underlying map, and
+     * the records expiring in the resolver cache. 
+     * 
+     * <p>Follows:</p>
+     * <ol>
+     * <li> Lookup SIGNING and ENCRYPTION credential, not cached, derived from the storage service.</li>
+     * <li> Clear the contents of the secrets file.</li>
+     * <li> Wait for the storage service to detect the emptied secrets file and load no entries (wipe exist).</li>
+     * <li> Resolve cached versions of a SIGNING and ENCRYPTION client secret.</li>
+     * <li> Wait for the cache to expire.</li>
+     * <li> Try and fail to load a SIGNING credential not in either the resolver cache on the backing storage service.</li>
+     * </ol>
+     * 
+     * @throws Exception
+     */
+    @Test
+    public void testStorageByAgentContextAndRelyingPartyID_Cached() throws Exception {
+    	
+    	// Create the secrets file
+    	final Path secretsFile = testRoot.resolve(FileSystems.getDefault().getPath("secret.txt"));
+        
+        // Generate key for testing.
+    	generateClientSecretEntries(secretsFile, 
+    			List.of(List.of("localhost:e73fd7b4d5d40e19ecd14b919b23fbd8ea99b748:secret1"),
+    					List.of("sp.example.org:e73fd7b4d5d40e19ecd14b919b23fbd8ea99b748:secret2")));
+        
+        
+        final ResourceBackedMemoryStorageService storage = new ResourceBackedMemoryStorageService();
+        storage.setId("test");
+        storage.setResource(ResourceHelper.of(new FileSystemResource(secretsFile)));
+        storage.setDelimiter(":");
+        storage.setCleanupInterval(Duration.ofSeconds(2));
+        storage.initialize();
+        
+        final ClientSecretStorageServiceResolver resolver = new ClientSecretStorageServiceResolver();
+        resolver.setId("test");
+        resolver.setStorageService(storage);
+        resolver.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        resolver.setContextTemplate("${agentID}");
+        resolver.setCacheEnabled(true);
+        resolver.setExpireAfterAccess(Duration.ofSeconds(5));        
+        resolver.initialize();
+        
+        JWKCredential credential = (JWKCredential) resolver.resolveSingle(
+                buildCriteriaSet("localhost", "default", "https://op.example.org/", UsageType.SIGNING, null, null));
+        assert credential != null;
+        
+        SecretKey secKey = credential.getSecretKey();
+        assert secKey != null;
+        
+        credential = (JWKCredential) resolver.resolveSingle(
+                buildCriteriaSet("sp.example.org", "default", "https://op.example.org/", UsageType.ENCRYPTION,
+                		JWEAlgorithm.A128KW, EncryptionMethod.A128CBC_HS256));
+        assert credential != null;
+        
+        secKey = credential.getSecretKey();
+        assertNotNull(secKey);
+        
+        // Clear contents of file so secrets can not come from the storage service once it reloads.
+        Files.writeString(secretsFile, "");
+        //Wait to ensure the empty map is reloaded
+        Thread.sleep(2000);
+
+        
+        // Test cached versions of both signing derived and encryption derived credentials
+        credential = (JWKCredential) resolver.resolveSingle(
+                buildCriteriaSet("localhost", "default", "https://op.example.org/", UsageType.SIGNING, null, null));
+        assertNotNull(credential);
+        
+        secKey = credential.getSecretKey();
+        assertNotNull(secKey);
+        
+        credential = (JWKCredential) resolver.resolveSingle(
+                buildCriteriaSet("sp.example.org", "default", "https://op.example.org/", UsageType.ENCRYPTION,
+                		JWEAlgorithm.A128KW, EncryptionMethod.A128CBC_HS256));
+        assert credential != null;
+        
+        secKey = credential.getSecretKey();
+        assertNotNull(secKey);
+        
+        
+        //Wait to expire the cache.
+        Thread.sleep(5000);
+        
+        credential = (JWKCredential) resolver.resolveSingle(
+                buildCriteriaSet("localhost", "default", "https://op.example.org/", UsageType.SIGNING, null, null));
+        assertNull(credential);
+        
+        storage.destroy();
+    }
+    
+    
+	
+    /**
+     * Write client secret entries (lines) to the batch file.
+     * 
+     * @param resourcePath the path to the secretes file that contains secret entries
+     * @param entries the client secret 'lines' to add to the client secret resource. Each entry is a line 
+     * 	containing the storage key and the secret.
+     * 
+     * @throws IOException on error
+     */
+    private void generateClientSecretEntries(final Path resourcePath, final List<List<String>> records) 
+    		throws IOException {
+    	final StringBuilder builder = new StringBuilder();
+    	for (final List<String> lineRecord : records) {
+    		final String line = lineRecord.stream().collect(Collectors.joining(":"));
+    		builder.append(line).append("\n");    		
+    	}
+    	final String lines = builder.toString();
+    	Files.writeString(resourcePath, lines, StandardCharsets.UTF_8);
+    }
+    
+    /**
+     * Builds a set of criteria for the resolver.
+     * 
+     * @param agentID agent ID
+     * @param applicationID application ID
+     * @param entityID entityID
+     * @param usage usage type
+     * 
+     * @return built criteria
+     */
+    @Nonnull private CriteriaSet buildCriteriaSet(@Nullable final String agentID, @Nullable final String applicationID,
+            @Nullable final String entityID, @Nullable final UsageType usage, final JWEAlgorithm keyManAlg, 
+            final EncryptionMethod dataEncAlg) {
+        
+        final CriteriaSet criteria = new CriteriaSet();
+        
+        if (agentID != null) {
+            criteria.add(new AgentIDCriterion(agentID));
+        }
+
+        if (applicationID != null) {
+            criteria.add(new ApplicationIDCriterion(applicationID));
+        }
+        
+        if (entityID != null) {
+            criteria.add(new EntityIdCriterion(entityID));
+        }
+        
+        if (usage != null) {
+            criteria.add(new UsageCriterion(usage));
+        }
+        
+        if (keyManAlg != null) {
+        	criteria.add(new KeyManagmentAlgorithmCriterion(keyManAlg.toString()));
+        }
+        
+        if (dataEncAlg != null) {
+        	criteria.add(new DataEncryptionAlgorithmCriterion(dataEncAlg.toString()));
+        }
+        
+        return criteria;
+    }
+	
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/credential/impl/JWKCredentialStorageServiceResolverTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/credential/impl/JWKCredentialStorageServiceResolverTest.java
index 04c6a38..12e3edf 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/credential/impl/JWKCredentialStorageServiceResolverTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/credential/impl/JWKCredentialStorageServiceResolverTest.java
@@ -271,7 +271,7 @@ public class JWKCredentialStorageServiceResolverTest {
         resolver.setVelocityEngine(VelocityEngine.newVelocityEngine());
         resolver.setContextTemplate("agents/$agentID/$applicationID");
         resolver.setCacheEnabled(true);
-        resolver.setExpireAfterAccess(Duration.ofSeconds(10));
+        resolver.setExpireAfterAccess(Duration.ofSeconds(5));
         resolver.initialize();
         
         JWKCredential credential = (JWKCredential) resolver.resolveSingle(
@@ -293,7 +293,7 @@ public class JWKCredentialStorageServiceResolverTest {
                 buildCriteriaSet("localhost", "override", "https://op.example.org/", UsageType.SIGNING));
         Assert.assertNotNull(credential);
         
-        Thread.sleep(15000);
+        Thread.sleep(10000);
         credential = (JWKCredential) resolver.resolveSingle(
                 buildCriteriaSet("localhost", "override", "https://op.example.org/", UsageType.SIGNING));
         Assert.assertNull(credential);
diff --git a/sp-oidc-impl/src/test/resources/logback-test.xml b/sp-oidc-impl/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..3779bda
--- /dev/null
+++ b/sp-oidc-impl/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%level [%logger:%line] - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <root level="TRACE">
+        <appender-ref ref="STDOUT" />
+    </root>
+    
+</configuration>
\ No newline at end of file

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


More information about the commits mailing list