[java-idp-plugin-duo] 01/16: JDUO-15 - Switch IdP Token Handling to Nimbus JWT
Phil Smart
philip.smart at jisc.ac.uk
Fri Oct 2 10:40:52 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=6cb19ec30b2831f34fe0ee08e7aa055ce2820716
commit 6cb19ec30b2831f34fe0ee08e7aa055ce2820716
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Sep 18 17:26:15 2020 +0100
JDUO-15 - Switch IdP Token Handling to Nimbus JWT
- First draft
https://issues.shibboleth.net/jira/browse/JDUO-15
---
.../idp/plugin/authn/duo/DuoOIDCAuthAPI.java | 9 +
.../idp/plugin/authn/duo/DuoOIDCClient.java | 11 +-
.../duo/context/DuoOIDCAuthenticationContext.java | 10 +-
.../authn/duo/impl/DuoJWTClaimsVerifier.java | 176 +++++++++++
.../authn/duo/impl/ExchangeCodeForDuoToken.java | 17 +-
.../authn/duo/impl/IdPJWTSecurityContext.java | 37 +++
.../authn/duo/impl/ValidateDuoResponseState.java | 2 +-
.../authn/duo/impl/ValidateDuoTokenAudience.java | 102 ------
.../impl/ValidateDuoTokenAuthenticationResult.java | 99 ++++--
.../impl/ValidateDuoTokenAuthenticationTime.java | 166 ----------
.../duo/impl/ValidateDuoTokenExpirationTime.java | 131 --------
.../authn/duo/impl/ValidateDuoTokenIssuedAt.java | 143 ---------
.../authn/duo/impl/ValidateDuoTokenIssuer.java | 149 ---------
.../authn/duo/impl/ValidateDuoTokenSubject.java | 106 -------
.../plugin/authn/duo/impl/ValidateTokenClaims.java | 242 ++++++++++++++
.../authn/duo/impl/ValidateTokenSignature.java | 131 ++++++++
.../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml | 20 +-
.../flows/authn/DuoOIDC/duo-oidc-authn-flow.xml | 7 +-
.../authn/duo/impl/AbstractDuoActionTest.java | 262 +++++++++++++--
.../duo/impl/ValidateDuoTokenAudienceTest.java | 95 ------
.../ValidateDuoTokenAuthenticationResultTest.java | 38 ++-
.../ValidateDuoTokenAuthenticationTimeTest.java | 165 ----------
.../impl/ValidateDuoTokenExpirationTimeTest.java | 130 --------
.../duo/impl/ValidateDuoTokenIssuedAtTest.java | 110 -------
.../authn/duo/impl/ValidateDuoTokenIssuerTest.java | 88 ------
.../duo/impl/ValidateDuoTokenSubjectTest.java | 83 -----
.../authn/duo/impl/ValidateTokenClaimsTest.java | 351 +++++++++++++++++++++
.../authn/duo/impl/ValidateTokenSignatureTest.java | 182 +++++++++++
.../plugin/authn/mock/MockDuoOIDCClient_FAIL.java | 108 +++++--
.../plugin/authn/mock/MockDuoOIDCClient_OK.java | 115 +++++--
.../mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java | 115 +++++--
.../authn/mock/MockDuoOIDCClient_UNKNOWN.java | 107 +++++--
.../authn/duo/sdk/impl/DuoSDKClientAdaptor.java | 119 ++++---
pom.xml | 16 +
34 files changed, 1896 insertions(+), 1746 deletions(-)
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 d17d7a7..cefb62e 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
@@ -45,6 +45,15 @@ public final class DuoOIDCAuthAPI {
/** Duo response success status. */
@Nonnull @NotEmpty public static final String DUO_RESPONSE_STATUS_OK = "OK";
+
+ /** The name of the JSON authentication result object.*/
+ @Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_JSON_OBJECT = "auth_result";
+
+ /** The name of the JSON result status property.*/
+ @Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_STATUS_JSON_OBJECT = "status";
+
+ /** The name of the JSON result status message property.*/
+ @Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_STATUS_MSG_JSON_OBJECT = "status_msg";
/** 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 fb9a839..69fcb19 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
@@ -19,7 +19,8 @@ package net.shibboleth.idp.plugin.authn.duo;
import javax.annotation.Nonnull;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import com.nimbusds.jwt.JWT;
+
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
/**
@@ -49,19 +50,19 @@ public interface DuoOIDCClient {
@Nonnull String createAuthUrl(@Nonnull final String username, @Nonnull final String state) throws DuoClientException;
/**
- * Verifies the code returned by Duo and exchanges it for a token which contains information pertaining to
- * the authentication.
+ * Verifies the code returned by Duo and exchanges it for a Json Web Token which contains information pertaining to
+ * the authentication. The JWT **must** be signed.
*
* @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.
*
- * @return the token, never {@code null}.
+ * @return the **signed** JWT, never {@code null}.
*
* @throws DuoClientException if there is an error exchanging the auth_code for a token result.
*/
- @Nonnull DuoAuthToken exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code,
+ @Nonnull JWT exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code,
@Nonnull final String username) throws DuoClientException;
}
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 30888af..0a0c0cd 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
@@ -22,6 +22,8 @@ import javax.annotation.Nullable;
import org.opensaml.messaging.context.BaseContext;
+import com.nimbusds.jwt.JWT;
+
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
@@ -51,8 +53,8 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
/** The authorization code return from the Duo authorization request.*/
@Nullable private String authCode;
- /** The token received from Duo as a result of 2FA.*/
- @Nullable private DuoAuthToken authToken;
+ /** The JWT token received from Duo as a result of 2FA.*/
+ @Nullable private JWT authToken;
/** The Duo OIDC client to use for the lifetime of this request.*/
@Nullable private DuoOIDCClient client;
@@ -111,7 +113,7 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
*
* @return this context
*/
- @Nonnull public DuoOIDCAuthenticationContext setAuthToken(@Nullable final DuoAuthToken token) {
+ @Nonnull public DuoOIDCAuthenticationContext setAuthToken(@Nullable final JWT token) {
authToken = token;
return this;
}
@@ -121,7 +123,7 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
*
* @return the token
*/
- @Nullable public DuoAuthToken getAuthToken() {
+ @Nullable public JWT getAuthToken() {
return authToken;
}
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
new file mode 100644
index 0000000..8b926c6
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoJWTClaimsVerifier.java
@@ -0,0 +1,176 @@
+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.utilities.java.support.component.ComponentSupport;
+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 when forced authentication is requested.</li>
+ * </ol>
+ */
+ at ThreadSafe
+public class DuoJWTClaimsVerifier extends DefaultJWTClaimsVerifier<IdPJWTSecurityContext>{
+
+ /** The name of the authentication time claim.*/
+ @Nonnull public static final String AUTH_TIME_CLAIM_NAME = "auth_time";
+
+ /** 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 IdPJWTSecurityContext context)
+ throws BadJWTException {
+ if (context == null) {
+ throw new BadJWTException("Duo claims verifier requires the IdP security context");
+ }
+ super.verify(claimsSet,context);
+ verifyIat(claimsSet);
+ verifyAuthenticationTime(claimsSet,context);
+ }
+
+ private void verifyAuthenticationTime(@Nonnull final JWTClaimsSet claimsSet,
+ @Nonnull final IdPJWTSecurityContext context) throws BadJWTException {
+
+ if (context.getPrc() == null) {
+ throw new BadJWTException("No profile request context found, can not validate authentication time");
+ }
+ 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(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/ExchangeCodeForDuoToken.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
index 9a005db..fd23054 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
@@ -17,6 +17,8 @@
package net.shibboleth.idp.plugin.authn.duo.impl;
+import java.text.ParseException;
+
import javax.annotation.Nonnull;
import org.opensaml.profile.action.ActionSupport;
@@ -24,13 +26,14 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.jwt.JWT;
+
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.DuoClientException;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
/**
@@ -77,11 +80,17 @@ public class ExchangeCodeForDuoToken extends AbstractDuoAuthenticationAction{
}
try {
- final DuoAuthToken token = client.exchangeAuthorizationCodeFor2FAResult(code,username);
- log.info("{} Duo 2FA token received for subject '{}'",getLogPrefix(),token.getSub());
+ final JWT token = client.exchangeAuthorizationCodeFor2FAResult(code,username);
+ if (log.isDebugEnabled()) {
+ //avoid parsing claims if debug not enabled,
+ //if debug is enabled and parsing fails here, you will get different behaviour than if
+ //debug is not enabled!
+ log.debug("{} Duo 2FA token received for subject '{}'",getLogPrefix(),
+ token.getJWTClaimsSet().getSubject());
+ }
duoContext.setAuthToken(token);
//success
- } catch (final DuoClientException e) {
+ } catch (final DuoClientException | ParseException e) {
log.error("{} Unable to exchange authorisation code for 2FA result",getLogPrefix(),e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
return;
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/IdPJWTSecurityContext.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/IdPJWTSecurityContext.java
new file mode 100644
index 0000000..97a6c59
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/IdPJWTSecurityContext.java
@@ -0,0 +1,37 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jose.proc.SecurityContext;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** Simple IdP Nimbus JWT Verification security context that holds the {@link ProfileRequestContext}.*/
+ at Immutable
+public class IdPJWTSecurityContext implements SecurityContext{
+
+ /** The profile request context.*/
+ @Nonnull private final ProfileRequestContext prc;
+
+ /**
+ * Constructor.
+ *
+ * @param requestContext the profile request context
+ */
+ public IdPJWTSecurityContext(@Nonnull final ProfileRequestContext requestContext) {
+ prc = Constraint.isNotNull(requestContext, "ProfileRequestContext can not be null");
+ }
+
+ /**
+ * Get the profile request context.
+ *
+ * @return the profile request context.
+ */
+ @Nonnull public ProfileRequestContext getPrc() {
+ return prc;
+ }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java
index e3b7491..39a39ba 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java
@@ -79,7 +79,7 @@ public class ValidateDuoResponseState extends AbstractDuoAuthenticationAction {
}
/**
- * Set the request and response states to null so they can't be reused. It is less
+ * Set the request and response states to null so they can't be reused - it is less
* relevant if they are removed (GC'd) from memory.
*
* @param context the duo context.
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudience.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudience.java
deleted file mode 100644
index 87b4316..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudience.java
+++ /dev/null
@@ -1,102 +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 javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-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.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-
-/**
- * An action that verifies the Audience (aud) claim in the Duo token contains the client_id of this client (as
- * registered at the issuer). See section 3.1.3.7 of OpenID Connect core 1.0.
- *
- * @pre <pre>
- * ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre <pre>
- * AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre n<pre>
- * DuoOIDCAuthenticationContext.getAuthToken() != null
- * </pre>
- *
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
- */
-public class ValidateDuoTokenAudience extends AbstractDuoAuthenticationAction {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenAudience.class);
-
- /** The Duo authentication token. */
- @Nullable private DuoAuthToken token;
-
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- token = duoContext.getAuthToken();
- if (token == null) {
- log.error("{} Duo 2FA token is not available", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
- return false;
- }
- return true;
- }
-
- /** {@inheritDoc} */
- @Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- final String audience = token.getAud();
- if (audience == null) {
- log.error("{} No audience found in the token response", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- if (duoContext.getIntegration() == null) {
- log.error("{} No Duo integration found in the Duo context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- if (!audience.equals(duoContext.getIntegration().getClientId())) {
- log.error("{} Client is not the intended audience", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- log.trace("{} Token has the correct audience '{}' for this client",getLogPrefix(),audience);
- //audience is fine
-
- }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
index 86eb33d..17cf1d8 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
@@ -17,7 +17,9 @@
package net.shibboleth.idp.plugin.authn.duo.impl;
import java.security.Principal;
+import java.text.ParseException;
import java.util.Collection;
+import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -27,15 +29,20 @@ import javax.security.auth.Subject;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.AuthnStatement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+
import net.shibboleth.idp.authn.AbstractValidationAction;
import net.shibboleth.idp.authn.AuthenticationResult;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
import net.shibboleth.idp.authn.duo.DuoPrincipal;
+import net.shibboleth.idp.plugin.authn.duo.DuoException;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
@@ -68,6 +75,9 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractValidationActi
/** The profile request context.*/
@Nullable private ProfileRequestContext prc;
+ /** The parsed claimset. */
+ @Nullable private JWTClaimsSet claimsSet;
+
/** Attempted username. */
@Nullable @NotEmpty private String username;
@@ -110,14 +120,33 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractValidationActi
duoContext = authenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class);
if (duoContext == null) {
- log.info("{} No DuoAuthenticationContext available", getLogPrefix());
+ log.error("{} No DuoAuthenticationContext available", getLogPrefix());
handleError(profileRequestContext, authenticationContext, "No DuoAuthenticationContext context available",
AuthnEventIds.INVALID_AUTHN_CTX);
recordFailure(profileRequestContext);
return false;
- }
+ }
//we get username from the original context, not the duo response.
- username = duoContext.getUsername();
+ username = duoContext.getUsername();
+
+ final JWT token = duoContext.getAuthToken();
+ if (token == null) {
+ log.error("{} Duo 2FA token is not available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return false;
+ }
+ try {
+ //parse the claimset here, so parsing only has to happen once, and we fail fast on error (e.g. bad JSON)
+ claimsSet = token.getJWTClaimsSet();
+ if (claimsSet == null) {
+ throw new DuoException("Duo JWT ClaimsSet is null");
+ }
+ } catch (final ParseException | DuoException e) {
+ log.error("{} Claimset of Duo 2FA token is not available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return false;
+ }
+
return true;
}
@@ -126,32 +155,58 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractValidationActi
@Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
- final DuoAuthToken token = duoContext.getAuthToken();
- if (token == null) {
- log.error("{} Duo 2FA token is not available",getLogPrefix());
- handleError(profileRequestContext, authenticationContext,"Duo 2FA result not available",
- AuthnEventIds.INVALID_CREDENTIALS);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+
+ Map<String, Object> authStatusObject = null;
+ try {
+ authStatusObject = claimsSet.getJSONObjectClaim(DuoOIDCAuthAPI.DUO_AUTH_RESULT_JSON_OBJECT);
+ if (authStatusObject == null) {
+ throw new DuoException("Authentication result object is null");
+ }
+ } catch (final ParseException | DuoException e) {
+ log.error("{} Duo 2FA access failed for '{}', auth_result missing",getLogPrefix(), username);
+ handleError(profileRequestContext, authenticationContext,"Unexepected Authentication Response",
+ AuthnEventIds.AUTHN_EXCEPTION);
recordFailure(profileRequestContext);
- return;
}
- if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW.equalsIgnoreCase(token.getAuthResultStatus())){
- log.debug("{} Duo 2FA authentication succeeded for '{}'",getLogPrefix(),duoContext.getUsername());
- recordSuccess(profileRequestContext);
- buildAuthenticationResult(profileRequestContext, authenticationContext);
- } else if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY.equalsIgnoreCase(token.getAuthResultStatus())) {
- log.error("{} Duo 2FA failed for '{}', 2FA status '{}'",getLogPrefix(), username,
- token.getAuthResultStatusMessage());
- handleError(profileRequestContext, authenticationContext, token.getAuthResultStatus(),
- AuthnEventIds.INVALID_CREDENTIALS);
- recordFailure(profileRequestContext);
- } else {
- log.error("{} Duo 2FA access failed for '{}', unknown response", getLogPrefix(), username);
+ final Object statusObj = authStatusObject.get(DuoOIDCAuthAPI.DUO_AUTH_RESULT_STATUS_JSON_OBJECT);
+ final Object statusMsgObj = authStatusObject.get(DuoOIDCAuthAPI.DUO_AUTH_RESULT_STATUS_MSG_JSON_OBJECT);
+
+ //instanceof includes null check
+ if (statusObj instanceof String && statusMsgObj instanceof String) {
+ final String authResultStatus = (String)statusObj;
+ final String authResultStatusMsg = (String)statusMsgObj;
+
+ if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW.equalsIgnoreCase(authResultStatus)){
+ log.debug("{} Duo 2FA authentication succeeded for '{}'",getLogPrefix(),duoContext.getUsername());
+ recordSuccess(profileRequestContext);
+ buildAuthenticationResult(profileRequestContext, authenticationContext);
+ return;
+ } else if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY.equalsIgnoreCase(authResultStatus)) {
+ log.error("{} Duo 2FA failed for '{}', 2FA status '{}'",getLogPrefix(), username,
+ authResultStatusMsg);
+ handleError(profileRequestContext, authenticationContext, authResultStatus,
+ AuthnEventIds.INVALID_CREDENTIALS);
+ recordFailure(profileRequestContext);
+ return;
+ } else {
+ log.error("{} Duo 2FA access failed for '{}', unknown response", getLogPrefix(), username);
+ handleError(profileRequestContext, authenticationContext,"Unexepected Authentication Response",
+ AuthnEventIds.AUTHN_EXCEPTION);
+ recordFailure(profileRequestContext);
+ return;
+ }
+ } else {
+ log.error("{} Duo 2FA access failed for '{}', auth_results missing",getLogPrefix(), username);
handleError(profileRequestContext, authenticationContext,"Unexepected Authentication Response",
AuthnEventIds.AUTHN_EXCEPTION);
recordFailure(profileRequestContext);
+ return;
}
+
+
+
+
}
/** {@inheritDoc} */
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTime.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTime.java
deleted file mode 100644
index 036c632..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTime.java
+++ /dev/null
@@ -1,166 +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.time.Duration;
-import java.time.Instant;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-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.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- *
- * <p> An action that checks if auth_time (when the End-User authentication took place) stored within the id_token
- * is within a valid expiration window.</p>
- *
- * <p> This only applies to forced authentication requests to ensure active 2FA was performed.</p>
- *
- * @pre <pre>
- * ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre <pre>
- * AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre <pre>
- * DuoOIDCAuthenticationContext.getAuthToken() != null
- * </pre>
- *
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#INVALID_AUTHN_CTX}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
- */
-public class ValidateDuoTokenAuthenticationTime extends AbstractDuoAuthenticationAction {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenAuthenticationTime.class);
-
- /** Positive clock skew adjustment to consider still acceptable (Default value: 1 minute). */
- @Nonnull private Duration clockSkew;
-
- /** Amount of time for which a token is valid after if it was issued. (Default value: 1 minute) */
- @Nonnull private Duration authnLifetime;
-
- /** The Duo authentication token. */
- @Nullable private DuoAuthToken token;
-
- /** Constructor. */
- public ValidateDuoTokenAuthenticationTime() {
- authnLifetime = Duration.ofMinutes(1);
- clockSkew = Duration.ofMinutes(1);
- }
-
- /**
- * Sets the amount of time for which a token is valid.
- *
- * @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");
-
- authnLifetime = lifetime;
- }
-
- /**
- * Set the clock skew.
- *
- * @param skew clock skew to set
- */
- public void setClockSkew(@Nonnull final Duration skew) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
- }
-
- /** {@inheritDoc} */
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- token = duoContext.getAuthToken();
- if (token == null) {
- log.error("{} Duo 2FA token is not available", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
- return false;
- }
- return true;
-
- }
-
- /** {@inheritDoc} */
- @Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- if (!authenticationContext.isForceAuthn()) {
- return;
- }
-
- //auth_time in seconds (as integer) since Unix EPOCH.
- final Integer authTimeEpochSeconds = token.getAuthTime();
- if (authTimeEpochSeconds == null) {
- log.error("{} No auth_time found in the token response", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
-
- final Instant authTime = Instant.ofEpochSecond(authTimeEpochSeconds);
- final Instant now = Instant.now();
- final Instant latestValid = now.plus(clockSkew.abs());
- final Instant expiration = authTime.plus(clockSkew.abs()).plus(authnLifetime);
-
- // Check time of authentication wasn't in the future
- if (authTime.isAfter(latestValid)) {
- log.warn("{} Authentication is not yet valid: auth_time was {}, latest valid is: {}", getLogPrefix(),
- authTime, latestValid);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
-
- // 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: '{}'",
- getLogPrefix(), authTime, expiration, now);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- //is OK.
-
- }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTime.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTime.java
deleted file mode 100644
index 3d6a9a2..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTime.java
+++ /dev/null
@@ -1,131 +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.time.Duration;
-import java.time.Instant;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-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.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- * An action that verifies the expiration time (exp) of an id_token. If the expiration time has past,
- * the id_token must not be accepted. A few minutes of leeway is allowed - clock skew.
- * See section 3.1.3.7 of OpenID Connect core 1.0.
- *
- * @pre <pre>
- * ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre <pre>
- * AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre <pre>
- * DuoOIDCAuthenticationContext.getAuthToken() != null
- * </pre>
- *
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
- */
-public class ValidateDuoTokenExpirationTime extends AbstractDuoAuthenticationAction {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenExpirationTime.class);
-
- /** Positive clock skew adjustment to consider still acceptable (Default value: 1 minute). */
- @Nonnull private Duration clockSkew;
-
- /** The Duo authentication token. */
- @Nullable private DuoAuthToken token;
-
- /** Constructor. */
- public ValidateDuoTokenExpirationTime() {
- clockSkew = Duration.ofMinutes(1);
- }
-
- /**
- * Set the clock skew.
- *
- * @param skew clock skew to set
- */
- public void setClockSkew(@Nonnull final Duration skew) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
- }
-
- /** {@inheritDoc} */
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- token = duoContext.getAuthToken();
- if (token == null) {
- log.error("{} Duo 2FA token is not available", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
- return false;
- }
- return true;
-
- }
-
- /** {@inheritDoc} */
- @Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- //exp is time in seconds since EPOCH.
- final Integer expEpochSeconds = token.getExp();
- if (expEpochSeconds == null) {
- log.error("{} No expiry found in the token response", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
-
- final Instant now = Instant.now();
- final Instant latestExpired = Instant.ofEpochSecond(expEpochSeconds).plus(clockSkew.abs());
-
- log.trace("{} Token expires at '{}', current time is '{}'",getLogPrefix(),latestExpired,now);
- if (now.isAfter(latestExpired)) {
- log.warn("{} Token is past expiry date: message expired at: '{}', current time: '{}'", getLogPrefix(),
- latestExpired, now);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- //has not expired.
-
-
- }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAt.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAt.java
deleted file mode 100644
index 0adee90..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAt.java
+++ /dev/null
@@ -1,143 +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.time.Duration;
-import java.time.Instant;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-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.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- *
- * An action that 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.
- *
- * @pre <pre>
- * ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre <pre>
- * AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre <pre>
- * DuoOIDCAuthenticationContext.getAuthToken() != null
- * </pre>
- *
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
- */
-public class ValidateDuoTokenIssuedAt extends AbstractDuoAuthenticationAction {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenIssuedAt.class);
-
- /**
- * Maximum amount (in either direction from now) of clock skew for which a token is valid after
- * it is issued (Default value: 3 minutes).
- */
- @Nonnull private Duration iatMaxClockSkew;
-
- /** The Duo authentication token. */
- @Nullable private DuoAuthToken token;
-
-
- /** Constructor. */
- public ValidateDuoTokenIssuedAt() {
- iatMaxClockSkew = Duration.ofMinutes(3);
- }
-
- /**
- * Sets the amount of time for which a token is valid.
- *
- * @param clockSkew amount of time for which a token is valid
- */
- public synchronized void setIatMaxClockSkew(@Nonnull final Duration clockSkew) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- Constraint.isNotNull(clockSkew, "Token issued at clock skew cannot be null");
- Constraint.isFalse(clockSkew.isNegative(), "Token issued at clock skew cannot be negative");
-
- iatMaxClockSkew = clockSkew;
- }
-
- /** {@inheritDoc} */
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- token = duoContext.getAuthToken();
- if (token == null) {
- log.error("{} Duo 2FA token is not available", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
- return false;
- }
- return true;
-
- }
-
- /** {@inheritDoc} */
- @Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- //iat is time in seconds (as double) since EPOCH.
- final Double iatEpochSeconds = token.getIat();
- if (iatEpochSeconds == null) {
- log.error("{} No expiry found in the token response", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
-
- final Instant now = Instant.now();
- //double to long conversation with rounding! it is unlikely this field will contain a decimal value
- final Instant iat = Instant.ofEpochSecond(Math.round(iatEpochSeconds));
-
- final Duration iatDifference = Duration.between(now, iat).abs();
-
- log.trace("{} Difference between when the token was issued '{}' and now '{}' is '{}'",
- getLogPrefix(),iat, now, iatDifference);
-
- if (iatMaxClockSkew.compareTo(iatDifference) < 0) {
- log.error("{} Token issued at '{}' was too far away from the current time '{}' with acceptable "
- + " deviation of '{}', difference is '{}'",
- getLogPrefix(), iat, now, iatMaxClockSkew, iatDifference);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- //is within token lifetime window.
-
-
- }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuer.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuer.java
deleted file mode 100644
index 10c7d96..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuer.java
+++ /dev/null
@@ -1,149 +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 javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-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.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- * <p>An action that verifies the issuer (iss) of the id_token exactly matches that of the configured
- * Duo token provider. See section 3.1.3.7 of OpenID Connect core 1.0.</p>
- *
- * <p>The logic here is specific to the Duo implementation, and mimics the native Duo client issuer validation.</p>
- *
- * @pre
- *
- * <pre>
- * ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre
- *
- * <pre>
- * AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre
- *
- * <pre>
- * DuoOIDCAuthenticationContext.getAuthToken() != null
- * </pre>
- *
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
- *
- */
-public class ValidateDuoTokenIssuer 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";
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenIssuer.class);
-
- /** The Duo authentication token. */
- @Nullable private DuoAuthToken token;
-
- /** The URL path component of the issuer.*/
- @Nonnull @NotEmpty private String issuerPath;
-
- /** Constructor.*/
- public ValidateDuoTokenIssuer() {
- issuerPath = DEFAULT_ISSUER_PATH;
- }
-
- /**
- * Sets the issuer URL path component.
- *
- * @param path the issuer path
- */
- public synchronized void setIssuerPath(@Nonnull final String path) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- issuerPath = Constraint.isNotNull(path, "Issuer URL path cannot be null");
- }
-
- /** {@inheritDoc} */
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- token = duoContext.getAuthToken();
- if (token == null) {
- log.error("{} Duo 2FA token is not available", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
- return false;
- }
- return true;
-
- }
-
- /** {@inheritDoc} */
- @Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- final String issuer = token.getIss();
- if (issuer == null) {
- log.error("{} No issuer found in the token response", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- if (duoContext.getIntegration() == null) {
- log.error("{} No Duo integration found in the Duo context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- 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 String issuerFromIntegration = HTTPS+apiHost+issuerPath;
- log.trace("{} Token issuer is '{}', expected '{}'",getLogPrefix(),issuer, issuerFromIntegration);
- if (!issuer.equals(issuerFromIntegration)) {
- log.error("{} Token issuer differs from that expected, issuer is '{}', expected '{}'",getLogPrefix(),
- issuer,issuerFromIntegration);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- //issuer is fine.
-
- }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubject.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubject.java
deleted file mode 100644
index ee95ce6..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubject.java
+++ /dev/null
@@ -1,106 +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 javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-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.context.DuoOIDCAuthenticationContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-
-/**
- * An action that verifies the Subject (sub) claim in the Duo token matches that user which
- * is currently authenticating.
- *
- * @pre <pre>
- * ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre <pre>
- * AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
- * </pre>
- *
- * @pre n<pre>
- * DuoOIDCAuthenticationContext.getAuthToken() != null
- * </pre>
- *
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#INVALID_CREDENTIALS}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
- */
-public class ValidateDuoTokenSubject extends AbstractDuoAuthenticationAction {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenSubject.class);
-
- /** The Duo authentication token. */
- @Nullable private DuoAuthToken token;
-
- /** {@inheritDoc} */
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- token = duoContext.getAuthToken();
- if (token == null) {
- log.error("{} Duo 2FA token is not available", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
- return false;
- }
- return true;
-
- }
-
- /** {@inheritDoc} */
- @Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext,
- @Nonnull final DuoOIDCAuthenticationContext duoContext) {
-
- final String subject = token.getSub();
- if (subject == null) {
- log.error("{} No subject found in the token response", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- final String username = duoContext.getUsername();
- if (username == null) {
- log.error("{} No username found in the Duo context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- if (!username.equals(subject)) {
- log.error("{} Username in the Duo context does not match with the subject of the"
- + " Duo token", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
- return;
- }
-
-
- }
-
-}
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
new file mode 100644
index 0000000..b2b1c5b
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaims.java
@@ -0,0 +1,242 @@
+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;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+
+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.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>
+ */
+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);
+
+ /** 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;
+ }
+
+ /**
+ * 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");
+ }
+ }
+
+ /**
+ * Sets the issuer URL path component.
+ *
+ * @param path the issuer path
+ */
+ public synchronized void setIssuerPath(@Nonnull final String path) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ issuerPath = Constraint.isNotNull(path, "Issuer URL path cannot be null");
+ }
+
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+ final JWT token = duoContext.getAuthToken();
+ if (token == null) {
+ log.error("{} Duo 2FA token is not available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return false;
+ }
+ try {
+ //parse the claimset here, so parsing only has to happen once, and we fail fast on error (e.g. bad JSON)
+ claimsSet = token.getJWTClaimsSet();
+ if (claimsSet == null) {
+ throw new DuoException("Duo JWT ClaimsSet is null");
+ }
+ } catch (final ParseException | DuoException e) {
+ 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;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @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 IdPJWTSecurityContext(profileRequestContext));
+ } catch (final BadJWTException e) {
+ log.error("{} Token verification failed for subject '{}'", getLogPrefix(),claimsSet.getSubject(),e);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+ //fine.
+ log.debug("{} Token claims are valid for subject '{}'",getLogPrefix(),claimsSet.getSubject());
+ }
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignature.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignature.java
new file mode 100644
index 0000000..6569b33
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignature.java
@@ -0,0 +1,131 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.text.ParseException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.MACVerifier;
+
+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.DuoOIDCIntegration;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+
+/**
+ * Action to validate the JWT signature. The JWT **must** be signed using the HMAC_SHA family.
+ * Any other type, or 'none', emits and error back to the flow.
+ */
+public class ValidateTokenSignature extends AbstractDuoAuthenticationAction {
+
+ /**
+ * The HMAC 'family' of signature algorithms is the only supported, based on the
+ * shared secret in the client integration.
+ */
+ @Nonnull private static final JWSAlgorithm.Family SUPPORTED_SIGNATURE_FAMILY =
+ JWSAlgorithm.Family.HMAC_SHA;
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateTokenSignature.class);
+
+ /** The Duo authentication token. */
+ @Nullable private JWT token;
+
+ /** The parsed claimset. */
+ @Nullable private JWTClaimsSet claimSet;
+
+ /** The Duo integration appropriate for this request.*/
+ @Nullable private DuoOIDCIntegration integration;
+
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+ token = duoContext.getAuthToken();
+ if (token == null) {
+ log.error("{} Duo 2FA token is not available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+ return false;
+ }
+ try {
+ //parse the claimset here, so parsing only has to happen once, and we fail fast on error (e.g. bad JSON)
+ claimSet = token.getJWTClaimsSet();
+ } catch (final ParseException e) {
+ 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;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+ log.info("{} Validating token signature for subject '{}'",getLogPrefix(),claimSet.getSubject());
+
+ //only supports HMAC signatures. Asymmetric or 'none' are not allowed.
+
+ if (token instanceof PlainJWT || JWSAlgorithm.NONE == token.getHeader().getAlgorithm()) {
+
+ log.error("{} Invalid token. Token must be signed using one of the supported algorithms '{}'",
+ getLogPrefix(),SUPPORTED_SIGNATURE_FAMILY);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+
+ } else if (token instanceof SignedJWT) {
+ try {
+ if (SUPPORTED_SIGNATURE_FAMILY.contains(((SignedJWT)token).getHeader().getAlgorithm())) {
+ final JWSVerifier verifier = new MACVerifier(duoContext.getIntegration().getSecretKey());
+ if (!((SignedJWT)token).verify(verifier)) {
+ log.error("{} Token signature is invalid for subject '{}' and client '{}'",getLogPrefix(),
+ claimSet.getSubject(),integration.getClientId());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+ log.debug("{} Token signature is valid; using algorithm '{}' for client '{}'",
+ getLogPrefix(),SUPPORTED_SIGNATURE_FAMILY, integration.getClientId());
+ } else {
+ log.error("{} Invalid token. Token signature algorithm not supported, token algorithm '{}',"
+ + " supported algorithms '{}', for client '{}'",getLogPrefix(),
+ ((SignedJWT)token).getHeader().getAlgorithm().getName(),
+ SUPPORTED_SIGNATURE_FAMILY,integration.getClientId());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+ } catch (final IllegalStateException | JOSEException e) {
+ log.error("{} Unable to validate token using algorithms '{}' for client '{}'",getLogPrefix(),
+ SUPPORTED_SIGNATURE_FAMILY,integration.getClientId(),e);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+ }
+ //all good.
+ }
+
+}
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 9df31aa..cc5d62d 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
@@ -62,21 +62,13 @@
<bean id="ValidateDuoResponseState" scope="prototype"
class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoResponseState" />
+
+ <bean id="ValidateTokenSignature" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenSignature" />
+
+ <bean id="ValidateTokenClaims" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenClaims" />
- <bean id="ValidateDuoTokenAudience" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAudience" />
-
- <bean id="ValidateDuoTokenIssuer" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenIssuer" />
-
- <bean id="ValidateDuoTokenExpirationTime" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenExpirationTime" />
-
- <bean id="ValidateDuoTokenAuthenticationTime" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAuthenticationTime" />
-
- <bean id="ValidateDuoTokenIssuedAt" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenIssuedAt" />
<bean id="ExchangeCodeForDuoToken" scope="prototype"
class="net.shibboleth.idp.plugin.authn.duo.impl.ExchangeCodeForDuoToken" />
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
index 3015391..c4cc3f3 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
@@ -34,11 +34,8 @@
<action-state id="ExchangeCodeForDuoToken">
<evaluate expression="ExchangeCodeForDuoToken"/>
<!-- validate the token and set principal -->
- <evaluate expression="ValidateDuoTokenAudience"/>
- <evaluate expression="ValidateDuoTokenIssuer"/>
- <evaluate expression="ValidateDuoTokenExpirationTime"/>
- <evaluate expression="ValidateDuoTokenAuthenticationTime"/>
- <evaluate expression="ValidateDuoTokenIssuedAt"/>
+ <evaluate expression="ValidateTokenSignature"/>
+ <evaluate expression="ValidateTokenClaims"/>
<!-- final validation of the response status to build an authn result -->
<evaluate expression="ValidateDuoTokenAuthenticationResult"/>
<evaluate expression="'proceed'" />
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 57138c1..abe125a 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
@@ -20,8 +20,11 @@ package net.shibboleth.idp.plugin.authn.duo.impl;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+import java.text.ParseException;
import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -34,21 +37,36 @@ import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
import com.codahale.metrics.MetricRegistry;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.KeyLengthException;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
import net.shibboleth.idp.plugin.authn.duo.model.DuoAccessDevice;
import net.shibboleth.idp.plugin.authn.duo.model.DuoApplication;
import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthContext;
import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shibboleth.idp.profile.RequestContextBuilder;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
-import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
/**
* Abstract class for Duo 2FA OIDC Action tests.
*/
@@ -94,8 +112,46 @@ public abstract class AbstractDuoActionTest {
}
/**
- * Create a dummy {@link DuoAuthToken}.
+ * 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 apiHost TODO
+ * @param factor TODO
+ * @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 String apiHost,
+ @Nonnull final String factor) {
+
+ final String jwtJson = createJWTJson(authResult,authResultMessage,aud,
+ exp, 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 signed Duo JWT token.
*
+ * @param headerJson the header that defines the crypto params.
+ * @param secret the secret used to sign the JWT.
* @param authResult the authentication result e.g. allow.
* @param authResultMessage a user friendly result message.
* @param aud audience
@@ -106,43 +162,178 @@ public abstract class AbstractDuoActionTest {
* @param factor TODO
* @return the duo auth token.
*/
- protected DuoAuthToken createDummyToken(@Nonnull final String authResult,
+ protected JWT createSignedDummyToken(@Nonnull final String headerJson,
+ @Nonnull final String secret,
+ @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 String apiHost,
+ @Nonnull final String factor) {
+
+ final String jwtJson = createJWTJson(authResult,authResultMessage,aud,
+ exp, iat, authTime, apiHost, factor);
+
+ try {
+ final JWSSigner signer = new MACSigner(secret);
+ final JWSHeader header = JWSHeader.parse(headerJson);
+ final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+ final SignedJWT jwt = new SignedJWT(header,claims);
+ jwt.sign(signer);
+ //test the claims exist by calling it
+ jwt.getJWTClaimsSet();
+ return jwt;
+ } catch (final ParseException | JOSEException e) {
+ fail("Error creating the Mock JWT",e);
+ }
+ fail();
+ return null;
+ }
+
+ /**
+ * Create a signed Duo JWT token using the supplied (not computed) signature.
+ * Can be used to generate a token with an invalid signature.
+ *
+ * @param headerJson the header that defines the crypto params.
+ * @param signatureBase64 the base64 encoded signature.
+ * @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 apiHost TODO
+ * @param factor TODO
+ * @return the duo auth token.
+ */
+ protected JWT createSignedDummyTokenFromGivenSignature(@Nonnull final String headerJson,
+ @Nonnull final String signatureBase64,
+ @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 String apiHost,
+ @Nonnull final String factor) {
+
+ final String jwtJson = createJWTJson(authResult,authResultMessage,aud,
+ exp, iat, authTime, apiHost, factor);
+
+ try {
+ final JWT jwt = new SignedJWT(new Base64URL(Base64Support.encodeURLSafe(headerJson.getBytes())),
+ new Base64URL(Base64Support.encodeURLSafe(jwtJson.getBytes())), new Base64URL(signatureBase64));
+ //test the claims exist by calling it
+ jwt.getJWTClaimsSet();
+ return jwt;
+ } catch (final ParseException | EncodingException e) {
+ fail("Error creating the Mock JWT",e);
+ }
+ fail();
+ return null;
+ }
+
+ /**
+ * Create a signed Duo JWT token which is NOT signed. Allows testing of unsupported algorithms.
+ *
+ * @param headerJson the header that defines the crypto params.
+ * @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 apiHost TODO
+ * @param factor TODO
+ * @return the duo auth token.
+ */
+ protected JWT createUnsignedSignedDummyToken(@Nonnull final String headerJson,
+ @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 String apiHost,
+ @Nonnull final String factor) {
+
+ final String jwtJson = createJWTJson(authResult,authResultMessage,aud,
+ exp, iat, authTime, apiHost, factor);
+
+ try {
+ final JWSHeader header = JWSHeader.parse(headerJson);
+ final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+ final SignedJWT jwt = new SignedJWT(header,claims);
+ //test the claims exist by calling it
+ jwt.getJWTClaimsSet();
+ return jwt;
+ } catch (final ParseException e) {
+ fail("Error creating the Mock JWT",e);
+ }
+ fail();
+ return null;
+ }
+
+ /**
+ * Create a JWT token in JSON using the claims arguments.
+ *
+ * @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 apiHost the api host.
+ * @param factor the factor.
+ * @return the duo auth token.
+ */
+ private String createJWTJson(@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 String apiHost,
@Nonnull final String factor) {
- return DuoAuthToken.builder()
- .withIss("https://"+apiHost+"/oauth/v1/token")
- .withSub("jdoe")
- .withAud(aud)
- .withExp(Math.toIntExact(exp.getEpochSecond()))
- .withIat((double)iat.getEpochSecond())
- .withAuthResultStatusMessage(authResultMessage)
- .withAuthResultStatus(authResult)
- .withAuthResult(authResult)
- .withAuthContext(DuoAuthContext.builder()
- .withResult("success")
- .withTimestamp(1590070939)
- .withAuthDevice(DuoAuthDevice.builder()
- .withIp("192.168.0.1")
- .withName("99999999")
- .build())
- .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b")
- .withEventType("authenticaiton")
- .withReason("user_approved")
- .withAccessDevice(DuoAccessDevice.builder()
- .withIp("192.168.0.1")
- .withHostname("99999999")
- .build())
- .withApplication(DuoApplication.builder()
- .withKey("DIU6GEFXXXXXXX")
- .withName("Test")
- .build())
- .withFactor(factor)
- .withUsername("jdoe")
- .withUserKey("XXXXXXX")
- .build()).withAuthTime(Math.toIntExact(authTime.getEpochSecond()))
- .build();
+ final String jwtJson = "{\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;
}
/**
@@ -186,6 +377,7 @@ public abstract class AbstractDuoActionTest {
/** Add the Duo authentication context to the authentication context.*/
protected void addDuoContext() {
dc = new DuoOIDCAuthenticationContext();
+ dc.setUsername("jdoe");
ac.addSubcontext(dc);
}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudienceTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudienceTest.java
deleted file mode 100644
index 8ff1a14..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudienceTest.java
+++ /dev/null
@@ -1,95 +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.
- */
-
-/*
- * 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.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import org.springframework.webflow.execution.Event;
-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 net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/**
- * Tests for the {@link ValidateDuoTokenAudience} class.
- */
-public class ValidateDuoTokenAudienceTest extends AbstractDuoActionTest {
-
- /** The action to test. */
- private ValidateDuoTokenAudience action;
-
- @BeforeMethod
- public void setUp() throws Exception {
- super.setup();
- action = new ValidateDuoTokenAudience();
-
- }
-
- /* Test Duo 2FA token response audience validation, success.*/
- @Test
- public final void testDoExecuteSuccess() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().plus(1,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);
-
- }
-
- /* Test Duo 2FA token response audience validation, wrong audience.*/
- @Test
- public final void testDoExecuteWrongAudience() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful","WRONG",
- Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertEventId(event,AuthnEventIds.NO_CREDENTIALS);
-
- }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
index acb1344..9dd4804 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
@@ -24,6 +24,7 @@ import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import java.security.Principal;
+import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -72,7 +73,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
dc.setUsername("jdoe");
action.initialize();
@@ -111,7 +112,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
classifiedMessages.put(AuthnEventIds.ACCOUNT_LOCKED, Arrays.asList(new String[] {"deny"}));
action.setClassifiedMessages(classifiedMessages);
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Account locked",CLIENT_ID,
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Account locked",CLIENT_ID,
Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
dc.setUsername("jdoe");
action.initialize();
@@ -135,7 +136,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.INVALID_CREDENTIALS);
+ assertEventId(event, AuthnEventIds.INVALID_AUTHN_CTX);
}
/**
@@ -151,7 +152,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
//made up fail message, will terminate on the duo site in reality.
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Login Failed",CLIENT_ID,
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Login Failed",CLIENT_ID,
Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
dc.setUsername("jdoe");
action.initialize();
@@ -170,7 +171,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
dc.setUsername("jdoe");
@@ -178,9 +179,14 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
DuoOIDCAuthenticationContext duoContext =
prc.getSubcontext(AuthenticationContext.class).getSubcontext(DuoOIDCAuthenticationContext.class);
- List<Principal> p = new ArrayList<>();
- if ("duo_push".equals(duoContext.getAuthToken().getAuthContext().getFactor())){
- p.add(new AuthnContextClassRefPrincipal("http://example.com/duoPush"));
+ final List<Principal> p = new ArrayList<>();
+ try {
+ if ("duo_push".equals((String)duoContext.getAuthToken().getJWTClaimsSet()
+ .getJSONObjectClaim("auth_context").get("factor"))){
+ p.add(new AuthnContextClassRefPrincipal("http://example.com/duoPush"));
+ }
+ } catch (final ParseException e) {
+ throw new RuntimeException("Can not find duo factor in mocked duo response",e);
}
return p;
});
@@ -207,7 +213,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
addDuoContext();
addDuoIntegrationToContext();
addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
dc.setUsername("jdoe");
@@ -215,10 +221,16 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
DuoOIDCAuthenticationContext duoContext =
prc.getSubcontext(AuthenticationContext.class).getSubcontext(DuoOIDCAuthenticationContext.class);
- List<Principal> p = new ArrayList<>();
- if ("sms".equals(duoContext.getAuthToken().getAuthContext().getFactor())){
- p.add(new AuthnContextClassRefPrincipal("http://example.com/sms"));
- }
+ final List<Principal> p = new ArrayList<>();
+ try {
+ if ("sms".equals((String)duoContext.getAuthToken().getJWTClaimsSet()
+ .getJSONObjectClaim("auth_context").get("factor"))){
+ p.add(new AuthnContextClassRefPrincipal("http://example.com/sms"));
+ }
+ } catch (final ParseException e) {
+ throw new RuntimeException("Can not find duo factor in mocked duo response",e);
+ }
+
return p;
});
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTimeTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTimeTest.java
deleted file mode 100644
index b8fb569..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTimeTest.java
+++ /dev/null
@@ -1,165 +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 static org.testng.Assert.assertNull;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-
-import org.springframework.webflow.execution.Event;
-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 net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/**
- * Tests for the {@link ValidateDuoTokenAuthenticationTime} action.
- */
-public class ValidateDuoTokenAuthenticationTimeTest extends AbstractDuoActionTest {
-
- /** The action to test. */
- private ValidateDuoTokenAuthenticationTime action;
-
- /**
- * Setup.
- *
- * @throws Exception on error.
- */
- @BeforeMethod
- public void setUp() throws Exception {
- super.setup();
- action = new ValidateDuoTokenAuthenticationTime();
-
- }
-
- /**
- * Test Duo 2FA token response authentication time validation, success.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteSuccess() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- ac.setForceAuthn(true);
- action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
- action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
- dc.setAuthToken(createDummyToken(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 Duo 2FA token response authentication time validation, not forced auth.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteSuccessNotForcedAuth() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- ac.setForceAuthn(false);
- action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
- action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
- dc.setAuthToken(createDummyToken(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 Duo 2FA token response authentication time validation, outside window but not force authn.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteSuccessOutsideLifetimeNotForcedAuthn() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- ac.setForceAuthn(false);
- action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
- action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now().plus(Duration.of(10, ChronoUnit.MINUTES)), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertNull(event);
-
- }
-
- /**
- * Test Duo 2FA token response authentication time validation, outside lifetime window.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteOutsideLifetime() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- ac.setForceAuthn(true);
- action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
- action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now().minus(Duration.of(10, ChronoUnit.MINUTES)), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
- /**
- * Test Duo 2FA token response authentication time validation, in the future.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteInTheFuture() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- ac.setForceAuthn(true);
- action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
- action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now(), Instant.now().plus(Duration.of(10, ChronoUnit.MINUTES)), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTimeTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTimeTest.java
deleted file mode 100644
index 28179d8..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTimeTest.java
+++ /dev/null
@@ -1,130 +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 static org.testng.Assert.assertNull;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import org.springframework.webflow.execution.Event;
-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 net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/** Tests for ValidateDuoTokenExpirationTime class.*/
-public class ValidateDuoTokenExpirationTimeTest extends AbstractDuoActionTest {
-
- /** The action to test. */
- private ValidateDuoTokenExpirationTime action;
-
- @BeforeMethod
- public void setUp() throws Exception {
- super.setup();
- action = new ValidateDuoTokenExpirationTime();
-
- }
-
-
- /**
- * Test Duo 2FA token response expiration time validation, success.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteSuccess() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().plus(1,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);
-
- }
-
- /**
- * Test Duo 2FA token response expiration time validation, expired token.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteExpiredToken() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- //create token, set expiry 1 day in the past.
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().minus(1,ChronoUnit.DAYS),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
- /**
- * Test Duo 2FA token response expiration time validation, within clock skew, success.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteWithinClockSkewSuccess() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
-
- //allow a 1 minute clock skew - should be default
- action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
- //create token, set expiry 50 second in the past.
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().minus(50,ChronoUnit.SECONDS),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertNull(event);
-
- }
-
- /**
- * Test Duo 2FA token response expiration time validation, set zero clock skew expired token.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteSetZeroClockSkewExpiredToken() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- //do not allow a clock skew on the expiry
- action.setClockSkew(Duration.of(0, ChronoUnit.SECONDS));
- //create token, set expiry 1 second in the past.
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().minus(1,ChronoUnit.SECONDS),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAtTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAtTest.java
deleted file mode 100644
index 8772963..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAtTest.java
+++ /dev/null
@@ -1,110 +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 static org.testng.Assert.assertNull;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-
-import org.springframework.webflow.execution.Event;
-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 net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/**
- * Test the {@link ValidateDuoTokenIssuedAt} class.
- */
-public class ValidateDuoTokenIssuedAtTest extends AbstractDuoActionTest {
-
- /** The action to test. */
- private ValidateDuoTokenIssuedAt action;
-
- @BeforeMethod
- public void setUp() throws Exception {
- super.setup();
- action = new ValidateDuoTokenIssuedAt();
-
- }
-
- /**
- * Test Duo 2FA token response token lifetime (issuedAt) validation, success.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteSuccess() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setAuthToken(createDummyToken(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 Duo 2FA token response token lifetime (issuedAt) validation, to far in the past.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteToFarInThePast() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- action.setIatMaxClockSkew(Duration.of(1, ChronoUnit.MINUTES));
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now().minus(3,ChronoUnit.MINUTES), Instant.now(), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
-
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
- /**
- * Test Duo 2FA token response token lifetime (issuedAt) validation, to far in the future.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testDoExecuteToFarInTheFuture() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- action.setIatMaxClockSkew(Duration.of(1, ChronoUnit.MINUTES));
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now(),Instant.now().plus(3,ChronoUnit.MINUTES), Instant.now(), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
-
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
-
- }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuerTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuerTest.java
deleted file mode 100644
index 08978bf..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuerTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import static org.testng.Assert.assertNull;
-
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import org.springframework.webflow.execution.Event;
-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 net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-public class ValidateDuoTokenIssuerTest extends AbstractDuoActionTest {
-
- /** The action to test. */
- private ValidateDuoTokenIssuer action;
-
- @BeforeMethod
- public void setUp() throws Exception {
- super.setup();
- action = new ValidateDuoTokenIssuer();
-
- }
-
- /**
- * Test Duo 2FA token issuer validation, success.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testExecuteSuccesful() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setUsername("jdoe");
-
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().plus(1,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);
- }
-
- /**
- * Test Duo 2FA token issuer validation, different issuer.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testExecuteDifferentIssuer() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setUsername("jdoe");
-
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "different.host.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
- }
-
- /**
- * Test Duo 2FA token issuer validation, no context.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testExecuteNoIntegration() throws ComponentInitializationException {
- addDuoContext();
-
- addAttemptedFlow("authn/DuoOIDC");
- dc.setUsername("jdoe");
-
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "different.host.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
- }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubjectTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubjectTest.java
deleted file mode 100644
index 5d09bc0..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubjectTest.java
+++ /dev/null
@@ -1,83 +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 static org.testng.Assert.assertNull;
-
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import org.springframework.webflow.execution.Event;
-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 net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-class ValidateDuoTokenSubjectTest extends AbstractDuoActionTest {
-
- /** The action to test. */
- private ValidateDuoTokenSubject action;
-
- @BeforeMethod
- public void setUp() throws Exception {
- super.setup();
- action = new ValidateDuoTokenSubject();
-
- }
-
- /**
- * Test Duo 2FA token subject validation, success.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testExecuteSuccesful() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setUsername("jdoe");
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().plus(1,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);
- }
-
- /**
- * Test Duo 2FA token subject validation, failure.
- *
- * @throws ComponentInitializationException on error.
- */
- @Test
- public final void testExecuteInvalidSubject() throws ComponentInitializationException {
- addDuoContext();
- addDuoIntegrationToContext();
- addAttemptedFlow("authn/DuoOIDC");
- dc.setUsername("different-user");
- dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
- Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
- action.initialize();
- final Event event = action.execute(src);
- // null event is success.
- assertEventId(event, AuthnEventIds.INVALID_CREDENTIALS);
- }
-
-}
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
new file mode 100644
index 0000000..321a546
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
@@ -0,0 +1,351 @@
+/*
+ * 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.
+ */
+
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+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 ValidateDuoTokenAudience} class.
+ */
+public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
+
+ /** The action to test. */
+ private ValidateTokenClaims action;
+
+ @BeforeMethod
+ 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.
+ *
+ * @throws ComponentInitializationException 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);
+
+ }
+
+ /**
+ * 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.
+ *
+ * @throws ComponentInitializationException on error.
+ */
+ @Test
+ public final void testInvalidSubject() throws ComponentInitializationException {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ //override subject
+ dc.setUsername("wrong-subject");
+
+ 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(),
+ "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.
+ *
+ * @throws ComponentInitializationException on error.
+ */
+ @Test
+ public final void testInvalidIssuedAtFuture() throws ComponentInitializationException {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ 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);
+
+ }
+
+ /**
+ * Validate a token which is outside the authentication lifetime window but authn is not forced.
+ *
+ * @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"));
+ 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/duo/impl/ValidateTokenSignatureTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignatureTest.java
new file mode 100644
index 0000000..44063b0
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignatureTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.
+ */
+
+/*
+ * 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.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.springframework.webflow.execution.Event;
+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 net.shibboleth.idp.saml.audit.impl.AuthnInstantAuditExtractor;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Tests for the {@link ValidateDuoTokenAudience} class.
+ */
+public class ValidateTokenSignatureTest extends AbstractDuoActionTest {
+
+ /** The action to test. */
+ private ValidateTokenSignature action;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ super.setup();
+ action = new ValidateTokenSignature();
+
+ }
+
+ /**
+ * Test Duo 2FA token signature validation with no signature, should fail, we need a signature.
+ *
+ * @throws ComponentInitializationException on error.
+ */
+ @Test
+ public final void testNoneSignature() throws ComponentInitializationException {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ action.initialize();
+ final Event event = action.execute(src);
+ assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+
+ }
+
+ /**
+ * Test Duo 2FA token signature validation with an unsupported signature, should fail.
+ *
+ * @throws ComponentInitializationException on error.
+ */
+ @Test
+ public final void testUnsuportedSignature() throws ComponentInitializationException, EncodingException {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ //unsupported asymmetric RSA algorithm
+ final String headerJson = "{\"typ\": \"JWT\",\"alg\": \"RS256\"}";
+
+ dc.setAuthToken(createUnsignedSignedDummyToken(headerJson,
+ DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ action.initialize();
+ final Event event = action.execute(src);
+ assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+ }
+
+ /**
+ * Test Duo 2FA token signature validation with a valid signature, should succeed.
+ *
+ * @throws ComponentInitializationException on error.
+ */
+ @Test
+ public final void testValidSignature() throws ComponentInitializationException, EncodingException {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ final String headerJson = "{\"typ\": \"JWT\",\"alg\": \"HS256\"}";
+
+ dc.setAuthToken(createSignedDummyToken(headerJson,dc.getIntegration().getSecretKey(),
+ DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now().plus(1,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);
+ }
+
+ /**
+ * Test Duo 2FA token signature validation with an invalid signature, should fail.
+ *
+ * @throws ComponentInitializationException on error.
+ */
+ @Test
+ public final void testInvalidSignature() throws ComponentInitializationException, EncodingException {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ final String headerJson = "{\"typ\": \"JWT\",\"alg\": \"HS256\"}";
+ //bad sig
+ final String signature = "dGhpc2lzbm90Z29pbmd0b3dvcms=";
+
+ dc.setAuthToken(createSignedDummyTokenFromGivenSignature(headerJson,signature,
+ DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ action.initialize();
+ final Event event = action.execute(src);
+ assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+ }
+
+
+ /**
+ * The JWT header is present with a signature algorithm other than none, but the token has not been signed.
+ *
+ * @throws ComponentInitializationException on error.
+ */
+ @Test
+ public final void testSignatureNotPresent() throws ComponentInitializationException, EncodingException {
+ addDuoContext();
+ addDuoIntegrationToContext();
+ addAttemptedFlow("authn/DuoOIDC");
+
+ final String headerJson = "{\"typ\": \"JWT\",\"alg\": \"HS256\"}";
+
+ dc.setAuthToken(createUnsignedSignedDummyToken(headerJson,
+ DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(),
+ "api.duosecurity.com", "duo_push"));
+ action.initialize();
+ final Event event = action.execute(src);
+ assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+ }
+
+
+
+}
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 0b6929a..17ca92d 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
@@ -19,11 +19,24 @@ package net.shibboleth.idp.plugin.authn.mock;
import static java.lang.String.format;
+import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import javax.annotation.Nonnull;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+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.DuoOIDCIntegration;
@@ -34,6 +47,8 @@ import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
/**
* Mock a Duo client which is not available (unhealthy).
@@ -68,35 +83,72 @@ public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
}
@Override
- public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code,
+ public JWT exchangeAuthorizationCodeFor2FAResult(final String code,
final String username) throws DuoClientException {
- return DuoAuthToken.builder()
- .withIss("https://api.duosecurity.com/oauth/v1/token")
- .withSub(SUB)
- .withAud(integration.getClientId())
- .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
- .withIat((double)Instant.now().getEpochSecond())
- .withAuthResultStatusMessage("Login Failed")
- .withAuthResultStatus("fail")
- .withAuthResult("fail")
- .withAuthContext(DuoAuthContext.builder()
- .withResult("fail").
- withTimestamp(1590070939)
- .withAuthDevice(DuoAuthDevice.builder()
- .withIp("192.168.0.1").
- withName("99999999")
- .build())
- .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
- withEventType("authenticaiton")
- .withReason("user_approved").
- withAccessDevice(DuoAccessDevice.builder()
- .withIp("192.168.0.1")
- .withHostname("localhost")
- .build()).
- withApplication(DuoApplication.builder()
- .withKey("DIU6GEFXXXXXXX")
- .withName("Test").build()).
- withFactor("duo_push").withUsername("jdoe").withUserKey("XXXXXXX").build()).build();
+
+ final String jwtJson = "{\n" +
+ " \"iss\": \"https://"+integration.getAPIHost()+"oauth/v1/token\",\n" +
+ " \"sub\": \""+SUB+"\",\n" +
+ " \"preferred_username\": \"philsmart\",\n" +
+ " \"aud\": \""+integration.getClientId()+"\",\n" +
+ " \"exp\": "+new String(Math.toIntExact(Instant.now().
+ plus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" +
+ " \"iat\": "+Long.toString(Instant.now().getEpochSecond()))+",\n" +
+ " \"auth_time\": "+Math.toIntExact(Instant.now().getEpochSecond())+",\n" +
+ " \"auth_result\": {\n" +
+ " \"status_msg\": \"Login Failed\",\n" +
+ " \"status\": \"fail\",\n" +
+ " \"result\": \"fail\"\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\": \"duo_push\",\n" +
+ " \"user\": {\n" +
+ " \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" +
+ " \"name\": \"philsmart\"\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+
+ try {
+ //re-sign the token using the client secret
+ final JWSSigner signer = new MACSigner(integration.getSecretKey());
+ //FIXME: needs to be HS512 (or not?)
+ final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+ final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+ final SignedJWT signedJWT = new SignedJWT(header,claims);
+ signedJWT.sign(signer);
+ return signedJWT;
+ } catch (final ParseException | JOSEException e) {
+ throw new DuoClientException(e);
+ }
+
}
}
\ 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 ac61765..8b569d3 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
@@ -19,11 +19,25 @@ package net.shibboleth.idp.plugin.authn.mock;
import static java.lang.String.format;
+import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import javax.annotation.Nonnull;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.KeyLengthException;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+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.DuoOIDCIntegration;
@@ -34,6 +48,8 @@ import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
/**
* Mock a Duo client which is available and returns a valid response.
@@ -65,41 +81,72 @@ public class MockDuoOIDCClient_OK implements DuoOIDCClient{
}
@Override
- public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code,
+ public JWT exchangeAuthorizationCodeFor2FAResult(final String code,
final String username) throws DuoClientException {
- return DuoAuthToken.builder()
- .withIss("https://"+integration.getAPIHost()+"/oauth/v1/token")
- .withSub(SUB)
- .withAud(integration.getClientId())
- .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
- .withIat((double)Instant.now().getEpochSecond())
- .withAuthResultStatusMessage("Login Successful")
- .withAuthResultStatus("allow")
- .withAuthResult("allow")
- .withAuthContext(DuoAuthContext.builder()
- .withResult("success")
- .withTimestamp(1590070939)
- .withAuthDevice(DuoAuthDevice.builder()
- .withIp("192.168.0.1").
- withName("99999999")
- .build())
- .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
- withEventType("authenticaiton")
- .withReason("user_approved")
- .withAccessDevice(DuoAccessDevice.builder()
- .withIp("192.168.0.1")
- .withHostname("localhost")
- .build())
- .withApplication(DuoApplication.builder()
- .withKey("DIU6GEFXXXXXXX")
- .withName("Test")
- .build()).
- withFactor("duo_push")
- .withUsername("jdoe")
- .withUserKey("XXXXXXX")
- .build()).
- withAuthTime(Math.toIntExact(Instant.now().getEpochSecond()))
- .build();
+
+ final String jwtJson = "{\n" +
+ " \"iss\": \"https://"+integration.getAPIHost()+"/oauth/v1/token\",\n" +
+ " \"sub\": \""+SUB+"\",\n" +
+ " \"preferred_username\": \"jdoe\",\n" +
+ " \"aud\": \""+integration.getClientId()+"\",\n" +
+ " \"exp\": "+new String(Math.toIntExact(Instant.now().
+ plus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" +
+ " \"iat\": "+Long.toString(Instant.now().getEpochSecond()))+",\n" +
+ " \"auth_time\": "+Math.toIntExact(Instant.now().getEpochSecond())+",\n" +
+ " \"auth_result\": {\n" +
+ " \"status_msg\": \"Login Successful\",\n" +
+ " \"status\": \"allow\",\n" +
+ " \"result\": \"allow\"\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\": \"duo_push\",\n" +
+ " \"user\": {\n" +
+ " \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" +
+ " \"name\": \"philsmart\"\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+
+ try {
+ //re-sign the token using the client secret
+ final JWSSigner signer = new MACSigner(integration.getSecretKey());
+ //FIXME: needs to be HS512 (or not?)
+ final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+ final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+ final SignedJWT signedJWT = new SignedJWT(header,claims);
+ signedJWT.sign(signer);
+
+ return signedJWT;
+ } catch (final ParseException | JOSEException e) {
+ throw new DuoClientException(e);
+ }
}
}
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 fd4c82d..f81b732 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
@@ -19,11 +19,25 @@ package net.shibboleth.idp.plugin.authn.mock;
import static java.lang.String.format;
+import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import javax.annotation.Nonnull;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.KeyLengthException;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+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.DuoOIDCIntegration;
@@ -34,6 +48,8 @@ import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
/**
* Mock a Duo client which is available and returns a valid response.
@@ -67,41 +83,72 @@ public class MockDuoOIDCClient_OK_OLD_AUTH_TIME implements DuoOIDCClient{
}
@Override
- public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code,
+ public JWT exchangeAuthorizationCodeFor2FAResult(final String code,
final String username) throws DuoClientException {
- return DuoAuthToken.builder()
- .withIss("https://"+integration.getAPIHost()+"/oauth/v1/token")
- .withSub(SUB)
- .withAud(integration.getClientId())
- .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
- .withIat((double)Instant.now().getEpochSecond())
- .withAuthResultStatusMessage("Login Successful")
- .withAuthResultStatus("allow")
- .withAuthResult("allow")
- .withAuthContext(DuoAuthContext.builder()
- .withResult("success")
- .withTimestamp(1590070939)
- .withAuthDevice(DuoAuthDevice.builder()
- .withIp("192.168.0.1").
- withName("99999999")
- .build())
- .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
- withEventType("authenticaiton")
- .withReason("user_approved")
- .withAccessDevice(DuoAccessDevice.builder()
- .withIp("192.168.0.1")
- .withHostname("localhost")
- .build())
- .withApplication(DuoApplication.builder()
- .withKey("DIU6GEFXXXXXXX")
- .withName("Test")
- .build()).
- withFactor("duo_push")
- .withUsername("jdoe")
- .withUserKey("XXXXXXX")
- .build())
- .withAuthTime(Math.toIntExact(Instant.now().minus(10,ChronoUnit.MINUTES).getEpochSecond()))
- .build();
+
+ final String jwtJson = "{\n" +
+ " \"iss\": \"https://"+integration.getAPIHost()+"/oauth/v1/token\",\n" +
+ " \"sub\": \""+SUB+"\",\n" +
+ " \"preferred_username\": \"jdoe\",\n" +
+ " \"aud\": \""+integration.getClientId()+"\",\n" +
+ " \"exp\": "+new String(Math.toIntExact(Instant.now().
+ plus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" +
+ " \"iat\": "+Long.toString(Instant.now().getEpochSecond()))+",\n" +
+ " \"auth_time\": "+Math.toIntExact(Instant.now().minus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" +
+ " \"auth_result\": {\n" +
+ " \"status_msg\": \"Login Successful\",\n" +
+ " \"status\": \"allow\",\n" +
+ " \"result\": \"allow\"\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\": \"duo_push\",\n" +
+ " \"user\": {\n" +
+ " \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" +
+ " \"name\": \"philsmart\"\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+
+ try {
+ //re-sign the token using the client secret
+ final JWSSigner signer = new MACSigner(integration.getSecretKey());
+ //FIXME: needs to be HS512 (or not?)
+ final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+ final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+ final SignedJWT signedJWT = new SignedJWT(header,claims);
+ signedJWT.sign(signer);
+ return signedJWT;
+ } catch (final ParseException | JOSEException e) {
+ throw new DuoClientException(e);
+ }
+
}
}
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 a626998..c8e0fa0 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
@@ -19,11 +19,24 @@ package net.shibboleth.idp.plugin.authn.mock;
import static java.lang.String.format;
+import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import javax.annotation.Nonnull;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+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.DuoOIDCIntegration;
@@ -34,6 +47,8 @@ import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
/**
* Mock a Duo client which returns an unknown response for the health check and a login failure in the
@@ -65,36 +80,70 @@ public class MockDuoOIDCClient_UNKNOWN implements DuoOIDCClient{
}
@Override
- public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code,
+ public JWT exchangeAuthorizationCodeFor2FAResult(final String code,
final String username) throws DuoClientException {
- return DuoAuthToken.builder()
- .withIss("https://api.duosecurity.com/oauth/v1/token")
- .withSub(SUB)
- .withAud(integration.getClientId())
- .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
- .withIat((double)Instant.now().getEpochSecond())
- .withAuthResultStatusMessage("Login Failed")
- .withAuthResultStatus("fail")
- .withAuthResult("fail")
- .withAuthContext(DuoAuthContext.builder()
- .withResult("fail").
- withTimestamp(1590070939)
- .withAuthDevice(DuoAuthDevice.builder()
- .withIp("192.168.0.1").
- withName("99999999")
- .build())
- .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
- withEventType("authenticaiton")
- .withReason("user_approved").
- withAccessDevice(DuoAccessDevice.builder()
- .withIp("192.168.0.1")
- .withHostname("localhost")
- .build()).
- withApplication(DuoApplication.builder()
- .withKey("DIU6GEFXXXXXXX")
- .withName("Test").build()).
- withFactor("duo_push").withUsername("jdoe").withUserKey("XXXXXXX").build()).
- withAuthTime(Math.toIntExact(Instant.now().getEpochSecond())).build();
+ final String jwtJson = "{\n" +
+ " \"iss\": \"https://"+integration.getAPIHost()+"oauth/v1/token\",\n" +
+ " \"sub\": \""+SUB+"\",\n" +
+ " \"preferred_username\": \"philsmart\",\n" +
+ " \"aud\": \""+integration.getClientId()+"\",\n" +
+ " \"exp\": "+new String(Math.toIntExact(Instant.now().
+ plus(10,ChronoUnit.MINUTES).getEpochSecond())+",\n" +
+ " \"iat\": "+Long.toString(Instant.now().getEpochSecond()))+",\n" +
+ " \"auth_time\": "+Math.toIntExact(Instant.now().getEpochSecond())+",\n" +
+ " \"auth_result\": {\n" +
+ " \"status_msg\": \"Login Failed\",\n" +
+ " \"status\": \"fail\",\n" +
+ " \"result\": \"fail\"\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\": \"duo_push\",\n" +
+ " \"user\": {\n" +
+ " \"key\": \"DUGL8U46QGJSOUJWG59W\",\n" +
+ " \"name\": \"philsmart\"\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+
+ try {
+ //re-sign the token using the client secret
+ final JWSSigner signer = new MACSigner(integration.getSecretKey());
+ //FIXME: needs to be HS512 (or not?)
+ final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+ final JWTClaimsSet claims = JWTClaimsSet.parse(jwtJson);
+ final SignedJWT signedJWT = new SignedJWT(header,claims);
+ signedJWT.sign(signer);
+ return signedJWT;
+ } catch (final ParseException | JOSEException e) {
+ throw new DuoClientException(e);
+ }
}
}
\ No newline at end of file
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 4f6c3d4..97fd948 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
@@ -1,25 +1,42 @@
package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
+import java.text.ParseException;
+import java.util.Date;
import java.util.List;
+import java.util.function.BiFunction;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import com.duosecurity.Client;
import com.duosecurity.exception.DuoException;
import com.duosecurity.model.HealthCheckResponse;
import com.duosecurity.model.Token;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.PlainHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+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.DuoOIDCIntegration;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAccessDevice;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoApplication;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthContext;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
-import net.shibboleth.idp.plugin.authn.duo.model.DuoAuthToken;
import net.shibboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
import net.shibboleth.utilities.java.support.logic.Constraint;
/**
@@ -30,14 +47,20 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
*/
final class DuoSDKClientAdaptor implements DuoOIDCClient{
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DuoSDKClientAdaptor.class);
+
/** The wrapped Duo native client.*/
@Nonnull private Client client;
/** Function to map the native Duo {@link HealthCheckResponse} object to the interface {@link DuoHealthCheck} object.*/
@Nonnull private Function<HealthCheckResponse,DuoHealthCheck> healthCheckResponseConverter;
- /** Function to map the native Duo {@link Token} object to the interface {@link DuoAuthToken} object.*/
- @Nonnull private Function<Token, DuoAuthToken> tokenResponseConverter;
+ /** Function to map the native Duo {@link Token} object to the interface {@link JWT} object.*/
+ @Nonnull private BiFunction<Token, DuoOIDCIntegration, JWT> tokenResponseConverter;
+
+ /** Save off the integration to help generate the JWT.*/
+ @Nonnull private DuoOIDCIntegration duoIntegration;
/**
@@ -51,9 +74,10 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
*/
protected DuoSDKClientAdaptor(@Nonnull final DuoOIDCIntegration integration, @Nullable List<String> caCerts)
throws DuoClientException {
- Constraint.isNotNull(integration,"Duo SDK Client requires a non-null Duo Integration");
+ duoIntegration = Constraint.isNotNull(integration,"Duo SDK Client requires a non-null Duo Integration");
healthCheckResponseConverter = new DefaultHealthCheckResponseConverter();
tokenResponseConverter = new DefaultTokenResponseConverter();
+ log.info("Secret has size '{}'",integration.getSecretKey().getBytes().length);
try {
if (caCerts == null) {
client = new Client(integration.getClientId(), integration.getSecretKey(),
@@ -114,7 +138,7 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
/** {@inheritDoc} */
@Override
- @Nonnull public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code,
+ @Nonnull public JWT exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code,
@Nonnull final String username)
throws DuoClientException {
try {
@@ -122,7 +146,11 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
if (token == null) {
throw new DuoClientException("Duo token was null");
}
- return tokenResponseConverter.apply(token);
+ final JWT tokenAsJWT = tokenResponseConverter.apply(token,duoIntegration);
+ if (tokenAsJWT == null) {
+ throw new DuoClientException("Duo token could not be converted to a JWT token");
+ }
+ return tokenAsJWT;
} catch (final DuoException e) {
//wrap duo specific exception.
throw new DuoClientException(e);
@@ -147,49 +175,40 @@ final class DuoSDKClientAdaptor implements DuoOIDCClient{
}
}
- //TODO: optional parts of the builder e.g. city are not being built optionally
- //TODO: no null checks on nested classes?
- //TODO: builders are a marginal mess?
- /** Default Duo token converter.*/
- private class DefaultTokenResponseConverter implements Function<Token, DuoAuthToken>{
+ /**
+ * Default Duo token converter. Creates a JWT by converting the Duo token (back) to a JSON String which it uses to
+ * create a **signed** JWT. As the signature is not returned from the Duo SDK, a new HMAC signature is computed
+ * using the integrations secret key - the flow requires a signed JWT.
+ */
+ private class DefaultTokenResponseConverter implements BiFunction<Token, DuoOIDCIntegration, JWT>{
+
+ /** JSON object mapper. */
+ @Nonnull private ObjectMapper objectMapper;
+
+ /** Constructor. */
+ private DefaultTokenResponseConverter() {
+ objectMapper = new ObjectMapper();
+ }
+
@Override
- public DuoAuthToken apply(Token t) {
- return DuoAuthToken.builder().
- withIss(t.getIss()).
- withSub(t.getSub()).
- withAud(t.getAud()).
- withExp(t.getExp()).
- withIat(t.getIat()).
- withAuthResultStatusMessage(t.getAuth_result().getStatus_msg()).
- withAuthResultStatus(t.getAuth_result().getStatus()).
- withAuthResult(t.getAuth_result().getResult())
- .withAuthContext(DuoAuthContext.builder().
- withResult(t.getAuth_context().getResult()).
- withTimestamp(t.getAuth_context().getTimestamp()).
- withAuthDevice(DuoAuthDevice.builder().
- withIp(t.getAuth_context().getAuth_device().getIp()).
- withName(t.getAuth_context().getAuth_device().getName()).
- withCity(t.getAuth_context().getAuth_device().getLocation().getCity()).
- withCountry(t.getAuth_context().getAuth_device().getLocation().getCountry()).
- withState(t.getAuth_context().getAuth_device().getLocation().getState()).build()).
- withTxid(t.getAuth_context().getTxid()).
- withEventType(t.getAuth_context().getEvent_type()).
- withReason(t.getAuth_context().getReason()).
- withAccessDevice(DuoAccessDevice.builder().
- withIp(t.getAuth_context().getAccess_device().getIp()).
- withHostname(t.getAuth_context().getAccess_device().getHostname()).
- withCity(t.getAuth_context().getAccess_device().getLocation().getCity()).
- withCountry(t.getAuth_context().getAccess_device().getLocation().getCountry()).
- withState(t.getAuth_context().getAccess_device().getLocation().getState()).build()).
- withApplication(DuoApplication.builder().withKey(t.getAuth_context().getApplication().getKey()).
- withName(t.getAuth_context().getApplication().getName()).build()).
- withFactor(t.getAuth_context().getFactor()).
- withUsername(t.getAuth_context().getUser().getName()).
- withUserKey(t.getAuth_context().getUser().getKey()).build()).
- withAuthTime(t.getAuth_time()).
- withPreferredUsername(t.getPreferred_username()).build();
+ @Nullable public JWT apply(final Token t, final DuoOIDCIntegration integ) {
+ try {
+ final String duoTokenAsJson = objectMapper.writeValueAsString(t);
+ //re-sign the token using the client secret
+ final JWSSigner signer = new MACSigner(integ.getSecretKey());
+ //FIXME: needs to be HS512 (or not?)
+ final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).build();
+ final JWTClaimsSet claims = JWTClaimsSet.parse(duoTokenAsJson);
+ final SignedJWT signedJWT = new SignedJWT(header,claims);
+ signedJWT.sign(signer);
+ return signedJWT;
+ } catch (final JsonProcessingException | ParseException | JOSEException e) {
+ log.error("Could not convert Duo Token to a Nimbus JWT Token",e);
+ return null;
+ }
}
+
}
diff --git a/pom.xml b/pom.xml
index e038192..78c8fff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,22 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
+
+ <!-- TODO REMOVE -->
+
+ <dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ <version>9.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ </dependency>
</dependencies>
<dependencyManagement>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list