[java-idp-plugin-duo] branch main updated: JDUO-23 and JDUO-22
Phil Smart
philip.smart at jisc.ac.uk
Fri Dec 4 14:21:20 UTC 2020
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch main
in repository java-idp-plugin-duo.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=fff59cb93f4c4208148312d4766ba7ce988dcfd0
The following commit(s) were added to refs/heads/main by this push:
new fff59cb JDUO-23 and JDUO-22
fff59cb is described below
commit fff59cb93f4c4208148312d4766ba7ce988dcfd0
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Dec 4 14:13:06 2020 +0000
JDUO-23 and JDUO-22
- JDUO-23 - Allow injection of the JWT claims verifier used to validate
the id_token
- JDUO-22 - Add OIDC nonce to the authorisation request
https://issues.shibboleth.net/jira/browse/JDUO-23
https://issues.shibboleth.net/jira/browse/JDUO-22
---
idp-duo-api/pom.xml | 6 +
.../plugin/authn/duo/AbstractDuoOIDCClient.java | 48 +++
.../idp/plugin/authn/duo/DuoOIDCAuthAPI.java | 57 +++-
.../idp/plugin/authn/duo/DuoOIDCClient.java | 29 +-
.../authn/duo/DuoOIDCClientCapabilities.java | 35 +++
.../duo/context/DuoOIDCAuthenticationContext.java | 24 ++
idp-duo-impl/pom.xml | 5 +
.../authn/duo/impl/BasicJWTClaimsVerifier.java | 265 ++++++++++++++++
.../authn/duo/impl/DuoJWTClaimsVerifier.java | 195 ------------
.../authn/duo/impl/DuoOIDCAuthnController.java | 15 +-
.../idp/plugin/authn/duo/impl/DuoSupport.java | 1 +
.../impl/DynamicDuoContextJWTClaimsVerifier.java | 344 +++++++++++++++++++++
.../plugin/authn/duo/impl/ValidateTokenClaims.java | 179 ++---------
.../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml | 22 +-
.../impl/AbstractAuthnXmlFlowExecutionTests.java | 30 +-
.../authn/duo/impl/AbstractDuoActionTest.java | 213 ++++++++++++-
.../authn/duo/impl/BasicJWTClaimsVerifierTest.java | 251 +++++++++++++++
.../authn/duo/impl/DuoOIDCAuthnControllerTest.java | 3 +-
...=> DynamicDuoContextJWTClaimsVerifierTest.java} | 288 +++++++++--------
.../authn/duo/impl/ValidateTokenClaimsTest.java | 271 +++-------------
.../plugin/authn/mock/MockDuoOIDCClient_FAIL.java | 19 +-
.../plugin/authn/mock/MockDuoOIDCClient_OK.java | 19 +-
.../mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java | 19 +-
.../authn/mock/MockDuoOIDCClient_UNKNOWN.java | 19 +-
idp-duo-native-client-impl/pom.xml | 13 +-
.../authn/duo/sdk/impl/DuoSDKClientAdaptor.java | 26 +-
.../authn/duo/sdk/impl/DuoSDKClientFactory.java | 2 -
.../authn/duo/sdk/conf/authn/duo-oidc.properties | 10 +-
idp-duo-nimbus-client-impl/pom.xml | 1 -
.../plugin/authn/duo/nimbus/impl/NimbusClient.java | 19 +-
.../duo/nimbus/conf/authn/duo-oidc.properties | 10 +-
.../authn/duo/nimbus/impl/NimbusClientTest.java | 3 +-
pom.xml | 33 +-
33 files changed, 1683 insertions(+), 791 deletions(-)
diff --git a/idp-duo-api/pom.xml b/idp-duo-api/pom.xml
index 4c9e9a6..507f229 100644
--- a/idp-duo-api/pom.xml
+++ b/idp-duo-api/pom.xml
@@ -19,6 +19,12 @@
<dependencies>
+ <!-- Compile time dependencies -->
+ <dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ <scope>compile</scope>
+ </dependency>
<!-- Provided dependencies -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AbstractDuoOIDCClient.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AbstractDuoOIDCClient.java
new file mode 100644
index 0000000..011bf1e
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AbstractDuoOIDCClient.java
@@ -0,0 +1,48 @@
+/*
+ * 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.authn.duo;
+
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * Abstract base class for {@link DuoOIDCClient} implementations. Handles the clientId and
+ * retrieval of the client's capabilities.
+ */
+public abstract class AbstractDuoOIDCClient implements DuoOIDCClient{
+
+ /** The client instance UUID for identification.*/
+ @Nonnull @NotEmpty private final String clientId;
+
+ /** Constructor.*/
+ public AbstractDuoOIDCClient() {
+ clientId = UUID.randomUUID().toString();
+ }
+
+ @Override @Nonnull public String getClientId() {
+ return clientId;
+ }
+
+ @Override @Nonnull public DuoOIDCClientCapabilities getCapabilities() {
+ return this;
+ }
+
+}
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
index 745b068..e0d6f7d 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
@@ -20,6 +20,8 @@ package net.shibboleth.idp.plugin.authn.duo;
import javax.annotation.Nonnull;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
/**
* Constants defined in the Duo OIDC Auth API.
@@ -54,8 +56,59 @@ public final class DuoOIDCAuthAPI {
/** The name of the JSON result status message property.*/
@Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_STATUS_MSG_JSON_OBJECT = "status_msg";
- /** The name of the authentication time claim.*/
- @Nonnull @NotEmpty public static final String DUO_AUTH_TIME_CLAIM_NAME = "auth_time";
+ //TODO MOVE these to a registered OIDC claims set in commons?
+
+ //There are others.
+ /** Core ID Token Claims, see OpenID Connect core 1.0 section 2.*/
+ @Nonnull public enum OIDC_ID_TOKEN_CLAIMS{
+
+ /**Issuer Identifier for the Issuer of the response.*/
+ ISSUER_CLAIM ("iss"),
+
+ /** Subject Identifier. A locally unique and never reassigned identifier within the
+ * Issuer for the End-User.*/
+ SUBJECT_CLAIM ("sub"),
+
+ /** Audience(s) that this ID Token is intended for.*/
+ AUDIENCE_CLAIM ("aud"),
+
+ /** Expiration time on or after which the ID Token MUST NOT be accepted for processing.*/
+ EXPIRATION_TIME_CLAIM ("exp"),
+
+ /**The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing.*/
+ NOT_BEFORE_CLAIM ("nbf"),
+
+ /** Time at which the JWT was issued.*/
+ ISSUED_AT_CLAIM ("iat"),
+
+ /** Time when the End-User authentication occurred.*/
+ AUTHENTICATION_TIME("auth_time"),
+
+ /** String value used to associate a Client session with an ID Token, and to mitigate replay attacks.*/
+ NONCE("nonce"),
+
+ /** Authentication Context Class Reference.*/
+ AUTHENTICATION_CONTEXT_CLASS_REFERENCE("acr"),
+
+ /** Authentication Methods References.*/
+ AUTHENTICATION_METHODS_REFERENCES("amr"),
+
+ /** Authorized party - the party to which the ID Token was issued.*/
+ AUTHORIZED_PARTY("azp"),
+
+ /** The "jti" (JWT ID) claim provides a unique identifier for the JWT.*/
+ JWT_ID_CLAIM ("jti");
+
+ @Nonnull @NotEmpty private final String claimName;
+
+ private OIDC_ID_TOKEN_CLAIMS(@Nonnull @NotEmpty final String name) {
+ claimName = Constraint.isNotNull(StringSupport.trimOrNull(name), "Claim name can not be null or empt");
+ }
+
+ @Nonnull public String getClaimName() {
+ return claimName;
+ }
+ }
/** Constructor. */
private DuoOIDCAuthAPI() {
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClient.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClient.java
index 056a647..96dc7c9 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClient.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClient.java
@@ -18,15 +18,24 @@
package net.shibboleth.idp.plugin.authn.duo;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import com.nimbusds.jwt.JWT;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
/**
* A client for handling Duo OIDC 2FA interactions.
*/
-public interface DuoOIDCClient {
+public interface DuoOIDCClient extends DuoOIDCClientCapabilities{
+
+ /**
+ * Returns the capabilities of the underlying client.
+ *
+ * @return interface to access the service's capabilities
+ */
+ @Nonnull DuoOIDCClientCapabilities getCapabilities();
/**
* Check the health of the Duo 2FA endpoint.
@@ -38,16 +47,22 @@ public interface DuoOIDCClient {
@Nonnull DuoHealthCheck healthCheck() throws DuoClientException;
/**
- * Constructs a redirection URL string with the query parameters required to initiate a Duo 2FA request.
+ * Constructs an authorization redirection URL string with the query parameters required to initiate
+ * a Duo 2FA request.
*
- * @param username The user to be authenticated by Duo.
- * @param state A randomly generated minimum 22 character String, which is relayed back to the client.
+ * @param username The user to be authenticated by Duo, never {@code null}.
+ * @param state A randomly generated minimum 22 character String, which is relayed back to the client,
+ * never {@code null}.
+ * @param nonce a randomly generated (minimum 22 character) cryptographically secure nonce that is
+ * replayed in the id_token. Can be {@code null} if not supported by the client as indicated
+ * by the clients described capabilities.
*
* @return the redirect URL as a string, never {@code null}.
*
* @throws DuoClientException if there is an error creating the authentication URL.
*/
- @Nonnull String createAuthUrl(@Nonnull final String username, @Nonnull final String state) throws DuoClientException;
+ @Nonnull String createAuthUrl(@Nonnull @NotEmpty final String username, @Nonnull @NotEmpty final String state,
+ @Nullable final String nonce) throws DuoClientException;
/**
* Exchanges the authorizaton code for a signed Json Web Token (JWT) which contains information pertaining to
@@ -56,8 +71,8 @@ public interface DuoOIDCClient {
*
* @param code An authentication identifier which is exchanged (per OAuth2.0 spec) with Duo for a token.
* the token can be used to determine if authentication was successful as well as obtain meta-data
- * about the authentication.
- * @param username The user to be authenticated by Duo.
+ * about the authentication, never {@code null}.
+ * @param username The user to be authenticated by Duo, never {@code null}.
*
* @return the **signed** JWT, never {@code null}.
*
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClientCapabilities.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClientCapabilities.java
new file mode 100644
index 0000000..9f13ac1
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCClientCapabilities.java
@@ -0,0 +1,35 @@
+/*
+ * 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.authn.duo;
+
+/**
+ * Exposes capabilities of a {@link DuoOIDCClient}.
+ */
+public interface DuoOIDCClientCapabilities {
+
+ /**
+ * <p>Does this client support the OIDC nonce parameter.</p>
+ *
+ * <p>If the client does support a nonce, it <b>must</b> be included by the client in the authorisation request URL,
+ * where it should then be returned by the OP in the id_token as part of the 2FA result.</p>
+ *
+ * @return true iff the client supports the nonce parameter, false otherwise.
+ */
+ boolean isSupportsNonce();
+
+}
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
index 2522d0f..d9bc15b 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
@@ -49,6 +49,9 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
/** A randomly generated 32 character minimum String returned in the Duo 2FA authorization response.*/
@Nullable private String responseState;
+ /** String value used to associate a Client session with an ID Token, and to mitigate replay attacks.*/
+ @Nullable private String nonce;
+
/** The authorization code return from the Duo authorization request.*/
@Nullable private String authCode;
@@ -63,6 +66,27 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
}
+ /**
+ * Get the nonce.
+ *
+ * @return the nonce.
+ */
+ @Nullable public String getNonce() {
+ return nonce;
+ }
+
+ /**
+ * Set the OIDC nonce.
+ *
+ * @param oidcNonce the nonce.
+ *
+ * @return this context.
+ */
+ @Nonnull public DuoOIDCAuthenticationContext setNonce(@Nonnull final String oidcNonce) {
+ nonce = oidcNonce;
+ return this;
+ }
+
/**
* Get the authorization code.
*
diff --git a/idp-duo-impl/pom.xml b/idp-duo-impl/pom.xml
index 6df0189..12f8808 100644
--- a/idp-duo-impl/pom.xml
+++ b/idp-duo-impl/pom.xml
@@ -25,6 +25,11 @@
<groupId>net.shibboleth.idp.plugin</groupId>
<artifactId>idp-plugin-duo-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ <scope>compile</scope>
+ </dependency>
<!-- Provided dependencies -->
<dependency>
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifier.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifier.java
new file mode 100644
index 0000000..f5e15b4
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifier.java
@@ -0,0 +1,265 @@
+
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * <p>
+ * {@link JWTClaimsSetVerifier JWT claims verifier} implementation that only checks statically configurable claims.
+ * That is, not those that are determined at runtime (for instance the issuer and audience), as these are taken
+ * from the Security Context - see the {@link DynamicDuoContextJWTClaimsVerifier}.
+ * </p>
+ * <p>
+ * This class is thread-safe once it has been initialised.
+ * </p>
+ *
+ * <p>
+ * Performs the following checks:
+ * <ol>
+ * <li>Checks all required claims are present.</li>
+ * <li>Checks prohibited claims are not present.</li>
+ * <li>If an issued-at time (iat) claim is present, rejects tokens that were issued (iat) to far away from
+ * the current time. See section 3.1.3.7 of OpenID Connect core 1.0.</li>
+ * <li>If an expiration time (exp) claim is present, makes sure it is ahead of the current time, else the JWT claims set
+ * is rejected. A few minutes of {@code clockSkew} is allowed. See section 3.1.3.7 of OpenID Connect core 1.0.</li>
+ * <li>If a not-before-time (nbf) claim is present, makes sure it is before the current time, else the JWT claims set is
+ * rejected.</li>
+ * </ol>
+ * <p>To make JWT verification OIDC v1.0 complaint for verifying id_tokens, the set of required claims needs to contain
+ * {'iss','sub','aud','exp','iat'}. This verifier does not check 'auth_time', which maybe required if requested
+ * or 'max_age' was set.
+ * </p>
+ * <p>
+ * This class may be extended to perform additional checks.
+ * </p>
+ *
+ * @param <ContextType> the {@link SecurityContext} type.
+ */
+ at ThreadSafeAfterInit
+public class BasicJWTClaimsVerifier<ContextType extends SecurityContext> extends AbstractInitializableComponent
+ implements JWTClaimsSetVerifier<ContextType> {
+
+ /**
+ * Positive clock skew adjustment to consider when checking JWT not before and expiration
+ * (Default value: 60 seconds).
+ */
+ @Nonnull private Duration clockSkew;
+
+ /**
+ * 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 names of the JWT claims that must be present, empty set if none.
+ */
+ @Nonnull @NonnullElements private Set<String> requiredClaims;
+
+ /**
+ * The names of the JWT claims that must not be present, empty set if none.
+ */
+ @Nonnull @NonnullElements private Set<String> prohibitedClaims;
+
+
+ /** Constructor. */
+ public BasicJWTClaimsVerifier() {
+ clockSkew = Duration.ofSeconds(60);
+ iatWindow = Duration.ofSeconds(60);
+ requiredClaims = Collections.emptySet();
+ prohibitedClaims = Collections.emptySet();
+ }
+
+ /**
+ * 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");
+ }
+
+ /**
+ * 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 required claims.
+ *
+ * @param claims the required claims.
+ */
+ public void setRequiredClaims(@Nullable final Set<String> claims) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ if (claims !=null) {
+ requiredClaims = Set.copyOf(StringSupport.normalizeStringCollection(claims));
+ } else {
+ requiredClaims = Collections.emptySet();
+ }
+ }
+
+ /**
+ * Set the prohibited claims.
+ *
+ * @param claims the prohibited claims.
+ */
+ public void setProhibitedClaims(@Nullable final Set<String> claims) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ if (claims !=null) {
+ prohibitedClaims = Set.copyOf(StringSupport.normalizeStringCollection(claims));
+ } else {
+ prohibitedClaims = Collections.emptySet();
+ }
+ }
+
+ @Override
+ public void verify(final JWTClaimsSet claimsSet, final ContextType context)
+ throws BadJWTException {
+
+ verifyRequiredClaims(claimsSet);
+ verifyProhibitedClaims(claimsSet);
+ verifyNotBefore(claimsSet);
+ verifyExpiration(claimsSet);
+ verifyIat(claimsSet);
+
+ }
+
+ /**
+ * Verify the claims set contains the given, required, claims.
+ *
+ * @param claimsSet the claims set.
+ *
+ * @throws BadJWTException if a required claim is not present.
+ */
+ private void verifyRequiredClaims(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+
+ if (!claimsSet.getClaims().keySet().containsAll(requiredClaims)) {
+ final Set<String> missingClaims = new HashSet<>(requiredClaims);
+ missingClaims.removeAll(claimsSet.getClaims().keySet());
+ throw new BadJWTException("JWT missing required claims: " + missingClaims);
+ }
+ }
+
+ /**
+ * Verify the claims set does not contain the given, prohibited, claims.
+ *
+ * @param claimsSet the claims set.
+ *
+ * @throws BadJWTException if a prohibited claim is present.
+ */
+ private void verifyProhibitedClaims(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+
+ final Set<String> presentProhibitedClaims = new HashSet<>();
+ for (final String prohibited : prohibitedClaims) {
+ if (claimsSet.getClaims().containsKey(prohibited)) {
+ presentProhibitedClaims.add(prohibited);
+ }
+ if (!presentProhibitedClaims.isEmpty()) {
+ throw new BadJWTException("JWT has prohibited claims: " + presentProhibitedClaims);
+ }
+ }
+ }
+
+ /**
+ * If present, verifies the not-before-time has passed.
+ *
+ * @param claimsSet the claims set.
+ *
+ * @throws BadJWTException if the not-before-time is in the future.
+ */
+ private void verifyNotBefore(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+
+ final Instant now = Instant.now();
+ final Date nbf = claimsSet.getNotBeforeTime();
+ if (nbf != null) {
+ final Instant nbfInstant = nbf.toInstant();
+ final Instant nbfInstantMinusSkew = nbfInstant.minus(clockSkew);
+ if (!nbfInstantMinusSkew.isBefore(now)) {
+ throw new BadJWTException("JWT before use time");
+ }
+ }
+
+ }
+
+ /**
+ * If present, verifies the expiration time has not passed (including the given clock skew).
+ *
+ * @param claimsSet the claims set.
+ *
+ * @throws BadJWTException if the token has expired.
+ */
+ private void verifyExpiration(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+
+ final Instant now = Instant.now();
+
+ final Date exp = claimsSet.getExpirationTime();
+ if (exp != null) {
+ final Instant expInstant = exp.toInstant();
+ final Instant expirationPlusSkew = expInstant.plus(clockSkew);
+
+ if (now.isAfter(expirationPlusSkew)) {
+ throw new BadJWTException("Expired JWT");
+ }
+ }
+ }
+
+ /**
+ * If present, verifies the IssuedAt claim exists and is within a specified window from the current time.
+ *
+ * @param claimsSet the claims set.
+ *
+ * @throws BadJWTException if the IssuedAt claim is invalid.
+ */
+ private void verifyIat(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
+
+ final Date iatDate = claimsSet.getIssueTime();
+ if (iatDate != null) {
+ final Instant iat = iatDate.toInstant();
+ final Instant now = Instant.now();
+ final Duration iatDifference = Duration.between(now, iat).abs();
+
+ if (iatWindow.compareTo(iatDifference) < 0) {
+ throw new BadJWTException("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 "
+ + "'"+iatWindow+"', difference is '"+iatDifference+"'");
+ }
+ }
+
+ }
+
+
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoJWTClaimsVerifier.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoJWTClaimsVerifier.java
deleted file mode 100644
index 56641c5..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoJWTClaimsVerifier.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * 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.authn.duo.impl;
-
-import java.text.ParseException;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Date;
-import java.util.Set;
-
-import javax.annotation.Nonnull;
-import javax.annotation.concurrent.ThreadSafe;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.proc.BadJWTException;
-import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
-
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- * Extension of the {@link DefaultJWTClaimsVerifier} that also checks:
- * <ol>
- * <li>The IssuedAt claim exists, and is within a specified window from the current time.</li>
- * <li>If the auth_time (when the End-User authentication took place) claim
- * is within a valid expiration window. Only applies when forced authentication is requested.</li>
- * </ol>
- */
- at ThreadSafe
-public class DuoJWTClaimsVerifier extends DefaultJWTClaimsVerifier<ProfileRequestJWTSecurityContext>{
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(DuoJWTClaimsVerifier.class);
-
- /**
- * Maximum amount (in either direction from now) of duration in seconds for which a token is valid after
- * it is issued (Default value: 180 seconds).
- */
- @Nonnull private int iatWindow;
-
- /**
- * If forced authentication, amount of time in seconds for which a token is valid
- * after if it was issued. (Default value: 60 seconds)
- */
- @Nonnull private int authnLifetime;
-
- /**
- * Creates new Duo Specific default JWT claims verifier.
- *
- * @param requiredAudience The required JWT audience, {@code null} if
- * not specified.
- * @param exactMatchClaims The JWT claims that must match exactly,
- * {@code null} if none.
- * @param requiredClaims The names of the JWT claims that must be
- * present, empty set or {@code null} if none.
- */
- public DuoJWTClaimsVerifier(final String requiredAudience,
- final JWTClaimsSet exactMatchClaims,
- final Set<String> requiredClaims) {
-
- super(requiredAudience,exactMatchClaims,requiredClaims);
- iatWindow = 180;
- authnLifetime = 60;
- }
-
- /**
- * Sets the amount of time in seconds for which a token is valid.
- *
- * @param window window of time in seconds for which a token is valid
- */
- public void setIatWindow(@Nonnull final int window) {
- iatWindow = Constraint.isNotNull(window, "Token issued at window cannot be null");
- }
-
- /**
- * Sets the amount of time for which a token is valid.
- *
- * @param lifetime amount of time for which a token is valid
- */
- public void setAuthnLifetime(@Nonnull final int lifetime) {
- authnLifetime = Constraint.isNotNull(lifetime, "Token authentication lifetime cannot be null");
- }
-
- @Override
- public void verify(@Nonnull final JWTClaimsSet claimsSet, @Nonnull final ProfileRequestJWTSecurityContext context)
- throws BadJWTException {
- if (context == null) {
- throw new BadJWTException("Duo claims verifier requires the ProfileRequest security context");
- }
- super.verify(claimsSet,context);
- verifyIat(claimsSet);
- verifyAuthenticationTime(claimsSet,context);
- }
-
- /**
- * Verifies if the auth_time (when the End-User authentication took place) is within a
- * valid expiration window. Only applies to forced authentications.
- *
- * @param claimsSet the JWT claimset.
- * @param context the profile request security context.
- *
- * @throws BadJWTException if the auth_time is invalid.
- */
- private void verifyAuthenticationTime(@Nonnull final JWTClaimsSet claimsSet,
- @Nonnull final ProfileRequestJWTSecurityContext context) throws BadJWTException {
-
- final AuthenticationContext ac = context.getPrc().getSubcontext(AuthenticationContext.class);
- if (ac == null) {
- throw new BadJWTException("No authentication request context found, can not validate authentication time");
- }
-
- if (!ac.isForceAuthn()) {
- //no forced authn, so do not validate authentication time
- return;
- } else {
- //forced authn, so check authentication time
- try {
- final Date authTimeDate = claimsSet.getDateClaim(DuoOIDCAuthAPI.DUO_AUTH_TIME_CLAIM_NAME);
- if (authTimeDate == null) {
- throw new BadJWTException("No authentication time found in token");
- }
- final Instant authTime = authTimeDate.toInstant();
- final Instant now = Instant.now();
- final Instant expiration = authTime.plus(Duration.ofSeconds(authnLifetime));
-
- // Check time of authentication wasn't in the future
- if (authTime.isAfter(now)) {
- log.warn("Authentication forced but is not yet valid: auth_time was {}, latest valid is: {}",
- authTime, now);
- throw new BadJWTException("JWT token authentication time is not yet valid");
- }
-
- // Check time of authentication has not expired
- if (expiration.isBefore(now)) {
- log.warn(
- "Authentication required (forced) but has expired: auth_time was '{}', "
- + "expired at: '{}', current time: '{}'",
- authTime, expiration, now);
- throw new BadJWTException("JWT token authentication time has expired");
- }
- //is OK.
-
- } catch (final ParseException e) {
- throw new BadJWTException("Autentication forced, but no authentication time found in token",e);
- }
- }
- }
-
- /**
- * Verifies the IssuedAt claim exists and is within a specified window from the current time.
- *
- * @param claimsSet the claimset.
- *
- * @throws BadJWTException if the IssuedAt claim is invalid.
- */
- private void verifyIat(@Nonnull final JWTClaimsSet claimsSet) throws BadJWTException {
-
- final Date iatDate = claimsSet.getIssueTime();
- if (iatDate == null) {
- throw new BadJWTException("JWT issued-at time missing");
- }
- final Instant iat = iatDate.toInstant();
- final Instant now = Instant.now();
- final Duration iatDifference = Duration.between(now, iat).abs();
- final Duration window = Duration.ofSeconds(iatWindow);
-
- if (window.compareTo(iatDifference) < 0) {
- log.error("Token issued at '{}' was too far away from the current time '{}' with acceptable "
- + " deviation of '{}', difference is '{}'",
- iat, now, window, iatDifference);
- throw new BadJWTException("JWT issued-at time is too far away from the current time");
- }
-
- }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
index 579c17a..2c70cd9 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
@@ -127,8 +127,19 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
final String nonce = DuoSupport.generateNonce(32);
final String state = DuoSupport.generateState(nonce, key);
//store only the nonce component as the request state. The key component is only used to resume the flow.
- duoContext.setRequestState(nonce);
- final String authURL = client.createAuthUrl(duoContext.getUsername(), state);
+ duoContext.setRequestState(nonce);
+
+ String authURL;
+ //if an OIDC id_token nonce is supported, add it to the authz request
+ if (client.getCapabilities().isSupportsNonce()) {
+ final String oidcNonce = DuoSupport.generateNonce(36);
+ authURL = client.createAuthUrl(duoContext.getUsername(), state, oidcNonce);
+ duoContext.setNonce(oidcNonce);
+
+ } else {
+ authURL = client.createAuthUrl(duoContext.getUsername(), state, null);
+ }
+
httpResponse.sendRedirect(authURL);
} catch (final DuoClientException e) {
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
index 0229bbd..774e006 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
@@ -37,6 +37,7 @@ public final class DuoSupport {
private DuoSupport() {
}
+
/**
* Generates a random identifier to be used as a nonce.
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifier.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifier.java
new file mode 100644
index 0000000..464ba87
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifier.java
@@ -0,0 +1,344 @@
+/*
+ * 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.authn.duo.impl;
+
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+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.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * <p>
+ * {@link JWTClaimsSetVerifier JWT claims verifier} implementation. Extends the {@link BasicJWTClaimsVerifier}
+ * to include claim checks constructed from values inside the {@link DuoOIDCIntegration}
+ * stashed in the {@link SecurityContext} at runtime, namely; the issuer and audience.
+ * </p>
+ * <p>
+ * This class is thread-safe once it has been initialised.
+ * </p>
+ *
+ * <p>
+ * In addition to the verification checks in the {@link BasicJWTClaimsVerifier}, verifies:
+ <ul>
+ * <li>The Audience (aud) claim contains the client_id of this client (as registered at the issuer). See
+ * section 3.1.3.7 of OpenID Connect core 1.0.</li>
+ * <li>The auth_time (when the End-User authentication took place) is within a valid expiration window. Only
+ * for forced authentications.</li>
+ * <li>The issuer (iss) of the token exactly matches that of the configured Duo token provider. See section
+ * 3.1.3.7 of OpenID Connect core 1.0.</li>
+ * <li>The preferred username standard claim in the token matches the user who is currently authenticating</li>
+ * <li>If present, the nonce matches that sent in the authorization request.</li>
+ * </ul>
+ *
+ * <p>
+ * This class may be extended to perform additional checks.
+ * </p>
+ */
+ at ThreadSafeAfterInit
+public class DynamicDuoContextJWTClaimsVerifier extends BasicJWTClaimsVerifier<ProfileRequestJWTSecurityContext> {
+
+ /** HTTPS scheme protocol.*/
+ @Nonnull @NotEmpty public static final String HTTPS = "https://";
+
+ /** The default issuer path, specific to the v1 Duo flow.*/
+ @Nonnull @NotEmpty public static final String DEFAULT_ISSUER_PATH = "/oauth/v1/token";
+
+ /** The default name of the username claim (an OIDC standard claims).*/
+ @Nonnull @NotEmpty public static final String DEFAULT_USERNAME_CLAIM = "preferred_username";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DynamicDuoContextJWTClaimsVerifier.class);
+
+ /**
+ * If forced authentication, amount of time for which a token is valid
+ * after if it was issued. (Default value: 60 seconds)
+ */
+ @Nonnull private Duration authnLifetime;
+
+ /** The URL path component of the issuer.*/
+ @Nonnull @NotEmpty private String issuerPath;
+
+ /** The name of the username claim.*/
+ @Nonnull @NotEmpty private String usernameClaim;
+
+ /** Lookup strategy to locate the Duo authentication context. */
+ @Nonnull
+ private Function<ProfileRequestContext, DuoOIDCAuthenticationContext> duoContextLookupStrategy;
+
+ /** Lookup strategy to locate the authentication context. */
+ @Nonnull
+ private Function<ProfileRequestContext, AuthenticationContext> authenticationContextLookupStrategy;
+
+ /** Constructor.*/
+ public DynamicDuoContextJWTClaimsVerifier() {
+ super();
+ // prc -> ac -> dc
+ duoContextLookupStrategy = new ChildContextLookup<>(DuoOIDCAuthenticationContext.class)
+ .compose(new ChildContextLookup<>(AuthenticationContext.class));
+ // prc -> ac
+ authenticationContextLookupStrategy = new ChildContextLookup<>(AuthenticationContext.class);
+ authnLifetime = Duration.ofSeconds(60);
+ issuerPath = DEFAULT_ISSUER_PATH;
+ usernameClaim = DEFAULT_USERNAME_CLAIM;
+ }
+
+ /**
+ * Set Duo authentication context lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setDuoContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,DuoOIDCAuthenticationContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ duoContextLookupStrategy = Constraint.isNotNull(strategy, "DuoContextLookup strategy cannot be null");
+ }
+
+ /**
+ * Set Duo authentication context lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setAuthenticationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,AuthenticationContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ authenticationContextLookupStrategy = Constraint.isNotNull(strategy,
+ "AuthenticationContextLookup strategy cannot be null");
+ }
+
+ /**
+ * Sets the amount of time for which a token is valid from when the original authentication took place.
+ * Only applies to forced authentications.
+ *
+ * @param lifetime amount of time for which a token is valid
+ */
+ public void setAuthnLifetime(@Nonnull final Duration lifetime) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ Constraint.isNotNull(lifetime, "Token authentication lifetime cannot be null");
+ Constraint.isFalse(lifetime.isNegative(), "Token authentication lifetime cannot be negative");
+
+ authnLifetime = lifetime;
+ }
+
+ /**
+ * Sets the issuer URL path component.
+ *
+ * @param path the issuer path
+ */
+ public void setIssuerPath(@Nonnull @NotEmpty final String path) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ issuerPath = Constraint.isNotEmpty(path, "Issuer URL path cannot be null or empty");
+ }
+
+ /**
+ * Sets the name of the username claim.
+ *
+ * @param claimName the username claim name
+ */
+ public void setUsernameClaim(@Nonnull @NotEmpty final String claimName) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ usernameClaim = Constraint.isNotEmpty(claimName, "Username claim name can not be null or empty");
+ }
+
+
+ @Override
+ public void verify(@Nonnull final JWTClaimsSet claimsSet, @Nonnull final ProfileRequestJWTSecurityContext context)
+ throws BadJWTException {
+
+ //do basic checks first.
+ super.verify(claimsSet,context);
+
+ //when used within the validation action, it is unlikely these could ever be null.
+ if (context == null) {
+ throw new BadJWTException("Duo claims verifier requires the ProfileRequest security context");
+ }
+
+ final AuthenticationContext authnContext = authenticationContextLookupStrategy.apply(context.getPrc());
+ if (authnContext == null) {
+ throw new BadJWTException("No authentication context returned by lookup strategy, audience and issuer"
+ + " could not be verified");
+ }
+
+ final DuoOIDCAuthenticationContext duoContext = duoContextLookupStrategy.apply(context.getPrc());
+ if (duoContext == null) {
+ throw new BadJWTException("No Duo context returned by lookup strategy, audience and issuer"
+ + " could not be verified");
+ }
+
+ final DuoOIDCIntegration duoIntegration = duoContext.getIntegration();
+ if (duoIntegration == null) {
+ throw new BadJWTException("No Duo integration contained within the Duo context");
+ }
+
+ verifyAudience(claimsSet,duoIntegration);
+ verifyAuthenticationTime(claimsSet,authnContext);
+
+ //if the nonce exists in the id_token it must be validated alongside the issuer and username.
+ if (claimsSet.getClaim(DuoOIDCAuthAPI.OIDC_ID_TOKEN_CLAIMS.NONCE.getClaimName()) != null) {
+ //verify with nonce if present
+ verifyExactMatchClaims(claimsSet, new JWTClaimsSet.Builder()
+ .issuer(HTTPS+duoContext.getIntegration().getAPIHost()+issuerPath)
+ .claim(usernameClaim, duoContext.getUsername())
+ .claim(DuoOIDCAuthAPI.OIDC_ID_TOKEN_CLAIMS.NONCE.getClaimName(),duoContext.getNonce())
+ .build());
+ } else {
+ //verify without a nonce, only issuer and username
+ verifyExactMatchClaims(claimsSet, new JWTClaimsSet.Builder()
+ .issuer(HTTPS+duoContext.getIntegration().getAPIHost()+issuerPath)
+ .claim(usernameClaim, duoContext.getUsername())
+ .build());
+ }
+ }
+
+ /**
+ * Verify the claims in <code>exactMatchClaims</code> exactly match (by Object equality) those
+ * in the <code>claimsSet</code>.
+ *
+ * @param claimsSet the JWT claimset.
+ * @param exactMatchClaims the claims to match exactly.
+ *
+ * @throws BadJWTException if the claim does not exist or does not match the given claim value.
+ */
+ private void verifyExactMatchClaims(@Nonnull final JWTClaimsSet claimsSet,
+ @Nonnull final JWTClaimsSet exactMatchClaims) throws BadJWTException {
+
+ for (final String exactMatch: exactMatchClaims.getClaims().keySet()) {
+ final Object actualClaim = claimsSet.getClaim(exactMatch);
+ if (actualClaim == null) {
+ throw new BadJWTException("Claim '"+exactMatch+"' does not exist");
+ }
+ final Object expectedClaim = exactMatchClaims.getClaim(exactMatch);
+ if (! actualClaim.equals(expectedClaim)) {
+ throw new BadJWTException("JWT \"" + exactMatch + "\" claim has value "
+ + actualClaim + " but should be " + expectedClaim);
+ }
+ }
+
+ }
+
+ /**
+ * Verify one of the audience values in the 'aud' claim matches with the Duo clientId.
+ *
+ * @param claimsSet the JWT claimset.
+ * @param duoIntegration the Duo integration information.
+ *
+ * @throws BadJWTException if the accepted audience (Duo ClientId) can not be found in
+ * the JWT's audience list.
+ */
+ private void verifyAudience(@Nonnull final JWTClaimsSet claimsSet,
+ @Nonnull final DuoOIDCIntegration duoIntegration) throws BadJWTException {
+
+ // Check audience based on that specified in the duo integration
+ final String acceptedAudience = duoIntegration.getClientId();
+
+ final List<String> audList = claimsSet.getAudience();
+ if (audList != null && !audList.isEmpty()) {
+ boolean audMatch = false;
+ for (final String aud : audList) {
+ if (acceptedAudience.equals(aud)) {
+ audMatch = true;
+ break;
+ }
+ }
+ if (!audMatch) {
+ throw new BadJWTException("JWT audience rejected: " + audList);
+ }
+ } else {
+ throw new BadJWTException("JWT missing required audience");
+ }
+
+ }
+
+ /**
+ * Verifies if the auth_time (when the End-User authentication took place) is within a
+ * valid expiration window. Only applies to forced authentications.
+ *
+ * @param claimsSet the JWT claimset.
+ * @param ac the authentication context.
+ *
+ * @throws BadJWTException if the auth_time is invalid.
+ */
+ private void verifyAuthenticationTime(@Nonnull final JWTClaimsSet claimsSet,
+ @Nonnull final AuthenticationContext ac) throws BadJWTException {
+
+ if (!ac.isForceAuthn()) {
+ //no forced authn, so do not validate authentication time
+ return;
+ } else {
+ //forced authn, so check authentication time
+ try {
+ final Date authTimeDate = claimsSet.getDateClaim(DuoOIDCAuthAPI.OIDC_ID_TOKEN_CLAIMS.
+ AUTHENTICATION_TIME.getClaimName());
+ if (authTimeDate == null) {
+ throw new BadJWTException("No authentication time found in token");
+ }
+ final Instant authTime = authTimeDate.toInstant();
+ final Instant now = Instant.now();
+ final Instant expiration = authTime.plus(authnLifetime);
+
+ // Check time of authentication wasn't in the future
+ if (authTime.isAfter(now)) {
+ log.warn("Authentication forced but is not yet valid: auth_time was {}, latest valid is: {}",
+ authTime, now);
+ throw new BadJWTException("JWT token authentication time is not yet valid");
+ }
+
+ // Check time of authentication has not expired
+ if (expiration.isBefore(now)) {
+ log.warn(
+ "Authentication required (forced) but has expired: auth_time was '{}', "
+ + "expired at: '{}', current time: '{}'",
+ authTime, expiration, now);
+ throw new BadJWTException("JWT token authentication time has expired");
+ }
+ //is OK.
+
+ } catch (final ParseException e) {
+ throw new BadJWTException("Autentication forced, but no authentication time found in token",e);
+ }
+ }
+ }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaims.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaims.java
index 48a78f5..8e14b4d 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaims.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaims.java
@@ -18,9 +18,6 @@
package net.shibboleth.idp.plugin.authn.duo.impl;
import java.text.ParseException;
-import java.time.Duration;
-import java.util.HashSet;
-import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -33,33 +30,23 @@ import org.slf4j.LoggerFactory;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
import net.shibboleth.idp.plugin.authn.duo.DuoException;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+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.logic.ConstraintViolationException;
/**
- * Action that validates the claims of the Duo id_token. More specifically:
- * <ul>
- * <li>Verifies the Audience (aud) claim contains the client_id of this client (as registered at the issuer). See
- * section 3.1.3.7 of OpenID Connect core 1.0.</li>
- * <li>Verifies if the auth_time (when the End-User authentication took place) is within a valid expiration window. Only
- * for forced authentications.</li>
- * <li>Verifies the expiration time (exp). If the expiration time has past, the token must not be accepted. A few
- * minutes of {@code clockSkew} is allowed. See section 3.1.3.7 of OpenID Connect core 1.0.</li>
- * <li>Rejects tokens that were issued (iat) to far away from the current time. See section 3.1.3.7 of OpenID Connect
- * core 1.0.</li>
- * <li>Verifies the issuer (iss) of the token exactly matches that of the configured Duo token provider. See section
- * 3.1.3.7 of OpenID Connect core 1.0.</li>
- * <li>Verifies the Subject (sub) claim in the token matches the user who is currently authenticating</li>
- * </ul>
+ * Action that validates the claims of the Duo id_token using the supplied {@link JWTClaimsSetVerifier}. The
+ * verifier <b>must</b> be thread-safe and validate the claims set against the OpenID Connect core 1.0
+ * section 3.1.3.7 specification, and those required by Duo. However, the action can not guarantee the
+ * verifier behaves in this way.
*
* @pre
*
@@ -88,17 +75,7 @@ import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
* @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
* @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
*/
-//TODO: maybe the claims checker could be injected here?
public class ValidateTokenClaims extends AbstractDuoAuthenticationAction {
-
- /** HTTPS scheme protocol.*/
- @Nonnull @NotEmpty public static final String HTTPS = "https://";
-
- /** The default issuer path, specific to the v1 Duo flow.*/
- @Nonnull @NotEmpty public static final String DEFAULT_ISSUER_PATH = "/oauth/v1/token";
-
- /** Name of the username claim.*/
- @Nonnull @NotEmpty public static final String USERNAME_CLAIM = "preferred_username";
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(ValidateTokenClaims.class);
@@ -106,106 +83,27 @@ public class ValidateTokenClaims extends AbstractDuoAuthenticationAction {
/** The parsed claimset. */
@Nullable private JWTClaimsSet claimsSet;
- /** The Duo integration appropriate for this request.*/
- @Nullable private DuoOIDCIntegration integration;
-
- /** The URL path component of the issuer.*/
- @Nonnull @NotEmpty private String issuerPath;
-
- /**
- * Positive clock skew adjustment to consider the JWT still acceptable from its expiration
- * in seconds (Default value: 60 seconds).
- */
- @Nonnull private int clockSkew;
-
- /**
- * Maximum amount (in either direction from now) of duration in seconds for which a token is valid after
- * it is issued (Default value: 180 seconds).
- */
- @Nonnull private int iatWindow;
-
- /**
- * Amount of time in seconds (for forced authentication) for which a token is valid after if it was issued.
- * (Default value: 60 seconds)
- */
- @Nonnull private int authnLifetime;
-
- /** Constructor. */
- public ValidateTokenClaims() {
- clockSkew = 60;
- iatWindow = 180;
- authnLifetime = 60;
- issuerPath = DEFAULT_ISSUER_PATH;
- }
+ /** The JWT claims verify use to verify the claimsset.*/
+ @NonnullAfterInit private JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> claimsVerifier;
- /**
- * 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 synchronized 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");
-
- final long windowLong = Constraint.isNotNull(window, "IssuedAt Clock skew cannot be null").getSeconds();
- try {
- iatWindow = Math.toIntExact(windowLong);
- } catch (final ArithmeticException e) {
- log.error("{} IssuedAt window in seconds ({}'s) is larger than max {}'s allowed",
- getLogPrefix(),windowLong,Integer.MAX_VALUE);
- throw new ConstraintViolationException("IssuedAt window in seconds value is too high");
- }
- }
-
- /**
- * Sets the amount of time for which a token is valid from when the original authentication took place.
- * Only applies to forced authentications.
- *
- * @param lifetime amount of time for which a token is valid
- */
- public synchronized void setAuthnLifetime(@Nonnull final Duration lifetime) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- Constraint.isNotNull(lifetime, "Token authentication lifetime cannot be null");
- Constraint.isFalse(lifetime.isNegative(), "Token authentication lifetime cannot be negative");
-
- final long lifetimeLong = Constraint.isNotNull(lifetime,
- "Token authentication lifetime cannot be null").getSeconds();
- try {
- authnLifetime = Math.toIntExact(lifetimeLong);
- } catch (final ArithmeticException e) {
- log.error("{} Token authentication lifetime in seconds ({}'s) is larger than max {}'s allowed",
- getLogPrefix(),lifetimeLong, Integer.MAX_VALUE);
- throw new ConstraintViolationException("Token authentication lifetime value in seconds is too high");
- }
- }
-
- /**
- * Set the clock skew.
- *
- * @param skew clock skew to set
- */
- public synchronized void setClockSkew(@Nonnull final Duration skew) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- final long durationLong = Constraint.isNotNull(skew, "Clock skew cannot be null").getSeconds();
- try {
- clockSkew = Math.toIntExact(durationLong);
- } catch (final ArithmeticException e) {
- log.error("{} Clock skew in seconds ({}'s) is larger than max {}'s allowed",getLogPrefix(),
- durationLong,Integer.MAX_VALUE);
- throw new ConstraintViolationException("Clock skew in seconds is too high");
+ /** {@inheritDoc} */
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (claimsVerifier == null) {
+ throw new ComponentInitializationException("Duo ClaimSet Verifier cannot be null");
}
}
/**
- * Sets the issuer URL path component.
+ * Set the JWT claims verifier to use.
*
- * @param path the issuer path
+ * @param verifier the claims verifier.
*/
- public synchronized void setIssuerPath(@Nonnull final String path) {
+ public synchronized void setClaimsVerifier(
+ @Nonnull final JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> verifier) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- issuerPath = Constraint.isNotNull(path, "Issuer URL path cannot be null");
+ claimsVerifier = Constraint.isNotNull(verifier, "Claims verifier cannot be null");
}
@Override
@@ -229,14 +127,7 @@ public class ValidateTokenClaims extends AbstractDuoAuthenticationAction {
log.error("{} Claimset of Duo 2FA token is not available", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
return false;
- }
- integration = duoContext.getIntegration();
- if (integration == null) {
- log.error("{} Duo integration is not available", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
- return false;
- }
-
+ }
return true;
}
@@ -247,33 +138,11 @@ public class ValidateTokenClaims extends AbstractDuoAuthenticationAction {
@Nonnull final DuoOIDCAuthenticationContext duoContext) {
log.debug("{} Validating token claims for subject '{}'",getLogPrefix(),claimsSet.getSubject());
-
- final String apiHost = duoContext.getIntegration().getAPIHost();
- if (apiHost == null) {
- log.error("{} No Duo integration API host found", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
-
- final DuoJWTClaimsVerifier claimsVerifier = new DuoJWTClaimsVerifier(
- //audience
- duoContext.getIntegration().getClientId(),
-
- //exact match claims
- new JWTClaimsSet.Builder().issuer(HTTPS+apiHost+issuerPath)
- .claim(USERNAME_CLAIM, duoContext.getUsername()).build(),
-
- //required claims, automatically including the exact match above
- new HashSet<>(List.of("exp","sub"))
-
- );
-
- claimsVerifier.setMaxClockSkew(clockSkew);
- claimsVerifier.setIatWindow(iatWindow);
- claimsVerifier.setAuthnLifetime(authnLifetime);
-
+
try {
claimsVerifier.verify(claimsSet, new ProfileRequestJWTSecurityContext(profileRequestContext));
+ //blank the nonce even if not used
+ duoContext.setNonce(null);
} catch (final BadJWTException e) {
log.error("{} Token verification failed for subject '{}'", getLogPrefix(),claimsSet.getSubject(),e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
index f8161f3..e5ad12b 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
@@ -69,7 +69,27 @@
class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenSignature" />
<bean id="ValidateTokenClaims" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenClaims" />
+ class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenClaims"
+ p:claimsVerifier="#{getObject('shibboleth.authn.DuoOIDC.DuoTokenClaimsVerifier') ?: getObject('shibboleth.authn.DuoOIDC.DefaultDuoTokenClaimsVerifier')}"/>
+
+ <bean id="shibboleth.authn.DuoOIDC.DefaultDuoTokenClaimsVerifier"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.DynamicDuoContextJWTClaimsVerifier"
+ p:clockSkew="%{idp.duo.oidc.jwt.verifier.clockSkew:PT60S}"
+ p:iatWindow="%{idp.duo.oidc.jwt.verifier.iatWindow:PT60S}"
+ p:authnLifetime="%{idp.duo.oidc.jwt.verifier.authLifetime:PT60S}"
+ p:issuerPath="%{idp.duo.oidc.jwt.verifier.issuerPath:/oauth/v1/token}"
+ p:usernameClaim="%{idp.duo.oidc.jwt.verifier.preferredUsername:preferred_username}"
+ p:requiredClaims="#{getObject('shibboleth.authn.DuoOIDC.RequiredOIDCClaims') ?: getObject('shibboleth.authn.DuoOIDC.DefaultRequiredOIDCClaims')}"
+ />
+
+ <!-- These represent the default set of id_token claims which are **required** by OIDC -->
+ <util:set id="shibboleth.authn.DuoOIDC.DefaultRequiredOIDCClaims">
+ <value>iss</value>
+ <value>sub</value>
+ <value>aud</value>
+ <value>exp</value>
+ <value>iat</value>
+ </util:set>
<bean id="ExchangeCodeForDuoToken" scope="prototype"
class="net.shibboleth.idp.plugin.authn.duo.impl.ExchangeCodeForDuoToken" />
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
index 8f49e1a..3d12a4a 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
@@ -17,10 +17,12 @@
package net.shibboleth.idp.plugin.authn.duo.impl;
+import java.time.Duration;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -33,10 +35,12 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
-
+import org.springframework.context.support.ConversionServiceFactoryBean;
import org.springframework.context.support.StaticApplicationContext;
+import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.mock.env.MockPropertySource;
@@ -52,6 +56,7 @@ import org.springframework.webflow.test.execution.AbstractExternalizedFlowExecut
import com.google.common.net.HttpHeaders;
+import net.shibboleth.ext.spring.config.StringToDurationConverter;
import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
@@ -65,6 +70,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElemen
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
+import net.shibboleth.utilities.java.support.xml.DOMTypeSupport;
/**
@@ -275,6 +281,28 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends CustomAbstractX
//create a real JSON parser
builderContext.registerBean("shibboleth.JSONObjectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
+
+
+
+ //create converter for Durations
+ ConversionServiceFactoryBean conversationService = new ConversionServiceFactoryBean();
+ //unable to register the spring-ext converter even though it is the same!
+ final Converter<String,Duration> durationConverter = new Converter<String,Duration>(){
+
+ @Override
+ public Duration convert(String source) {
+ if (source.startsWith("P") || source.startsWith("-P")) {
+ return DOMTypeSupport.stringToDuration(source.trim());
+ }
+ // Treat as milliseconds.
+ return Duration.ofMillis(Long.valueOf(source));
+ }
+
+ };
+ conversationService.setConverters(Set.of(durationConverter));
+ conversationService.afterPropertiesSet();
+ builderContext.registerBean("conversionService", conversationService.getObject());
+
//register shibboleth function constants.
addBeanDefinition(builderContext, "shibboleth.Functions.Constant",BeanDefinitionBuilder.
genericBeanDefinition(net.shibboleth.utilities.java.support.logic.FunctionSupport.class)
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
index 9fc31de..83a9a17 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
@@ -105,8 +105,8 @@ public abstract class AbstractDuoActionTest {
* @param exp expiration time.
* @param iat issued at.
* @param authTime the auth time.
- * @param apiHost TODO
- * @param factor TODO
+ * @param apiHost the api host
+ * @param factor the duo factor
* @return the duo auth token.
*/
protected JWT createPlainDummyToken(@Nonnull final String authResult,
@@ -132,6 +132,77 @@ public abstract class AbstractDuoActionTest {
return null;
}
+ /**
+ * Create a dummy Duo plain (no sig or enc) JWT token which is missing the 'exp' and 'aud' claims.
+ *
+ * @param authResult the authentication result e.g. allow.
+ * @param authResultMessage a user friendly result message.
+ * @param iat issued at.
+ * @param authTime the auth time.
+ * @param apiHost the api host
+ * @param factor the duo factor
+ * @return the duo auth token.
+ */
+ protected JWT createPlainDummyToken(@Nonnull final String authResult,
+ @Nonnull final String authResultMessage, @Nonnull final Instant iat,
+ @Nonnull final Instant authTime, @Nonnull final String apiHost,
+ @Nonnull final String factor) {
+
+ final String jwtJson = createJWTJsonMissingAudAndExpClaims(authResult,authResultMessage,
+ iat, authTime, apiHost, factor);
+
+ try {
+
+ final JWT jwt = new PlainJWT(new PlainHeader().toBase64URL(),
+ new Base64URL(Base64Support.encodeURLSafe(jwtJson.getBytes())));
+ //test the claims exist by calling it
+ jwt.getJWTClaimsSet();
+ return jwt;
+ } catch (final EncodingException | ParseException e) {
+ fail("Error creating the Mock JWT",e);
+ }
+ fail();
+ return null;
+ }
+
+ /**
+ * Create a dummy Duo plain (no sig or enc) JWT token.
+ *
+ * @param authResult the authentication result e.g. allow.
+ * @param authResultMessage a user friendly result message.
+ * @param aud audience
+ * @param exp expiration time.
+ * @param iat issued at.
+ * @param authTime the auth time.
+ * @param nbf not before time.
+ * @param apiHost the api host
+ * @param factor the duo factor
+ * @return the duo auth token.
+ */
+ protected JWT createPlainDummyToken(@Nonnull final String authResult,
+ @Nonnull final String authResultMessage, @Nonnull final String aud,
+ @Nonnull final Instant exp, @Nonnull final Instant iat,
+ @Nonnull final Instant authTime, @Nonnull final Instant nbf,
+ @Nonnull final String apiHost,
+ @Nonnull final String factor) {
+
+ final String jwtJson = createJWTJsonWithNbf(authResult,authResultMessage,aud,
+ exp, iat, authTime, nbf, apiHost, factor);
+
+ try {
+
+ final JWT jwt = new PlainJWT(new PlainHeader().toBase64URL(),
+ new Base64URL(Base64Support.encodeURLSafe(jwtJson.getBytes())));
+ //test the claims exist by calling it
+ jwt.getJWTClaimsSet();
+ return jwt;
+ } catch (final EncodingException | ParseException e) {
+ fail("Error creating the Mock JWT",e);
+ }
+ fail();
+ return null;
+ }
+
/**
* Create a signed Duo JWT token.
*
@@ -321,6 +392,144 @@ public abstract class AbstractDuoActionTest {
return jwtJson;
}
+ /**
+ * Create a JWT token in JSON using the claims arguments present, but missing the 'aud' and
+ * 'exp' claims.
+ *
+ * @param authResult the authentication result e.g. allow.
+ * @param authResultMessage a user friendly result message.
+ * @param iat issued at.
+ * @param authTime the auth time.
+ * @param apiHost the api host.
+ * @param factor the factor.
+ * @return the duo auth token.
+ */
+ private String createJWTJsonMissingAudAndExpClaims(@Nonnull final String authResult,
+ @Nonnull final String authResultMessage, @Nonnull final Instant iat,
+ @Nonnull final Instant authTime, @Nonnull final String apiHost,
+ @Nonnull final String factor) {
+ final String jwtJson = "{\n" +
+ " \"iss\": \"https://"+apiHost+"/oauth/v1/token\",\n" +
+ " \"sub\": \"jdoe\",\n" +
+ " \"preferred_username\": \"jdoe\",\n" +
+ " \"iat\": "+(double)iat.getEpochSecond()+",\n" +
+ " \"auth_time\": "+authTime.getEpochSecond()+",\n" +
+ " \"auth_result\": {\n" +
+ " \"status_msg\": \""+authResultMessage+"\",\n" +
+ " \"status\": \""+authResult+"\",\n" +
+ " \"result\": \""+authResult+"\"\n" +
+ " },\n" +
+ " \"auth_context\": {\n" +
+ " \"result\": \"success\",\n" +
+ " \"timestamp\": 1599749128,\n" +
+ " \"auth_device\": {\n" +
+ " \"ip\": \"82.17.89.232\",\n" +
+ " \"name\": \"+44 7852 119881\",\n" +
+ " \"location\": {\n" +
+ " \"state\": \"Wales\",\n" +
+ " \"city\": \"Cardiff\",\n" +
+ " \"country\": \"United Kingdom\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"txid\": \"1684599c-bb16-4250-af85-904291bfe7cc\",\n" +
+ " \"event_type\": \"authentication\",\n" +
+ " \"reason\": \"user_approved\",\n" +
+ " \"access_device\": {\n" +
+ " \"hostname\": null,\n" +
+ " \"ip\": \"82.17.89.232\",\n" +
+ " \"location\": {\n" +
+ " \"state\": \"Wales\",\n" +
+ " \"city\": \"Cardiff\",\n" +
+ " \"country\": \"United Kingdom\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"application\": {\n" +
+ " \"key\": \"DIU6GEFWG5LIUTVV2M3N\",\n" +
+ " \"name\": \"Shibboleth Integration Testing\"\n" +
+ " },\n" +
+ " \"factor\": \""+factor+"\",\n" +
+ " \"user\": {\n" +
+ " \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" +
+ " \"name\": \"philsmart\"\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ return jwtJson;
+ }
+
+ /**
+ * Create a JWT token in JSON using the claims arguments. Includes the
+ * not before claim which is not part of the normal Duo (or OIDC) response, but is part
+ * of the JWT spec.
+ *
+ * @param authResult the authentication result e.g. allow.
+ * @param authResultMessage a user friendly result message.
+ * @param aud audience
+ * @param exp expiration time.
+ * @param iat issued at.
+ * @param authTime the auth time.
+ * @param nbf the not before time.
+ * @param apiHost the api host.
+ * @param factor the factor.
+ * @return the duo auth token.
+ */
+ private String createJWTJsonWithNbf(@Nonnull final String authResult,
+ @Nonnull final String authResultMessage, @Nonnull final String aud,
+ @Nonnull final Instant exp, @Nonnull final Instant iat,
+ @Nonnull final Instant authTime, @Nonnull final Instant nbf, @Nonnull final String apiHost,
+ @Nonnull final String factor) {
+ final String jwtJson = "{\n" +
+ " \"nbf\": "+Math.toIntExact(nbf.getEpochSecond())+",\n" +
+ " \"iss\": \"https://"+apiHost+"/oauth/v1/token\",\n" +
+ " \"sub\": \"jdoe\",\n" +
+ " \"preferred_username\": \"jdoe\",\n" +
+ " \"aud\": \""+aud+"\",\n" +
+ " \"exp\": "+Math.toIntExact(exp.getEpochSecond()) +",\n" +
+ " \"iat\": "+(double)iat.getEpochSecond()+",\n" +
+ " \"auth_time\": "+authTime.getEpochSecond()+",\n" +
+ " \"auth_result\": {\n" +
+ " \"status_msg\": \""+authResultMessage+"\",\n" +
+ " \"status\": \""+authResult+"\",\n" +
+ " \"result\": \""+authResult+"\"\n" +
+ " },\n" +
+ " \"auth_context\": {\n" +
+ " \"result\": \"success\",\n" +
+ " \"timestamp\": 1599749128,\n" +
+ " \"auth_device\": {\n" +
+ " \"ip\": \"82.17.89.232\",\n" +
+ " \"name\": \"+44 7852 119881\",\n" +
+ " \"location\": {\n" +
+ " \"state\": \"Wales\",\n" +
+ " \"city\": \"Cardiff\",\n" +
+ " \"country\": \"United Kingdom\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"txid\": \"1684599c-bb16-4250-af85-904291bfe7cc\",\n" +
+ " \"event_type\": \"authentication\",\n" +
+ " \"reason\": \"user_approved\",\n" +
+ " \"access_device\": {\n" +
+ " \"hostname\": null,\n" +
+ " \"ip\": \"82.17.89.232\",\n" +
+ " \"location\": {\n" +
+ " \"state\": \"Wales\",\n" +
+ " \"city\": \"Cardiff\",\n" +
+ " \"country\": \"United Kingdom\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"application\": {\n" +
+ " \"key\": \"DIU6GEFWG5LIUTVV2M3N\",\n" +
+ " \"name\": \"Shibboleth Integration Testing\"\n" +
+ " },\n" +
+ " \"factor\": \""+factor+"\",\n" +
+ " \"user\": {\n" +
+ " \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" +
+ " \"name\": \"philsmart\"\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ return jwtJson;
+ }
+
/**
* Add an {@link AuthenticationFlowDescriptor} to the {@link AuthenticationContext}
* with the specified flow ID.
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifierTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifierTest.java
new file mode 100644
index 0000000..fcd8d0d
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/BasicJWTClaimsVerifierTest.java
@@ -0,0 +1,251 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.proc.BadJWTException;
+
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.UnmodifiableComponentException;
+
+/**
+ * Tests for the {@link BasicJWTClaimsVerifier}.
+ */
+public class BasicJWTClaimsVerifierTest extends AbstractDuoActionTest{
+
+ /** The verifier to test.*/
+ @Nonnull private BasicJWTClaimsVerifier<ProfileRequestJWTSecurityContext> verifier;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ super.setup();
+ verifier = new BasicJWTClaimsVerifier<ProfileRequestJWTSecurityContext>();
+
+ }
+
+ @Test(expectedExceptions = UnmodifiableComponentException.class)
+ public final void testSetupAfterInit() throws ComponentInitializationException {
+ verifier.initialize();
+ verifier.setClockSkew(Duration.ofMillis(100));
+ }
+
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testNotBeforeTimeInFuture() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(), Instant.now().plus(Duration.ofMinutes(2)),
+ "api.duosecurity.com", "duo_push"));
+
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ @Test
+ public final void testNotBeforeTimeInFutureButWithinSkew() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+ verifier.setClockSkew(Duration.ofMinutes(3));
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(), Instant.now().plus(Duration.ofMinutes(2)),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testNotBeforeTimeInFutureBeyondSkew() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+ verifier.setClockSkew(Duration.ofMinutes(1));
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(), Instant.now().plus(Duration.ofMinutes(2)),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ @Test
+ public final void testNotBeforeTimeInPast() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(), Instant.now().minus(Duration.ofMinutes(1)),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ /**
+ * Test the nbf time is equal to the current time - which should be allowed per JWT spec (rfc7519)
+ *
+ * @throws BadJWTException on error.
+ * @throws ParseException on error.
+ */
+ @Test
+ public final void testNotBeforeTimeIsNow() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ /**
+ * Validate an expired token.
+ *
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
+ */
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testExpiredToken() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now().minus(10,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ /**
+ * Validate an expired token but with a large clock skew.
+ *
+ * @throws ParseException on error.
+ * @throws BadJWTException on error.
+ */
+ @Test
+ public final void testExpiredTokenLargeClockSkew() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+ //set clock skew
+ verifier.setClockSkew(Duration.ofMinutes(20));
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now().minus(10,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ @Test
+ public final void testAllRequiredOIDCClaimsPresent() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ verifier.setRequiredClaims(Set.of("iss","exp","sub","aud","iat"));
+
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+ }
+
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testAllRequiredOIDCClaimsPresentSomeMissing() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ verifier.setRequiredClaims(Set.of("iss","exp","sub","aud","iat"));
+
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",
+ Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+ }
+
+
+ @Test
+ public final void testRequireClaimsValid() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ verifier.setRequiredClaims(Set.of("iss","exp","sub"));
+
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testRequireClaimsInvalid() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ verifier.setRequiredClaims(Set.of("iss","exp","sub","not_in_jwt"));
+
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testProhibitedClaims() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ verifier.setProhibitedClaims(Set.of("iss"));
+
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+ @Test
+ public final void testProhibitedClaimsDoNotExist() throws Exception {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ //should be fine, as not in JWT
+ verifier.setProhibitedClaims(Set.of("not_in_jwt"));
+
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ verifier.initialize();
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
index be4f144..5dd0741 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
@@ -129,7 +129,7 @@ public class DuoOIDCAuthnControllerTest extends AbstractTestNGSpringContextTests
}
/**
- * Start a Duo 2FA request. Make sure the webflow execution key is in the HttpSession.
+ * Start a Duo 2FA request.
*
* @throws Exception on exception.
*/
@@ -139,6 +139,7 @@ public class DuoOIDCAuthnControllerTest extends AbstractTestNGSpringContextTests
MvcResult result = mockMvc.perform(get("/Authn/Duo/2FA/authorize").param("conversation", "e1s1")).andDo(print())
.andExpect(status().is3xxRedirection()).andReturn();
assertNotNull(result.getResponse().getHeader("Location"));
+ //basic check of the URL - contains the API host
assertTrue(result.getResponse().getHeader("Location").contains(API_HOST));
}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifierTest.java
similarity index 51%
copy from idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
copy to idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifierTest.java
index bdd45cf..d68e912 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DynamicDuoContextJWTClaimsVerifierTest.java
@@ -15,175 +15,168 @@
* limitations under the License.
*/
-/*
- * 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.authn.duo.impl;
-import static org.testng.Assert.assertNull;
-
+import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
-import org.springframework.webflow.execution.Event;
+import javax.annotation.Nonnull;
+
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.proc.BadJWTException;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
-
-/**
- * Tests for the {@link ValidateTokenClaims} class.
- */
-public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
-
- /** The action to test. */
- private ValidateTokenClaims action;
+import net.shibboleth.utilities.java.support.component.UnmodifiableComponentException;
+public class DynamicDuoContextJWTClaimsVerifierTest extends AbstractDuoActionTest{
+
+ @Nonnull private DynamicDuoContextJWTClaimsVerifier verifier;
+
@BeforeMethod
public void setUp() throws Exception {
super.setup();
- action = new ValidateTokenClaims();
-
+ verifier = new DynamicDuoContextJWTClaimsVerifier();
+
}
/**
- * Test oversized clock skew. To large for the internal int used by Nimbus, fail fast.
+ * Test component can not be altered after initialisation
*
* @throws ComponentInitializationException on error.
*/
- @Test(expectedExceptions = ConstraintViolationException.class)
- public final void testOversizedClockSkew() throws ComponentInitializationException {
- action.setClockSkew(Duration.ofDays(1000000000));
-
+ @Test(expectedExceptions = UnmodifiableComponentException.class)
+ public final void testSetupAfterInit() throws ComponentInitializationException {
+ verifier.initialize();
+ verifier.setIssuerPath("/test");
}
+
/**
* Test blank duo integration.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
*/
- @Test
- public final void testBlankDuoIntegration() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testBlankDuoIntegration() throws BadJWTException, ParseException {
addDuoContext();
- addAttemptedFlow("authn/DuoOIDC");
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now(),Instant.now(), Instant.now(),
"api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.INVALID_AUTHN_CTX);
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
-
/**
- * Validate a valid token.
+ * Blank the Authentication Context, this is fatal and should result in a bad JWT exception.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error.
*/
- @Test
- public final void testValidToken() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now(),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertNull(event);
-
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testBlankAuthenticationContext() throws BadJWTException, ParseException {
+
+ final JWT duoToken = createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now().minus(10,ChronoUnit.MINUTES),
+ "api.duosecurity.com", "duo_push");
+
+ //blank Authentication Context, should throw.
+ prc.removeSubcontext(AuthenticationContext.class);
+
+ verifier.verify(duoToken.getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
-
+
/**
- * Validate an expired token.
+ * Blank the Duo Context, this is fatal and should result in a bad JWT exception.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error.
*/
- @Test
- public final void testExpiredToken() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testBlankDuoContext() throws BadJWTException, ParseException {
+
+ final JWT duoToken = createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now(),Instant.now(), Instant.now().minus(10,ChronoUnit.MINUTES),
+ "api.duosecurity.com", "duo_push");
+
+ verifier.verify(duoToken.getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
+ }
+
+ /**
+ * Test if no security context is input - and is required.
+ *
+ * @throws ParseException on error.
+ * @throws BadJWTException on error.
+ */
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testBlankSecurityContext() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().minus(10,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
+ Instant.now(),Instant.now(), Instant.now(),
+ "incorrect-issuer", "duo_push"));
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), null);
}
/**
- * Validate an expired token but with a large clock skew.
+ * Validate a token which has the incorrect issuer.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
*/
- @Test
- public final void testExpiredTokenLargeClockSkew() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testInvalidIssuer() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
- //set clock skew
- action.setClockSkew(Duration.ofMinutes(20));
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().minus(10,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertNull(event);
+ Instant.now(),Instant.now(), Instant.now(),
+ "incorrect-issuer", "duo_push"));
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
-
/**
- * Validate a token which has the incorrect issuer.
+ * If the username field does not exist in the claimsset, expect and error.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
*/
- @Test
- public final void testInvalidIssuer() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testClaimDoesNotExist() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
+
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now(),Instant.now(), Instant.now(),
- "incorrect-issuer", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
+ "api.duosecurity.com", "duo_push"));
+
+ //set the preferred name to a claim that does not exist
+ verifier.setUsernameClaim("not_a_claim");
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
/**
* Validate a token which has the incorrect audience.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
*/
- @Test
- public final void testInvalidAudience() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testInvalidAudience() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
@@ -191,18 +184,20 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
"Login Succesful","wrong-client-audience",
Instant.now(),Instant.now(), Instant.now(),
"api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
+
+
/**
* Validate a token which has the incorrect subject.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
*/
- @Test
- public final void testInvalidSubject() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testInvalidSubject() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
@@ -213,19 +208,19 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now(),Instant.now(), Instant.now(),
"api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
/**
* Validate a token which was issued too far in the past.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
*/
- @Test
- public final void testInvalidIssuedAtPast() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testInvalidIssuedAtPast() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
@@ -233,19 +228,19 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now(),Instant.now().minus(10,ChronoUnit.MINUTES), Instant.now(),
"api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
/**
* Validate a token which was issued too far in the future.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
*/
- @Test
- public final void testInvalidIssuedAtFuture() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testInvalidIssuedAtFuture() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
@@ -253,9 +248,8 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now(),Instant.now().plus(10,ChronoUnit.MINUTES), Instant.now(),
"api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
@@ -263,10 +257,11 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
* Validate a token which was outside (in the future)
* the authentication lifetime window when using forced authn.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
*/
- @Test
- public final void testInvalidAuthenticationTimeFuture() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testInvalidAuthenticationTimeFuture() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
@@ -274,19 +269,19 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now(),Instant.now(), Instant.now().plus(10,ChronoUnit.MINUTES),
"api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
/**
* Validate a token which was outside (expired) the authentication lifetime window when using forced authn.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error. Expected.
*/
- @Test
- public final void testInvalidAuthenticationTimePast() throws ComponentInitializationException {
+ @Test(expectedExceptions = BadJWTException.class)
+ public final void testInvalidAuthenticationTimePast() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
@@ -294,42 +289,41 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now(),Instant.now(), Instant.now().minus(10,ChronoUnit.MINUTES),
"api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
/**
- * Validate a token which is inside the authentication lifetime window when using forced authn.
+ * Validate a token which is inside the authentication lifetime window when using forced authn
+ * even though it was performed 2 minutes in the past.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error.
*/
@Test
- public final void testInvalidAuthenticationWithinWindow() throws ComponentInitializationException {
+ public final void testValidAuthenticationWithinWindow() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
ac.setForceAuthn(true);
//set a lifetime
- action.setAuthnLifetime(Duration.ofMinutes(5));
+ verifier.setAuthnLifetime(Duration.ofMinutes(5));
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now(),Instant.now(), Instant.now().minus(2,ChronoUnit.MINUTES),
"api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertNull(event);
-
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
/**
* Validate a token which is outside the authentication lifetime window but authn is not forced.
*
- * @throws ComponentInitializationException on error.
+ * @throws ParseException on error.
+ * @throws BadJWTException on error.
*/
@Test
- public final void testInvalidAuthenticationOutsideWindowNotForced() throws ComponentInitializationException {
+ public final void testInvalidAuthenticationOutsideWindowNotForced() throws BadJWTException, ParseException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
@@ -337,14 +331,12 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now(),Instant.now(), Instant.now().minus(10,ChronoUnit.MINUTES),
"api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertNull(event);
-
+
+ verifier.verify(dc.getAuthToken().getJWTClaimsSet(), new ProfileRequestJWTSecurityContext(prc));
}
-
-
+
+
+
}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
index bdd45cf..b4685b9 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
@@ -36,19 +36,20 @@ package net.shibboleth.idp.plugin.authn.duo.impl;
import static org.testng.Assert.assertNull;
-import java.time.Duration;
import java.time.Instant;
-import java.time.temporal.ChronoUnit;
+import org.mockito.Mockito;
import org.springframework.webflow.execution.Event;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
+
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
-
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
/**
* Tests for the {@link ValidateTokenClaims} class.
@@ -62,47 +63,23 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
public void setUp() throws Exception {
super.setup();
action = new ValidateTokenClaims();
-
- }
-
- /**
- * Test oversized clock skew. To large for the internal int used by Nimbus, fail fast.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test(expectedExceptions = ConstraintViolationException.class)
- public final void testOversizedClockSkew() throws ComponentInitializationException {
- action.setClockSkew(Duration.ofDays(1000000000));
-
- }
-
- /**
- * Test blank duo integration.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testBlankDuoIntegration() throws ComponentInitializationException {
- addDuoContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now(),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.INVALID_AUTHN_CTX);
-
+
}
-
-
-
+
/**
- * Validate a valid token.
+ * Validate a valid token. The mocked verifier always returns without throwing a validation exception
+ * irrespective of the input.
*
* @throws ComponentInitializationException on error.
*/
@Test
public final void testValidToken() throws ComponentInitializationException {
+
+ //default mock will just return
+ final JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> verifier =
+ Mockito.mock(JWTClaimsSetVerifier.class);
+ action.setClaimsVerifier(verifier);
+
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
@@ -113,237 +90,69 @@ public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
final Event event = action.execute(src);
// null event is success.
assertNull(event);
-
- }
-
- /**
- * Validate an expired token.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testExpiredToken() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().minus(10,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
- /**
- * Validate an expired token but with a large clock skew.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testExpiredTokenLargeClockSkew() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- //set clock skew
- action.setClockSkew(Duration.ofMinutes(20));
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().minus(10,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertNull(event);
-
- }
-
-
- /**
- * Validate a token which has the incorrect issuer.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testInvalidIssuer() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now(),
- "incorrect-issuer", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
- /**
- * Validate a token which has the incorrect audience.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testInvalidAudience() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,
- "Login Succesful","wrong-client-audience",
- Instant.now(),Instant.now(), Instant.now(),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
}
/**
- * Validate a token which has the incorrect subject.
+ * Validate an invalid token. The mocked verifier always throw a {@link BadJWTException}
+ * irrespective of the input.
*
* @throws ComponentInitializationException on error.
+ * @throws BadJWTException on error.
*/
@Test
- public final void testInvalidSubject() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
+ public final void testInValidToken() throws ComponentInitializationException, BadJWTException {
- //override subject
- dc.setUsername("wrong-subject");
+ //default mock will always throw a validation exception
+ final JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> verifier =
+ Mockito.mock(JWTClaimsSetVerifier.class);
+ Mockito.doThrow(new BadJWTException("Invalid JWT")).when(verifier).
+ verify(Mockito.any(JWTClaimsSet.class), Mockito.any());
+ action.setClaimsVerifier(verifier);
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now(),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
- /**
- * Validate a token which was issued too far in the past.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testInvalidIssuedAtPast() throws ComponentInitializationException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
-
dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now().minus(10,ChronoUnit.MINUTES), Instant.now(),
+ Instant.now(),Instant.now(), Instant.now(),
"api.duosecurity.com", "duo_push"));
action.initialize();
final Event event = action.execute(src);
assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
}
-
+
+
/**
- * Validate a token which was issued too far in the future.
+ * Test when no JWT is present in the context.
*
* @throws ComponentInitializationException on error.
*/
@Test
- public final void testInvalidIssuedAtFuture() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
+ public final void testBlankJWT() throws ComponentInitializationException {
+
+ //default mock will just return
+ final JWTClaimsSetVerifier<ProfileRequestJWTSecurityContext> verifier =
+ Mockito.mock(JWTClaimsSetVerifier.class);
+ action.setClaimsVerifier(verifier);
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now().plus(10,ChronoUnit.MINUTES), Instant.now(),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
- /**
- * Validate a token which was outside (in the future)
- * the authentication lifetime window when using forced authn.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testInvalidAuthenticationTimeFuture() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- ac.setForceAuthn(true);
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now().plus(10,ChronoUnit.MINUTES),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
- /**
- * Validate a token which was outside (expired) the authentication lifetime window when using forced authn.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testInvalidAuthenticationTimePast() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- ac.setForceAuthn(true);
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now().minus(10,ChronoUnit.MINUTES),
- "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
- /**
- * Validate a token which is inside the authentication lifetime window when using forced authn.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testInvalidAuthenticationWithinWindow() throws ComponentInitializationException {
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
- ac.setForceAuthn(true);
- //set a lifetime
- action.setAuthnLifetime(Duration.ofMinutes(5));
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now().minus(2,ChronoUnit.MINUTES),
- "api.duosecurity.com", "duo_push"));
+
action.initialize();
final Event event = action.execute(src);
- // null event is success.
- assertNull(event);
-
+ assertEventId(event, AuthnEventIds.INVALID_AUTHN_CTX);
}
/**
- * Validate a token which is outside the authentication lifetime window but authn is not forced.
+ * Test when no claims verifier has been set.
*
* @throws ComponentInitializationException on error.
*/
- @Test
- public final void testInvalidAuthenticationOutsideWindowNotForced() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- ac.setForceAuthn(false);
- dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now().minus(10,ChronoUnit.MINUTES),
- "api.duosecurity.com", "duo_push"));
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public final void testNoClaimsVerifierSet() throws ComponentInitializationException {
action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertNull(event);
-
}
-
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
index d665f85..701d1e4 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
@@ -36,6 +36,7 @@ import com.nimbusds.jwt.SignedJWT;
import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClientCapabilities;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -66,7 +67,7 @@ public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
}
@Override
- public String createAuthUrl(final String username, final String state) throws DuoClientException {
+ public String createAuthUrl(final String username, final String state, final String nonce) throws DuoClientException {
return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(),
integration.getClientId(), "JWT");
@@ -144,5 +145,21 @@ public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
public String getClientId() {
return "MockDuoClient_FAIL";
}
+
+ @Override
+ public boolean isSupportsNonce() {
+ return false;
+ }
+
+ @Override
+ public DuoOIDCClientCapabilities getCapabilities() {
+ return new DuoOIDCClientCapabilities() {
+
+ @Override
+ public boolean isSupportsNonce() {
+ return false;
+ }
+ };
+ }
}
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
index bb7018c..f479252 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
@@ -36,6 +36,7 @@ import com.nimbusds.jwt.SignedJWT;
import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClientCapabilities;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -63,7 +64,7 @@ public class MockDuoOIDCClient_OK implements DuoOIDCClient{
}
@Override
- public String createAuthUrl(final String username, final String state) throws DuoClientException {
+ public String createAuthUrl(final String username, final String state,final String nonce) throws DuoClientException {
return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(),
integration.getClientId(), "JWT");
@@ -141,5 +142,21 @@ public class MockDuoOIDCClient_OK implements DuoOIDCClient{
public String getClientId() {
return "MockDuoClient_OK";
}
+
+ @Override
+ public boolean isSupportsNonce() {
+ return false;
+ }
+
+ @Override
+ public DuoOIDCClientCapabilities getCapabilities() {
+ return new DuoOIDCClientCapabilities() {
+
+ @Override
+ public boolean isSupportsNonce() {
+ return false;
+ }
+ };
+ }
}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java
index 480e4e3..6e47901 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java
@@ -36,6 +36,7 @@ import com.nimbusds.jwt.SignedJWT;
import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClientCapabilities;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -65,7 +66,7 @@ public class MockDuoOIDCClient_OK_OLD_AUTH_TIME implements DuoOIDCClient{
}
@Override
- public String createAuthUrl(final String username, final String state) throws DuoClientException {
+ public String createAuthUrl(final String username, final String state,final String nonce) throws DuoClientException {
return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(),
integration.getClientId(), "JWT");
@@ -143,5 +144,21 @@ public class MockDuoOIDCClient_OK_OLD_AUTH_TIME implements DuoOIDCClient{
public String getClientId() {
return "MockDuoClient_OLD_AUTH_TIME";
}
+
+ @Override
+ public boolean isSupportsNonce() {
+ return false;
+ }
+
+ @Override
+ public DuoOIDCClientCapabilities getCapabilities() {
+ return new DuoOIDCClientCapabilities() {
+
+ @Override
+ public boolean isSupportsNonce() {
+ return false;
+ }
+ };
+ }
}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
index a934585..07215dc 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
@@ -36,6 +36,7 @@ import com.nimbusds.jwt.SignedJWT;
import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClientCapabilities;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -63,7 +64,7 @@ public class MockDuoOIDCClient_UNKNOWN implements DuoOIDCClient{
}
@Override
- public String createAuthUrl(final String username, final String state) throws DuoClientException {
+ public String createAuthUrl(final String username, final String state,final String nonce) throws DuoClientException {
return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(),
integration.getClientId(), "JWT");
@@ -140,4 +141,20 @@ public class MockDuoOIDCClient_UNKNOWN implements DuoOIDCClient{
return "MockDuoClient_UNKNOWN";
}
+ @Override
+ public boolean isSupportsNonce() {
+ return false;
+ }
+
+ @Override
+ public DuoOIDCClientCapabilities getCapabilities() {
+ return new DuoOIDCClientCapabilities() {
+
+ @Override
+ public boolean isSupportsNonce() {
+ return false;
+ }
+ };
+ }
+
}
\ No newline at end of file
diff --git a/idp-duo-native-client-impl/pom.xml b/idp-duo-native-client-impl/pom.xml
index 4e7bbbd..74dccda 100644
--- a/idp-duo-native-client-impl/pom.xml
+++ b/idp-duo-native-client-impl/pom.xml
@@ -34,11 +34,16 @@
<artifactId>jackson-databind</artifactId>
<scope>compile</scope>
</dependency>
- <dependency>
+ <dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>compile</scope>
</dependency>
+ <dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ <scope>compile</scope>
+ </dependency>
<!-- indirectly referenced (transitive) dependencies -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
@@ -57,7 +62,7 @@
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>runtime</scope>
- </dependency>
+ </dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
@@ -81,7 +86,7 @@
<artifactId>okio</artifactId>
<version>1.15.0</version>
<scope>runtime</scope>
- </dependency>
+ </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
@@ -114,7 +119,7 @@
<groupId>${idp.groupId}</groupId>
<artifactId>idp-admin-impl</artifactId>
<scope>provided</scope>
- </dependency>
+ </dependency>
</dependencies>
<build>
diff --git a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java
index 8056847..fcef7fd 100644
--- a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java
+++ b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java
@@ -20,7 +20,6 @@ package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
import java.text.ParseException;
import java.util.List;
-import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -45,11 +44,13 @@ import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
+import net.shibboleth.idp.plugin.authn.duo.AbstractDuoOIDCClient;
import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheckResponse;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.logic.Constraint;
/**
@@ -58,7 +59,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
*
* <p>This is package private, and can/should only be instantiated by the {@link DuoSDKClientFactory}.</p>
*/
-final class DuoSDKClientAdaptor implements DuoOIDCClient{
+final class DuoSDKClientAdaptor extends AbstractDuoOIDCClient{
/** Class logger. */
@Nonnull private Logger log = LoggerFactory.getLogger(DuoSDKClientAdaptor.class);
@@ -78,10 +79,6 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
/** Save off the integration to help generate the JWT.*/
@Nonnull private final DuoOIDCIntegration duoIntegration;
- /** The client instance UUID for identification.*/
- @Nonnull private final String clientId;
-
-
/**
*
* Constructor. Initialises the native Duo SDK client.
@@ -93,12 +90,11 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
*/
protected DuoSDKClientAdaptor(@Nonnull final DuoOIDCIntegration integration,
@Nullable final List<String> caCerts) throws DuoClientException {
-
+ super();
duoIntegration = Constraint.isNotNull(integration,"Duo SDK Client requires a non-null Duo Integration");
healthCheckResponseConverter = new DefaultHealthCheckResponseConverter();
tokenResponseConverter = new DefaultTokenResponseConverter();
- clientId = UUID.randomUUID().toString();
- log.info("Secret has size '{}'",integration.getSecretKey().getBytes().length);
+
try {
if (caCerts == null) {
client = new Client(integration.getClientId(), integration.getSecretKey(),
@@ -147,8 +143,12 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
/** {@inheritDoc} */
@Override
- @Nonnull public String createAuthUrl(@Nonnull final String username, @Nonnull final String state)
+ @Nonnull public String createAuthUrl(@Nonnull @NotEmpty final String username,
+ @Nonnull @NotEmpty final String state, @Nullable final String nonce)
throws DuoClientException {
+ Constraint.isNotEmpty(username, "Username can not be null or empty");
+ Constraint.isNotEmpty(state, "State can not be null or empty");
+ //does not support the nonce.
try {
return client.createAuthUrl(username, state);
} catch (final DuoException e) {
@@ -162,6 +162,7 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
@Nonnull public JWT exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code,
@Nonnull final String username)
throws DuoClientException {
+ Constraint.isNotEmpty(code, "Auth_code can not be null");
try {
final Token token = client.exchangeAuthorizationCodeFor2FAResult(code,username);
if (token == null) {
@@ -233,9 +234,10 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
}
+
@Override
- public String getClientId() {
- return clientId;
+ public boolean isSupportsNonce() {
+ return false;
}
diff --git a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
index 2304121..94a1f7c 100644
--- a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
+++ b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
@@ -15,8 +15,6 @@
* limitations under the License.
*/
-
-
package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
import java.util.List;
diff --git a/idp-duo-native-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties b/idp-duo-native-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
index 2f3f053..d1a87b9 100644
--- a/idp-duo-native-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
+++ b/idp-duo-native-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
@@ -28,4 +28,12 @@ idp.duo.oidc.clientFactoryBean = shibboleth.authn.DuoOIDC.native.sdk.clientFacto
# If you want to enable certificate revocation checking with the Nimbus "client",
# you need to enable this property in addition to configuring a suitable means to
# locate a revocation response, see the plugin documentation.
-#idp.duo.oidc.nimbus.checkRevocation=false
+#idp.duo.oidc.nimbus.checkRevocation = false
+
+# Shouldn't need changing, controls the OIDC id_token claims verification
+#idp.duo.oidc.jwt.verifier.clockSkew = PT60S
+#idp.duo.oidc.jwt.verifier.iatWindow = PT60S
+#idp.duo.oidc.jwt.verifier.issuerPath = /oauth/v1/token
+#idp.duo.oidc.jwt.verifier.preferredUsername = preferred_username
+# Applies only to forced authentication
+#idp.duo.oidc.jwt.verifier.authLifetime = PT60S
\ No newline at end of file
diff --git a/idp-duo-nimbus-client-impl/pom.xml b/idp-duo-nimbus-client-impl/pom.xml
index 7650cfb..f4d992f 100644
--- a/idp-duo-nimbus-client-impl/pom.xml
+++ b/idp-duo-nimbus-client-impl/pom.xml
@@ -26,7 +26,6 @@
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
- <version>9.0</version>
<scope>compile</scope>
</dependency>
diff --git a/idp-duo-nimbus-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClient.java b/idp-duo-nimbus-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClient.java
index c3124b1..81c5ebc 100644
--- a/idp-duo-nimbus-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClient.java
+++ b/idp-duo-nimbus-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClient.java
@@ -21,7 +21,6 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
-import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -44,8 +43,8 @@ import com.nimbusds.jose.util.IOUtils;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.SignedJWT;
+import net.shibboleth.idp.plugin.authn.duo.AbstractDuoOIDCClient;
import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
@@ -54,7 +53,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
/**
* A Duo client using the Nimbus OIDC library. Can/should only be instantiated by the corresponding factory method.
*/
-final class NimbusClient implements DuoOIDCClient{
+final class NimbusClient extends AbstractDuoOIDCClient{
/** The only supported client assertion type.*/
@Nonnull @NotEmpty private static final String CLIENT_ASSERTION_TYPE =
@@ -77,9 +76,7 @@ final class NimbusClient implements DuoOIDCClient{
/** JSON object mapper. */
@Nonnull private ObjectMapper objectMapper;
-
- /** The client instance UUID.*/
- @Nonnull @NotEmpty private final String clientId;
+
/**
* Constructor.
@@ -91,11 +88,11 @@ final class NimbusClient implements DuoOIDCClient{
*/
protected NimbusClient(@Nonnull final DuoOIDCIntegration integration, @Nonnull final HttpClient client,
@Nullable final HttpClientSecurityParameters params, @Nonnull final ObjectMapper oMapper) {
+ super();
duoIntegration = Constraint.isNotNull(integration,"Nimbus Client requires a non-null Duo Integration");
httpClient = Constraint.isNotNull(client,"Nimbus Client requires a non-null http client");
httpClientSecurityParameters = params;
objectMapper = oMapper;
- clientId = UUID.randomUUID().toString();
//We do not validate the integration parameters e.g. because we know Duo use an invalid secret key
}
@@ -123,9 +120,10 @@ final class NimbusClient implements DuoOIDCClient{
/** {@inheritDoc} */
@Override
@Nonnull public String createAuthUrl(@Nonnull @NotEmpty final String username,
- @Nonnull @NotEmpty final String state) throws DuoClientException {
+ @Nonnull @NotEmpty final String state, @Nonnull final String nonce) throws DuoClientException {
Constraint.isNotEmpty(username, "Username can not be null or empty");
Constraint.isNotEmpty(state, "State can not be null or empty");
+ Constraint.isNotEmpty(nonce, "Nonce can not be null or empty for this client");
Constraint.isGreaterThan(21, state.length(), "State must be at least 22 characters");
Constraint.isLessThan(1025, state.length(),"State must be at maximum 1024 characters");
@@ -137,6 +135,7 @@ final class NimbusClient implements DuoOIDCClient{
.setHost(duoIntegration.getAPIHost())
.setPath(duoIntegration.getAuthorizeEndpoint())
.setParameter("scope", "openid")
+ .setParameter("nonce", nonce)
.setParameter("response_type", "code")
.setParameter("redirect_uri", duoIntegration.getRedirectURI())
.setParameter("client_id", duoIntegration.getClientId())
@@ -229,8 +228,8 @@ final class NimbusClient implements DuoOIDCClient{
}
@Override
- public String getClientId() {
- return clientId;
+ public boolean isSupportsNonce() {
+ return true;
}
}
diff --git a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
index 4b90f32..5218962 100644
--- a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
+++ b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
@@ -28,4 +28,12 @@ idp.duo.oidc.clientFactoryBean = shibboleth.authn.DuoOIDC.nimbus.clientFactory
# If you want to enable certificate revocation checking with the Nimbus "client",
# you need to enable this property in addition to configuring a suitable means to
# locate a revocation response, see the plugin documentation.
-#idp.duo.oidc.nimbus.checkRevocation=false
+#idp.duo.oidc.nimbus.checkRevocation = false
+
+# Shouldn't need changing, controls the OIDC id_token claims verification
+#idp.duo.oidc.jwt.verifier.clockSkew = PT60S
+#idp.duo.oidc.jwt.verifier.iatWindow = PT60S
+#idp.duo.oidc.jwt.verifier.issuerPath = /oauth/v1/token
+#idp.duo.oidc.jwt.verifier.preferredUsername = preferred_username
+# Applies only to forced authentication
+#idp.duo.oidc.jwt.verifier.authLifetime = PT60S
\ No newline at end of file
diff --git a/idp-duo-nimbus-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClientTest.java b/idp-duo-nimbus-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClientTest.java
index f5a8392..5b5d448 100644
--- a/idp-duo-nimbus-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClientTest.java
+++ b/idp-duo-nimbus-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/nimbus/impl/NimbusClientTest.java
@@ -316,13 +316,14 @@ public class NimbusClientTest {
final HttpClient httpClient = Mockito.mock(HttpClient.class);
// create new client with mock response
client = new NimbusClient(integ, httpClient, null, new ObjectMapper());
- final String authUrl = client.createAuthUrl("jdoe", "RRFSFERGERGSDDSXXXXQWERG");
+ final String authUrl = client.createAuthUrl("jdoe", "RRFSFERGERGSDDSXXXXQWERG","a_nonce");
assertNotNull(authUrl);
assertTrue(authUrl.contains("response_type"));
assertTrue(authUrl.contains("client_id"));
assertTrue(authUrl.contains("request"));
assertTrue(authUrl.contains("redirect_uri"));
assertTrue(authUrl.contains("scope"));
+ assertTrue(authUrl.contains("nonce"));
}
diff --git a/pom.xml b/pom.xml
index 307389d..79d1ab4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,6 +26,8 @@
<opensaml.groupId>org.opensaml</opensaml.groupId>
<opensaml.version>4.1.0-SNAPSHOT</opensaml.version>
<java-support.version>8.2.0-SNAPSHOT</java-support.version>
+ <odic.common.crypto.version>0.0.1-SNAPSHOT</odic.common.crypto.version>
+ <nimbus.version>9.0</nimbus.version>
<checkstyle.configLocation>${project.basedir}/checkstyle.xml</checkstyle.configLocation>
</properties>
@@ -66,7 +68,7 @@
<!-- Project wide test Dependencies -->
<dependency>
<groupId>org.testng</groupId>
- <artifactId>testng</artifactId>
+ <artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@@ -78,15 +80,9 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
-
- <!-- review this dep when we move to central oidc-commons-lib -->
- <dependency>
- <groupId>com.nimbusds</groupId>
- <artifactId>nimbus-jose-jwt</artifactId>
- <version>9.0</version>
- </dependency>
-
-
+
+
+
</dependencies>
<dependencyManagement>
@@ -122,6 +118,23 @@
<artifactId>idp-plugin-duo-native-client-impl</artifactId>
<version>${project.version}</version>
</dependency>
+ <!-- commons OIDC -->
+ <dependency>
+ <groupId>net.shibboleth.oidc</groupId>
+ <artifactId>oidc-common-crypto-api</artifactId>
+ <version>${odic.common.crypto.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.oidc</groupId>
+ <artifactId>oidc-common-crypto-impl</artifactId>
+ <version>${odic.common.crypto.version}</version>
+ </dependency>
+ <!-- review this dep when we move to central oidc-commons-lib -->
+ <dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ <version>${nimbus.version}</version>
+ </dependency>
<!-- test bom dependencies -->
<dependency>
<groupId>${idp.groupId}</groupId>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list