Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -101,7 +101,7 @@
{
if (!string.IsNullOrEmpty(Input.Passkey?.Error))
{
errorMessage = $"Error: Could not log in using the provided passkey: {Input.Passkey.Error}";
errorMessage = $"Error: {Input.Passkey.Error}";
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ else

if (!string.IsNullOrEmpty(Input.Error))
{
RedirectManager.RedirectToCurrentPageWithStatus($"Error: Could not add a passkey: {Input.Error}", HttpContext);
RedirectManager.RedirectToCurrentPageWithStatus($"Error: {Input.Error}", HttpContext);
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
async function fetchWithErrorHandling(url, options = {}) {
const browserSupportsPasskeys =
typeof navigator.credentials !== 'undefined' &&
typeof window.PublicKeyCredential !== 'undefined' &&
typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&
typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function';

async function fetchWithErrorHandling(url, options = {}) {
const response = await fetch(url, {
credentials: 'include',
...options
Expand Down Expand Up @@ -45,7 +51,7 @@ customElements.define('passkey-submit', class extends HTMLElement {
this.internals.form.addEventListener('submit', (event) => {
if (event.submitter?.name === '__passkeySubmit') {
event.preventDefault();
this.obtainCredentialAndSubmit();
this.obtainAndSubmitCredential();
}
});

Expand All @@ -56,39 +62,49 @@ customElements.define('passkey-submit', class extends HTMLElement {
this.abortController?.abort();
}

async obtainCredentialAndSubmit(useConditionalMediation = false) {
async obtainCredential(useConditionalMediation, signal) {
if (!browserSupportsPasskeys) {
throw new Error('Some passkey features are missing. Please update your browser.');
}

if (this.attrs.operation === 'Create') {
return await createCredential(signal);
} else if (this.attrs.operation === 'Request') {
const email = new FormData(this.internals.form).get(this.attrs.emailName);
const mediation = useConditionalMediation ? 'conditional' : undefined;
return await requestCredential(email, mediation, signal);
} else {
throw new Error(`Unknown passkey operation '${this.attrs.operation}'.`);
}
}

async obtainAndSubmitCredential(useConditionalMediation = false) {
this.abortController?.abort();
this.abortController = new AbortController();
const signal = this.abortController.signal;
const formData = new FormData();
try {
let credential;
if (this.attrs.operation === 'Create') {
credential = await createCredential(signal);
} else if (this.attrs.operation === 'Request') {
const email = new FormData(this.internals.form).get(this.attrs.emailName);
const mediation = useConditionalMediation ? 'conditional' : undefined;
credential = await requestCredential(email, mediation, signal);
} else {
throw new Error(`Unknown passkey operation '${operation}'.`);
}
const credential = await this.obtainCredential(useConditionalMediation, signal);
const credentialJson = JSON.stringify(credential);
formData.append(`${this.attrs.name}.CredentialJson`, credentialJson);
} catch (error) {
if (error.name === 'AbortError') {
// Canceled by user action, do not submit the form
console.error(error);
if (useConditionalMediation || error.name === 'AbortError') {
// We do not relay the error to the user if:
// 1. We are attempting conditional mediation, meaning the user did not initiate the operation.
// 2. The user explicitly canceled the operation.
return;
}
formData.append(`${this.attrs.name}.Error`, error.message);
console.error(error);
const errorMessage = error.name === 'NotAllowedError' ? 'Unable to authenticate.' : error.message;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should put something in the error message to indicate that there's more information in the developer console. And now that Login.razor doesn't add anything to the message mentioning "passkey", I wonder if we should mention that here.

Suggested change
const errorMessage = error.name === 'NotAllowedError' ? 'Unable to authenticate.' : error.message;
const errorMessage = error.name === 'NotAllowedError' ? 'Unable to authenticate with passkey. See developer console for more details.' : error.message;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe we could improve the wording here. The thing about "Unable to authenticate with passkey" is that IMO it could be interpreted as if passkey was obtained, but we just couldn't authenticate with it (implying a possible error on the server). Whereas, what actually happened is that the authenticator couldn't authenticate the user, so no passkey was provided to the browser. Maybe something like "No passkey was provided by the authenticator" could work?

And regarding "See developer console for more details" - this is definitely helpful to the developer, but might it be unusual to display this to the user? The fact that this error shows up in a user-facing part of the UI makes me a little hesitant to put that there. I guess the developer could always remove that bit if they want.

formData.append(`${this.attrs.name}.Error`, errorMessage);
}
this.internals.setFormValue(formData);
this.internals.form.submit();
}

async tryAutofillPasskey() {
if (this.attrs.operation === 'Request' && await PublicKeyCredential.isConditionalMediationAvailable()) {
await this.obtainCredentialAndSubmit(/* useConditionalMediation */ true);
if (browserSupportsPasskeys && this.attrs.operation === 'Request' && await PublicKeyCredential.isConditionalMediationAvailable?.()) {
await this.obtainAndSubmitCredential(/* useConditionalMediation */ true);
}
}
});
Loading