diff --git a/CHANGES b/CHANGES index 48cc37f7d..b82d6392c 100644 --- a/CHANGES +++ b/CHANGES @@ -3184,4 +3184,5 @@ * backup sites: identify conflicting site locations * update: add policy to update apps and platform separately * passkey: fix issue where passkeys were lost on restart +* passkey: implement passwordless login diff --git a/dashboard/oidc_login.html b/dashboard/oidc_login.html index 544b2019d..61dc495a4 100644 --- a/dashboard/oidc_login.html +++ b/dashboard/oidc_login.html @@ -9,6 +9,8 @@ name: name, note: note, submitUrl: submitUrl, + passkeyAuthOptionsUrl: passkeyAuthOptionsUrl, + passkeyLoginUrl: passkeyLoginUrl, footer: footer, language: language }) %>; diff --git a/dashboard/public/translation/en.json b/dashboard/public/translation/en.json index fa4f44473..601c0af31 100644 --- a/dashboard/public/translation/en.json +++ b/dashboard/public/translation/en.json @@ -1543,7 +1543,8 @@ "errorInternal": "Internal error, try again later", "loginAction": "Log in", "usePasskeyAction": "Use passkey", - "errorPasskeyFailed": "Failed to login with passkey" + "errorPasskeyFailed": "Failed to login with passkey", + "passkeyAction": "Log in with a passkey" }, "passwordReset": { "title": "Password reset", diff --git a/dashboard/src/views/LoginView.vue b/dashboard/src/views/LoginView.vue index 38001eceb..a0d148374 100644 --- a/dashboard/src/views/LoginView.vue +++ b/dashboard/src/views/LoginView.vue @@ -25,6 +25,8 @@ const note = window.cloudron.note; const iconUrl = window.cloudron.iconUrl; const footer = window.cloudron.footer; const submitUrl = window.cloudron.submitUrl; +const passkeyAuthOptionsUrl = window.cloudron.passkeyAuthOptionsUrl; +const passkeyLoginUrl = window.cloudron.passkeyLoginUrl; async function onSubmit() { busy.value = true; @@ -123,6 +125,41 @@ async function onUsePasskey() { busy.value = false; } +async function onLoginWithPasskey() { + busy.value = true; + passkeyError.value = false; + internalError.value = false; + + try { + const optionsRes = await fetcher.post(passkeyAuthOptionsUrl, {}); + if (optionsRes.status !== 200) { + internalError.value = true; + busy.value = false; + return; + } + + const credential = await startAuthentication({ optionsJSON: optionsRes.body }); + + const res = await fetcher.post(passkeyLoginUrl, { passkeyResponse: credential }); + + if (res.status === 200 && res.body.redirectTo) { + window.location.href = res.body.redirectTo; + return; + } else if (res.status === 401) { + passkeyError.value = true; + } else { + internalError.value = true; + } + } catch (error) { + if (error.name !== 'NotAllowedError') { + console.error('Passkey login failed', error); + passkeyError.value = true; + } + } + + busy.value = false; +} + onMounted(async () => { // placed optionally in local storage by setupaccount.js const autoLoginToken = localStorage.cloudronFirstTimeToken; @@ -171,8 +208,10 @@ onMounted(async () => {
+