[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-42 - credential policies are not evaluated for all keys
Phil Smart
philip.smart at jisc.ac.uk
Mon Jan 27 15:42:51 UTC 2025
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch main
in repository java-idp-plugin-webauthn.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-webauthn.git;a=commit;h=8ee5d6ddfa320443c2af54252bb90ba362eced25
The following commit(s) were added to refs/heads/main by this push:
new 8ee5d6d JWEBAUTHN-42 - credential policies are not evaluated for all keys
8ee5d6d is described below
commit 8ee5d6ddfa320443c2af54252bb90ba362eced25
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Mon Jan 27 15:42:47 2025 +0000
JWEBAUTHN-42 - credential policies are not evaluated for all keys
- Added a injectable strategy to find the userHandle used to filter
registered credentials to that which signed the assertion.
- The default strategy uses either the userHandle from the assertion
itself, or the userId stored in the context (which came from the initial
username to credential lookup).
https://shibboleth.atlassian.net/browse/JWEBAUTHN-42
---
.../authn/webauthn/impl/CheckCredentialPolicy.java | 61 +++--
.../impl/DefaultUserHandleLookupStrategy.java | 78 +++++++
.../webauthn/impl/LookupRegisteredCredentials.java | 4 +-
.../YubicoWebauthnAuthenticationClientTest.java | 10 +-
.../webauthn/flow/AbstractWebAuthnFlowTest.java | 19 +-
.../webauthn/flow/TestAdminManagementFlow.java | 2 +-
.../authn/webauthn/flow/TestPasswordlessFlow.java | 61 ++++-
...ow.java => TestPasswordlessFlowWithPolicy.java} | 91 +++++---
.../authn/webauthn/flow/TestRegistrationFlow.java | 2 +-
.../authn/webauthn/flow/TestSecondFactorFlow.java | 2 +-
.../authn/webauthn/flow/TestUsernameslessFlow.java | 17 +-
.../flow/TestUsernameslessFlowWithPolicy.java | 4 +-
...essWithPolicyApplicationContextInitializer.java | 50 ++++
.../authn/webauthn/impl/AbstractWebAuthnTest.java | 2 +-
.../webauthn/impl/CheckCredentialPolicyTest.java | 253 +++++++++++++++++++++
...kupRegisteredCredentialsFromUserHandleTest.java | 8 +-
.../impl/ValidateWebAuthnAssertionTest.java | 4 +-
.../policy/impl/RejectAllCredentialPolicyRule.java | 35 +++
.../webauthn/storage/impl/MockAuthenticator.java | 12 +-
.../test-beans-authentication-reject-policy.xml | 31 +++
20 files changed, 662 insertions(+), 84 deletions(-)
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckCredentialPolicy.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckCredentialPolicy.java
index a7e4803..663c936 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckCredentialPolicy.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckCredentialPolicy.java
@@ -15,7 +15,8 @@
package net.shibboleth.idp.plugin.authn.webauthn.impl;
import java.util.Collection;
-import java.util.Optional;
+import java.util.List;
+import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -43,6 +44,7 @@ import net.shibboleth.idp.plugin.authn.webauthn.storage.WebAuthnCredentialReposi
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
@@ -62,6 +64,9 @@ public class CheckCredentialPolicy extends AbstractWebAuthnAction<WebAuthnAuthen
@NonnullBeforeExec
private PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion;
+ /** Get the userHandle to help find the credential used to sign the assertion.*/
+ @NonnullAfterInit private Function<ProfileRequestContext, byte[]> userHandleLookupStrategy;
+
/** The credential policy to check.*/
@Nullable private CredentialPolicy credentialPolicy;
@@ -77,6 +82,7 @@ public class CheckCredentialPolicy extends AbstractWebAuthnAction<WebAuthnAuthen
protected CheckCredentialPolicy() {
super(new ChildContextLookup<>(WebAuthnAuthenticationContext.class).
compose(new ChildContextLookup<>(AuthenticationContext.class)));
+ userHandleLookupStrategy = new DefaultUserHandleLookupStrategy();
}
/**
@@ -89,6 +95,18 @@ public class CheckCredentialPolicy extends AbstractWebAuthnAction<WebAuthnAuthen
credentialPolicy = policy;
}
+ /**
+ * Set the strategy to locate the userHandle of the user of the credential used to sign the
+ * assertion.
+ *
+ * @param strategy The user handle lookup strategy to set.
+ */
+ public void setUserHandleLookupStrategy(final Function<ProfileRequestContext, byte[]> strategy) {
+ checkSetterPreconditions();
+ userHandleLookupStrategy = Constraint.isNotNull(strategy,
+ "UserHandle lookup strategy can not be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -133,45 +151,58 @@ public class CheckCredentialPolicy extends AbstractWebAuthnAction<WebAuthnAuthen
// If no policy, nothing can be applied
log.trace("{} No authenticator policy to apply", getLogPrefix());
return;
- }
-
- final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
- if (userHandle.isEmpty()) {
- log.trace("{} UserHandle could not be found in the response",getLogPrefix());
+ }
+
+ final byte[] userHandleBytes = userHandleLookupStrategy.apply(profileRequestContext);
+ if (userHandleBytes == null) {
+ log.debug("{} UserHandle could not be found, policy can not be applied",getLogPrefix());
return;
}
final Collection<EnhancedCredentialRecord> registeredCredentials = context.getExistingCredentials();
- final Optional<EnhancedCredentialRecord> credential = registeredCredentials.stream().filter(cred -> {
+ final ByteArray userHandle = new ByteArray(userHandleBytes);
+ final List<EnhancedCredentialRecord> credentials = registeredCredentials.stream().filter(cred -> {
final CredentialRecord credRecord = cred.getCredentialRecord();
return assertion.getId().equals(credRecord.getCredential().getCredentialId()) &&
- userHandle.get().equals(credRecord.getUserIdentity().getId());
- }).findFirst();
+ userHandle.equals(credRecord.getUserIdentity().getId());
+ }).toList();
- if (credential.isEmpty()) {
- log.trace("{} UserHandle '{}' has no registered credential",getLogPrefix(), userHandle.get().getHex());
+ if (credentials.isEmpty()) {
+ log.trace("{} UserHandle '{}' has no registered credential, policy can not be applied",getLogPrefix(),
+ userHandle.getHex());
+ return;
+ }
+
+ // Reject if more than one credential that matches the credentialID and userHandle in the assertion.
+ if (credentials.size() != 1) {
+ log.debug("{} Credential ID '{}' for userHandle '{}' has more than one registered credential, "
+ + "policy can not be applied, rejecting",getLogPrefix(), assertion.getId(),
+ userHandle.getHex());
+ authnContext.ensureSubcontext(AuthenticationErrorContext.class).getClassifiedErrors().add(
+ WebAuthnAuthenticationEventIds.CREDENTIAL_POLICY_REJECTION);
+ ActionSupport.buildEvent(profileRequestContext, WebAuthnAuthenticationEventIds.CREDENTIAL_POLICY_REJECTION);
return;
}
- final EnhancedCredentialRecord credentialToEvaluate = credential.get();
+ final EnhancedCredentialRecord credentialToEvaluate = credentials.get(0);
assert credentialToEvaluate != null;
final CredentialPolicyOutcome outcome = localPolicy.evaluate(credentialToEvaluate, profileRequestContext);
if (outcome == CredentialPolicyOutcome.REJECT) {
log.warn("{} CredentialPolicy '{}' has rejected credential '{}'", getLogPrefix(), localPolicy.getId(),
- credential.get().getCredentialRecord().getCredentialIdBase64());
+ credentialToEvaluate.getCredentialRecord().getCredentialIdBase64());
authnContext.ensureSubcontext(AuthenticationErrorContext.class).getClassifiedErrors().add(
WebAuthnAuthenticationEventIds.CREDENTIAL_POLICY_REJECTION);
ActionSupport.buildEvent(profileRequestContext, WebAuthnAuthenticationEventIds.CREDENTIAL_POLICY_REJECTION);
return;
} else if (outcome == CredentialPolicyOutcome.IGNORE) {
log.trace("{} CredentialPolicy '{}' was not active for credential '{}', accepting", getLogPrefix(),
- localPolicy.getId(), credential.get().getCredentialRecord().getCredentialIdBase64());
+ localPolicy.getId(), credentialToEvaluate.getCredentialRecord().getCredentialIdBase64());
return;
}
log.debug("{} CredentialPolicy '{}' accepted credential '{}'", getLogPrefix(), localPolicy.getId(),
- credential.get().getCredentialRecord().getCredentialIdBase64());
+ credentialToEvaluate.getCredentialRecord().getCredentialIdBase64());
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/DefaultUserHandleLookupStrategy.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/DefaultUserHandleLookupStrategy.java
new file mode 100644
index 0000000..827ffaa
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/DefaultUserHandleLookupStrategy.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A strategy to locate the userHandle first from an assertion response and then, if that does not exist, from the
+ * userId in the WebAuthn authentication context.
+ */
+ at ThreadSafe
+public class DefaultUserHandleLookupStrategy implements Function<ProfileRequestContext, byte[]> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultUserHandleLookupStrategy.class);
+
+ /** {@inheritDoc} */
+ @Override
+ public byte[] apply(final ProfileRequestContext prc) {
+
+ final AuthenticationContext authnCtx = prc.getSubcontext(AuthenticationContext.class);
+ if (authnCtx == null) {
+ return null;
+ }
+ final WebAuthnAuthenticationContext webAuthnContext =
+ authnCtx.getSubcontext(WebAuthnAuthenticationContext.class);
+ if (webAuthnContext == null) {
+ return null;
+ }
+
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion =
+ webAuthnContext.getPublicKeyCredentialAssertionResponse();
+
+ if (assertion != null && assertion.getResponse() != null
+ && assertion.getResponse().getUserHandle().isPresent()) {
+
+ // Find first from assertion response if the userHandle exists
+ log.trace("Found userHandle from public key credential assertion");
+ return assertion.getResponse().getUserHandle().get().getBytes();
+
+ } else if (webAuthnContext.getUserId() != null){
+ // Find from the userId in the context if it exists
+ log.trace("Found userHandle from userId in authentication context");
+ return webAuthnContext.getUserId();
+ } else {
+ return null;
+ }
+
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java
index 3be8627..2a14de7 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java
@@ -168,8 +168,6 @@ public class LookupRegisteredCredentials extends AbstractWebAuthnAction<BaseWebA
final Collection<CredentialRecord> credentials =
repository.getRegistrationsByUsername(username);
- log.debug("{} Found '{}' registered credentials for '{}'", getLogPrefix(), credentials.size(), username);
-
context.setExistingCredentials(enhancedCredentialRecord(credentials));
final Optional<ByteArray> userHandle = repository.getUserHandleForUsername(username);
@@ -178,6 +176,8 @@ public class LookupRegisteredCredentials extends AbstractWebAuthnAction<BaseWebA
assert userId != null;
context.setUserId(userId);
}
+ log.debug("{} Found '{}' registered credential(s) for user '{}' with userId '{}'", getLogPrefix(),
+ credentials.size(), username, userHandle.isPresent() ? userHandle.get().getBase64() : "");
if (triggerEventOnNoCredentialsPredicate.test(profileRequestContext) && credentials.isEmpty()) {
log.debug("{} Triggering event '{}' ", getLogPrefix(), noCredentialsEventId);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
index ac153fd..e2ba9c7 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
@@ -200,7 +200,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
final AssertionResult result =
client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
@@ -226,7 +226,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(
credentialRegistration.getCredential().getCredentialId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
// The username used here is not the same as has user that has the credential registered for
final AssertionResult result =
@@ -274,7 +274,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
credentialRequestOptions, assertion);
@@ -319,7 +319,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
credentialRequestOptions, assertion);
@@ -365,7 +365,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64),
credentialRequestOptions, assertion);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/AbstractWebAuthnFlowTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/AbstractWebAuthnFlowTest.java
index f9822f7..a5c4dde 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/AbstractWebAuthnFlowTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/AbstractWebAuthnFlowTest.java
@@ -485,25 +485,29 @@ public abstract class AbstractWebAuthnFlowTest extends AbstractFlowTest {
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
return assertion;
}
+
/**
* Create a credential authentication assertion response from the mock authenticator.
*
* @param credentialId the credential ID to find the correct credential to sign the request with
* @param challenge the challenge to use
+ * @param blankUserHandle should the userHandle be empty in the assertion? this can happen for passwordless or 2FA
+ * authentications.
* @return the credential registration
*
* @throws Exception on error
*/
protected PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
createAssertionReponseFrom(
- final byte[] credentialId, final byte[] challenge) throws Exception {
+ final byte[] credentialId, final byte[] challenge, final boolean blankUserHandle)
+ throws Exception {
- return createAssertionReponseFrom(credentialId, challenge, ORIGIN);
+ return createAssertionReponseFrom(credentialId, challenge, ORIGIN, blankUserHandle);
}
/**
@@ -512,6 +516,8 @@ public abstract class AbstractWebAuthnFlowTest extends AbstractFlowTest {
* @param credentialId the credential ID to find the correct credential to sign the request with
* @param challenge the challenge to use
* @param the origin to use
+ * @param blankUserHandle should the userHandle be empty in the assertion? this can happen for passwordless or 2FA
+ * authentications.
*
* @return the credential registration
*
@@ -519,7 +525,8 @@ public abstract class AbstractWebAuthnFlowTest extends AbstractFlowTest {
*/
protected PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
createAssertionReponseFrom(
- final byte[] credentialId, final byte[] challenge, final String origin) throws Exception {
+ final byte[] credentialId, final byte[] challenge, final String origin,
+ final boolean blankUserHandle) throws Exception {
final String challengeB64 = Base64Support.encodeURLSafe(challenge);
final Map<String, String> clientDataGet = createClientData("webauthn.get", origin, challengeB64);
@@ -527,7 +534,7 @@ public abstract class AbstractWebAuthnFlowTest extends AbstractFlowTest {
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(credentialId,
- clientDataGet, null);
+ clientDataGet, null, blankUserHandle);
return assertion;
}
@@ -556,7 +563,7 @@ public abstract class AbstractWebAuthnFlowTest extends AbstractFlowTest {
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(credentialId,
- clientDataGet, userHandle);
+ clientDataGet, userHandle, false);
return assertion;
}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestAdminManagementFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestAdminManagementFlow.java
index c4a99ce..603a408 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestAdminManagementFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestAdminManagementFlow.java
@@ -147,7 +147,7 @@ public class TestAdminManagementFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
index 1de07d9..57e6bea 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
@@ -100,7 +100,66 @@ public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
+
+ final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
+
+ // Re-set external context to holder
+ ExternalContextHolder.setExternalContext(externalContext);
+ setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAssertionFromFormRequest.DEFAULT_PARAMETER_NAME,
+ assertionResponseJson));
+ externalContext.setEventId("proceed");
+ result.getSecond().setCurrentState("DisplayWebAuthnView");
+ result.getSecond().resume(externalContext);
+
+ // assert end conditions, no existing result or principal. So this should produce a Username principal
+ assertEquals(result.getSecond().getOutcome().getId(), "proceed");
+ assertAuthenticationSuccessConditions(prc, true);
+
+ }
+
+ /*
+ * Tests the passwordless flow when there is not userHandle in the assertion response. Which is allowable if
+ * the 'allowedCredentials' are supplied to the WebAuthn get request.
+ */
+ @SuppressWarnings("null")
+ @Test
+ public void testPasswordlessFlow_NoUserHandleInAssertion() throws Exception {
+ //Register a credential for use.
+ final CredentialRecord registration =
+ createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64);
+ credentialRepo.addRegistrationByUsername(USERNAME, registration);
+
+ final var prc = buildProfileRequestContext(false, false, USERNAME);
+
+ final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext,
+ addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
+
+ assertFlowExecutionActive(result.getSecond());
+ assertCurrentStateEquals("CollectUsernameView", result.getSecond());
+
+ // Do next part of flow, populate the username
+ ExternalContextHolder.setExternalContext(externalContext);
+ setHttpFormRequest("POST", Map.of("j_username", USERNAME));
+ externalContext.setEventId("proceed");
+
+ result.getSecond().setCurrentState("CollectUsernameView");
+ result.getSecond().resume(externalContext);
+
+ assertFlowExecutionActive(result.getSecond());
+ assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
+ assertPublicKeyCredentialRequestOptions(prc, true, true);
+
+ // Do assertion validation half of flow
+
+ // Get the challenge that was set into the PublicKeyCredentialRequestOptions.
+ // otherwise we will end up signing a different one, which will provide its own test.
+ final WebAuthnAuthenticationContext authnContext = getWebAuthnAuthenticationContext(prc);
+ final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
+
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertionResponse = createAssertionReponseFrom(
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), true);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlowWithPolicy.java
similarity index 57%
copy from webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
copy to webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlowWithPolicy.java
index e230574..f93b363 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlowWithPolicy.java
@@ -14,12 +14,9 @@
package net.shibboleth.idp.plugin.authn.webauthn.flow;
-import static org.testng.Assert.assertEquals;
-
import java.util.Map;
import javax.annotation.Nonnull;
-import javax.security.auth.Subject;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.webflow.context.ExternalContextHolder;
@@ -32,9 +29,6 @@ import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
-import net.shibboleth.idp.authn.AuthenticationResult;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialAssertionFromFormRequest;
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRecord;
@@ -42,19 +36,20 @@ import net.shibboleth.shared.collection.Pair;
/**
- * Flow tests for the 2fa flow.
+ * Flow tests for the passwordless flow.
*/
@ContextConfiguration(
locations = {
"classpath*:/META-INF/net.shibboleth.idp/postconfig.xml",
- "classpath*:/net/shibboleth/idp/plugin/authn/webauthn/test-beans-authentication.xml",
+ "classpath*:/net/shibboleth/idp/plugin/authn/webauthn/test-beans-authentication-reject-policy.xml",
"classpath*:/net/shibboleth/idp/plugin/authn/webauthn/test-beans.xml"},
initializers = {
TestWebAuthnEnvironmentApplicationContextInitializer.class,
- TestWebAuthn2FaApplicationContextInitializer.class
+ TestWebAuthnPasswordlessWithPolicyApplicationContextInitializer.class
}
)
-public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
+/* The policy is set to REJECT.*/
+public class TestPasswordlessFlowWithPolicy extends AbstractWebAuthnFlowTest{
/** Flow ID. */
@Nonnull public static final String FLOW_ID = "authn/WebAuthn";
@@ -62,27 +57,38 @@ public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
/**
* Constructor.
*/
- protected TestSecondFactorFlow() {
+ protected TestPasswordlessFlowWithPolicy() {
super(FLOW_ID);
}
+ /* Should end on username collection when starting the flow.*/
@SuppressWarnings("null")
@Test
- public void testSecondFactorFlow() throws Exception {
+ public void testPasswordlessFlow_WithPolicy() throws Exception {
//Register a credential for use.
final CredentialRecord registration =
createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64);
credentialRepo.addRegistrationByUsername(USERNAME, registration);
final var prc = buildProfileRequestContext(false, false, USERNAME);
- buildMfaContext(prc.ensureSubcontext(AuthenticationContext.class), "authn/Password");
final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext,
addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
+ assertFlowExecutionActive(result.getSecond());
+ assertCurrentStateEquals("CollectUsernameView", result.getSecond());
+
+ // Do next part of flow, populate the username
+ ExternalContextHolder.setExternalContext(externalContext);
+ setHttpFormRequest("POST", Map.of("j_username", USERNAME));
+ externalContext.setEventId("proceed");
+
+ result.getSecond().setCurrentState("CollectUsernameView");
+ result.getSecond().resume(externalContext);
+
assertFlowExecutionActive(result.getSecond());
assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
- assertPublicKeyCredentialRequestOptions(prc, true, false);
+ assertPublicKeyCredentialRequestOptions(prc, true, true);
// Do assertion validation half of flow
@@ -93,7 +99,7 @@ public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
@@ -105,45 +111,68 @@ public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
result.getSecond().setCurrentState("DisplayWebAuthnView");
result.getSecond().resume(externalContext);
- // assert end conditions, assume existing username principal from the faked password flow
- assertEquals(result.getSecond().getOutcome().getId(), "proceed");
- assertAuthenticationSuccessConditions(prc, false);
+ // Policy failure will return the user to the webauthn login page
+ assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
}
/*
- * The flow will proceed as if first factor passwordless, as no acceptable previous result. So we will end up on the
- * CollectUsernameView instead of DisplayWebAuthnView.
+ * Tests the passwordless flow when there is no userHandle in the assertion response. Which is allowable if
+ * a set of 'allowedCredentials' are supplied to the WebAuthn get request. Policy is set to reject, so it should
+ * be rejected.
*/
@SuppressWarnings("null")
@Test
- public void testSecondFactorFlow_WrongPreviousResult() throws Exception {
+ public void testPasswordlessFlow_WithPolicy_NoUserHandleInAssertion() throws Exception {
//Register a credential for use.
final CredentialRecord registration =
createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64);
credentialRepo.addRegistrationByUsername(USERNAME, registration);
final var prc = buildProfileRequestContext(false, false, USERNAME);
- buildMfaContext(prc.ensureSubcontext(AuthenticationContext.class), "authn/Wrong");
final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext,
addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
assertFlowExecutionActive(result.getSecond());
- // Stop here, because we are not testing the passwordless flow that we are now in.
assertCurrentStateEquals("CollectUsernameView", result.getSecond());
+ // Do next part of flow, populate the username
+ ExternalContextHolder.setExternalContext(externalContext);
+ setHttpFormRequest("POST", Map.of("j_username", USERNAME));
+ externalContext.setEventId("proceed");
+ result.getSecond().setCurrentState("CollectUsernameView");
+ result.getSecond().resume(externalContext);
- }
-
- private void buildMfaContext(final AuthenticationContext ac, final String previousFlow) {
+ assertFlowExecutionActive(result.getSecond());
+ assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
+ assertPublicKeyCredentialRequestOptions(prc, true, true);
+
+ // Do assertion validation half of flow
+
+ // Get the challenge that was set into the PublicKeyCredentialRequestOptions.
+ // otherwise we will end up signing a different one, which will provide its own test.
+ final WebAuthnAuthenticationContext authnContext = getWebAuthnAuthenticationContext(prc);
+ final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
+
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertionResponse = createAssertionReponseFrom(
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), true);
+
+ final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
+
+ // Re-set external context to holder
+ ExternalContextHolder.setExternalContext(externalContext);
+ setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAssertionFromFormRequest.DEFAULT_PARAMETER_NAME,
+ assertionResponseJson));
+ externalContext.setEventId("proceed");
+ result.getSecond().setCurrentState("DisplayWebAuthnView");
+ result.getSecond().resume(externalContext);
+
+ // Policy failure will return the user to the webauthn login page
+ assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
- final MultiFactorAuthenticationContext mfaCtx = new MultiFactorAuthenticationContext();
- // Create a fake password flow result
- final AuthenticationResult result = new AuthenticationResult("password", new Subject());
- mfaCtx.getActiveResults().put(previousFlow, result);
- ac.addSubcontext(mfaCtx);
}
}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java
index aef5fc4..916845d 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java
@@ -320,7 +320,7 @@ public class TestRegistrationFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
index e230574..be9dfc6 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
@@ -93,7 +93,7 @@ public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlow.java
index 36db8d0..a20c4ad 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlow.java
@@ -90,7 +90,7 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
@@ -148,7 +148,7 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
@@ -195,7 +195,7 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
mockAuthenticator.setUVNotPerformed(true);
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
@@ -242,7 +242,7 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
mockAuthenticator.setProduceBadAssertionSignatures(true);
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
@@ -286,7 +286,8 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), "wrong.origin.com");
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), "wrong.origin.com"
+ , false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
@@ -394,7 +395,7 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
@@ -434,7 +435,7 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
// Generate a challenge which is not the same as the one generated by the flow
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), generateRandomBytes(32));
+ registration.getCredential().getCredentialId().getBytes(), generateRandomBytes(32), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
@@ -477,7 +478,7 @@ public class TestUsernameslessFlow extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
index 511c082..93936cd 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
@@ -64,7 +64,7 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
* initializer.*/
@SuppressWarnings("null")
@Test
- public void testUsernamelessFlow_CredentialRejected_2FAOnly() throws Exception {
+ public void testUsernamelessFlow_WithPolicy_CredentialRejected_2FAOnly() throws Exception {
//Register a credential for use that is only suitable for 2FA
final CredentialRecord registration =
createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64);
@@ -88,7 +88,7 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertionResponse = createAssertionReponseFrom(
- registration.getCredential().getCredentialId().getBytes(), challenge.getBytes());
+ registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessWithPolicyApplicationContextInitializer.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessWithPolicyApplicationContextInitializer.java
new file mode 100644
index 0000000..ea383b3
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessWithPolicyApplicationContextInitializer.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.flow;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.mock.env.MockPropertySource;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An {@link ApplicationContextInitializer} which prepends properties.
+ *
+ * <p>This needs to include the original IdP-test-layer properties and has to be
+ * set to {@link Ordered#LOWEST_PRECEDENCE} or things blow up.</p>
+ */
+ at Order(Ordered.LOWEST_PRECEDENCE)
+public class TestWebAuthnPasswordlessWithPolicyApplicationContextInitializer
+ implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(TestWebAuthnPasswordlessWithPolicyApplicationContextInitializer.class);
+
+ /** {@inheritDoc} */
+ @Override public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
+ final MockPropertySource mock = new MockPropertySource("passwordless-mock-properties");
+ mock.setProperty("idp.authn.webauthn.usernameless.enabled", "false");
+ mock.setProperty("idp.authn.webauthn.credential.policy.enabled", "true");
+ applicationContext.getEnvironment().getPropertySources().addFirst(mock);
+ log.info("Prepending usernameless properties '{}'", mock.getSource());
+ }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
index 36380e6..792c5a0 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
@@ -410,7 +410,7 @@ public abstract class AbstractWebAuthnTest {
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
return assertion;
}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckCredentialPolicyTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckCredentialPolicyTest.java
new file mode 100644
index 0000000..b229ade
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckCredentialPolicyTest.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeSet;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.AuthenticatorTransport;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.UserIdentity;
+
+import net.shibboleth.idp.plugin.authn.webauthn.authn.WebAuthnAuthenticationEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.policy.CredentialPolicy;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRecord;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.MockAuthenticator;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Tests for {@link CheckCredentialPolicy}.
+ */
+public class CheckCredentialPolicyTest extends AbstractWebAuthnTest {
+
+ /** The action to test.*/
+ private CheckCredentialPolicy action;
+
+ /** The WebAuthn context.*/
+ private WebAuthnAuthenticationContext context;
+
+ /** The stashed attestation.*/
+ private PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation;
+
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ context = addWebAuthnAuthenticationContext();
+
+ action = new CheckCredentialPolicy();
+ action.setWebAuthnClient(client);
+ action.setCredentialRepository(credentialRepo);
+
+ mockAuthenticator = new MockAuthenticator(RPID);
+
+ final var user = UserIdentity.builder()
+ .name("jdoe")
+ .displayName("John Doe")
+ .id(ByteArray.fromBase64(USER_HANDLE_B64))
+ .build();
+
+
+ attestation = createAttestationReponse();
+
+ final var uHandleArray = ByteArray.fromBase64(USER_HANDLE_B64);
+ assert uHandleArray.getBase64().equals(USER_HANDLE_B64);
+ assert Arrays.equals(uHandleArray.getBytes(), Base64Support.decode(USER_HANDLE_B64));
+
+
+ final RegisteredCredential credential = RegisteredCredential.builder()
+ .credentialId(attestation.getId())
+ .userHandle(ByteArray.fromBase64(USER_HANDLE_B64))
+ .publicKeyCose(attestation.getResponse().getParsedAuthenticatorData()
+ .getAttestedCredentialData().get().getCredentialPublicKey())
+ .build();
+
+ final var reg = CredentialRecord.builder()
+ .withUserIdentity(user)
+ .withUsername(USERNAME)
+ .withTransports(new TreeSet<AuthenticatorTransport>())
+ .withRegistrationTime(Instant.now())
+ .withCredential(credential)
+ .withCredentialNickname("nickname")
+ .withDiscoverable(Optional.of(Boolean.TRUE))
+ .withAaguid(attestation.getResponse().getParsedAuthenticatorData()
+ .getAttestedCredentialData().get().getAaguid().getBytes())
+ .withUserVerified(true)
+ .build();
+
+
+ credentialRepo.addRegistrationByUsername(USERNAME, reg);
+ context.setExistingCredentials(CollectionSupport.setOf(
+ new EnhancedCredentialRecord(reg)));
+ context.setUserId(uHandleArray.getBytes());
+ context.setUsername(USERNAME);
+
+ }
+
+ @Test
+ public void testCredentialPolicy_Accept() throws Exception {
+
+ action.setCredentialPolicy(new CredentialPolicy() {
+
+ @Override
+ public String getId() {
+ return "Dummy Accept Policy";
+ }
+
+ @Override
+ public CredentialPolicyOutcome evaluate(final EnhancedCredentialRecord credential, final ProfileRequestContext prc) {
+ return CredentialPolicyOutcome.ACCEPT;
+ }
+
+ });
+ action.initialize();
+
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64);
+
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
+ clientDataGet, null, false);
+
+ //Set the assertion (authentication) response based on the credential we've already registered
+ context.setPublicKeyCredentialAssertionResponse(assertion);
+
+ final Event event = action.execute(src);
+ assertNull(event);
+ }
+
+ @Test
+ public void testCredentialPolicy_Rejected() throws Exception {
+
+ action.setCredentialPolicy(new CredentialPolicy() {
+
+ @Override
+ public String getId() {
+ return "Dummy Reject Policy";
+ }
+
+ @Override
+ public CredentialPolicyOutcome evaluate(final EnhancedCredentialRecord credential, final ProfileRequestContext prc) {
+ return CredentialPolicyOutcome.REJECT;
+ }
+
+ });
+ action.initialize();
+
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64);
+
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
+ clientDataGet, null, false);
+
+ //Set the assertion (authentication) response based on the credential we've already registered
+ context.setPublicKeyCredentialAssertionResponse(assertion);
+
+ final Event event = action.execute(src);
+ assertNotNull(event);
+ assert event != null;
+ assertEquals(event.getId(), WebAuthnAuthenticationEventIds.CREDENTIAL_POLICY_REJECTION);
+ }
+
+ /* Should be evaluated with or without a userHandle in the response. Passwordless and 2FA responses do not need
+ * to contain a userHandle, the IdP already knows it.*/
+ @Test
+ public void testCredentialPolicy_NoUserHandle() throws Exception {
+
+ action.setCredentialPolicy(new CredentialPolicy() {
+
+ @Override
+ public String getId() {
+ return "Dummy Reject Policy";
+ }
+
+ @Override
+ public CredentialPolicyOutcome evaluate(final EnhancedCredentialRecord credential, final ProfileRequestContext prc) {
+ return CredentialPolicyOutcome.REJECT;
+ }
+
+ });
+ action.initialize();
+
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64);
+
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
+ clientDataGet, null, true);
+
+ //Set the assertion (authentication) response based on the credential we've already registered
+ context.setPublicKeyCredentialAssertionResponse(assertion);
+
+ final Event event = action.execute(src);
+ assert event != null;
+ assertEquals(event.getId(), WebAuthnAuthenticationEventIds.CREDENTIAL_POLICY_REJECTION);
+ }
+
+ /* Policy can not run because userHandle is not present in the assertion and userId is not present in the context.*/
+ @Test
+ public void testCredentialPolicy_NoUserHandle_NoUserId() throws Exception {
+
+ action.setCredentialPolicy(new CredentialPolicy() {
+
+ @Override
+ public String getId() {
+ return "Dummy Reject Policy";
+ }
+
+ @Override
+ public CredentialPolicyOutcome evaluate(final EnhancedCredentialRecord credential, final ProfileRequestContext prc) {
+ return CredentialPolicyOutcome.REJECT;
+ }
+
+ });
+ action.initialize();
+
+ final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64);
+
+ final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+ assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
+ clientDataGet, null, true);
+
+ //Set the assertion (authentication) response based on the credential we've already registered
+ context.setPublicKeyCredentialAssertionResponse(assertion);
+
+
+ final Event event = action.execute(src);
+ assert event != null;
+ assertEquals(event.getId(), WebAuthnAuthenticationEventIds.CREDENTIAL_POLICY_REJECTION);
+ }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
index 1b6888a..5ea3e78 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
@@ -109,7 +109,7 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
webAuthnContext.setPublicKeyCredentialAssertionResponse(assertion);
@@ -164,7 +164,7 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
webAuthnContext.setPublicKeyCredentialAssertionResponse(assertion);
@@ -193,7 +193,7 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
webAuthnContext.setPublicKeyCredentialAssertionResponse(assertion);
@@ -223,7 +223,7 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
// Now generate an assertion (authentication) and check it is valid
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
webAuthnContext.setPublicKeyCredentialAssertionResponse(assertion);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertionTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertionTest.java
index a5134ea..e0d7d04 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertionTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertionTest.java
@@ -102,7 +102,7 @@ public class ValidateWebAuthnAssertionTest extends AbstractWebAuthnTest {
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
//Set the assertion (authentication) response based on the credential we've already registered
context.setPublicKeyCredentialAssertionResponse(assertion);
@@ -123,7 +123,7 @@ public class ValidateWebAuthnAssertionTest extends AbstractWebAuthnTest {
final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(),
- clientDataGet, null);
+ clientDataGet, null, false);
//Set the assertion (authentication) response based on the credential we've already registered
context.setPublicKeyCredentialAssertionResponse(assertion);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/policy/impl/RejectAllCredentialPolicyRule.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/policy/impl/RejectAllCredentialPolicyRule.java
new file mode 100644
index 0000000..3600f2c
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/policy/impl/RejectAllCredentialPolicyRule.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.policy.impl;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord;
+
+/**
+ * A simple mock policy that REJECTS any credential.
+ */
+public class RejectAllCredentialPolicyRule extends AbstractCredentialPolicyRule{
+
+ /** {@inheritDoc} */
+ @Override
+ protected CredentialPolicyOutcome doEvaluate(final EnhancedCredentialRecord credential,
+ final ProfileRequestContext prc,
+ final WebAuthnAuthenticationContext webAuthnContext) {
+ return CredentialPolicyOutcome.REJECT;
+ }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/MockAuthenticator.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/MockAuthenticator.java
index fd9f8eb..01dc0b1 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/MockAuthenticator.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/MockAuthenticator.java
@@ -74,8 +74,8 @@ import net.shibboleth.shared.primitive.LoggerFactory;
*
* <p>A new credential is created and stored for every call to
* {@link #createAuthenticatorAttestationResponse(String, Map, byte[])}. Calls to
- * {@link #createAuthenticatorAssertionResponse(String, byte[], Map)} require the credentialId so it can use information
- * stored when the credential was created. Hence to use {@link #createAuthenticatorAssertionResponse(String, byte[], Map)}
+ * {@link #createAuthenticatorAssertionResponse(String, byte[], Map, boolean)} require the credentialId so it can use information
+ * stored when the credential was created. Hence to use {@link #createAuthenticatorAssertionResponse(String, byte[], Map, boolean)}
* you must first call {@link #createAuthenticatorAttestationResponse(String, Map, byte[])}.</p>
*
* <p> Does not support the attestation statement 'attStmt' (which provides data provenance information for the public
@@ -236,13 +236,14 @@ public class MockAuthenticator {
* @param clientData the client data passed by the 'client' (browser) during registration.
* @param overrideUserHandle if set, override the userHandle in the assertion response. Normally the userHandle
* comes from what was stored in the attestation when the key was created.
+ * @param blankUserHandle TODO
* @return the public key credential assertion
*
* @throws Exception on error
*/
public com.yubico.webauthn.data.PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
createAuthenticatorAssertionResponse(@Nonnull final byte[] credentialId, final Map<String, String> clientData,
- final byte[] overrideUserHandle) throws Exception {
+ final byte[] overrideUserHandle, final boolean blankUserHandle) throws Exception {
final var credentialb64 = Base64Support.encodeURLSafe(credentialId);
final var publicKeyAttestation = createdCredentialsMaps.get(credentialb64);
@@ -258,8 +259,11 @@ public class MockAuthenticator {
sign(authenticatorData, clientDataCompactSerialization,
publicKeyAttestation.getResponse().getPrivateKey());
- final var userHandle = overrideUserHandle != null ? overrideUserHandle :
+ var userHandle = overrideUserHandle != null ? overrideUserHandle :
publicKeyAttestation.getResponse().getUserHandle();
+ if (blankUserHandle) {
+ userHandle = null;
+ }
final var pkCred = new PublicKeyCredential<Assertion, AuthenticatonExtensionsClientOutputs>(rawCredentialIdentifier,
new Assertion(clientDataCompactSerialization.getBytes(), authenticatorData, signature, userHandle),
new AuthenticatonExtensionsClientOutputs());
diff --git a/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans-authentication-reject-policy.xml b/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans-authentication-reject-policy.xml
new file mode 100644
index 0000000..6b29aa0
--- /dev/null
+++ b/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans-authentication-reject-policy.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:jdbc="http://www.springframework.org/schema/jdbc"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize"
+ default-destroy-method="destroy">
+
+ <bean id="shibboleth.authn.WebAuthn.AbstractCredentialPolicyRule" scope="prototype" abstract="true"
+ p:fidoMetadataService="#{'false'.equals('%{idp.authn.webauthn.metadata.enabled:false}') ? null : getObject('shibboleth.authn.WebAuthn.DefaultWebAuthnFidoMetadataServiceFactory')}"/>
+
+
+ <!--
+ The default list to add credential policies used during authentication. The property
+ idp.authn.webauthn.credential.policy.enabled must be set to true for these to take effect.
+
+ -->
+ <util:list id="shibboleth.authn.WebAuthn.ChainedCredentialPolicyList">
+ <bean id="RejectAllCredentialPolicy" parent="shibboleth.authn.WebAuthn.AbstractCredentialPolicyRule"
+ class="net.shibboleth.idp.plugin.authn.webauthn.policy.impl.RejectAllCredentialPolicyRule"/>
+ </util:list>
+
+</beans>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list