[java-plugin-shibd-oidc] 05/06: WIP: JSHIBDOIDC-31 - Support new CredentialResolver service
Codeberg
noreply at shibboleth.net
Wed Aug 12 16:18:29 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/b5a21bab12a8b188335e0773a0d73d9846e19a95
commit b5a21bab12a8b188335e0773a0d73d9846e19a95
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Aug 12 17:01:19 2026 +0100
WIP: JSHIBDOIDC-31 - Support new CredentialResolver service
- Inject the new credential resolver service to resolve client secrets
- There is currently only one, default, global, client secret
- New resolvers will be developed to lookup secrets for a given
OP/Agent/Application
https://shibboleth.atlassian.net/browse/JSHIBDOIDC-31
---
.../net/shibboleth/sp/oidc/testing/TestHelper.java | 66 ++-
.../META-INF/net.shibboleth.idp/postconfig.xml | 5 +
.../idp/flows/sp/consumer/oidc/oidc-beans.xml | 7 +-
.../net/shibboleth/sp/service/agent/postconfig.xml | 30 +-
.../sp/service/credentials/postconfig.xml | 17 +
.../flows/AbstractOIDCTokenConsumerFlowTest.java | 4 +
.../sp/oidc/flows/OIDCTokenConsumerFlowTest.java | 59 +++
...DCEnvironmentApplicationContextInitializer.java | 4 +-
...cationContextInitializerWithStorageService.java | 2 +-
.../idp/module/conf/sp/oidc-test-agents.xml | 12 +-
.../net/shibboleth/sp/oidc-test-beans.xml | 8 +-
.../impl/ClientSecretCredentialResolver.java | 168 ++++++++
.../impl/StaticClientSecretCredentialResolver.java | 179 +++++++++
...izeOAuth2ClientAuthenticationMethodHandler.java | 113 ++++--
.../profile/impl/BaseOIDCAuthenticationTest.java | 12 +
...Auth2ClientAuthenticationMethodHandlerTest.java | 444 +++++++++++++++++++++
16 files changed, 1073 insertions(+), 57 deletions(-)
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java
index 28290c8..24efc75 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java
@@ -18,6 +18,7 @@ import static org.testng.Assert.fail;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
+import java.security.KeyException;
import java.security.PublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
@@ -28,11 +29,14 @@ import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.crypto.KeySupport;
import org.testng.Assert;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -73,6 +77,8 @@ import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
+import net.shibboleth.oidc.security.JWSAssemblyUtils;
+import net.shibboleth.oidc.security.credential.BasicExpiringJWKCredential;
import net.shibboleth.oidc.security.credential.JWKCredential;
import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.collection.CollectionSupport;
@@ -164,13 +170,15 @@ public final class TestHelper {
* Build a basic {@link JWTClaimsSet} from the supplied parameters to mock a UserInfo response.
*
* @param overrideClaims claims to override
+ * @param extraClaims, any extra claims.
*
* @return the JWT claims set.
*/
@SuppressWarnings("null")
- @Nonnull public static JWTClaimsSet createBasicUserInfoClaims(final Map<String, Object> overrideClaims) {
+ @Nonnull public static JWTClaimsSet createBasicUserInfoClaimsWithExtra(final Map<String, Object> overrideClaims,
+ final Map<String, Object> extraClaims) {
- return new JWTClaimsSet.Builder()
+ JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
.issuer(getClaimValue(overrideClaims, "iss", "test-issuer", String.class))
.audience(getClaimValue(overrideClaims, "aud", CollectionSupport.listOf("test-client"), List.class))
.subject(getClaimValue(overrideClaims, "sub", "jdoe", String.class))
@@ -178,8 +186,36 @@ public final class TestHelper {
.claim("given_name", getClaimValue(overrideClaims, "given_name", "Demo", String.class))
.claim("family_name", getClaimValue(overrideClaims, "family_name", "User", String.class))
.claim("nickname", getClaimValue(overrideClaims, "nickname", "Dee", String.class))
- .claim("name",getClaimValue(overrideClaims, "name", "Demo T. User", String.class))
- .build();
+ .claim("name",getClaimValue(overrideClaims, "name", "Demo T. User", String.class));
+
+ if (extraClaims != null && !extraClaims.isEmpty()) {
+ if (extraClaims.get("exp") instanceof Instant expI) {
+ builder.expirationTime(Date.from(expI));
+ }
+ if (extraClaims.get("iat") instanceof Instant iatI) {
+ builder.issueTime(Date.from(iatI));
+ }
+ extraClaims.forEach((k,v) -> {
+ if (!Set.of("iss", "iat", "exp").contains(k)) {
+ builder.claim(k, v);
+ }
+ });
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Build a basic {@link JWTClaimsSet} from the supplied parameters to mock a UserInfo response.
+ *
+ * @param overrideClaims claims to override
+ *
+ * @return the JWT claims set.
+ */
+ @SuppressWarnings("null")
+ @Nonnull public static JWTClaimsSet createBasicUserInfoClaims(final Map<String, Object> overrideClaims) {
+
+ return createBasicUserInfoClaimsWithExtra(overrideClaims, null);
}
/**
@@ -253,6 +289,28 @@ public final class TestHelper {
return new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),claims);
}
+ /**
+ * Create a direct encryption {@link JWKCredential} from the given shared secret.
+ *
+ * @param secret the secret to convert to a {@link JWKCredential}.
+ *
+ * @return the credential
+ * @throws KeyException on error creating the key
+ */
+ public static JWKCredential createDirectEncryptionCredentialFromSharedSecret(final String secret)
+ throws KeyException {
+ final BasicExpiringJWKCredential jwkCredential = new BasicExpiringJWKCredential();
+ jwkCredential.setSecretKey(KeySupport.decodeSecretKey(
+ JWSAssemblyUtils.getSecretBytes(secret), "AES"));
+ jwkCredential.setCredentialExpiresAt(Duration.ZERO);
+ jwkCredential.setUsageType(UsageType.UNSPECIFIED);
+
+ jwkCredential.setKid("mockKey");
+ jwkCredential.getKeyNames().add("mockKey");
+ jwkCredential.setAlgorithm(JWEAlgorithm.DIR);
+ return jwkCredential;
+ }
+
/**
* Create a JWT from the given payload. The JWT can either be plain, or signed and encrypted. If encrypted, it must
* be signed.
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 cd705eb..12da393 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
@@ -164,5 +164,10 @@
</constructor-arg>
</bean>
+ <!-- Global abstract bean for client secret resolvers which are specified in a number of different bean files -->
+ <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"/>
+
</beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
index 81409fe..7a16862 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
@@ -87,7 +87,12 @@
<bean id="InitializeOAuth2ClientAuthenticationMethodHandler" scope="prototype"
class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientAuthenticationMethodHandler"
p:securityParametersContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.SecurityParametersFromOAuth2ClientAuthenticationContext"
- p:jwtBearerExpiryOffset="%{sp.oidc.authenticationMethod.jwt.expiryOffset:PT30S}"/>
+ p:jwtBearerExpiryOffset="%{sp.oidc.authenticationMethod.jwt.expiryOffset:PT30S}"
+ p:issuerLookupStrategy-ref="shibboleth.ClientIdLookup.Simple">
+ <property name="clientSecretResolver">
+ <bean id="ClientSecretCredentialResolver" parent="shibboleth.sp.oidc.ClientSecretCredentialResolver"/>
+ </property>
+ </bean>
</list>
</property>
</bean>
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 d938d4f..b122fa4 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
@@ -58,14 +58,12 @@
<bean id="AbstractOIDCProfile" abstract="true"
p:securityConfiguration-ref="%{sp.oidc.security.config:shibboleth.oidc.DefaultSecurityConfiguration}" />
- <bean id="AbstractOIDCSSOProfile" parent="AbstractOIDCProfile" abstract="true"
- p:issuer="#{getObject('shibboleth.oidc.issuer')}"
+ <bean id="AbstractOIDCSSOProfile" parent="AbstractOIDCProfile" abstract="true"
p:claimsValidator="#{getObject('DefaultJWTClaimsValidator')}"
p:forcePKCE="%{sp.oidc.forcePKCE:false}"
p:allowPKCEPlain="%{sp.oidc.allowPKCEPlain:false}"
p:tokenEndpointAuthMethod="%{sp.oidc.authenticationMethod:client_secret_basic}"
p:useTargetedEndpointAsJWTAudience="%{sp.oauth2.jwtAuth.targetedEndpointAsJWTAudience:true}"
- p:clientCredential="#{%{sp.oidc.discoveryRequired:false} == true ? {null} : getObject('shibboleth.oidc.DefaultCredential')}"
p:extractStandardAttributes="%{sp.oidc.extractStandardAttributes:false}"/>
<bean id="OIDC.SSO" parent="AbstractOIDCSSOProfile" lazy-init="true"
@@ -134,13 +132,15 @@
<!--
- Bridging beans to link the basic security configuration to the CredentialResolver service to aquire credentials
+ Bridging beans to link the basic security configuration to the CredentialResolver service to acquire credentials
-->
<bean id="shibboleth.sp.oidc.BasicSignatureSigningConfiguration" parent="shibboleth.oidc.BasicSignatureSigningConfiguration"
class="net.shibboleth.sp.jose.config.impl.BasicSignatureSigningConfiguration"
c:resolver-ref="shibboleth.sp.CredentialResolverBridge" />
+
+
<!--
Security configuration beans.
-->
@@ -265,8 +265,7 @@
class="net.shibboleth.oidc.security.credential.impl.ProviderMetadataCredentialResolver"
c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
c:keyFetchInterval="%{sp.oidc.keyfetch.interval:PT30M}" />
- <bean id="ClientSecretCriterionCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
+ <bean id="ClientSecretCredentialResolver" parent="shibboleth.sp.oidc.ClientSecretCredentialResolver"/>
</list>
</constructor-arg>
</bean>
@@ -275,8 +274,7 @@
class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
<constructor-arg>
<list>
- <bean id="ClientSecretCriterionCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
+ <bean id="ClientSecretCredentialResolver" parent="shibboleth.sp.oidc.ClientSecretCredentialResolver"/>
</list>
</constructor-arg>
</bean>
@@ -289,9 +287,7 @@
<!--
A resolver to public/private key encryption keys global to the RP
- -->
-
-
+ -->
<bean id="defaultOIDCKeyDecryptionCredentialResolver"
class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
@@ -325,8 +321,7 @@
</bean>
</constructor-arg>
</bean>
- <bean id="CriterionCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
+ <bean id="ClientSecretCredentialResolver" parent="shibboleth.sp.oidc.ClientSecretCredentialResolver"/>
</list>
</constructor-arg>
</bean>
@@ -335,9 +330,7 @@
class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
<constructor-arg>
<list>
- <!-- Used by the RP -->
- <bean id="CriterionCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
+ <bean id="ClientSecretCredentialResolver" parent="shibboleth.sp.oidc.ClientSecretCredentialResolver"/>
</list>
</constructor-arg>
</bean>
@@ -357,18 +350,15 @@
class="net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver" />
<!-- A resolver for resolving trusted credentials to match against those resolved from the JWT -->
- <!-- TODO the client_secret credential should come from the new credential resolver -->
<bean id="defaultSignedJWTTrustedCredentialResolver"
class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
<constructor-arg>
<list>
- <!-- Used by the RP -->
<bean id="OIDCProviderMetadataCredentialResolver"
class="net.shibboleth.oidc.security.credential.impl.ProviderMetadataCredentialResolver"
c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
c:keyFetchInterval="%{sp.oidc.provider.keyfetch.interval:PT30M}"/>
- <bean id="CriterionCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
+ <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 67eab5f..56cc768 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
@@ -21,6 +21,23 @@
</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" />
+ </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>
+ </property>
+ </bean>
+
<bean id="DefaultCredentials" class="org.springframework.beans.factory.config.ListFactoryBean" lazy-init="true">
<property name="sourceList">
<list>
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/AbstractOIDCTokenConsumerFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/AbstractOIDCTokenConsumerFlowTest.java
index fd136a5..ae43a09 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/AbstractOIDCTokenConsumerFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/AbstractOIDCTokenConsumerFlowTest.java
@@ -69,6 +69,7 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
import net.minidev.json.JSONObject;
+import net.shibboleth.oidc.security.credential.ClientSecretCredential;
import net.shibboleth.oidc.security.credential.JWKCredential;
import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.collection.Pair;
@@ -95,6 +96,9 @@ public abstract class AbstractOIDCTokenConsumerFlowTest extends AbstractSPFlowTe
/** Dummy encryption key of the RP/SP. */
@Autowired @Qualifier("dummy.sp.encryption.Credential") protected JWKCredential rpEncryptionCredential;
+ /** Dummy client_secret credential of the RP/SP. */
+ @Autowired @Qualifier("dummy.sp.DefaultClientSecretCredential") protected ClientSecretCredential clientSecretCredential;
+
/** The mocked HttpClient to use when responding to Token and UserInfo requests.*/
private HttpClient httpClient;
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 c45892a..f030ac5 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
@@ -18,6 +18,7 @@ import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
+import java.util.List;
import java.util.Map;
import org.opensaml.profile.action.EventIds;
@@ -28,8 +29,17 @@ import org.springframework.webflow.executor.FlowExecutionResult;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.oauth2.sdk.ResponseMode;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
import net.shibboleth.idp.spring.IdPPropertiesApplicationContextInitializer;
import net.shibboleth.idp.test.PreferFileSystemApplicationContextInitializer;
@@ -321,6 +331,55 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
TestConstants.RESOURCE_URL);
}
+ /**
+ * Test successful flow with a signed and encrypted id_token and a plain user info response. Encryption is direct
+ * encryption using the client_secret as a shared key, 'dir'.
+ *
+ * @throws IOException on error
+ */
+ @Test
+ public void testSuccess_SignedDirectEncryptedIDToken_WithDecryptionKID_PlainUserInfo() throws Exception {
+ final JWTClaimsSet claimsSet = TestHelper.createBasicUserInfoClaimsWithExtra(
+ Map.of("sub","jdoe","iss", TestConstants.ISSUER,
+ "name", "John Doe", "aud", List.of(TestConstants.CLIENT_ID)),
+ Map.of("iat", Instant.now(), "exp", Instant.now().plusSeconds(3600)));
+
+ final JWT encryptedIdToken =
+ TestHelper.createJWT(claimsSet, JWSAlgorithm.RS256, JWEAlgorithm.DIR,
+ EncryptionMethod.A256GCM, opSigningCredential,
+ clientSecretCredential.toEncryptionCredential(JWEAlgorithm.DIR, EncryptionMethod.A256GCM),
+ "client_secret_credential");
+
+ final OIDCTokenResponse tokenResponse =
+ new OIDCTokenResponse((new OIDCTokens(encryptedIdToken,
+ new BearerAccessToken("fake-access-token-value", 3600, null),
+ new RefreshToken("fake-refresh-token-value"))));
+
+ mockOIDCEndpoints(tokenResponse, 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_REQUEST_OBJECT,
+ TestHelper.buildAuthenticationState(null, false, null), true));
+
+ setApplicationRequest(TestConstants.APPLICATION_ID_REQUEST_OBJECT, 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 failure flow with a signed and encrypted id_token and a plain user info response. The kid of the key
* used to encrypt the IDToken is not one that matches to a local key defined.
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
index 0faa3dc..132526f 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
@@ -40,6 +40,8 @@ public class TestSPOIDCEnvironmentApplicationContextInitializer extends TestSPEn
mock.setProperty("idp.webflows", "classpath*:/flows");
mock.setProperty("idp.service.logging.resource", "/logback-flow-test.xml");
mock.setProperty("sp.service.agents.resources", "test.sp.oidc.AgentResolverResources");
+ // Default authority identifier
+ mock.setProperty("sp.defaultAuthority", "https://op.example.org");
// Use cookie based state management
mock.setProperty("sp.stateToken.Manager","shibboleth.sp.CookieStateManager");
// Use a mocked HTTP client
@@ -53,7 +55,7 @@ public class TestSPOIDCEnvironmentApplicationContextInitializer extends TestSPEn
// Turn off the sealing state data for testing
mock.setProperty("sp.stateToken.sealed", "false");
// Create a basic default client secret
- mock.setProperty("sp.oidc.defaultClientSecret", "secret");
+ mock.setProperty("sp.oidc.defaultClientSecret", "zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ");
// Turn on the replay cache for testing
mock.setProperty("sp.stateToken.checkReplay", "true");
mock.setProperty("idp.additionalProperties",
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializerWithStorageService.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializerWithStorageService.java
index 11f7250..64ca5ee 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializerWithStorageService.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializerWithStorageService.java
@@ -57,7 +57,7 @@ public class TestSPOIDCEnvironmentApplicationContextInitializerWithStorageServic
// Turn off the sealing state data for testing
mock.setProperty("sp.stateToken.sealed", "false");
// Create a basic default client secret
- mock.setProperty("sp.oidc.defaultClientSecret", "secret");
+ mock.setProperty("sp.oidc.defaultClientSecret", "zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ");
// Turn on the replay cache for testing
mock.setProperty("sp.stateToken.checkReplay", "true");
mock.setProperty("idp.additionalProperties",
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 579df9f..2880e10 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
@@ -60,7 +60,7 @@
<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="secret"/>
+ <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
</property>
</bean>
</util:list>
@@ -72,7 +72,7 @@
<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="secret"/>
+ <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
</property>
</bean>
</util:list>
@@ -80,7 +80,7 @@
<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="secret"/>
+ <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
</property>
</bean>
</util:list>
@@ -88,7 +88,7 @@
<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="secret"/>
+ <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
</property>
</bean>
</util:list>
@@ -97,7 +97,7 @@
<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="secret"/>
+ <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
</property>
</bean>
</util:list>
@@ -108,7 +108,7 @@
<bean id="basicRequestedClaims" class="net.shibboleth.sp.oidc.functions.RequestedClaimsExampleFunction"/>
</property>
<property name="clientCredential">
- <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="secret"/>
+ <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ"/>
</property>
</bean>
</util:list>
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
index 084a430..4c5b857 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
@@ -27,13 +27,19 @@
<bean id="shibboleth.oidc.JWKCredential" abstract="true"
class="net.shibboleth.oidc.security.credential.BasicJWKCredentialFactoryBean" />
+
+ <bean id="shibboleth.oidc.ClientSecretCredential" abstract="true"
+ class="net.shibboleth.oidc.security.credential.BasicClientSecretCredentialFactoryBean" />
<!-- These need to be the same as those resolvable by the security configuration used -->
<bean id="dummy.op.signing.Credential" parent="shibboleth.oidc.JWKCredential"
p:resource="%{idp.home}/credentials/op/op-signing-rsa.jwk" p:throwIfNull="false" />
- <bean id="dummy.sp.encryption.Credential" parent="shibboleth.oidc.JWKCredential"
+ <bean id="dummy.sp.encryption.Credential" parent="shibboleth.oidc.JWKCredential"
p:resource="%{idp.home}/credentials/sp/sp-encryption-rsa.jwk" p:throwIfNull="false" />
+
+ <bean id="dummy.sp.DefaultClientSecretCredential" parent="shibboleth.oidc.ClientSecretCredential"
+ p:secret="%{sp.oidc.defaultClientSecret:#{null}}"/>
<!-- Mockito mock for HttpClient -->
<bean id="Mock.HttpClient"
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
new file mode 100644
index 0000000..116cb44
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/config/impl/ClientSecretCredentialResolver.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.jose.config.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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.slf4j.Logger;
+
+import jakarta.servlet.ServletRequest;
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.service.ReloadableService;
+import net.shibboleth.shared.service.ServiceableComponent;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.AgentIDCriterion;
+import net.shibboleth.sp.Application;
+import net.shibboleth.sp.ApplicationIDCriterion;
+import net.shibboleth.sp.context.AgentRequestContext;
+
+/**
+ * A {@link JOSEObjectCredentialResolver} that extracts a client_secret credential from the {@link CredentialResolver}
+ * service.
+ *
+ * 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.
+ */
+//TODO: Should be in common once finished
+public class ClientSecretCredentialResolver extends AbstractCriteriaFilteringCredentialResolver
+ implements JOSEObjectCredentialResolver {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ClientSecretCredentialResolver.class);
+
+ /** Credential resolver service. */
+ @Nonnull private final ReloadableService<CredentialResolver> credentialResolver;
+
+ /** Access to servlet request. */
+ @Nonnull private final NonnullSupplier<ServletRequest> servletRequestSupplier;
+
+
+ /**
+ * Constructor.
+ *
+ * @param resolver credential resolver service
+ */
+ public ClientSecretCredentialResolver(
+ @Nonnull @ParameterName(name="resolver") final ReloadableService<CredentialResolver> resolver,
+ @Nonnull @ParameterName(name="requestSupplier") final NonnullSupplier<ServletRequest> requestSupplier) {
+ 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)
+ throws ResolverException {
+
+ final CriteriaSet finalCriteria = buildCriteriaSet(criteriaSet);
+
+ try (final ServiceableComponent<CredentialResolver> component = credentialResolver.getServiceableComponent()) {
+ return component.getComponent().resolve(finalCriteria);
+ }
+
+ }
+
+ /**
+ * 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.
+ *
+ * @param criteria existing criteria to add to, if any
+ *
+ * @return the constructed criteria set
+ *
+ * @throws ResolverException if there is a failure to find the identifier of the OpenID Provider
+ */
+ @Nullable private CriteriaSet buildCriteriaSet(@Nullable final CriteriaSet criteria) throws ResolverException {
+
+ final ProfileRequestContext prc = getProfileRequestContext();
+ if (prc == null) {
+ throw new ResolverException(
+ "Client secret credential resolution requires a ProfileRequestContext");
+ }
+
+ final CriteriaSet finalCriteria = criteria != null ? criteria : new CriteriaSet();
+ finalCriteria.add(new ProfileRequestContextCriterion(prc));
+
+ // 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) {
+ final String id = agent.getId();
+ if (id != null) {
+ finalCriteria.add(new AgentIDCriterion(id));
+ }
+ }
+ final Application app = arc.getApplication();
+ if (app != null) {
+ finalCriteria.add(new ApplicationIDCriterion(app.getApplicationId()));
+ }
+ }
+
+ final RelyingPartyContext rpc = prc.getSubcontext(RelyingPartyContext.class);
+ 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");
+ }
+ } 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
+
+ return finalCriteria;
+ }
+
+ /**
+ * Get the current {@link ProfileRequestContext}.
+ *
+ * @return current profile request context or null
+ */
+ @Nullable private ProfileRequestContext getProfileRequestContext() {
+ if (servletRequestSupplier.get().getAttribute(ProfileRequestContext.BINDING_KEY)
+ instanceof final ProfileRequestContext prc) {
+ return prc;
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/config/impl/StaticClientSecretCredentialResolver.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/config/impl/StaticClientSecretCredentialResolver.java
new file mode 100644
index 0000000..a31ade8
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/jose/config/impl/StaticClientSecretCredentialResolver.java
@@ -0,0 +1,179 @@
+
+package net.shibboleth.sp.jose.config.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialResolver;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+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.impl.DataEncryptionAlgorithmCriterion;
+import net.shibboleth.oidc.security.credential.impl.KeyManagmentAlgorithmCriterion;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.sp.credential.AbstractOrderedCredentialResolver;
+
+/**
+ * A {@link CredentialResolver} that holds {@link ClientSecretCredential}s when instantiated, and derives the
+ * appropriate {@link Credential} at runtime based on the supplied criteria.
+ *
+ * <p>A different key credential is derived for different usage types. MAC ('signing') keys are generated directly off
+ * the UTF-8 octets of the client_secret. Encryption credentials are derived using the key management and content
+ * encryption algorithms supplied in the criteria set. Requests for encryption credentials therefore require both
+ * algorithm criteria to be present.</p>
+ */
+public class StaticClientSecretCredentialResolver extends AbstractOrderedCredentialResolver {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(StaticClientSecretCredentialResolver.class);
+
+ /** List of credentials held by this resolver. */
+ @Nonnull private final List<ClientSecretCredential> creds;
+
+ /**
+ * Constructor.
+ *
+ * @param credentials static credentials
+ */
+ public StaticClientSecretCredentialResolver(
+ @Nonnull @ParameterName(name="credentials") final List<ClientSecretCredential> credentials) {
+ Constraint.isNotNull(credentials, "Input credentials list cannot be null");
+
+ creds = CollectionSupport.copyToList(credentials);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull @NotLive @Unmodifiable public Iterable<Credential> doResolve(@Nullable final CriteriaSet criteria)
+ throws ResolverException {
+
+ if (!creds.isEmpty()) {
+
+ verifyCriteria(criteria);
+
+ final List<Credential> credentials = new ArrayList<>(creds.size());
+ for (final ClientSecretCredential cred : creds) {
+ try {
+ final Credential derivedCredential = deriveClientSecretCredential(cred, criteria);
+ credentials.add(derivedCredential);
+ } catch (final ResolverException e) {
+ log.trace("Unable to create suitable credential from client_secret: ",e);
+ // Ignore and try next
+ }
+ }
+ return credentials;
+ }
+ return CollectionSupport.emptyList();
+ }
+
+ /**
+ * Verify the criteria set has the required criterion to correctly process the client secret credential.
+ *
+ * @param criteriaSet the criteria set to check
+ *
+ * @throws ResolverException if the required criterion are not present
+ */
+ private void verifyCriteria(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+
+ if (criteriaSet == null) {
+ throw new ResolverException("No criteria set found, can not process client secrets");
+ }
+
+ final UsageCriterion usageTypeCriterion = criteriaSet.get(UsageCriterion.class);
+ if (usageTypeCriterion == null) {
+ throw new ResolverException("No usage type criterion supplied, unable to derive client_secret credential");
+ }
+
+ if (usageTypeCriterion.getUsage() == UsageType.ENCRYPTION) {
+
+ final KeyManagmentAlgorithmCriterion alg = criteriaSet.get(KeyManagmentAlgorithmCriterion.class);
+ if (alg == null) {
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of KeyManagmentAlgorithmCriterion");
+ }
+ // Technically the encryption method is only relevant to key derivation for the Direct Encryption mode
+ final DataEncryptionAlgorithmCriterion enc = criteriaSet.get(DataEncryptionAlgorithmCriterion.class);
+ if (enc == null) {
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of DataEncryptionAlgorithmCriterion");
+ }
+ }
+ }
+
+ /**
+ * 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
+ *
+ * @throws ResolverException if there is an error deriving the key
+ */
+ @Nonnull 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 {
+ throw new ResolverException("Asymmetric key requested, client_secret not appropriate");
+ }
+ } else {
+ throw new ResolverException("Unable to create key from client_secret, incompatible usage type");
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientAuthenticationMethodHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientAuthenticationMethodHandler.java
index 82c318f..ade55f2 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientAuthenticationMethodHandler.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientAuthenticationMethodHandler.java
@@ -14,6 +14,7 @@
package net.shibboleth.sp.oidc.profile.impl;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
@@ -29,6 +30,10 @@ import org.opensaml.messaging.handler.MessageHandlerException;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialResolver;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
import org.slf4j.Logger;
import com.nimbusds.jose.Algorithm;
@@ -51,17 +56,21 @@ import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
-import net.shibboleth.oidc.security.credential.ClientSecretCredential;
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
import net.shibboleth.oidc.security.impl.JWSTokenSigner;
import net.shibboleth.oidc.security.jose.SignatureException;
import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
import net.shibboleth.profile.context.RelyingPartyContext;
import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+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.sp.oidc.profile.OIDCSupport;
/**
@@ -130,9 +139,6 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
/** The stashed provider metadata.*/
@NonnullBeforeExec private OIDCProviderMetadata providerMetadata;
- /** The stashed client_secret to use if required.*/
- @Nullable private ClientSecretCredential clientCredential;
-
/** The stashed client authentication method to use.*/
@Nullable private String clientAuthMethod;
@@ -141,6 +147,9 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
/** The stashed peer entity context.*/
@Nullable private OIDCPeerEntityContext peerEntityContext;
+
+ /** The resolver used to resolve client secrets for client_secret based authentication.*/
+ @NonnullAfterInit private JOSEObjectCredentialResolver clientSecretResolver;
/** Constructor.*/
public InitializeOAuth2ClientAuthenticationMethodHandler() {
@@ -245,6 +254,25 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
issuerLookupStrategy = Constraint.isNotNull(strategy, "IssuerLookupStrategy lookup strategy cannot be null");
}
+ /**
+ * Set the {@link CredentialResolver} used to determine the client_secret to use for client_secret based
+ * authentication methods.
+ *
+ * @param resolver the resolver used to find the client_secret
+ */
+ public void setClientSecretResolver(@Nonnull final JOSEObjectCredentialResolver resolver) {
+ checkSetterPreconditions();
+ clientSecretResolver = Constraint.isNotNull(resolver, "Client secret resolver can not be null");
+ }
+
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (clientSecretResolver == null) {
+ throw new ComponentInitializationException("Client secret resolver can not be null");
+ }
+ }
+
/** {@inheritDoc} */
@Override
@@ -269,7 +297,7 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
}
if (profileConfiguration == null) {
log.error("{} Profile configuration not found", getLogPrefix());
- throw new MessageHandlerException("No OAuth2 client authentication context found or created");
+ throw new MessageHandlerException("No profile configuration found");
}
// Can be null
@@ -300,8 +328,6 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
throw new MessageHandlerException("No client_id found from issuer lookup strategy");
}
- clientCredential = profileConfiguration.getClientCredential(PRC_LOOKUP.apply(messageContext));
-
return true;
}
@@ -315,12 +341,7 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) ||
method.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
- if (clientCredential == null) {
- throw new MessageHandlerException("No client secret credential found from profile configuration, "
- + "can not construct client authenticaton");
- }
- assert clientCredential != null;
- final Secret secret = new Secret(clientCredential.getSecret());
+ final Secret secret = resolveClientSecret();
// TODO redundent check for now, as the secret can not expire. Add back?
if (secret.expired()) {
log.warn("{} Client secret has expired for client '{}'", getLogPrefix(), clientId);
@@ -336,15 +357,12 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
verifySuitableClientSecretJWTSecurityContext();
clientAuthentication = new ClientSecretJWT(buildClientAuthenticationJwt(messageContext));
} else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
- verifySuitablePrivateKetJWTSecurityContext();
+ verifySuitablePrivateKeyJWTSecurityContext();
clientAuthentication = new PrivateKeyJWT(buildClientAuthenticationJwt(messageContext));
} else {
log.warn("{}: Client authentication method '{}' not supported for client '{}'", getLogPrefix(),
method, clientId);
- }
-
- if (clientAuthentication == null) {
- throw new MessageHandlerException("Client authentication could not be constructed");
+ throw new MessageHandlerException("Unsupported client authentication method: " + method);
}
oauth2ClientAuthenticationContext.setClientAuthentication(clientAuthentication);
@@ -354,6 +372,48 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
}
+ /**
+ * Resolve a client_secret credential using the provided resolver.
+ *
+ * <p>The secret is defined to be a SIGNING type in the absence of another suitable type. This should give back
+ * the raw secret, and not derive it into a different sequence.</p>
+ *
+ * <p>The credential is checked to ensure it is appropriate for the target OpenID Provider, and it is a
+ * secret and not a public key. These checks may also happen in the resolver, so this serves to guarantee that
+ * behaviour.</p>
+ *
+ * TODO: When we have a way to signal a 'PASSWORD' usage type, the resolvers will know not to return assymmetric keys.
+ *
+ * @return the secret if found
+ *
+ * @throws MessageHandlerException if a suitable secret can not be resolved
+ */
+ @Nonnull private Secret resolveClientSecret() throws MessageHandlerException {
+
+ try {
+ final CriteriaSet criteria = new CriteriaSet();
+ // Use the SIGNING type, to ensure direct key usage.
+ // TODO: if another type is specified, we can just say RAW etc. This is a hack. It is really a 'PASSWORD'.
+ criteria.add(new UsageCriterion(UsageType.SIGNING));
+ // Return the first that fits
+ for (final Credential secret : clientSecretResolver.resolve(criteria)) {
+ // Guard we have a secret for the correct provider (even if filtered by the resolver), and is a secret
+ // TODO: Maybe we guard on entityId, but for now that is not possible for the client_secret
+// if (providerMetadata.getIssuer().getValue().equals(secret.getEntityId()) &&
+// secret.getSecretKey() != null){
+ if (secret.getSecretKey() != null){
+ final byte[] encodedKey = secret.getSecretKey().getEncoded();
+ if (encodedKey != null) {
+ return new Secret(new String(encodedKey, StandardCharsets.UTF_8));
+ }
+ }
+ }
+ throw new MessageHandlerException("Unable to find a suitable client_secret");
+ } catch (final ResolverException e) {
+ throw new MessageHandlerException("Unable to resolve client secret", e);
+ }
+ }
+
/**
* Check the populated security context is using the correct algorithm family for client_secret_jwt client
* authentication.
@@ -389,7 +449,7 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
*
* @throws MessageHandlerException if the wrong algorithm family is specified in the security context
*/
- private void verifySuitablePrivateKetJWTSecurityContext() throws MessageHandlerException {
+ private void verifySuitablePrivateKeyJWTSecurityContext() throws MessageHandlerException {
final SecurityParametersContext bearerSecurityParams = jwtBearerClientAuthSecurityParameters;
if (bearerSecurityParams == null || bearerSecurityParams.getSignatureSigningParameters() == null) {
@@ -403,6 +463,9 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
if (signatureSigningParameters.getSigningCredential() == null) {
throw new MessageHandlerException("Missing credential needed to sign private_key_jwt");
}
+ if (signatureSigningParameters.getSignatureAlgorithm() == null) {
+ throw new MessageHandlerException("Missing signing algorithm needed to sign private_key_jwt");
+ }
final Algorithm jwsAlgorithm = new JWSAlgorithm(signatureSigningParameters.getSignatureAlgorithm());
if (!JWSAlgorithm.Family.SIGNATURE.contains(jwsAlgorithm)) {
throw new MessageHandlerException("Trying to construct private_key_jwt using the wrong algorithm: "
@@ -427,6 +490,10 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
final String audience;
if (profileConfiguration.isUseTargetedEndpointAsJWTAudience(PRC_LOOKUP.apply(messageContext))) {
+ if (providerMetadata.getTokenEndpointURI() == null) {
+ throw new MessageHandlerException("Token endpoint URI expected as the audience, but no token endpoint"
+ + " exists in provider metadata");
+ }
audience = providerMetadata.getTokenEndpointURI().toString();
} else {
final var localPeerEntityCtx = peerEntityContext;
@@ -439,14 +506,14 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
if (StringSupport.trimOrNull(audience) == null) {
throw new MessageHandlerException("JWT audience can not be null");
}
-
+ final Instant now = Instant.now();
final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject(clientId)
.issuer(clientId)
.audience(audience)
.jwtID(OIDCSupport.generateRandom(32))
- .issueTime(Date.from(Instant.now()))
- .expirationTime(Date.from(Instant.now().plus(jwtBearerExpiryOffset)))
+ .issueTime(Date.from(now))
+ .expirationTime(Date.from(now.plus(jwtBearerExpiryOffset)))
.build();
assert claimsSet != null;
return claimsSet;
@@ -488,7 +555,7 @@ public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractM
final JWSTokenSigner signer = new JWSTokenSigner(signingParams);
final SignedJWT signed = signer.sign(claims, jwtType);
if (log.isDebugEnabled() && !log.isTraceEnabled()) {
- log.debug("{} Signed JWT Bearer Token for client authentication'", getLogPrefix());
+ log.debug("{} Signed JWT Bearer Token for client authentication", getLogPrefix());
} else if (log.isTraceEnabled()) {
log.trace("{} Signed JWT Bearer Token for client authentication: {}", getLogPrefix()
,signed.serialize());
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java
index 80af108..0b308a3 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java
@@ -40,6 +40,8 @@ import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
import net.shibboleth.oidc.profile.config.impl.DefaultOIDCAuthorizationConfiguration;
import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.relyingparty.BasicRelyingPartyConfiguration;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.sp.oidc.testing.TestConstants;
@@ -112,6 +114,16 @@ public abstract class BaseOIDCAuthenticationTest extends BaseApplicationActionTe
peerEntityCtxInbound.addSubcontext(providerCtxInbound);
inMsgCtx.addSubcontext(peerEntityCtxInbound);
prc.setInboundMessageContext(inMsgCtx);
+
+ final RelyingPartyContext partyContext = prc.ensureSubcontext(RelyingPartyContext.class);
+ partyConfig = new DefaultOIDCAuthorizationConfiguration();
+ partyContext.setProfileConfig(partyConfig);
+ partyConfig.setTokenEndpointAuthMethod("client_secret_basic");
+ partyConfig.setIssuer(TestConstants.CLIENT_ID);
+ final BasicRelyingPartyConfiguration rPartyConfig = new BasicRelyingPartyConfiguration();
+ rPartyConfig.setIssuer(TestConstants.CLIENT_ID);
+ partyContext.setConfiguration(rPartyConfig);
+
}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientAuthenticationMethodHandlerTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientAuthenticationMethodHandlerTest.java
new file mode 100644
index 0000000..4c7ca67
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientAuthenticationMethodHandlerTest.java
@@ -0,0 +1,444 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.profile.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.security.credential.BasicCredential;
+import org.opensaml.security.credential.Credential;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
+import com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT;
+
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.oidc.security.credential.DefaultClientSecretCredential;
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.sp.oidc.testing.TestConstants;
+
+/** Tests for {@link InitializeOAuth2ClientAuthenticationMethodHandler}.*/
+public class InitializeOAuth2ClientAuthenticationMethodHandlerTest extends BaseOIDCAuthenticationTest {
+
+ /** Example client_secret for client secret type client authentication methods.*/
+ private static final String CLIENT_SECRET = "zIq2qS8w8k7qzShC0Z6j8l0mKmdEFdt02KDv2Qz4xXQ";
+
+ /** The handler.*/
+ private InitializeOAuth2ClientAuthenticationMethodHandler handler;
+
+ @Override
+ @BeforeMethod
+ public void beforeMethod() throws ComponentInitializationException {
+ super.beforeMethod();
+ handler = new InitializeOAuth2ClientAuthenticationMethodHandler();
+ handler.setClientSecretResolver(new JOSEObjectCredentialResolver() {
+
+ @Override
+ @Nullable public Credential resolveSingle(@Nullable final CriteriaSet criteria) throws ResolverException {
+ final DefaultClientSecretCredential secret =
+ new DefaultClientSecretCredential(CLIENT_SECRET, "client_secret_credential");
+ return secret.toSigningCredential();
+ }
+
+ @Override
+ @Nonnull public Iterable<Credential> resolve(@Nullable final CriteriaSet criteria) throws ResolverException {
+ final Credential cred = resolveSingle(criteria);
+ return cred != null ? CollectionSupport.singletonList(cred) : CollectionSupport.emptyList();
+ }
+ });
+ }
+
+
+ @Test
+ public void testInitialiseClientSecretBasic_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_basic");
+ handler.initialize();
+ final var outboundMsgCtx = prc.getOutboundMessageContext();
+ assert outboundMsgCtx != null;
+ handler.invoke(outboundMsgCtx);
+
+ final var peerEntityCtx = outboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assert peerEntityCtx != null;
+ final var context = peerEntityCtx.getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assert context != null;
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof ClientSecretBasic);
+ final var basicClientAuth = (ClientSecretBasic) context.getClientAuthentication();
+ assert basicClientAuth != null;
+ assertEquals(basicClientAuth.getClientSecret().getValue(),CLIENT_SECRET);
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseClientSecretBasic_NoCredential() throws Exception {
+ handler.setClientSecretResolver(new JOSEObjectCredentialResolver() {
+
+ @Override
+ @Nullable public Credential resolveSingle(@Nullable final CriteriaSet criteria) throws ResolverException {
+ return null;
+ }
+
+ @Override
+ @Nonnull public Iterable<Credential> resolve(@Nullable final CriteriaSet criteria) throws ResolverException {
+ return CollectionSupport.emptyList();
+ }
+ });
+
+ partyConfig.setTokenEndpointAuthMethod("client_secret_basic");
+ partyConfig.setClientCredential(null);
+
+ handler.initialize();
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ handler.invoke(outboundMsgCtx);
+
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseUnsportedClientAuthenticationMethod() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("unsupported");
+ partyConfig.setClientCredential(null);
+
+ handler.initialize();
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ handler.invoke(outboundMsgCtx);
+
+ }
+
+ @Test
+ public void testInitialiseClientSecretPost_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_post");
+ handler.initialize();
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ handler.invoke(outboundMsgCtx);
+
+ final var peerEntityCtx = outboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assert peerEntityCtx != null;
+ final var context = peerEntityCtx.getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assert context != null;
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof ClientSecretPost);
+ final var basicClientAuth = (ClientSecretPost) context.getClientAuthentication();
+ assert basicClientAuth != null;
+ assertEquals(basicClientAuth.getClientSecret().getValue(),CLIENT_SECRET);
+ }
+
+ @Test
+ public void testInitialiseClientSecretJWT_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+ partyConfig.setUseTargetedEndpointAsJWTAudience(true);
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("HS256");
+ secContext.setSignatureSigningParameters(secParams);
+ secParams.setSigningCredential(new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential());
+
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+
+ final var peerEntityCtx = outboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assert peerEntityCtx != null;
+ peerEntityCtx.setIdentifier("https://op.example.com");
+
+ outboundMsgCtx.addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(outboundMsgCtx);
+
+ final var context = peerEntityCtx.getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assert context != null;
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof ClientSecretJWT);
+ final var clientSecretJwt = (ClientSecretJWT) context.getClientAuthentication();
+ assert clientSecretJwt != null;
+ assertNotNull(clientSecretJwt);
+ assertNotNull(clientSecretJwt.getJWTAuthenticationClaimsSet());
+ assertNotNull(clientSecretJwt.getClientAssertion());
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getClientID().toString(), TestConstants.CLIENT_ID);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getSubject().toString(), TestConstants.CLIENT_ID);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getIssuer().toString(), TestConstants.CLIENT_ID);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getAudience().size(),1);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getAudience().get(0).toString(),
+ "https://oauth2.op.example.org/token");
+ assertNotNull(clientSecretJwt.getJWTAuthenticationClaimsSet().getJWTID());
+ assertNotNull(clientSecretJwt.getJWTAuthenticationClaimsSet().getExpirationTime());
+ }
+
+ @Test
+ public void testInitialiseClientSecretJWT_TokenEndpointURL_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+ partyConfig.setUseTargetedEndpointAsJWTAudience(true);
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("HS256");
+ secContext.setSignatureSigningParameters(secParams);
+ secParams.setSigningCredential(new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential());
+
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+
+ final var peerEntityCtx = outboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assert peerEntityCtx != null;
+ peerEntityCtx.setIdentifier("https://op.example.com");
+ partyConfig.setUseTargetedEndpointAsJWTAudience(true);
+
+ outboundMsgCtx.addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(outboundMsgCtx);
+
+ final var context = peerEntityCtx.getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assert context != null;
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof ClientSecretJWT);
+ final var clientSecretJwt = (ClientSecretJWT) context.getClientAuthentication();
+ assert clientSecretJwt != null;
+ assertNotNull(clientSecretJwt);
+ assertNotNull(clientSecretJwt.getJWTAuthenticationClaimsSet());
+ assertNotNull(clientSecretJwt.getClientAssertion());
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getClientID().toString(), TestConstants.CLIENT_ID);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getSubject().toString(), TestConstants.CLIENT_ID);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getIssuer().toString(), TestConstants.CLIENT_ID);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getAudience().size(),1);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getAudience().get(0).toString(),
+ "https://oauth2.op.example.org/token");
+ assertNotNull(clientSecretJwt.getJWTAuthenticationClaimsSet().getJWTID());
+ assertNotNull(clientSecretJwt.getJWTAuthenticationClaimsSet().getExpirationTime());
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseClientSecretJWT_NoAudience_Fail() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+ partyConfig.setUseTargetedEndpointAsJWTAudience(false);
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("HS256");
+ secContext.setSignatureSigningParameters(secParams);
+ secParams.setSigningCredential(new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential());
+
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+
+ final var peerEntityCtx = outboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assert peerEntityCtx != null;
+ // Should not be empty
+ peerEntityCtx.setIdentifier("");
+
+ outboundMsgCtx.addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(outboundMsgCtx);
+
+ }
+
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseClientSecretJWT_NoSecurityParams() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+ handler.initialize();
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ handler.invoke(outboundMsgCtx);
+ }
+
+ @Test
+ public void testInitialisePrivateKeyJWT_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+ partyConfig.setUseTargetedEndpointAsJWTAudience(true);
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("RS256");
+ secContext.setSignatureSigningParameters(secParams);
+ final RSAKey key = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+ final var publicKey = key.toPublicKey();
+ assert publicKey != null;
+ secParams.setSigningCredential(new BasicCredential(publicKey, key.toPrivateKey()));
+
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ outboundMsgCtx.addSubcontext(secContext);
+
+ final var peerEntityCtx = outboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assert peerEntityCtx != null;
+ peerEntityCtx.setIdentifier("https://op.example.com");
+
+ handler.initialize();
+ handler.invoke(outboundMsgCtx);
+
+ final var context = peerEntityCtx.getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assert context != null;
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof PrivateKeyJWT);
+ final var privateKeyJwt = (PrivateKeyJWT) context.getClientAuthentication();
+ assert privateKeyJwt != null;
+ assertNotNull(privateKeyJwt);
+ assertNotNull(privateKeyJwt.getJWTAuthenticationClaimsSet());
+ assertNotNull(privateKeyJwt.getClientAssertion());
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getClientID().toString(), TestConstants.CLIENT_ID);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getSubject().toString(), TestConstants.CLIENT_ID);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getIssuer().toString(), TestConstants.CLIENT_ID);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getAudience().size(),1);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getAudience().get(0).toString(),
+ "https://oauth2.op.example.org/token");
+ assertNotNull(privateKeyJwt.getJWTAuthenticationClaimsSet().getJWTID());
+ assertNotNull(privateKeyJwt.getJWTAuthenticationClaimsSet().getExpirationTime());
+ }
+
+ @Test
+ public void testInitialisePrivateKeyJWT_TokenEndpointURL_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("RS256");
+ secContext.setSignatureSigningParameters(secParams);
+ final RSAKey key = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+ final var publicKey = key.toPublicKey();
+ assert publicKey != null;
+ secParams.setSigningCredential(new BasicCredential(publicKey, key.toPrivateKey()));
+
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ outboundMsgCtx.addSubcontext(secContext);
+
+ final var peerEntityCtx = outboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assert peerEntityCtx != null;
+ peerEntityCtx.setIdentifier("https://op.example.com");
+ partyConfig.setUseTargetedEndpointAsJWTAudience(true);
+
+ handler.initialize();
+ handler.invoke(outboundMsgCtx);
+
+
+
+ final var context = peerEntityCtx.getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assert context != null;
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof PrivateKeyJWT);
+ final var privateKeyJwt = (PrivateKeyJWT) context.getClientAuthentication();
+ assert privateKeyJwt != null;
+ assertNotNull(privateKeyJwt);
+ assertNotNull(privateKeyJwt.getJWTAuthenticationClaimsSet());
+ assertNotNull(privateKeyJwt.getClientAssertion());
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getClientID().toString(), TestConstants.CLIENT_ID);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getSubject().toString(), TestConstants.CLIENT_ID);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getIssuer().toString(), TestConstants.CLIENT_ID);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getAudience().size(),1);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getAudience().get(0).toString(),
+ "https://oauth2.op.example.org/token");
+ assertNotNull(privateKeyJwt.getJWTAuthenticationClaimsSet().getJWTID());
+ assertNotNull(privateKeyJwt.getJWTAuthenticationClaimsSet().getExpirationTime());
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialisePrivateKeyJWT_WrongAlgorithm() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("HS256");
+ secContext.setSignatureSigningParameters(secParams);
+ final RSAKey key = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+ final var publicKey = key.toPublicKey();
+ assert publicKey != null;
+ secParams.setSigningCredential(new BasicCredential(publicKey, key.toPrivateKey()));
+
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ outboundMsgCtx.addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(outboundMsgCtx);
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseClientSecretJWT_WrongAlgorithm() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("RS256");
+ secContext.setSignatureSigningParameters(secParams);
+ secParams.setSigningCredential(new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential());
+
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ outboundMsgCtx.addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(outboundMsgCtx);
+
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialisePrivateKeyJWT_NoCredential() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("RS256");
+ secContext.setSignatureSigningParameters(secParams);
+
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ outboundMsgCtx.addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(outboundMsgCtx);
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseClientSecretJWT_NoCredential() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("HS256");
+ secContext.setSignatureSigningParameters(secParams);
+
+ final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+ outboundMsgCtx.addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(outboundMsgCtx);
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list