[java-idp-oidc] branch main updated: JOIDC-65 JWT client authentication support is incomplete
Scott Cantor
cantor.2 at osu.edu
Thu Dec 30 20:27:22 UTC 2021
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch main
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=10603b0153101ffb4defbfceb75cbdc55d9afeb0
The following commit(s) were added to refs/heads/main by this push:
new 10603b01 JOIDC-65 JWT client authentication support is incomplete
10603b01 is described below
commit 10603b0153101ffb4defbfceb75cbdc55d9afeb0
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu Dec 30 15:27:20 2021 -0500
JOIDC-65 JWT client authentication support is incomplete
https://shibboleth.atlassian.net/browse/JOIDC-65
Consolidated patch redone for new login flow.
---
.../oidc/op/authn/impl/JWTCredentialValidator.java | 67 ++++-
...entIDFromOIDCMetadataContextLookupFunction.java | 78 +++++
.../authn/OAuth2Client/OAuth2Client-beans.xml | 53 +++-
.../op/authn/impl/JWTCredentialValidatorTest.java | 208 +++++++++++--
.../AbstractOidcClientAuthenticationFlowTest.java | 321 +++++++++++++++++++++
.../oidc/op/profile/flow/AbstractOidcFlowTest.java | 25 +-
.../op/profile/flow/IntrospectionFlowTest.java | 36 ++-
.../oidc/op/profile/flow/RevocationFlowTest.java | 30 +-
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 44 +--
9 files changed, 817 insertions(+), 45 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java
index dbfc8aec..47fe1e68 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java
@@ -17,6 +17,7 @@
package net.shibboleth.idp.plugin.oidc.op.authn.impl;
+import java.text.ParseException;
import java.util.function.Function;
import javax.annotation.Nonnull;
@@ -30,7 +31,10 @@ import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.principal.UsernamePrincipal;
import net.shibboleth.idp.plugin.oidc.op.authn.context.OAuth2ClientAuthenticationContext;
import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.oidc.jwt.claims.JWTClaimsValidation;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -43,6 +47,7 @@ import org.opensaml.xmlsec.context.SecurityParametersContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
@@ -68,6 +73,9 @@ public class JWTCredentialValidator extends AbstractCredentialValidator {
/** Strategy used to locate the {@link SecurityParametersContext} to use for verification. */
@Nonnull private Function<ProfileRequestContext,SecurityParametersContext> securityParametersLookupStrategy;
+ /** The claims validation to be applied for validating the incoming JWTs. */
+ @NonnullAfterInit private JWTClaimsValidation claimsValidation;
+
/** Whether to save the JWT in the Java Subject's public credentials. */
private boolean saveTokenToCredentialSet;
@@ -107,6 +115,16 @@ public class JWTCredentialValidator extends AbstractCredentialValidator {
Constraint.isNotNull(strategy, "SecurityParameterContext lookup strategy cannot be null");
}
+ /**
+ * Set the claims validator used for validating the incoming JWTs.
+ *
+ * @param validation claims validator
+ */
+ public void setClaimsValidation(@Nonnull final JWTClaimsValidation validation) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ claimsValidation = Constraint.isNotNull(validation, "JWTClaimsValidation cannot be null");
+ }
/**
* Set whether to save the JWT in the Java Subject's public credentials.
@@ -126,8 +144,12 @@ public class JWTCredentialValidator extends AbstractCredentialValidator {
protected void doInitialize() throws ComponentInitializationException {
super.doInitialize();
+ if (claimsValidation == null) {
+ throw new ComponentInitializationException("ClaimsValidator cannot be null");
+ }
}
+// Checkstyle: CyclomaticComplexity OFF
/** {@inheritDoc} */
@Override
@Nullable protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
@@ -176,11 +198,54 @@ public class JWTCredentialValidator extends AbstractCredentialValidator {
throw e;
}
+ try {
+ validateJWTClaims(profileRequestContext, jwtAuth.getClientAssertion(), clientAuth.getClientID());
+ } catch (final Exception e) {
+ log.info("{} Login by '{}' failed", getLogPrefix(), clientAuth.getClientID());
+ if (errorHandler != null) {
+ errorHandler.handleError(profileRequestContext, authenticationContext, e,
+ AuthnEventIds.INVALID_CREDENTIALS);
+ }
+ throw e;
+ }
+
log.info("{} Login by '{}' succeeded", getLogPrefix(), clientAuth.getClientID());
return populateSubject(clientAuth.getClientID(), jwtAuth.getClientAssertion());
- }
+ }
+// Checkstyle: CyclomaticComplexity ON
+ /**
+ * Validates the contents of the given JWT against the requirements set in the OIDC core specification section 9.
+ *
+ * @param jwt JWT to be validated
+ * @param clientId client ID from which the JWT is coming from
+ * @param profileRequestContext profile request context
+ *
+ * @throws ParseException if unable to parse the claim set
+ * @throws JWTValidationException if the claims fail to validate
+ */
+ protected void validateJWTClaims(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final SignedJWT jwt, @Nonnull final ClientID clientId)
+ throws ParseException, JWTValidationException {
+
+ final JWTClaimsSet claimsSet;
+
+ try {
+ claimsSet = jwt.getJWTClaimsSet();
+ } catch (final ParseException e) {
+ log.warn("{} Could not parse the JWT from client '{}' into claims set", getLogPrefix(), clientId);
+ throw e;
+ }
+
+ try {
+ claimsValidation.validate(claimsSet, profileRequestContext);
+ } catch (final JWTValidationException e) {
+ log.warn("{} JWT validation failed for client '{}': {}", getLogPrefix(), clientId, e.getMessage());
+ throw e;
+ }
+ }
+
/**
* Builds a subject with "standard" content from the validation.
*
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/ClientIDFromOIDCMetadataContextLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/ClientIDFromOIDCMetadataContextLookupFunction.java
new file mode 100644
index 00000000..f06e2db2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/ClientIDFromOIDCMetadataContextLookupFunction.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.oidc.op.profile.logic;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A function that returns client_id via client information stored in {@link OIDCMetadataContext}.
+ */
+public class ClientIDFromOIDCMetadataContextLookupFunction
+ implements BiFunction<ProfileRequestContext, JWTClaimsSet, String> {
+
+ /** Strategy that will return {@link OIDCMetadataContext}. */
+ @Nonnull private Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public ClientIDFromOIDCMetadataContextLookupFunction() {
+ oidcMetadataContextLookupStrategy = new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ * Set the strategy used to return the {@link OIDCMetadataContext}.
+ *
+ * @param strategy The lookup strategy.
+ */
+ public void setOIDCMetadataContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, OIDCMetadataContext> strategy) {
+ oidcMetadataContextLookupStrategy =
+ Constraint.isNotNull(strategy, "OIDCMetadataContext lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String apply(@Nonnull final ProfileRequestContext prc, @Nullable final JWTClaimsSet claimsSet) {
+ final OIDCMetadataContext oidcMetadataContext = oidcMetadataContextLookupStrategy.apply(prc);
+ if (oidcMetadataContext == null) {
+ return null;
+ }
+ final OIDCClientInformation clientInformation = oidcMetadataContext.getClientInformation();
+ if (clientInformation == null || clientInformation.getID() == null) {
+ return null;
+ }
+ return clientInformation.getID().toString();
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml
index 3ebbed75..b046a4a8 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml
@@ -60,7 +60,58 @@
<bean class="net.shibboleth.idp.plugin.oidc.op.authn.impl.OIDCClientInfoCredentialValidator"
p:id="oauth2-clientinfo" />
<bean class="net.shibboleth.idp.plugin.oidc.op.authn.impl.JWTCredentialValidator"
- p:id="oauth2-jwt" />
+ p:id="oauth2-jwt"
+ p:claimsValidation-ref="#{'%{idp.auth.OAuth2Client.JWTValidation:DefaultJWTClaimsValidation}'.trim()}" />
+ </util:list>
+
+ <!-- JWT validation wiring. -->
+
+ <bean id="DefaultJWTClaimsValidation"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidation"
+ p:claimValidators-ref="ClaimsValidators" />
+
+ <bean id="ClientIDFromOIDCMetadataContextLookupFunction"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.logic.ClientIDFromOIDCMetadataContextLookupFunction" />
+
+ <bean id="ExpiryClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+ <bean id="IssuedAtClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+ p:messageLifetime="%{idp.policy.messageLifetime:PT1M}"
+ p:requiredRule="false" />
+
+ <bean id="IssuerClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ p:claimName="iss" p:valueToMatchLookupStrategy-ref="ClientIDFromOIDCMetadataContextLookupFunction" />
+
+ <bean id="SubjectClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ p:claimName="sub" p:valueToMatchLookupStrategy-ref="ClientIDFromOIDCMetadataContextLookupFunction" />
+
+ <bean id="AudienceClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator">
+ <property name="audienceLookupStrategy">
+ <bean parent="shibboleth.BiFunctions.Expression"
+ c:expression="#custom.getRequestURL().toString()"
+ p:customObject-ref="shibboleth.HttpServletRequest" />
+ </property>
+ </bean>
+
+ <bean id="JWTIdentifierClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+ p:replayCache-ref="shibboleth.ReplayCache" />
+
+ <util:list id="ClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+ <ref bean="ExpiryClaimsValidator" />
+ <ref bean="IssuedAtClaimsValidator" />
+ <ref bean="IssuerClaimsValidator" />
+ <ref bean="SubjectClaimsValidator" />
+ <ref bean="AudienceClaimsValidator" />
+ <ref bean="JWTIdentifierClaimsValidator" />
</util:list>
<!-- Validator parent beans -->
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java
index 6a6416fa..541617e5 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java
@@ -24,12 +24,17 @@ import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
+import java.time.Instant;
import java.util.Collections;
import java.util.Date;
+import java.util.List;
import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.http.HttpServletRequest;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.storage.ReplayCache;
+import org.opensaml.storage.impl.MemoryStorageService;
import org.opensaml.xmlsec.context.SecurityParametersContext;
import org.springframework.webflow.execution.Event;
import org.testng.Assert;
@@ -39,7 +44,12 @@ import org.testng.annotations.Test;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
@@ -60,9 +70,17 @@ import net.shibboleth.idp.authn.impl.testing.BaseAuthenticationContextTest;
import net.shibboleth.idp.authn.principal.UsernamePrincipal;
import net.shibboleth.idp.plugin.oidc.op.authn.context.OAuth2ClientAuthenticationContext;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.ClientIDFromOIDCMetadataContextLookupFunction;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.oidc.jwt.claims.JWTClaimsValidation;
import net.shibboleth.oidc.security.credential.BasicJWKCredential;
import net.shibboleth.oidc.security.impl.OIDCSignatureValidationParameters;
+import net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidation;
+import net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
/**
@@ -91,16 +109,28 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
}
@BeforeMethod
- public void setUo() throws URISyntaxException, ComponentInitializationException {
+ public void setUp() throws ComponentInitializationException {
super.setUp();
clientId = new ClientID("mockId");
clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
- endpointUri = new URI("https://mock.example.org/");
+ try {
+ endpointUri = new URI("http://localhost");
+ } catch (final URISyntaxException e) {
+ throw new ComponentInitializationException(e);
+ }
+ ReplayCache replayCache = new ReplayCache();
+ MemoryStorageService storageService = new MemoryStorageService();
+ storageService.setId("mockId");
+ storageService.initialize();
+ replayCache.setStorage(storageService);
+
validator = new JWTCredentialValidator();
validator.setId("test");
validator.setSecurityParametersLookupStrategy(new ChildContextLookup<>(SecurityParametersContext.class));
+ validator.setClaimsValidation(
+ constructClaimsValidation((HttpServletRequest) src.getExternalContext().getNativeRequest(), replayCache));
validator.initialize();
action = new ValidateCredentials();
@@ -137,7 +167,8 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
if (sameSecret) {
credential.setSecretKey(new SecretKeySpec(clientSecret.getValueBytes(), "NONE"));
} else {
- credential.setSecretKey(new SecretKeySpec("secret1234567890secret1234567890secretWRONG".getBytes(), "NONE"));
+ credential.setSecretKey(
+ new SecretKeySpec("secret1234567890secret1234567890secretWRONG".getBytes(), "NONE"));
}
}
@@ -154,14 +185,14 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
prc.getInboundMessageContext().addSubcontext(oidcContext);
}
- protected void initializeTokenRequest(final ClientAuthenticationMethod method, final boolean success)
- throws JOSEException, NoSuchAlgorithmException {
+ protected void initializeTokenRequest(final ClientAuthenticationMethod method, final SignedJWT jwt,
+ final boolean sameSecret) throws JOSEException, NoSuchAlgorithmException {
final ClientAuthentication clientAuth;
if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
- clientAuth = new ClientSecretJWT(clientId, endpointUri, JWSAlgorithm.HS256, clientSecret);
+ clientAuth = new ClientSecretJWT(jwt);
} else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
- clientAuth = new PrivateKeyJWT(clientId, endpointUri, JWSAlgorithm.RS256, rsaPrivateKey, null, null);
+ clientAuth = new PrivateKeyJWT(jwt);
} else {
clientAuth = null;
}
@@ -171,12 +202,107 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
ac.getSubcontext(OAuth2ClientAuthenticationContext.class, true).setClientAuthentication(clientAuth);
final AuthorizationGrant authzGrant = new AuthorizationCodeGrant(new AuthorizationCode(), null);
- completeSetup(new TokenRequest(null, clientAuth, authzGrant), method, success);
+ completeSetup(new TokenRequest(null, clientAuth, authzGrant), method, sameSecret);
+ }
+
+ protected JWTClaimsValidation constructClaimsValidation(final HttpServletRequest httpRequest,
+ final ReplayCache replayCache) {
+ final ChainingJWTClaimsValidation claimsValidation = new ChainingJWTClaimsValidation();
+ final ExpiryClaimsValidator expValidator = new ExpiryClaimsValidator();
+ final IssuedAtClaimsValidator iatValidator = new IssuedAtClaimsValidator();
+ iatValidator.setRequiredRule(false);
+ final ExactMatchClaimsValidator issValidator = new ExactMatchClaimsValidator();
+ issValidator.setClaimName("iss");
+ issValidator.setValueToMatchLookupStrategy(new ClientIDFromOIDCMetadataContextLookupFunction());
+ final ExactMatchClaimsValidator subValidator = new ExactMatchClaimsValidator();
+ subValidator.setClaimName("sub");
+ subValidator.setValueToMatchLookupStrategy(new ClientIDFromOIDCMetadataContextLookupFunction());
+ final AudienceClaimsValidator audValidator = new AudienceClaimsValidator();
+ audValidator.setAudienceLookupStrategy((prc, claims) -> httpRequest.getRequestURL().toString());
+ final JWTIdentifierClaimsValidator jitValidator = new JWTIdentifierClaimsValidator();
+ jitValidator.setReplayCache(replayCache);
+ claimsValidation.setClaimValidators(List.of(expValidator, iatValidator, issValidator, subValidator,
+ audValidator, jitValidator));
+ return claimsValidation;
+ }
+
+ protected void testFailingJwtAuth(final ClientAuthenticationMethod method, final SignedJWT jwt,
+ final boolean replay, final boolean sameSecret) throws Exception {
+ initializeTokenRequest(method, jwt, sameSecret);
+
+ Event event = action.execute(src);
+ if (replay) {
+ ActionTestingSupport.assertProceedEvent(event);
+ event = action.execute(src);
+ }
+ ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+ }
+
+ protected JWTClaimsSet claimsSetWithIatInTheFuture() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId.toString())
+ .issuer(clientId.toString())
+ .audience(endpointUri.toString())
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .issueTime(Date.from(Instant.now().plusSeconds(600)))
+ .jwtID("mockId")
+ .build();
+ }
+
+ protected JWTClaimsSet claimsSetWithExpInThePast() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId.toString())
+ .issuer(clientId.toString())
+ .audience(endpointUri.toString())
+ .expirationTime(Date.from(Instant.now().minusSeconds(600)))
+ .issueTime(Date.from(Instant.now()))
+ .jwtID("mockId")
+ .build();
+ }
+
+ protected JWTClaimsSet claimsSetWithoutJit() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId.toString())
+ .issuer(clientId.toString())
+ .audience(endpointUri.toString())
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .issueTime(Date.from(Instant.now()))
+ .build();
+ }
+
+ protected JWTClaimsSet validClaimsSet() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId.toString())
+ .issuer(clientId.toString())
+ .audience(endpointUri.toString())
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .issueTime(Date.from(Instant.now()))
+ .jwtID("mockId")
+ .build();
+ }
+
+ protected SignedJWT createSecretJWT(final JWTClaimsSet claimsSet) throws JOSEException {
+ return createSecretJWT(claimsSet, clientSecret.getValue());
+ }
+
+ protected SignedJWT createSecretJWT(final JWTClaimsSet claimsSet, final String clientSecret)
+ throws JOSEException {
+ final SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
+ final MACSigner signer = new MACSigner(clientSecret);
+ jwt.sign(signer);
+ return jwt;
+ }
+
+ protected SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet) throws JOSEException {
+ final SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet);
+ final RSASSASigner signer = new RSASSASigner(rsaPrivateKey);
+ jwt.sign(signer);
+ return jwt;
}
@Test
public void testSecretJwt() throws JOSEException, NoSuchAlgorithmException {
- initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, true);
+ initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, createSecretJWT(validClaimsSet()), true);
final Event event = action.execute(src);
ActionTestingSupport.assertProceedEvent(event);
@@ -189,7 +315,7 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
@Test
public void testPrivateKeyJwt() throws JOSEException, NoSuchAlgorithmException {
- initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT, true);
+ initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT, createPrivateKeyJWT(validClaimsSet()), true);
final Event event = action.execute(src);
ActionTestingSupport.assertProceedEvent(event);
@@ -201,19 +327,63 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
}
@Test
- public void testFailingSecretJwt() throws JOSEException, NoSuchAlgorithmException {
- initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, false);
+ public void testInvalidSecretJwt_signature() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+ createSecretJWT(validClaimsSet()), false, false);
+ }
- final Event event = action.execute(src);
- ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+ @Test
+ public void testInvalidSecretJwt_iatInTheFuture() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+ createSecretJWT(claimsSetWithIatInTheFuture()), false, true);
}
@Test
- public void testFailingPrivateKeyJwt() throws JOSEException, NoSuchAlgorithmException {
- initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT, false);
-
- final Event event = action.execute(src);
- ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+ public void testInvalidSecretJwt_expInThePast() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+ createSecretJWT(claimsSetWithExpInThePast()), false, true);
+ }
+
+ @Test
+ public void testInvalidSecretJwt_withoutJit() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+ createSecretJWT(claimsSetWithoutJit()), false, true);
+ }
+
+ @Test
+ public void testInvalidSecretJwt_jitReplayDetected() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+ createSecretJWT(validClaimsSet()), true, true);
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJwt_signature() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+ createPrivateKeyJWT(validClaimsSet()), false, false);
}
+ @Test
+ public void testInvalidPrivateKeyJwt_iatInTheFuture() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+ createPrivateKeyJWT(claimsSetWithIatInTheFuture()), false, true);
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJwt_expInThePast() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+ createPrivateKeyJWT(claimsSetWithExpInThePast()), false, true);
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJwt_withoutJit() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+ createPrivateKeyJWT(claimsSetWithoutJit()), false, true);
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJwt_jitReplayDetected() throws Exception {
+ testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+ createPrivateKeyJWT(validClaimsSet()), true, true);
+ }
+
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
new file mode 100644
index 00000000..d497e21a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
@@ -0,0 +1,321 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.oidc.op.profile.flow;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Map;
+
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
+import com.nimbusds.oauth2.sdk.auth.JWTAuthentication;
+import com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.utilities.java.support.collection.Pair;
+
+/**
+ * Base unit test class for flows involving JWT based authentication (client_secret_jwt
+ * or private_key_jwt).
+ */
+public abstract class AbstractOidcClientAuthenticationFlowTest extends AbstractOidcApiFlowTest {
+
+ String clientId = "mockClientId";
+ String clientSecret = "1234567890mockClientSecretmockClientSecretmockClientSecret";
+ String clientIdSaml = "mockSamlClientId";
+ String clientSecretSaml = "mockClientSecretmockClientSecretmockClientSecret";
+
+ String jwtAud = "http://localhost";
+
+ RSAPrivateKey rsaPrivateKey;
+ RSAPublicKey rsaPublicKey;
+
+ public AbstractOidcClientAuthenticationFlowTest(final String flowId) {
+ super(flowId);
+ }
+
+ @BeforeClass
+ public void initKeys() throws NoSuchAlgorithmException {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ final KeyPair keyPair = keyGen.genKeyPair();
+ rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
+ rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
+ }
+
+ protected void populateClientAssertionParams(final Map<String, String> requestParameters,
+ final SignedJWT jwt) {
+ requestParameters.put("client_assertion", jwt.serialize());
+ requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+ }
+
+ protected SignedJWT createSecretJWT(final JWTClaimsSet claimsSet, final String clientSecret)
+ throws JOSEException {
+ final SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
+ final MACSigner signer = new MACSigner(clientSecret);
+ jwt.sign(signer);
+ return jwt;
+ }
+
+ protected SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet) throws JOSEException {
+ final SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet);
+ final RSASSASigner signer = new RSASSASigner(rsaPrivateKey);
+ jwt.sign(signer);
+ return jwt;
+ }
+
+ @Test
+ public void testInvalidSecretJWT_missingSub() throws Exception {
+ final SignedJWT jwt = createSecretJWT(claimsSetMissingSub(), clientSecret);
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+ ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "UnableToDecode");
+ }
+
+ @Test
+ public void testInvalidSecretJWT_missingIss() throws Exception {
+ final SignedJWT jwt = createSecretJWT(claimsSetMissingIss(), clientSecret);
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+ ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "UnableToDecode");
+ }
+
+ @Test
+ public void testInvalidSecretJWT_missingAud() throws Exception {
+ final SignedJWT jwt = createSecretJWT(claimsSetMissingAud(), clientSecret);
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+ ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "UnableToDecode");
+ }
+
+ @Test
+ public void testInvalidSecretJWT_missingExp() throws Exception {
+ final SignedJWT jwt = createSecretJWT(claimsSetMissingExp(), clientSecret);
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+ ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "UnableToDecode");
+ }
+
+ @Test
+ public void testInvalidSecretJWT_expiredExp() throws Exception {
+ final SignedJWT jwt = createSecretJWT(claimsSetExpiredExp(), clientSecret);
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+ ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+ assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+ assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+ }
+
+ @Test
+ public void testInvalidSecretJWT_issuedInTheFuture() throws Exception {
+ final SignedJWT jwt = createSecretJWT(claimsSetIssuedInTheFuture(), clientSecret);
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+ ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+ assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+ assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+ }
+
+ @Test
+ public void testInvalidSecretJWT_missingJti() throws Exception {
+ final SignedJWT jwt = createSecretJWT(claimsSetMissingJti(), clientSecret);
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+ ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+ assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+ assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJWT_missingSub() throws Exception {
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingSub());
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+ ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "UnableToDecode");
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJWT_missingIss() throws Exception {
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingIss());
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+ ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "UnableToDecode");
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJWT_missingAud() throws Exception {
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingAud());
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+ ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "UnableToDecode");
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJWT_missingExp() throws Exception {
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingExp());
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+ ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "UnableToDecode");
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJWT_expiredExp() throws Exception {
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetExpiredExp());
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+ ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+ assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+ assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJWT_issuedInTheFuture() throws Exception {
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetIssuedInTheFuture());
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+ ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+ assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+ assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJWT_missingJti() throws Exception {
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingJti());
+ final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+ ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+ assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+ assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+ }
+
+ protected JWTClaimsSet claimsSetMissingSub() {
+ return new JWTClaimsSet.Builder()
+ .issuer(clientId)
+ .audience(jwtAud)
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .build();
+ }
+
+ protected JWTClaimsSet claimsSetMissingIss() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .audience(jwtAud)
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .jwtID("mockId")
+ .build();
+ }
+
+ protected JWTClaimsSet claimsSetMissingAud() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .jwtID("mockId")
+ .build();
+ }
+
+ protected JWTClaimsSet claimsSetMissingExp() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .audience(jwtAud)
+ .jwtID("mockId")
+ .build();
+ }
+
+ protected JWTClaimsSet claimsSetExpiredExp() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .audience(jwtAud)
+ .expirationTime(Date.from(Instant.now().minusSeconds(600)))
+ .jwtID("mockId")
+ .build();
+ }
+
+ protected JWTClaimsSet claimsSetIssuedInTheFuture() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .audience(jwtAud)
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .issueTime(Date.from(Instant.now().plusSeconds(600)))
+ .jwtID("mockId")
+ .build();
+ }
+
+ protected JWTClaimsSet claimsSetMissingJti() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .audience(jwtAud)
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .build();
+ }
+
+ protected ClientSecretJWT buildSecretJwtAuth(String secret) throws JOSEException, URISyntaxException {
+ return new ClientSecretJWT(new ClientID(clientId), new URI(jwtAud),
+ JWSAlgorithm.HS256, new Secret(secret));
+ }
+
+ protected PrivateKeyJWT buildPrivateKeyJwtAuth() throws JOSEException, URISyntaxException {
+ return new PrivateKeyJWT(new ClientID(clientId), new URI(jwtAud),
+ JWSAlgorithm.RS256, rsaPrivateKey, null, null);
+ }
+
+ protected void populateClientAssertionParams(final Map<String, String> requestParameters,
+ final JWTAuthentication clientAuth) {
+ requestParameters.put("client_assertion", clientAuth.getClientAssertion().serialize());
+ requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+ }
+
+
+ protected abstract FlowExecutionResult launchWithJwtAuthentication(final SignedJWT jwt,
+ final JWSAlgorithm algorithm, final ClientAuthenticationMethod method) throws Exception;
+
+ /**
+ * Get the pair of error code and error description for the error produced via event
+ * {@link EventIds.ACCESS_DENIED}. This is abstract due to the fact that each endpoint
+ * may have its own mappings.
+ *
+ * @return The pair of error code and error description.
+ */
+ protected abstract Pair<String, String> getErrorDetaisForJWTValidation();
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index 1accfa5d..e0ebd7d6 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.oidc.op.profile.flow;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
+import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Arrays;
import java.util.Date;
@@ -41,6 +42,8 @@ import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.oauth2.sdk.ErrorResponse;
import com.nimbusds.oauth2.sdk.GrantType;
import com.nimbusds.oauth2.sdk.Response;
@@ -124,6 +127,7 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
protected void assertErrorDescriptionContains(final FlowExecutionResult result, final String errorDescription) {
final ErrorResponse errorResponse = parseErrorResponse(result);
Assert.assertNotNull(errorResponse.getErrorObject().getDescription());
+ System.out.println("Error " + errorResponse.getErrorObject().getDescription());
Assert.assertTrue(errorResponse.getErrorObject().getDescription().contains(errorDescription));
}
@@ -151,20 +155,27 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
protected void storeMetadata(final StorageService storageService, final String clientId, final String secret,
final String... redirectUri) throws IOException {
storeMetadata(storageService, clientId, secret, null, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null,
- redirectUri);
+ null, redirectUri);
}
protected void storeMetadata(final StorageService storageService, final String clientId, final String secret,
final JWSAlgorithm tokenEndpointSigAlg, final ClientAuthenticationMethod tokenEndpointMethod,
final String... redirectUri)
throws IOException {
- storeMetadata(storageService, clientId, secret, tokenEndpointSigAlg, tokenEndpointMethod, null, redirectUri);
+ storeMetadata(storageService, clientId, secret, tokenEndpointSigAlg, tokenEndpointMethod, null, null, redirectUri);
}
protected void storeMetadata(final StorageService storageService, final String clientId, final String secret,
final JWSAlgorithm tokenEndpointSigAlg, final ClientAuthenticationMethod tokenEndpointMethod,
final JWSAlgorithm userInfoSigAlg, final String... redirectUri)
throws IOException {
+ storeMetadata(storageService, clientId, secret, tokenEndpointSigAlg, tokenEndpointMethod, userInfoSigAlg, null, redirectUri);
+ }
+
+ protected void storeMetadata(final StorageService storageService, final String clientId, final String secret,
+ final JWSAlgorithm tokenEndpointSigAlg, final ClientAuthenticationMethod tokenEndpointMethod,
+ final JWSAlgorithm userInfoSigAlg, final RSAPublicKey publicKey, final String... redirectUri)
+ throws IOException {
final OIDCClientMetadata metadata = new OIDCClientMetadata();
metadata.setGrantTypes(new HashSet<GrantType>(Arrays.asList(GrantType.AUTHORIZATION_CODE,
GrantType.REFRESH_TOKEN)));
@@ -184,8 +195,16 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
metadata.setTokenEndpointAuthJWSAlg(tokenEndpointSigAlg);
metadata.setTokenEndpointAuthMethod(tokenEndpointMethod);
metadata.setUserInfoJWSAlg(userInfoSigAlg);
- final OIDCClientInformation information = new OIDCClientInformation(new ClientID(clientId), new Date(),
+ final OIDCClientInformation information;
+ if (publicKey == null) {
+ information = new OIDCClientInformation(new ClientID(clientId), new Date(),
metadata, new Secret(secret));
+ } else {
+ RSAKey rsaKey = new RSAKey.Builder(publicKey).build();
+ JWKSet jwkSet = new JWKSet(rsaKey);
+ metadata.setJWKSet(jwkSet);
+ information = new OIDCClientInformation(new ClientID(clientId), metadata);
+ }
storageService.create(BaseStorageServiceClientInformationComponent.CONTEXT_NAME, clientId,
information.toJSONObject().toJSONString(), System.currentTimeMillis() + (60 * 60 * 1000));
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
index 0959c0f1..b897750c 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
@@ -20,7 +20,11 @@ package net.shibboleth.idp.plugin.oidc.op.profile.flow;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
+import java.time.Instant;
import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
import org.opensaml.storage.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,18 +34,22 @@ import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.OAuth2Error;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenIntrospectionErrorResponse;
import com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import net.shibboleth.utilities.java.support.collection.Pair;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.security.DataSealerException;
/**
* Unit tests for the OAuth2 introspection flow.
*/
-public class IntrospectionFlowTest extends AbstractOidcApiFlowTest {
+public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowTest {
public static final String FLOW_ID = "oauth2/introspection";
@@ -163,4 +171,30 @@ public class IntrospectionFlowTest extends AbstractOidcApiFlowTest {
TokenIntrospectionErrorResponse resp = (TokenIntrospectionErrorResponse) parseErrorResponse(result);
Assert.assertEquals(resp.getErrorObject().getCode(), OAuth2Error.INVALID_CLIENT_CODE);
}
+
+ protected FlowExecutionResult launchWithJwtAuthentication(final SignedJWT jwt, final JWSAlgorithm algorithm,
+ final ClientAuthenticationMethod method) throws Exception {
+ if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(method)) {
+ storeMetadata(storageService, clientId, clientSecret, algorithm, method);
+ } else {
+ storeMetadata(storageService, clientId, null, algorithm, method, null, rsaPublicKey);
+ }
+ final String accessToken = super.buildToken(clientId, "sub", Scope.parse("openid")).toJSONObject().getAsString("access_token");
+ Map<String, String> requestParameters = createRequestParameters(accessToken, clientId);
+ populateClientAssertionParams(requestParameters, jwt);
+ setHttpFormRequest("POST", requestParameters);
+ return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ }
+
+ protected Map<String, String> createRequestParameters(final String token, final String clientId) {
+ final Map<String, String> result = new HashMap<>();
+ result.put("token", token);
+ result.put("client_id", clientId);
+ return result;
+ }
+
+ protected Pair<String, String> getErrorDetaisForJWTValidation() {
+ return new Pair<>("invalid_client", "Client authentication failed");
+ }
+
}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
index 7174eccb..f458edb7 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
@@ -21,6 +21,8 @@ import java.io.IOException;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import org.opensaml.storage.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,19 +32,23 @@ import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.OAuth2Error;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenIntrospectionErrorResponse;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.OAuth2RevocationSuccessResponse;
+import net.shibboleth.utilities.java.support.collection.Pair;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.security.DataSealerException;
/**
* Unit tests for the OAuth2 revocation flow.
*/
-public class RevocationFlowTest extends AbstractOidcApiFlowTest {
+public class RevocationFlowTest extends AbstractOidcClientAuthenticationFlowTest {
public static final String FLOW_ID = "oauth2/revocation";
@@ -86,7 +92,7 @@ public class RevocationFlowTest extends AbstractOidcApiFlowTest {
parseSuccessResponse(result, OAuth2RevocationSuccessResponse.class);
}
- @Test
+ @Test
public void testSuccessWithSamlMetadata() throws IOException, NoSuchAlgorithmException, URISyntaxException,
DataSealerException, ComponentInitializationException {
setBasicAuth(clientIdSaml, clientSecretSaml);
@@ -117,6 +123,26 @@ public class RevocationFlowTest extends AbstractOidcApiFlowTest {
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
parseSuccessResponse(result, OAuth2RevocationSuccessResponse.class);
}
+
+ protected FlowExecutionResult launchWithJwtAuthentication(final SignedJWT jwt, final JWSAlgorithm algorithm,
+ final ClientAuthenticationMethod method) throws Exception {
+ if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(method)) {
+ storeMetadata(storageService, clientId, clientSecret, algorithm, method);
+ } else {
+ storeMetadata(storageService, clientId, null, algorithm, method, null, rsaPublicKey);
+ }
+ final String accessToken = super.buildToken(clientId, "sub", Scope.parse("openid")).toJSONObject().getAsString("access_token");
+ Map<String, String> requestParameters = new HashMap<>();
+ requestParameters.put("token", accessToken);
+ populateClientAssertionParams(requestParameters, jwt);
+ setHttpFormRequest("POST", requestParameters);
+ return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ }
+
+ protected Pair<String, String> getErrorDetaisForJWTValidation() {
+ return new Pair<>("invalid_client", "Client authentication failed");
+ }
+
@Test
public void testFailedAuthentication() throws IOException, NoSuchAlgorithmException, URISyntaxException,
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 3e953e2f..6e0f61fb 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -18,7 +18,6 @@
package net.shibboleth.idp.plugin.oidc.op.profile.flow;
import java.io.IOException;
-import java.net.URI;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
@@ -37,6 +36,7 @@ import org.testng.annotations.Test;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.OAuth2Error;
import com.nimbusds.oauth2.sdk.Scope;
@@ -58,25 +58,22 @@ import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest
import net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrantTest;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.utilities.java.support.collection.Pair;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.security.DataSealerException;
/**
* Unit tests for the token flow.
*/
-public class TokenFlowTest extends AbstractOidcFlowTest {
+public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
public static final String FLOW_ID = "oidc/token";
String redirectUri = "https://example.org/cb";
- String clientId = "mockClientId";
- String clientIdSaml = "mockSamlClientId";
String clientIdPkcePlain = "mockClientIdPKCEPlain";
String clientIdPkcePlainUnforced = "mockClientIdPKCEPlainUnforced";
String clientIdPkceS256 = "mockClientIdPKCES256";
- String clientSecret = "mockClientSecretmockClientSecretmockClientSecret";
String codeVerifier = "9234567812345678123456781234567812345678123456781234567812345678";
-
@Autowired
@Qualifier("shibboleth.StorageService")
@@ -194,7 +191,7 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
DataSealerException, ComponentInitializationException, java.text.ParseException {
initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
buildAuthorizationCode(clientIdSaml), clientIdSaml));
- setBasicAuth(clientIdSaml, clientSecret);
+ setBasicAuth(clientIdSaml, clientSecretSaml);
storeConsent(storageService, "jdoe", clientIdSaml, "mail");
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
@@ -444,7 +441,7 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
try {
return AccessTokenClaimsSet.parse(accessToken.getValue(),
BaseOIDCResponseActionTest.initializeDataSealer());
- } catch (NoSuchAlgorithmException | java.text.ParseException | DataSealerException
+ } catch (final NoSuchAlgorithmException | java.text.ParseException | DataSealerException
| ComponentInitializationException e) {
return null;
}
@@ -470,16 +467,22 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
setHttpFormRequest("POST", requestParameters);
return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
}
-
- protected ClientSecretJWT buildSecretJwtAuth(String secret) throws JOSEException, URISyntaxException {
- return new ClientSecretJWT(new ClientID(clientId), new URI("https://op.example.org"),
- JWSAlgorithm.HS256, new Secret(secret));
- }
-
- protected void populateClientAssertionParams(final Map<String, String> requestParameters,
- final JWTAuthentication clientAuth) {
- requestParameters.put("client_assertion", clientAuth.getClientAssertion().serialize());
- requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+
+ protected FlowExecutionResult launchWithJwtAuthentication(final SignedJWT jwt, final JWSAlgorithm algorithm,
+ final ClientAuthenticationMethod method)
+ throws NoSuchAlgorithmException, URISyntaxException, DataSealerException, ComponentInitializationException,
+ IOException {
+ String code = ValidateGrantTest.buildAuthorizationCode(clientId, "https://op.example.org", "jdoe", "mock",
+ redirectUri).toString();
+ if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(method)) {
+ storeMetadata(storageService, clientId, clientSecret, algorithm, method);
+ } else {
+ storeMetadata(storageService, clientId, null, algorithm, method, null, rsaPublicKey);
+ }
+ Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code", code, clientId);
+ populateClientAssertionParams(requestParameters, jwt);
+ setHttpFormRequest("POST", requestParameters);
+ return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
}
protected Map<String, String> createRequestParameters(String redirectUri, String grantType, String code,
@@ -510,4 +513,9 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
map.put(key, value);
}
}
+
+ protected Pair<String, String> getErrorDetaisForJWTValidation() {
+ return new Pair<>("invalid_client", "Client authentication failed");
+ }
+
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list