Enable to set mediation property for WebAuthn passwordless authentication (#46960)

possible values: conditional, optional, required, silent
conditional remains the default to not break the current behavior

when optional or required and the user dismissed the modal, it will stay hidden for this auth-session, can still be opened by button

adjusted all related resources, like JS files (also consolidated duplicated logic), Java classes and freemarker template

tests extended

passkey documentation extended/updated

closes #46959

Signed-off-by: Niko Köbler <niko@n-k.de>
This commit is contained in:
Niko Köbler 2026-04-27 10:07:09 +02:00 committed by GitHub
parent a8aaed2904
commit e5ca2a6709
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 255 additions and 120 deletions

View file

@ -159,6 +159,7 @@ public class RealmRepresentation {
protected List<String> webAuthnPolicyPasswordlessAcceptableAaguids;
protected List<String> webAuthnPolicyPasswordlessExtraOrigins;
protected Boolean webAuthnPolicyPasswordlessPasskeysEnabled;
protected String webAuthnPolicyPasswordlessMediation;
// Client Policies/Profiles
@ -1286,6 +1287,14 @@ public class RealmRepresentation {
this.webAuthnPolicyPasswordlessPasskeysEnabled = webAuthnPolicyPasswordlessPasskeysEnabled;
}
public String getWebAuthnPolicyPasswordlessMediation() {
return webAuthnPolicyPasswordlessMediation;
}
public void setWebAuthnPolicyPasswordlessMediation(String webAuthnPolicyPasswordlessMediation) {
this.webAuthnPolicyPasswordlessMediation = webAuthnPolicyPasswordlessMediation;
}
// Client Policies/Profiles
@JsonIgnore

View file

@ -7,7 +7,9 @@
Passkey registration and authentication are performed using the same features of xref:webauthn_{context}[WebAuthn]. More specifically *Passkeys* are related to xref:_webauthn_loginless[LoginLess WebAuthn] as they try to avoid any password during login.
Therefore, users of {project_name} can do Passkey registration and authentication by existing xref:webauthn_{context}[WebAuthn registration and authentication], using the *passwordless* variants.
The *Passkeys* feature has been integrated seamlessly in the default authentication forms in two different ways. When activated, both conditional UI and modal UI are available in the forms in which the username input is displayed (for example *Username Password Form* or *Username Form*). Besides, the password forms, when the username was already selected, always show the modal UI button to login by passkey if the current user has passwordless WebAuthn credentials associated. This way modal and conditional UI can be used to perform a complete login from scratch that needs username and password, and only modal UI is presented when the username is already selected in the authentication process (because of re-authentication or because the user was selected before in the process not using a passkey).
The *Passkeys* feature has been integrated seamlessly in the default authentication forms in two different ways. When activated, both conditional UI and modal UI are available in the forms in which the username input is displayed (for example *Username Password Form* or *Username Form*).
The exact behavior on page load — whether a passkey selection dialog appears automatically or credentials are offered only through browser autofill — is controlled by the *Passkey Mediation* setting in the *WebAuthn Passwordless Policy*.
Besides, the password forms, when the username was already selected, always show the modal UI button to login by passkey if the current user has passwordless WebAuthn credentials associated. This way modal and conditional UI can be used to perform a complete login from scratch that needs username and password, and only modal UI is presented when the username is already selected in the authentication process (because of re-authentication or because the user was selected before in the process not using a passkey).
*Passkeys* have been added to the following authenticator implementations:
@ -51,6 +53,37 @@ The modal UI button is also presented in the password forms when the user is alr
.Passkey Authentication with Modal UI using Chrome browser
image:images/passkey-modal-ui.png[Passkey Authentication with Modal UI using Chrome browser]
[[_passkeys-mediation]]
==== Passkey Mediation
The *Passkey Mediation* setting in the *WebAuthn Passwordless Policy* controls how the browser interacts with the user's passkeys when the login page loads. It maps directly to the https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get#mediation[`mediation` parameter of the WebAuthn `navigator.credentials.get()` API].
.Passkey Mediation options
[cols="1,3",options="header"]
|===
|Value |Behavior
|`conditional` (default)
|No dialog is shown on page load. Passkeys are offered only through the browser's autofill dropdown when the user focuses the username input field. This is the least intrusive option and corresponds to the <<_passkeys-conditional-ui,Conditional UI>> behavior. If the browser does not support conditional mediation, the behaviour is the same as `none`.
|`none`
|No automatic passkey action is taken on page load. The user can still authenticate using a passkey by clicking the *Sign in with Passkey* button. Use this option when you want passkeys to be available but prefer not to display any prompt automatically.
|`optional`
|A passkey selection dialog is shown automatically on page load. The browser may skip the prompt if no credentials are found. Use this to invite the user to use a passkey without strictly enforcing it.
|`required`
|A passkey selection dialog is shown immediately on page load. The user is forced to authenticate or manually dismiss the prompt to proceed. Use this when passkey verification is a mandatory requirement for the current action.
|`silent`
|The browser attempts to authenticate without any user interaction or visible UI. Authentication fails silently if user interaction is required. This is intended for advanced scenarios and is unlikely to succeed for most passkey types.
|===
[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.
====
==== Setup
Set up Passkey Authentication for the default forms as follows:
@ -61,4 +94,6 @@ Set up Passkey Authentication for the default forms as follows:
+
NOTE: Storage capacity is usually very limited on hardware passkeys meaning that you cannot store many discoverable credentials on your passkey. However, this limitation may be mitigated for instance if you use an Android phone backed by a Google account as a passkey device or an iPhone backed by Bitwarden.
+
. In the *WebAuthn Passwordless Policy* tab, activate the *Enable Passkeys* option at the bottom. This switch is the one that really enables passkeys in the realm.
. In the *WebAuthn Passwordless Policy* tab, activate the *Enable Passkeys* option at the bottom. This switch is the one that really enables passkeys in the realm.
. Once *Enable Passkeys* is on, the *Passkey Mediation* select appears. Choose the value that best fits your use case. The default `conditional` value keeps behavior unobtrusive — passkeys are offered through autofill only. Switch to `optional` if you want the browser to automatically open a passkey selection dialog on page load while still allowing the user to dismiss it and fall back to autofill. See <<_passkeys-mediation,Passkey Mediation>> for a description of all available values.

View file

@ -2296,6 +2296,12 @@ realmExplain=A realm manages a set of users, credentials, roles, and groups. A u
inputHelperTextBefore=Helper text (above) the input field
webAuthnPolicyExtraOrigins=Extra Origins
webAuthnPolicyPasskeysEnabled=Enable Passkeys
webAuthnPolicyMediation=Passkey Mediation
mediation.conditional=Conditional (autofill only)
mediation.none=None (button only, no automatic prompt)
mediation.optional=Optional (show dialog on page load)
mediation.required=Required (force immediate dialog)
mediation.silent=Silent (no user interaction)
samlSignatureKeyName=SAML signature key name
validateUsersDn=You must enter users DN
importError=Could not import certificate {{error}}
@ -3071,6 +3077,7 @@ generatedUserInfoHelp=See the example User Info, which will be provided by the U
dynamicScopeFormat=Dynamic scope format
webAuthnPolicyExtraOriginsHelp=The list of extra origins for non-web applications.
webAuthnPolicyPasskeysEnabledHelp=Enable passkeys (conditional UI) authentication in the username forms.
webAuthnPolicyMediationHelp=Controls how the browser presents the passkey selection dialog when the login page loads.
updatePermissionSuccess=Successfully updated the permission
idpLinkSuccess=Identity provider has been linked
removeAnnotationText=Remove annotation

View file

@ -64,6 +64,14 @@ const USER_VERIFY = [
"discouraged",
] as const;
const MEDIATION_OPTIONS = [
"conditional",
"none",
"optional",
"required",
"silent",
] as const;
type WeauthnSelectProps = {
name: string;
label: string;
@ -118,6 +126,7 @@ export const WebauthnPolicy = ({
const {
setValue,
handleSubmit,
watch,
formState: { isDirty },
} = form;
@ -265,13 +274,24 @@ export const WebauthnPolicy = ({
/>
</FormGroup>
{isPasswordLess && isFeatureEnabled(Feature.Passkeys) && (
<SwitchControl
name={`${namePrefix}PasskeysEnabled`}
label={t("webAuthnPolicyPasskeysEnabled")}
labelIcon={t("webAuthnPolicyPasskeysEnabledHelp")}
labelOn={t("on")}
labelOff={t("off")}
/>
<>
<SwitchControl
name={`${namePrefix}PasskeysEnabled`}
label={t("webAuthnPolicyPasskeysEnabled")}
labelIcon={t("webAuthnPolicyPasskeysEnabledHelp")}
labelOn={t("on")}
labelOff={t("off")}
/>
{watch(`${namePrefix}PasskeysEnabled`) && (
<WebauthnSelect
name={`${namePrefix}Mediation`}
label={t("webAuthnPolicyMediation")}
labelIcon={t("webAuthnPolicyMediationHelp")}
options={MEDIATION_OPTIONS}
labelPrefix="mediation"
/>
)}
</>
)}
</FormProvider>

View file

@ -1080,6 +1080,12 @@ public class RealmAdapter implements StorageProviderRealmModel, JpaModel<RealmEn
: defaultConfig.isPasskeysEnabled();
policy.setPasskeysEnabled(passKeysEnabled);
String mediation = getAttribute(RealmAttributes.WEBAUTHN_POLICY_MEDIATION + attributePrefix);
if (mediation == null || mediation.isEmpty()) {
mediation = defaultConfig.getMediation();
}
policy.setMediation(mediation);
return policy;
}
@ -1138,6 +1144,13 @@ public class RealmAdapter implements StorageProviderRealmModel, JpaModel<RealmEn
} else {
removeAttribute(RealmAttributes.WEBAUTHN_POLICY_PASSKEYS_ENABLED + attributePrefix);
}
String mediation = policy.getMediation();
if (mediation != null && !mediation.isBlank()) {
setAttribute(RealmAttributes.WEBAUTHN_POLICY_MEDIATION + attributePrefix, mediation);
} else {
removeAttribute(RealmAttributes.WEBAUTHN_POLICY_MEDIATION + attributePrefix);
}
}
@Override

View file

@ -50,6 +50,7 @@ public interface RealmAttributes {
String WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS = "webAuthnPolicyAcceptableAaguids";
String WEBAUTHN_POLICY_EXTRA_ORIGINS = "webAuthnPolicyExtraOrigins";
String WEBAUTHN_POLICY_PASSKEYS_ENABLED = "webAuthnPolicyPasskeysEnabled";
String WEBAUTHN_POLICY_MEDIATION = "webAuthnPolicyMediation";
String ADMIN_EVENTS_EXPIRATION = "adminEventsExpiration";

View file

@ -1427,6 +1427,12 @@ public class DefaultExportImportManager implements ExportImportManager {
}
webAuthnPolicy.setPasskeysEnabled(webAuthnPolicyPasswordlessPasskeysEnabled);
String webAuthnPolicyPasswordlessMediation = rep.getWebAuthnPolicyPasswordlessMediation();
if (webAuthnPolicyPasswordlessMediation == null || webAuthnPolicyPasswordlessMediation.isEmpty()) {
webAuthnPolicyPasswordlessMediation = defaultConfig.getMediation();
}
webAuthnPolicy.setMediation(webAuthnPolicyPasswordlessMediation);
return webAuthnPolicy;
}
public static Map<String, String> importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) {

View file

@ -46,6 +46,7 @@ public class WebAuthnPolicyTwoFactorDefaults extends WebAuthnPolicy {
this.acceptableAaguids = Collections.emptyList();
this.extraOrigins = Collections.emptyList();
this.passkeysEnabled = null;
this.mediation = null;
}
@Override
@ -108,6 +109,11 @@ public class WebAuthnPolicyTwoFactorDefaults extends WebAuthnPolicy {
throwReadOnlyException();
}
@Override
public void setMediation(String mediation) {
throwReadOnlyException();
}
private void throwReadOnlyException() {
throw new ReadOnlyException("Default WebAuthnPolicy!");
}

View file

@ -187,6 +187,7 @@ public class ModelToRepresentation {
REALM_EXCLUDED_ATTRIBUTES.add("webAuthnPolicyAvoidSameAuthenticatorRegisterPasswordless");
REALM_EXCLUDED_ATTRIBUTES.add("webAuthnPolicyAcceptableAaguidsPasswordless");
REALM_EXCLUDED_ATTRIBUTES.add("webAuthnPolicyPasskeysEnabledPasswordless");
REALM_EXCLUDED_ATTRIBUTES.add("webAuthnPolicyMediationPasswordless");
REALM_EXCLUDED_ATTRIBUTES.add(Constants.CLIENT_POLICIES);
REALM_EXCLUDED_ATTRIBUTES.add(Constants.CLIENT_PROFILES);
@ -589,6 +590,7 @@ public class ModelToRepresentation {
rep.setWebAuthnPolicyPasswordlessAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
rep.setWebAuthnPolicyPasswordlessExtraOrigins(webAuthnPolicy.getExtraOrigins());
rep.setWebAuthnPolicyPasswordlessPasskeysEnabled(webAuthnPolicy.isPasskeysEnabled());
rep.setWebAuthnPolicyPasswordlessMediation(webAuthnPolicy.getMediation());
CibaConfig cibaPolicy = realm.getCibaPolicy();
Map<String, String> attrMap = ofNullable(rep.getAttributes()).orElse(new HashMap<>());

View file

@ -43,6 +43,7 @@ public class WebAuthnPolicy implements Serializable {
protected List<String> acceptableAaguids;
protected List<String> extraOrigins;
protected Boolean passkeysEnabled; // only used for passwordless
protected String mediation; // only used for passwordless
public WebAuthnPolicy() {
}
@ -149,4 +150,12 @@ public class WebAuthnPolicy implements Serializable {
public void setPasskeysEnabled(Boolean passkeysEnabled) {
this.passkeysEnabled = passkeysEnabled;
}
public String getMediation() {
return mediation;
}
public void setMediation(String mediation) {
this.mediation = mediation;
}
}

View file

@ -46,6 +46,7 @@ public interface WebAuthnConstants {
String USER_VERIFICATION = "userVerification";
String TRANSPORTS = "transports";
String ENABLE_WEBAUTHN_CONDITIONAL_UI = "enableWebAuthnConditionalUI";
String MEDIATION = "mediation";
String IS_SET_RETRY = "isSetRetry";
String SHOULD_DISPLAY_AUTHENTICATORS = "shouldDisplayAuthenticators";

View file

@ -120,6 +120,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
String userVerificationRequirement = policy.getUserVerificationRequirement();
form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement);
form.setAttribute(WebAuthnConstants.SHOULD_DISPLAY_AUTHENTICATORS, shouldDisplayAuthenticators(context));
form.setAttribute(WebAuthnConstants.MEDIATION, policy.getMediation());
return form;
}

View file

@ -464,6 +464,11 @@ public class RealmBuilder {
return this;
}
public RealmBuilder webAuthnPolicyPasswordlessMediation(String mediation) {
rep.setWebAuthnPolicyPasswordlessMediation(mediation);
return this;
}
public RealmBuilder webAuthnPolicyAcceptableAaguids(List<String> aaguids) {
rep.setWebAuthnPolicyAcceptableAaguids(aaguids);
return this;

View file

@ -40,6 +40,8 @@ import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
@ -69,8 +71,9 @@ public class PasskeysUsernameFormTest extends AbstractWebAuthnVirtualTest {
return true;
}
@Test
public void webauthnLoginWithDiscoverableKey() {
@ParameterizedTest
@ValueSource(strings = {"conditional", "optional"})
public void webauthnLoginWithDiscoverableKey(String mediation) {
getVirtualAuthManager().useAuthenticator(DefaultVirtualAuthOptions.PASSKEYS.getOptions());
// set passwordless policy for discoverable keys
@ -78,7 +81,8 @@ public class PasskeysUsernameFormTest extends AbstractWebAuthnVirtualTest {
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyPasswordlessRpEntityName("localhost")
.webAuthnPolicyPasswordlessRequireResidentKey(Constants.WEBAUTHN_POLICY_OPTION_YES)
.webAuthnPolicyPasswordlessUserVerificationRequirement(Constants.WEBAUTHN_POLICY_OPTION_REQUIRED)
.webAuthnPolicyPasswordlessPasskeysEnabled(Boolean.TRUE));
.webAuthnPolicyPasswordlessPasskeysEnabled(Boolean.TRUE)
.webAuthnPolicyPasswordlessMediation(mediation));
checkWebAuthnConfiguration(Constants.WEBAUTHN_POLICY_OPTION_YES, Constants.WEBAUTHN_POLICY_OPTION_REQUIRED);
registerDefaultUser();

View file

@ -35,6 +35,8 @@ import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
@ -58,8 +60,9 @@ public class PasskeysUsernamePasswordFormTest extends AbstractWebAuthnVirtualTes
return true;
}
@Test
public void webauthnLoginWithDiscoverableKey() {
@ParameterizedTest
@ValueSource(strings = {"conditional", "optional"})
public void webauthnLoginWithDiscoverableKey(String mediation) {
getVirtualAuthManager().useAuthenticator(DefaultVirtualAuthOptions.PASSKEYS.getOptions());
// set passwordless policy for discoverable keys
@ -67,7 +70,8 @@ public class PasskeysUsernamePasswordFormTest extends AbstractWebAuthnVirtualTes
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyPasswordlessRpEntityName("localhost")
.webAuthnPolicyPasswordlessRequireResidentKey(null)
.webAuthnPolicyPasswordlessUserVerificationRequirement(null)
.webAuthnPolicyPasswordlessPasskeysEnabled(Boolean.TRUE));
.webAuthnPolicyPasswordlessPasskeysEnabled(Boolean.TRUE)
.webAuthnPolicyPasswordlessMediation(mediation));
checkWebAuthnConfiguration(Constants.WEBAUTHN_POLICY_OPTION_YES, Constants.WEBAUTHN_POLICY_OPTION_REQUIRED);

View file

@ -25,7 +25,8 @@
challenge : ${challenge?c},
userVerification : ${userVerification?c},
rpId : ${rpId?c},
createTimeout : ${createTimeout?c}
createTimeout : ${createTimeout?c},
mediation : ${(mediation!'conditional')?c},
};
document.addEventListener("DOMContentLoaded", (event) => initAuthenticate({errmsg : ${msg("passkey-unsupported-browser-text")?c}, ...args}));

View file

@ -1,80 +1,86 @@
import { base64url } from "rfc4648";
import { returnSuccess, signal } from "./webauthnAuthenticate.js";
import { doAuthenticate, returnSuccess } from "./webauthnAuthenticate.js";
export function initAuthenticate(input, availableCallback = (available) => {}) {
const PASSKEY_MODAL_DISMISSED = 'kc_passkey_modal_dismissed';
/**
* Returns the current cookie KC_AUTH_SESSION_HASH value if present.
* Undefined if not present.
*/
function getModalDismissedHash() {
for (const cookie of document.cookie.split(';')) {
const [key, value] = cookie.trim().split('=');
if (key === 'KC_AUTH_SESSION_HASH' && value) {
return value;
}
}
return undefined;
}
/**
* Entry point for passkey authentication on page load.
*
* Calls navigator.credentials.get() once with the mediation value configured
* in the WebAuthn Passwordless Policy (conditional/none/optional/required/silent).
* For "none", unsupported browsers, or an already-identified user, nothing is
* attempted automatically the user can always initiate via the button.
*
* For modal mediations (optional/required), the dialog is shown at most once
* per authentication session: if the user dismisses it, it will not reappear
* on subsequent page loads (e.g. after a failed password attempt).
*/
export async function initAuthenticate(input, availableCallback = () => {}) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
// Fail silently as WebAuthn Conditional UI is not required
return;
}
if (input.isUserIdentified || typeof PublicKeyCredential.isConditionalMediationAvailable === "undefined") {
const mediation = input.mediation ?? 'conditional';
if (input.isUserIdentified || mediation === 'none') {
availableCallback(false);
} else {
tryAutoFillUI(input, availableCallback);
}
}
function doAuthenticate(input) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
// Fail silently as WebAuthn Conditional UI is not required
return;
}
const publicKey = {
rpId : input.rpId,
challenge: base64url.parse(input.challenge, { loose: true })
};
publicKey.allowCredentials = !input.isUserIdentified ? [] : getAllowCredentials();
if (input.createTimeout !== 0) {
publicKey.timeout = input.createTimeout * 1000;
}
if (input.userVerification !== 'not specified') {
publicKey.userVerification = input.userVerification;
}
return navigator.credentials.get({
publicKey: publicKey,
signal: signal(),
...input.additionalOptions
});
}
async function tryAutoFillUI(input, availableCallback = (available) => {}) {
const isConditionalMediationAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
if (isConditionalMediationAvailable) {
// The isConditionalMediationAvailable() check is only relevant for
// conditional (autofill) mediation — other modes do not depend on it.
if (mediation === 'conditional') {
if (typeof PublicKeyCredential.isConditionalMediationAvailable === 'undefined') {
availableCallback(false);
return;
}
const isAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
if (!isAvailable) {
// Treat unavailable conditional UI the same as 'none'
availableCallback(false);
return;
}
availableCallback(true);
input.additionalOptions = { mediation: 'conditional'};
try {
const result = await doAuthenticate(input);
returnSuccess(result);
} catch {
// Fail silently as WebAuthn Conditional UI is not required
}
} else {
availableCallback(false);
}
}
function getAllowCredentials() {
const allowCredentials = [];
const authnUse = document.forms['authn_select'].authn_use_chk;
if (authnUse !== undefined) {
if (authnUse.length === undefined) {
allowCredentials.push({
id: base64url.parse(authnUse.value, {loose: true}),
type: 'public-key',
});
} else {
authnUse.forEach((entry) =>
allowCredentials.push({
id: base64url.parse(entry.value, {loose: true}),
type: 'public-key',
}));
// For modal mediations, skip if the user already dismissed the dialog in
// this authentication session — avoids re-interrupting on every page load.
const modalDismissedHash = getModalDismissedHash();
if ((!modalDismissedHash || modalDismissedHash === sessionStorage.getItem(PASSKEY_MODAL_DISMISSED)) &&
(mediation === 'optional' || mediation === 'required')) {
return;
}
try {
const result = await doAuthenticate({
...input,
allowCredentials: [],
additionalOptions: { mediation },
});
if (result) returnSuccess(result);
} catch (err) {
// If the user explicitly dismissed the modal, remember it so it is not
// shown again during the same authentication session.
if ((mediation === 'optional' || mediation === 'required') &&
(err?.name === 'NotAllowedError' || err?.name === 'AbortError')) {
sessionStorage.setItem(PASSKEY_MODAL_DISMISSED, modalDismissedHash);
}
}
return allowCredentials;
}

View file

@ -16,70 +16,75 @@ export function signal() {
}
export async function authenticateByWebAuthn(input) {
if (!input.isUserIdentified) {
try {
const result = await doAuthenticate([], input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
returnSuccess(result);
} catch (error) {
returnFailure(error);
}
return;
}
checkAllowCredentials(input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
}
async function checkAllowCredentials(challenge, userVerification, rpId, createTimeout, errmsg) {
const allowCredentials = [];
const authnUse = document.forms['authn_select'].authn_use_chk;
if (authnUse !== undefined) {
if (authnUse.length === undefined) {
allowCredentials.push({
id: base64url.parse(authnUse.value, {loose: true}),
type: 'public-key',
});
} else {
authnUse.forEach((entry) =>
allowCredentials.push({
id: base64url.parse(entry.value, {loose: true}),
type: 'public-key',
}));
}
}
const allowCredentials = input.isUserIdentified ? getAllowCredentials() : [];
try {
const result = await doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg);
returnSuccess(result);
const result = await doAuthenticate({ ...input, allowCredentials });
if (result) returnSuccess(result);
} catch (error) {
returnFailure(error);
}
}
function doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg) {
/**
* Reads the allowed credentials from the hidden authn_select form.
* Exported so that passkeysConditionalAuth.js can use them as well.
*/
export function getAllowCredentials() {
const allowCredentials = [];
const authnUse = document.forms['authn_select']?.authn_use_chk;
if (authnUse !== undefined) {
if (authnUse.length === undefined) {
allowCredentials.push({
id: base64url.parse(authnUse.value, { loose: true }),
type: 'public-key',
});
} else {
authnUse.forEach((entry) =>
allowCredentials.push({
id: base64url.parse(entry.value, { loose: true }),
type: 'public-key',
}));
}
}
return allowCredentials;
}
/**
* Core function for navigator.credentials.get().
* Exported so that passkeysConditionalAuth.js does not need its own copy.
*
* input: { challenge, userVerification, rpId, createTimeout, errmsg,
* allowCredentials?: PublicKeyCredentialDescriptor[],
* additionalOptions?: object e.g. { mediation: "conditional" | "optional" | "required" | "silent" } }
*/
export function doAuthenticate(input) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
returnFailure(errmsg);
returnFailure(input.errmsg);
return;
}
const publicKey = {
rpId : rpId,
challenge: base64url.parse(challenge, { loose: true })
rpId: input.rpId,
challenge: base64url.parse(input.challenge, { loose: true }),
};
if (createTimeout !== 0) {
publicKey.timeout = createTimeout * 1000;
if (input.createTimeout !== 0) {
publicKey.timeout = input.createTimeout * 1000;
}
if (allowCredentials.length) {
publicKey.allowCredentials = allowCredentials;
if (input.allowCredentials !== undefined) {
publicKey.allowCredentials = input.allowCredentials;
}
if (userVerification !== 'not specified') {
publicKey.userVerification = userVerification;
if (input.userVerification !== 'not specified') {
publicKey.userVerification = input.userVerification;
}
return navigator.credentials.get({
publicKey: publicKey,
signal: signal()
signal: signal(),
...input.additionalOptions,
});
}