[java-idp-oidc] branch maint-3.0 updated: JOIDC-65 JWT client authentication support is incomplete
Henri Mikkonen
henri.mikkonen at iki.fi
Wed Dec 22 19:48:30 UTC 2021
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch maint-3.0
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=a9f4850def8c8f1de0208f0e87ef9257d16600de
The following commit(s) were added to refs/heads/maint-3.0 by this push:
new a9f4850d JOIDC-65 JWT client authentication support is incomplete
a9f4850d is described below
commit a9f4850def8c8f1de0208f0e87ef9257d16600de
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Dec 22 21:45:18 2021 +0200
JOIDC-65 JWT client authentication support is incomplete
https://shibboleth.atlassian.net/browse/JOIDC-65
Initial version and tests for improving JWT client authentication
as required by the OIDC core spec. To start with, only 'exp', 'iat'
and 'aud' claims are validated via ClaimsValidators. Jti replay is
included in the ValidateEndpointAuthentication in order to make
wiring of ReplayCache straightforward. 'sub' and 'iss' are not currently
validated in ValidateEndpointAuthentication, as Nimbus takes care of
validating their presence and values as a part of the message decoding.
That's verified in the flow tests (token, introspection and revocation).
---
.../impl/ValidateEndpointAuthentication.java | 121 +++++++-
.../jwt/claims/impl/IssuedAtClaimsValidator.java | 109 +++++++
.../op/security/jwt/claims/impl/package-info.java | 21 ++
.../oauth2/introspection/introspection-beans.xml | 42 ++-
.../oauth2/introspection/introspection-flow.xml | 1 +
.../flows/oauth2/revocation/revocation-beans.xml | 43 ++-
.../flows/oauth2/revocation/revocation-flow.xml | 1 +
.../idp/flows/oidc/token/token-beans.xml | 11 +-
.../AbstractOidcClientAuthenticationFlowTest.java | 321 +++++++++++++++++++++
.../oidc/op/profile/flow/AbstractOidcFlowTest.java | 25 +-
.../op/profile/flow/IntrospectionFlowTest.java | 44 ++-
.../oidc/op/profile/flow/RevocationFlowTest.java | 38 ++-
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 47 +--
.../impl/ValidateEndpointAuthenticationTest.java | 135 ++++++++-
14 files changed, 907 insertions(+), 52 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthentication.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthentication.java
index 24443f3d..26774daf 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthentication.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthentication.java
@@ -17,6 +17,9 @@
package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
import java.util.function.Function;
@@ -33,6 +36,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.AbstractOptionallyAuthenticatedRequest;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
@@ -47,12 +51,19 @@ import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
import net.shibboleth.idp.plugin.oidc.op.config.navigate.TokenEndpointAuthMethodLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.IssuedAtClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
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.ExpiryClaimsValidator;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
/**
* Validates the endpoint authentication with the token_endpoint_auth_method stored to the client's metadata.
@@ -83,7 +94,15 @@ public class ValidateEndpointAuthentication extends AbstractOIDCRequestAction<Ab
*/
@Nonnull private Function<ProfileRequestContext, SecurityParametersContext> securityParametersLookupStrategy;
-
+ /** The validator used for the expiration time of the incoming JWTs. */
+ @Nonnull private ClaimsValidator expiryClaimsValidator;
+
+ /** The validator used for the issuance time of the incoming JWTs. */
+ @Nonnull private ClaimsValidator issuedAtClaimsValidator;
+
+ /** The validator used for the audience of the incoming JWTs. */
+ @Nonnull private ClaimsValidator audienceClaimsValidator;
+
/**
* Constructor.
*/
@@ -92,6 +111,11 @@ public class ValidateEndpointAuthentication extends AbstractOIDCRequestAction<Ab
new InboundMessageContextLookup());
tokenEndpointAuthMethodsLookupStrategy = new TokenEndpointAuthMethodLookupFunction();
securityParametersLookupStrategy = new ChildContextLookup<>(SecurityParametersContext.class);
+ audienceClaimsValidator = new AudienceClaimsValidator();
+ ((AudienceClaimsValidator) audienceClaimsValidator).setAudienceLookupStrategy(
+ (P, J) -> getHttpServletRequest().getRequestURL().toString());
+ expiryClaimsValidator = new ExpiryClaimsValidator();
+ issuedAtClaimsValidator = new IssuedAtClaimsValidator();
}
/**
@@ -142,6 +166,37 @@ public class ValidateEndpointAuthentication extends AbstractOIDCRequestAction<Ab
Constraint.isNotNull(strategy, "SecurityParameterContext lookup strategy cannot be null");
}
+ /**
+ * Set the validator used for the expiration time of the incoming JWTs.
+ *
+ * @param validator What to set.
+ */
+ public void setExpiryClaimsValidator(@Nonnull final ClaimsValidator validator) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ expiryClaimsValidator = Constraint.isNotNull(validator, "ExpiryClaimsValidator cannot be null");
+ }
+
+ /**
+ * Set the validator used for the issuance time of the incoming JWTs.
+ *
+ * @param validator What to set.
+ */
+ public void setIssuedAtClaimsValidator(@Nonnull final ClaimsValidator validator) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ issuedAtClaimsValidator = Constraint.isNotNull(validator, "IssuedAtClaimsValidator cannot be null");
+ }
+
+ /**
+ * Set the validator used for the audience of the incoming JWTs.
+ *
+ * @param validator What to set.
+ */
+ public void setAudienceClaimsValidator(@Nonnull final ClaimsValidator validator) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ audienceClaimsValidator = Constraint.isNotNull(validator, "AudienceClaimsValidator cannot be null");
+ }
+
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -212,6 +267,10 @@ public class ValidateEndpointAuthentication extends AbstractOIDCRequestAction<Ab
securityParametersLookupStrategy.apply(profileRequestContext), jwt, EventIds.ACCESS_DENIED);
if (errorEventId != null) {
ActionSupport.buildEvent(profileRequestContext, errorEventId);
+ return;
+ }
+ if (!validateJwtClaims(jwt, clientInformation.getID().toString(), profileRequestContext)) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
}
return;
}
@@ -221,8 +280,66 @@ public class ValidateEndpointAuthentication extends AbstractOIDCRequestAction<Ab
ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
}
+ /**
+ * Validates the contents of the given JWT against the requirements set in the OIDC core specification section 9.
+ *
+ * @param jwt The JWT to be validated.
+ * @param clientId The client ID from which the JWT is coming from.
+ * @param profileRequestContext The profile request context.
+ * @return true if JWT meets the requirements, false otherwise.
+ */
+ protected boolean validateJwtClaims(final SignedJWT jwt, final String clientId, @Nonnull final ProfileRequestContext profileRequestContext ) {
+ final JWTClaimsSet claimsSet;
+ try {
+ claimsSet = jwt.getJWTClaimsSet();
+ } catch (ParseException e) {
+ log.error("{} Could not parse the JWT into claims set, it cannot be validated", getLogPrefix());
+ return false;
+ }
+ final List<String> aud = claimsSet.getAudience();
+ if (aud == null || aud.isEmpty()) {
+ log.warn("{} The incoming JWT from {} is missing audience (aud)", getLogPrefix(), clientId);
+ return false;
+ }
+ final Date exp = claimsSet.getExpirationTime();
+ if (exp == null) {
+ log.warn("{} The incoming JWT from {} is missing expiration time (exp)", getLogPrefix(), clientId);
+ return false;
+ }
+ final String jit = claimsSet.getJWTID();
+ if (StringSupport.trimOrNull(jit) == null) {
+ log.warn("{} The incoming JWT from {} is missing JWT identifier (jit)", getLogPrefix(), clientId);
+ return false;
+ }
+ final ChainingJWTClaimsValidation validatorChain = new ChainingJWTClaimsValidation();
+ final List<ClaimsValidator> validators = new ArrayList<>();
+
+ log.info("{} Validating against {}", getLogPrefix(), getHttpServletRequest().getRequestURL());
+
+ final Date iat = claimsSet.getIssueTime();
+ if (iat != null) {
+ validators.add(issuedAtClaimsValidator);
+ }
+ validators.add(expiryClaimsValidator);
+ validators.add(audienceClaimsValidator);
+
+ validatorChain.setClaimValidators(validators);
+ try {
+ validatorChain.validate(claimsSet, profileRequestContext);
+ } catch (final JWTValidationException e) {
+ log.warn("{} JWT validation failed for the client {}", getLogPrefix(), clientId, e);
+ return false;
+ }
+ if (!replayCache.check(getClass().getName(), jit, exp.toInstant())) {
+ log.warn("{} Replay detected for the incoming JWT from {}", getLogPrefix(), clientId);
+ return false;
+ }
+ return true;
+
+ }
+
// Checkstyle: CyclomaticComplexity ON
-
+
/**
* Checks whether the requested authentication method is enabled and matching to the desired method.
* @param enabledMethods The list of enabled authentication method.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/IssuedAtClaimsValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/IssuedAtClaimsValidator.java
new file mode 100644
index 00000000..4645fe6b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/IssuedAtClaimsValidator.java
@@ -0,0 +1,109 @@
+/*
+ * 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.security.jwt.claims.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Iff the 'iat' claim is present in the ID Token, verifies it is not to far away from
+ * the current time. A configured window/deviation is allowed. See section 3.1.3.7 of OpenID Connect core 1.0.
+ *
+ * Also added clockSkew. This to be moved into commons: temporarily here due to 3.0.3 patch.
+ */
+ at ThreadSafeAfterInit
+public class IssuedAtClaimsValidator extends AbstractClaimsValidator {
+
+ /**
+ * Maximum amount (in either direction from now) of duration for which a token is valid after
+ * it is issued (Default value: 60 seconds).
+ */
+ @Nonnull private Duration iatWindow;
+
+ /** The clock skew allowed (Default value: 60 seconds). */
+ @Nonnull private Duration clockSkew;
+
+ /** Constructor.*/
+ public IssuedAtClaimsValidator() {
+ iatWindow = Duration.ofSeconds(60);
+ clockSkew = Duration.ofSeconds(60);
+ }
+
+ /**
+ * Sets the amount of time for which a token is valid from when it was issued.
+ *
+ * @param window amount of time for which a token is valid
+ */
+ public void setIatWindow(@Nonnull final Duration window) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ Constraint.isNotNull(window, "Token issued at window cannot be null");
+ Constraint.isFalse(window.isNegative(), "Token issued at window cannot be negative");
+
+ iatWindow = window;
+ }
+
+ /**
+ * Set the clock skew.
+ *
+ * @param skew clock skew to set
+ */
+ public void setClockSkew(@Nonnull final Duration skew) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void doValidate(@Nonnull final JWTClaimsSet claims,
+ @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+
+ final Date iatDate = claims.getIssueTime();
+ if (iatDate != null) {
+ final Instant iat = iatDate.toInstant();
+ final Instant now = Instant.now();
+ final Duration iatDifference = Duration.between(now, iat).abs();
+
+ final Duration window = iatWindow.plus(clockSkew);
+
+ if (window.compareTo(iatDifference) < 0) {
+ throw new JWTValidationException("JWT issued-at time is too far away from the current time. "
+ + "Token issued at '"+iat+"' was too far away from the current time '"+now+"' "
+ + "with acceptable deviation of "
+ + "'"+window+"', difference is '"+iatDifference+"'");
+ }
+ }
+
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/package-info.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/package-info.java
new file mode 100644
index 00000000..f376714c
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes related to JWT claims validation.
+ */
+package net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl;
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
index 65e9bb80..b6da4dfd 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
@@ -27,7 +27,22 @@
<bean id="ValidateEndpointAuthentication"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateEndpointAuthentication" scope="prototype"
- p:httpServletRequest-ref="shibboleth.HttpServletRequest" p:replayCache-ref="shibboleth.ReplayCache" />
+ p:httpServletRequest-ref="shibboleth.HttpServletRequest" p:replayCache-ref="shibboleth.ReplayCache"
+ p:expiryClaimsValidator-ref="ExpiryClaimsValidator" p:issuedAtClaimsValidator-ref="IssuedAtClaimsValidator">
+ <property name="securityParametersLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+ </property>
+ </bean>
+
+ <bean id="ExpiryClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+ <bean id="IssuedAtClaimsValidator"
+ class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.IssuedAtClaimsValidator"
+ p:iatWindow="%{idp.policy.messageLifetime:PT1M}" p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
<bean id="FormOutboundMessage"
class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.FormOutboundIntrospectionResponseMessage" scope="prototype"
@@ -48,4 +63,29 @@
</property>
</bean>
+ <bean id="PopulateTokenEndpointJwtSignatureValidationParameters"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
+ p:configurationLookupStrategy-ref="shibboleth.oidc.SignatureValidationConfigurationLookup"
+ p:signatureSigningParametersResolver-ref="shibboleth.oidc.IntrospectionEndpointJwtSignatureValidationParametersResolver">
+ <property name="securityParametersContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+ </property>
+ <property name="existingParametersContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+ c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+ </property>
+ </bean>
+
+ <bean id="shibboleth.oidc.SignatureValidationConfigurationLookup"
+ class="net.shibboleth.idp.plugin.oidc.op.config.navigate.TokenEndpointJwtSignatureValidationConfigurationLookupFunction"
+ p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyConfigurationResolver" />
+
+ <bean id="shibboleth.oidc.IntrospectionEndpointJwtSignatureValidationParametersResolver"
+ class="net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationSignatureValidationParametersResolver"
+ p:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache" p:keyFetchInterval="%{idp.oidc.jwksuri.fetchInterval:PT30M}"
+ p:parameterType="#{T(net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationSignatureValidationParametersResolver.ParameterType).TOKEN_ENDPOINT_JWT_VALIDATION}" />
+
</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-flow.xml
index 439263c1..a8174188 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-flow.xml
@@ -29,6 +29,7 @@
</action-state>
<action-state id="OutboundContextsAndSecurityParameters">
+ <evaluate expression="PopulateTokenEndpointJwtSignatureValidationParameters"/>
<evaluate expression="ValidateEndpointAuthentication" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="BuildResponseMessage" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
index 4407f098..6a8eb098 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
@@ -21,13 +21,29 @@
class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestClientIDLookupFunction"
scope="prototype" />
+
<bean id="InitializeOutboundMessageContext"
class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.InitializeOutboundRevokeTokenResponseMessageContext"
scope="prototype" />
<bean id="ValidateEndpointAuthentication"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateEndpointAuthentication" scope="prototype"
- p:httpServletRequest-ref="shibboleth.HttpServletRequest" p:replayCache-ref="shibboleth.ReplayCache" />
+ p:httpServletRequest-ref="shibboleth.HttpServletRequest" p:replayCache-ref="shibboleth.ReplayCache"
+ p:expiryClaimsValidator-ref="ExpiryClaimsValidator" p:issuedAtClaimsValidator-ref="IssuedAtClaimsValidator">
+ <property name="securityParametersLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+ </property>
+ </bean>
+
+ <bean id="ExpiryClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+ <bean id="IssuedAtClaimsValidator"
+ class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.IssuedAtClaimsValidator"
+ p:iatWindow="%{idp.policy.messageLifetime:PT1M}" p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
<bean id="RevokeToken" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.RevokeToken" scope="prototype"
c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
@@ -51,4 +67,29 @@
</property>
</bean>
+ <bean id="PopulateTokenEndpointJwtSignatureValidationParameters"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
+ p:configurationLookupStrategy-ref="shibboleth.oidc.SignatureValidationConfigurationLookup"
+ p:signatureSigningParametersResolver-ref="shibboleth.oidc.RevocationEndpointJwtSignatureValidationParametersResolver">
+ <property name="securityParametersContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+ </property>
+ <property name="existingParametersContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+ c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+ </property>
+ </bean>
+
+ <bean id="shibboleth.oidc.SignatureValidationConfigurationLookup"
+ class="net.shibboleth.idp.plugin.oidc.op.config.navigate.TokenEndpointJwtSignatureValidationConfigurationLookupFunction"
+ p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyConfigurationResolver" />
+
+ <bean id="shibboleth.oidc.RevocationEndpointJwtSignatureValidationParametersResolver"
+ class="net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationSignatureValidationParametersResolver"
+ p:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache" p:keyFetchInterval="%{idp.oidc.jwksuri.fetchInterval:PT30M}"
+ p:parameterType="#{T(net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationSignatureValidationParametersResolver.ParameterType).TOKEN_ENDPOINT_JWT_VALIDATION}" />
+
</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-flow.xml
index b9148a5a..4b3edf0e 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-flow.xml
@@ -29,6 +29,7 @@
</action-state>
<action-state id="OutboundContextsAndSecurityParameters">
+ <evaluate expression="PopulateTokenEndpointJwtSignatureValidationParameters"/>
<evaluate expression="ValidateEndpointAuthentication" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="RevokeToken" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
index 2e0c5b17..6b4b27a0 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
@@ -26,13 +26,22 @@
<bean id="ValidateEndpointAuthentication"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateEndpointAuthentication" scope="prototype"
- p:httpServletRequest-ref="shibboleth.HttpServletRequest" p:replayCache-ref="shibboleth.ReplayCache">
+ p:httpServletRequest-ref="shibboleth.HttpServletRequest" p:replayCache-ref="shibboleth.ReplayCache"
+ p:expiryClaimsValidator-ref="ExpiryClaimsValidator" p:issuedAtClaimsValidator-ref="IssuedAtClaimsValidator">
<property name="securityParametersLookupStrategy">
<bean parent="shibboleth.Functions.Compose"
c:g-ref="shibboleth.ChildLookup.SecurityParameters"
c:f-ref="shibboleth.ChildLookup.RelyingParty" />
</property>
</bean>
+
+ <bean id="ExpiryClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+ <bean id="IssuedAtClaimsValidator"
+ class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.IssuedAtClaimsValidator"
+ p:iatWindow="%{idp.policy.messageLifetime:PT1M}" p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
<bean id="ValidateGrantType" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrantType"
scope="prototype" />
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 10507631..6e1499e9 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,27 +34,25 @@ import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
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";
- String clientId = "mockClientId";
-
- String clientSecret = "mockClientSecret";
-
- String clientIdSaml = "mockSamlClientId";
- String clientSecretSaml = "mockClientSecretmockClientSecretmockClientSecret";
-
@Autowired
@Qualifier("shibboleth.StorageService")
StorageService storageService;
@@ -163,4 +165,30 @@ public class IntrospectionFlowTest extends AbstractOidcApiFlowTest {
TokenIntrospectionErrorResponse resp = (TokenIntrospectionErrorResponse) parseErrorResponse(result);
Assert.assertEquals(resp.getErrorObject().getCode(), "invalid_client");
}
+
+ 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 af081fbf..9e746524 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;
@@ -29,26 +31,24 @@ import org.springframework.webflow.executor.FlowExecutionResult;
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.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
+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";
-
- String clientId = "mockClientId";
- String clientSecret = "mockClientSecret";
-
- String clientIdSaml = "mockSamlClientId";
- String clientSecretSaml = "mockClientSecretmockClientSecretmockClientSecret";
-
+
@Autowired
@Qualifier("shibboleth.StorageService")
StorageService storageService;
@@ -84,7 +84,7 @@ public class RevocationFlowTest extends AbstractOidcApiFlowTest {
parseSuccessResponse(result, OAuth2RevocationSuccessResponse.class);
}
- @Test
+ @Test
public void testSuccessWithSamlMetadata() throws IOException, NoSuchAlgorithmException, URISyntaxException,
DataSealerException, ComponentInitializationException {
setBasicAuth(clientIdSaml, clientSecretSaml);
@@ -115,4 +115,24 @@ 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<>("access_denied", "Access denied by resource owner");
+ }
+
}
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 b83c10ea..567e2c9b 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,13 +36,12 @@ 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.Scope;
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.Secret;
-import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.pkce.CodeChallenge;
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
@@ -57,25 +55,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")
@@ -195,7 +190,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);
@@ -238,9 +233,8 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
consentedClaims);
return new RefreshToken(BaseOIDCResponseActionTest.initializeDataSealer().wrap(json,
Instant.now().plusSeconds(30))).getValue();
-}
+ }
-
@Test
public void testValidSecretJWT() throws ParseException, IOException, NoSuchAlgorithmException, URISyntaxException,
DataSealerException, ComponentInitializationException, JOSEException {
@@ -473,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,
@@ -513,4 +513,9 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
map.put(key, value);
}
}
+
+ protected Pair<String, String> getErrorDetaisForJWTValidation() {
+ return new Pair<>("invalid_request", "AccessDenied");
+ }
+
}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthenticationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthenticationTest.java
index a15d2900..377e34f8 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthenticationTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthenticationTest.java
@@ -24,6 +24,7 @@ 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.Arrays;
import java.util.Date;
import java.util.List;
@@ -36,6 +37,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.storage.ReplayCache;
import org.opensaml.storage.impl.MemoryStorageService;
import org.opensaml.xmlsec.context.SecurityParametersContext;
+import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
import org.testng.Assert;
@@ -45,7 +47,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;
@@ -62,7 +69,6 @@ import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateEndpointAuthentication;
import net.shibboleth.idp.profile.context.navigate.AbstractRelyingPartyLookupFunction;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
@@ -97,7 +103,7 @@ public class ValidateEndpointAuthenticationTest {
public void init() throws URISyntaxException {
clientId = new ClientID("mockId");
clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
- endpointUri = new URI("https://mock.example.org/");
+ endpointUri = new URI("http://localhost");
}
protected RequestContext initializeRequestCtx(final TokenRequest request,
@@ -147,17 +153,27 @@ public class ValidateEndpointAuthenticationTest {
prc.getInboundMessageContext().addSubcontext(oidcContext);
return requestCtx;
}
-
- protected TokenRequest initializeTokenRequest(final ClientAuthenticationMethod method) throws JOSEException {
+
+ protected TokenRequest initializeTokenRequest(final ClientAuthenticationMethod method)
+ throws JOSEException {
+ return initializeTokenRequest(method, null);
+ }
+
+ protected TokenRequest initializeTokenRequest(final ClientAuthenticationMethod method, final SignedJWT jwt)
+ throws JOSEException {
final ClientAuthentication clientAuth;
if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
clientAuth = new ClientSecretBasic(clientId, clientSecret);
} else if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
clientAuth = new ClientSecretPost(clientId, clientSecret);
} else if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
- clientAuth = new ClientSecretJWT(clientId, endpointUri, JWSAlgorithm.HS256, clientSecret);
+ clientAuth = jwt == null ?
+ new ClientSecretJWT(clientId, endpointUri, JWSAlgorithm.HS256, clientSecret) :
+ new ClientSecretJWT(jwt);
} else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
- clientAuth = new PrivateKeyJWT(clientId, endpointUri, JWSAlgorithm.RS256, rsaPrivateKey, null, null);
+ clientAuth = jwt == null ?
+ new PrivateKeyJWT(clientId, endpointUri, JWSAlgorithm.RS256, rsaPrivateKey, null, null) :
+ new PrivateKeyJWT(jwt);
} else {
clientAuth = null;
}
@@ -177,6 +193,7 @@ public class ValidateEndpointAuthenticationTest {
if (newFunction != null) {
action.setTokenEndpointAuthMethodsLookupStrategy(newFunction);
}
+ action.setHttpServletRequest(new MockHttpServletRequest());
action.initialize();
return action;
}
@@ -250,6 +267,112 @@ public class ValidateEndpointAuthenticationTest {
public void testFailingPrivateKeyJwt() throws Exception {
testFailingClientAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
}
+
+ @Test
+ public void testInvalidPrivateKeyJwt_iatInTheFuture() throws Exception {
+ ClientAuthenticationMethod method = ClientAuthenticationMethod.PRIVATE_KEY_JWT;
+ ValidateEndpointAuthentication action =
+ constructAction(new ListMethodsFunction(method));
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetWithIatInTheFuture());
+ final Event event =
+ action.execute(initializeRequestCtx(
+ initializeTokenRequest(method, jwt), method, true));
+ ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJwt_expInThePast() throws Exception {
+ ClientAuthenticationMethod method = ClientAuthenticationMethod.PRIVATE_KEY_JWT;
+ ValidateEndpointAuthentication action =
+ constructAction(new ListMethodsFunction(method));
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetWithExpInThePast());
+ final Event event =
+ action.execute(initializeRequestCtx(
+ initializeTokenRequest(method, jwt), method, true));
+ ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJwt_withoutJit() throws Exception {
+ ClientAuthenticationMethod method = ClientAuthenticationMethod.PRIVATE_KEY_JWT;
+ ValidateEndpointAuthentication action =
+ constructAction(new ListMethodsFunction(method));
+ final SignedJWT jwt = createPrivateKeyJWT(claimsSetWithoutJit());
+ final Event event =
+ action.execute(initializeRequestCtx(
+ initializeTokenRequest(method, jwt), method, true));
+ ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
+ }
+
+ @Test
+ public void testInvalidPrivateKeyJwt_jitReplayDetected() throws Exception {
+ ClientAuthenticationMethod method = ClientAuthenticationMethod.PRIVATE_KEY_JWT;
+ ValidateEndpointAuthentication action =
+ constructAction(new ListMethodsFunction(method));
+ final SignedJWT jwt = createPrivateKeyJWT(validClaimsSet());
+ final RequestContext requestCtx = initializeRequestCtx(
+ initializeTokenRequest(method, jwt), method, true);
+ Assert.assertNull(action.execute(requestCtx));
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), EventIds.ACCESS_DENIED);
+ }
+
+ 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, 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;
+ }
class ListMethodsFunction extends AbstractRelyingPartyLookupFunction<List<ClientAuthenticationMethod>> {
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list