Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ The *Passkey Mediation* setting in the *WebAuthn Passwordless Policy* controls h
[NOTE]
====
The behavior of the `mediation` option, although defined in the specification, can vary between browsers and authenticators. {project_name} simply passes this option during the registration process. The actual final behavior is beyond its control. For example, the support for `required` and `silent` mediation are known to be different among browsers. Refer to your target browser's documentation before relying on these values in production.

When the *Authenticator Attachment* in the policy is set to `platform` and the device does not have a user-verifying platform authenticator available, the automatic passkey flow on page load is skipped regardless of the mediation value. The user can still authenticate using the *Sign in with Passkey* button or fall back to password login.
====

==== Setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public LoginFormsProvider fillContextForm(AuthenticationFlowContext context) {
form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement);
form.setAttribute(WebAuthnConstants.SHOULD_DISPLAY_AUTHENTICATORS, shouldDisplayAuthenticators(context));
form.setAttribute(WebAuthnConstants.MEDIATION, policy.getMediation());
form.setAttribute(WebAuthnConstants.AUTHENTICATOR_ATTACHMENT, policy.getAuthenticatorAttachment());

return form;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,33 @@ public void passwordLoginWithExternalKey() {
webAuthnLoginPage.clickAuthenticate();
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());

EventAssertion.assertSuccess(events.poll())
.type(EventType.LOGIN)
.hasSessionId()
.userId(user.getId())
.isCodeId()
.details(Details.USERNAME, user.getUsername())
.details(Details.CREDENTIAL_TYPE, WebAuthnCredentialModel.TYPE_PASSWORDLESS)
.details(WebAuthnConstants.USER_VERIFICATION_CHECKED, "true");

logout();
events.clear();

// set authenticatorAttachment to platform with modal mediation;
// no platform authenticator is available so the modal must not be shown
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyPasswordlessAuthenticatorAttachment("platform")
.webAuthnPolicyPasswordlessMediation("optional"));

oAuthClient.openLoginForm();

loginPage.assertCurrent();
MatcherAssert.assertThat(loginPage.getUsernameAutocomplete(), Matchers.is("username webauthn"));
MatcherAssert.assertThat(driver.findElement(By.xpath("//form[@id='webauth']")), Matchers.notNullValue());

// no modal was shown, force login using webauthn link
webAuthnLoginPage.clickAuthenticate();
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());

EventAssertion.assertSuccess(events.poll())
.type(EventType.LOGIN)
.hasSessionId()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,33 @@ public void passwordLoginWithExternalKey() {
webAuthnLoginPage.clickAuthenticate();
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());

EventAssertion.assertSuccess(events.poll())
.type(EventType.LOGIN)
.hasSessionId()
.userId(user.getId())
.isCodeId()
.details(Details.USERNAME, user.getUsername())
.details(Details.CREDENTIAL_TYPE, WebAuthnCredentialModel.TYPE_PASSWORDLESS)
.details(WebAuthnConstants.USER_VERIFICATION_CHECKED, "true");

logout();
events.clear();

// set authenticatorAttachment to platform with modal mediation;
// no platform authenticator is available so the modal must not be shown
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyPasswordlessAuthenticatorAttachment("platform")
.webAuthnPolicyPasswordlessMediation("optional"));

oAuthClient.openLoginForm();

loginPage.assertCurrent();
MatcherAssert.assertThat(loginPage.getUsernameAutocomplete(), Matchers.is("username webauthn"));
MatcherAssert.assertThat(driver.findElement(By.xpath("//form[@id='webauth']")), Matchers.notNullValue());

// no modal was shown, force login using webauthn link
webAuthnLoginPage.clickAuthenticate();
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());

EventAssertion.assertSuccess(events.poll())
.type(EventType.LOGIN)
.hasSessionId()
Expand Down
1 change: 1 addition & 0 deletions themes/src/main/resources/theme/base/login/passkeys.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
rpId : ${rpId?c},
createTimeout : ${createTimeout?c},
mediation : ${(mediation!'conditional')?c},
authenticatorAttachment : ${authenticatorAttachment?c},
};

document.addEventListener("DOMContentLoaded", (event) => initAuthenticate({errmsg : ${msg("passkey-unsupported-browser-text")?c}, ...args}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export async function initAuthenticate(input, availableCallback = () => {}) {
return;
}

if (input.authenticatorAttachment === 'platform'
&& !await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) {
availableCallback(false);
return;
}

// The isConditionalMediationAvailable() check is only relevant for
// conditional (autofill) mediation — other modes do not depend on it.
if (mediation === 'conditional') {
Expand Down
Loading