[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-37 - Lookup user credentials from user handle expects user handle
Phil Smart
philip.smart at jisc.ac.uk
Tue Jan 28 16:01:06 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=c84fb90d970a099fca26377e21e746757736311d
The following commit(s) were added to refs/heads/main by this push:
new c84fb90 JWEBAUTHN-37 - Lookup user credentials from user handle expects user handle
c84fb90 is described below
commit c84fb90d970a099fca26377e21e746757736311d
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue Jan 28 16:01:00 2025 +0000
JWEBAUTHN-37 - Lookup user credentials from user handle expects user
handle
- Separated the authn flow after the assertion has been received from
the view into usernameless, passwordless, and 2FA.
- this means the LookupRegisteredCredentialsFromUserHandle action is
only run in a usernameless context. But it opens the possibility of
adding more post assertion flow actions specific to the mode in the
future.
https://shibboleth.atlassian.net/browse/JWEBAUTHN-37
---
.../logic/IsPasswordlessAuthenticationMode.java | 62 ++++++++++++++++++++++
.../logic/IsSecondFactorAuthenticationMode.java | 62 ++++++++++++++++++++++
.../logic/IsUsernamelessAuthenticationMode.java | 62 ++++++++++++++++++++++
.../authn/webauthn/impl/CheckCredentialPolicy.java | 12 +++--
.../webauthn/impl/LookupRegisteredCredentials.java | 3 +-
.../idp/flows/authn/WebAuthn/webauthn-beans.xml | 9 ++++
.../idp/flows/authn/WebAuthn/webauthn-flow.xml | 55 ++++++++++++++++---
...IdPStorageServiceCredentialRespositoryTest.java | 16 ++++++
8 files changed, 269 insertions(+), 12 deletions(-)
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsPasswordlessAuthenticationMode.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsPasswordlessAuthenticationMode.java
new file mode 100644
index 0000000..ef8885b
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsPasswordlessAuthenticationMode.java
@@ -0,0 +1,62 @@
+/*
+ * 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.context.logic;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A predicate that lookups into the context to find if the authentication flow is passwordless.
+ *
+ * @since 1.1.0
+ */
+ at ThreadSafe
+public class IsPasswordlessAuthenticationMode implements Predicate<ProfileRequestContext> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(IsPasswordlessAuthenticationMode.class);
+
+ @Override
+ public boolean test(@Nullable final ProfileRequestContext input) {
+ if (input == null) {
+ log.trace("Profile context was null, can not determine if operating in passwordless mode");
+ return false;
+ }
+ final AuthenticationContext authnContext = input.getSubcontext(AuthenticationContext.class);
+ if (authnContext == null) {
+ log.trace("Authentication context was null, can not determine if operating in passwordless mode");
+ return false;
+ }
+ final WebAuthnAuthenticationContext webauthnContext =
+ authnContext.getSubcontext(WebAuthnAuthenticationContext.class);
+ if (webauthnContext == null) {
+ log.trace("WebAuthn authentication context was null, can not determine if operating in passwordless mode");
+ return false;
+ }
+ final boolean isPasswordless = webauthnContext.isPasswordless();
+ log.trace("Currently operating in passwordless mode '{}'", isPasswordless);
+ return isPasswordless;
+ }
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsSecondFactorAuthenticationMode.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsSecondFactorAuthenticationMode.java
new file mode 100644
index 0000000..ae875f1
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsSecondFactorAuthenticationMode.java
@@ -0,0 +1,62 @@
+/*
+ * 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.context.logic;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A predicate that lookups into the context to find if the authentication flow is acting as a second factor.
+ *
+ * @since 1.1.0
+ */
+ at ThreadSafe
+public class IsSecondFactorAuthenticationMode implements Predicate<ProfileRequestContext> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(IsSecondFactorAuthenticationMode.class);
+
+ @Override
+ public boolean test(@Nullable final ProfileRequestContext input) {
+ if (input == null) {
+ log.trace("Profile context was null, can not determine if operating as a second factor");
+ return false;
+ }
+ final AuthenticationContext authnContext = input.getSubcontext(AuthenticationContext.class);
+ if (authnContext == null) {
+ log.trace("Authentication context was null, can not determine if operating as a second factor");
+ return false;
+ }
+ final WebAuthnAuthenticationContext webauthnContext =
+ authnContext.getSubcontext(WebAuthnAuthenticationContext.class);
+ if (webauthnContext == null) {
+ log.trace("WebAuthn authentication context was null, can not determine if operating as a second factor");
+ return false;
+ }
+ final boolean isSecondFactor = webauthnContext.isSecondFactor();
+ log.trace("Currently operating in 2FA mode '{}'", isSecondFactor);
+ return isSecondFactor;
+ }
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsUsernamelessAuthenticationMode.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsUsernamelessAuthenticationMode.java
new file mode 100644
index 0000000..862213c
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsUsernamelessAuthenticationMode.java
@@ -0,0 +1,62 @@
+/*
+ * 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.context.logic;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A predicate that lookups into the context to find if the authentication flow is usernameless.
+ *
+ * @since 1.1.0
+ */
+ at ThreadSafe
+public class IsUsernamelessAuthenticationMode implements Predicate<ProfileRequestContext> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(IsUsernamelessAuthenticationMode.class);
+
+ @Override
+ public boolean test(@Nullable final ProfileRequestContext input) {
+ if (input == null) {
+ log.trace("Profile context was null, can not determine if operating in usernameless mode");
+ return false;
+ }
+ final AuthenticationContext authnContext = input.getSubcontext(AuthenticationContext.class);
+ if (authnContext == null) {
+ log.trace("Authentication context was null, can not determine if operating in usernameless mode");
+ return false;
+ }
+ final WebAuthnAuthenticationContext webauthnContext =
+ authnContext.getSubcontext(WebAuthnAuthenticationContext.class);
+ if (webauthnContext == null) {
+ log.trace("WebAuthn authentication context was null, can not determine if operating in usernameless mode");
+ return false;
+ }
+ final boolean isUsernameless = webauthnContext.isUsernameless();
+ log.trace("Currently operating in usernameless mode '{}'", isUsernameless);
+ return isUsernameless;
+ }
+}
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 1d5bcd8..c8a3f86 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
@@ -51,9 +51,10 @@ import net.shibboleth.shared.primitive.LoggerFactory;
/**
* A policy engine action that checks with the configured policy if the credential, used to sign the assertion, can
- * be used to authenticate. If #rejectIfNoCredentialsFound is true, and the credential used to sign the assertion
- * can not be found from the credential repository, a {@link WebAuthnAuthenticationEventIds#CREDENTIAL_POLICY_REJECTION}
- * will be returned, else the policy is ignored.
+ * be used to authenticate. If it is rejected by the policy, a
+ * {@link WebAuthnAuthenticationEventIds#CREDENTIAL_POLICY_REJECTION} is returned. If #rejectIfNoCredentialsFound is
+ * true, and the credential used to sign the assertion can not be found from the credential repository, a
+ * {@link WebAuthnAuthenticationEventIds#CREDENTIAL_POLICY_REJECTION} will be returned, else the policy is ignored.
*
* @event {EventIds#INVALID_PROFILE_CTX}\
* @event {WebAuthnAuthenticationEventIds#CREDENTIAL_POLICY_REJECTION}
@@ -216,7 +217,7 @@ public class CheckCredentialPolicy extends AbstractWebAuthnAction<WebAuthnAuthen
}).toList();
if (credentials.isEmpty()) {
- log.trace("{} UserHandle '{}' has no registered credential, policy can not be applied",getLogPrefix(),
+ log.trace("{} UserHandle '{}' does not have a registered credential, policy can not be applied",getLogPrefix(),
userHandle.getBase64());
if (!rejectIfNoCredentialFound.test(profileRequestContext)) {
return;
@@ -229,7 +230,8 @@ public class CheckCredentialPolicy extends AbstractWebAuthnAction<WebAuthnAuthen
}
}
- // Reject if more than one credential that matches the credentialID and userHandle in the assertion.
+ // Reject if more than one credential that matches the credentialID and userHandle in the assertion. Should
+ // never happen.
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(),
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 2a14de7..ffeceb5 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
@@ -43,7 +43,8 @@ import net.shibboleth.shared.primitive.LoggerFactory;
/**
* An action that lookups existing registered credentials based on the username contained in the WebAuthn context
- * and sets them back onto the context. Also sets the user.id from the userHandle associated with the username.
+ * and sets them back onto the context. Also sets the user.id from the userHandle associated with the username back
+ * onto the WebAuthn context.
*
* <p>
* If the username is not required and is not found in the context (e.g. failed c14n) the set of existing credentials
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
index 1aee6c5..7b7f33f 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
@@ -132,6 +132,15 @@
<bean id="CreatePublicKeyCredentialRequestOptions" scope="prototype" parent="AbstractWebAuthnAuthenticationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialRequestOptions"/>
+
+ <bean id="IsUsernamelessAuthenticationMode" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsUsernamelessAuthenticationMode"/>
+
+ <bean id="IsPasswordlessAuthenticationMode" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsPasswordlessAuthenticationMode"/>
+
+ <bean id="IsSecondFactorAuthenticationMode" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsSecondFactorAuthenticationMode"/>
<bean id="ExtractPublicKeyCredentialAssertionFromFormRequest" scope="prototype" parent="AbstractWebAuthnAuthenticationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialAssertionFromFormRequest"
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
index d5b6d37..6468cfb 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
@@ -133,15 +133,58 @@
<evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
<evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
</on-render>
- <transition on="proceed" to="ValidatePublicKeyCredential" />
-
+ <transition on="proceed" to="ExtractPublicKeyCredentialAssertion" />
</view-state>
-
- <action-state id="ValidatePublicKeyCredential">
+
+ <action-state id="ExtractPublicKeyCredentialAssertion">
<evaluate expression="ExtractPublicKeyCredentialAssertionFromFormRequest"/>
- <!-- lookup credentials here so we can exit the process before validation if no registered credentials exist and
- the authentication plugin has been configured to trigger a custom event. Useful for the usernameless flow. -->
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="BranchOnAuthenticationMode" />
+ </action-state>
+
+ <!-- Determine which authentication mode we are using so different actions can apply -->
+ <decision-state id="BranchOnAuthenticationMode">
+ <if test="IsSecondFactorAuthenticationMode.test(opensamlProfileRequestContext)"
+ then="SecondFactorAssertion"
+ else="BranchOnPasswordlessOrUsernamelessMode" />
+ </decision-state>
+
+ <decision-state id="BranchOnPasswordlessOrUsernamelessMode">
+ <if test="IsUsernamelessAuthenticationMode.test(opensamlProfileRequestContext)"
+ then="UsernamelessAssertion"
+ else="BranchOnPasswordlessMode" />
+ </decision-state>
+
+ <!-- By this point, if we are not operating in any mode, this represents a fundamental problem with the flow -->
+ <decision-state id="BranchOnPasswordlessMode">
+ <if test="IsPasswordlessAuthenticationMode.test(opensamlProfileRequestContext)"
+ then="PasswordlessAssertion"
+ else="RuntimeException" />
+ </decision-state>
+
+ <action-state id="UsernamelessAssertion">
+ <!--
+ lookup credentials here so we can exit the process before validation if no registered credentials exist and
+ the plugin has been configured to trigger a custom event. Also set the known credentials onto the context
+ for the credential policies to run.
+ -->
<evaluate expression="LookupRegisteredCredentialsFromUserHandle"/>
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="ValidatePublicKeyCredential" />
+ </action-state>
+
+ <action-state id="SecondFactorAssertion">
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="ValidatePublicKeyCredential" />
+ </action-state>
+
+ <action-state id="PasswordlessAssertion">
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="ValidatePublicKeyCredential" />
+ </action-state>
+
+ <action-state id="ValidatePublicKeyCredential">
<evaluate expression="CheckCredentialPolicy"/>
<evaluate expression="ValidateWebAuthnAssertion"/>
<evaluate expression="'proceed'" />
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
index fe30485..c11ebf0 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
@@ -124,6 +124,22 @@ public class IdPStorageServiceCredentialRespositoryTest extends AbstractWebAuthn
registration.getCredential().getCredentialId());
}
+ @Test
+ public void testAddTwoRegistrationByUsernameWithSameHandleAndCredentialID() throws Exception {
+
+ final CredentialRecord registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+ repo.addRegistrationByUsername("jdoe", registration);
+ // add the same credential a second time. There should only be 1 registered.
+ repo.addRegistrationByUsername("jdoe", registration);
+
+ final var registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+ assertEquals(registrations.iterator().next().getUsername(),"jdoe");
+ assertEquals(registrations.iterator().next().getCredential().getCredentialId(),
+ registration.getCredential().getCredentialId());
+ }
+
@Test
public void testAddTwoRegistrationsByUsername() throws Exception {
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list