[java-plugin-shibd-oidc] branch dev/JSHIBDOIDC-31-cred-resolver updated: WIP: JSHIBDOIDC-35 - Add Storage Service ClientSecret Credential Resolver
Codeberg
noreply at shibboleth.net
Thu Oct 15 16:47:30 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/b0b1d5d3d2c5f2696493bf4744a733fa40ead46d
The following commit(s) were added to refs/heads/dev/JSHIBDOIDC-31-cred-resolver by this push:
new b0b1d5d WIP: JSHIBDOIDC-35 - Add Storage Service ClientSecret Credential Resolver
b0b1d5d is described below
commit b0b1d5d3d2c5f2696493bf4744a733fa40ead46d
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 12da393..e4f9cd6 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 f030ac5..453d8ce 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
@@ -195,6 +195,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