Просмотр исходного кода

security(webauthn)!: require discoverable passkeys

Remove the username-based WebAuthn login flow because it allowed
username enumeration before password verification.

WebAuthn login now uses discoverable credentials only, and new
registrations require resident keys. Existing non-resident credentials
are no longer usable for first-factor login; they should only be used
in a post-password MFA flow, which Miniflux does not currently
implement.

BREAKING CHANGE: Users with existing non-resident WebAuthn credentials
must register a new passkey.
Fred 1 неделя назад
Родитель
Сommit
059ec55f52

+ 0 - 1
internal/locale/translations/ar_SA.json

@@ -561,7 +561,6 @@
     "page.login.title": "تسجيل الدخول",
     "page.login.title": "تسجيل الدخول",
     "page.login.webauthn_login": "تسجيل الدخول عبر مفتاح مرور (Passkey)",
     "page.login.webauthn_login": "تسجيل الدخول عبر مفتاح مرور (Passkey)",
     "page.login.webauthn_login.error": "تعذر تسجيل الدخول باستخدام مفتاح المرور",
     "page.login.webauthn_login.error": "تعذر تسجيل الدخول باستخدام مفتاح المرور",
-    "page.login.webauthn_login.help": "يرجى إدخال اسم المستخدم إذا كنت تستخدم مفتاح أمان. هذا غير مطلوب إذا كنت تستخدم مفتاح مرور (بيانات اعتماد قابلة للاكتشاف).",
     "page.new_api_key.title": "مفتاح API جديد",
     "page.new_api_key.title": "مفتاح API جديد",
     "page.new_category.title": "فئة جديدة",
     "page.new_category.title": "فئة جديدة",
     "page.new_user.title": "مستخدم جديد",
     "page.new_user.title": "مستخدم جديد",

+ 0 - 1
internal/locale/translations/de_DE.json

@@ -537,7 +537,6 @@
     "page.login.title": "Anmeldung",
     "page.login.title": "Anmeldung",
     "page.login.webauthn_login": "Melden Sie sich mit dem Passkey an",
     "page.login.webauthn_login": "Melden Sie sich mit dem Passkey an",
     "page.login.webauthn_login.error": "Anmeldung mit Passkey nicht möglich",
     "page.login.webauthn_login.error": "Anmeldung mit Passkey nicht möglich",
-    "page.login.webauthn_login.help": "Bitte geben Sie Ihren Benutzernamen ein, sofern Sie einen Sicherheitsschlüssel verwenden. Dies ist nicht nötig, wenn Sie einen Passkey verwenden (auffindbare Anmeldeinformationen).",
     "page.new_api_key.title": "Neuer API-Schlüssel",
     "page.new_api_key.title": "Neuer API-Schlüssel",
     "page.new_category.title": "Neue Kategorie",
     "page.new_category.title": "Neue Kategorie",
     "page.new_user.title": "Neuer Benutzer",
     "page.new_user.title": "Neuer Benutzer",

+ 0 - 1
internal/locale/translations/el_EL.json

@@ -537,7 +537,6 @@
     "page.login.title": "Είσοδος",
     "page.login.title": "Είσοδος",
     "page.login.webauthn_login": "Είσοδος με κωδικό πρόσβασης",
     "page.login.webauthn_login": "Είσοδος με κωδικό πρόσβασης",
     "page.login.webauthn_login.error": "Δεν είναι δυνατή η σύνδεση με κωδικό πρόσβασης",
     "page.login.webauthn_login.error": "Δεν είναι δυνατή η σύνδεση με κωδικό πρόσβασης",
-    "page.login.webauthn_login.help": "Παρακαλώ εισαγάγετε το όνομα χρήστη σας εάν χρησιμοποιείτε κλειδί ασφαλείας. Αυτό δεν απαιτείται εάν χρησιμοποιείτε Passkey (ανακαλύψιμα διαπιστευτήρια).",
     "page.new_api_key.title": "Νέο κλειδί API",
     "page.new_api_key.title": "Νέο κλειδί API",
     "page.new_category.title": "Νέα Κατηγορία",
     "page.new_category.title": "Νέα Κατηγορία",
     "page.new_user.title": "Νέος Χρήστης",
     "page.new_user.title": "Νέος Χρήστης",

+ 0 - 1
internal/locale/translations/en_US.json

@@ -537,7 +537,6 @@
     "page.login.title": "Sign In",
     "page.login.title": "Sign In",
     "page.login.webauthn_login": "Login with passkey",
     "page.login.webauthn_login": "Login with passkey",
     "page.login.webauthn_login.error": "Unable to login with passkey",
     "page.login.webauthn_login.error": "Unable to login with passkey",
-    "page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
     "page.new_api_key.title": "New API Key",
     "page.new_api_key.title": "New API Key",
     "page.new_category.title": "New Category",
     "page.new_category.title": "New Category",
     "page.new_user.title": "New User",
     "page.new_user.title": "New User",

+ 0 - 1
internal/locale/translations/es_ES.json

@@ -537,7 +537,6 @@
     "page.login.title": "Iniciar sesión",
     "page.login.title": "Iniciar sesión",
     "page.login.webauthn_login": "Iniciar sesión con clave de acceso",
     "page.login.webauthn_login": "Iniciar sesión con clave de acceso",
     "page.login.webauthn_login.error": "No se puede iniciar sesión con la clave de acceso",
     "page.login.webauthn_login.error": "No se puede iniciar sesión con la clave de acceso",
-    "page.login.webauthn_login.help": "Por favor, introduce tu nombre de usuario si usas una clave de seguridad. Esto no es necesario si usas una Passkey (credenciales detectables).",
     "page.new_api_key.title": "Nueva clave API",
     "page.new_api_key.title": "Nueva clave API",
     "page.new_category.title": "Nueva categoría",
     "page.new_category.title": "Nueva categoría",
     "page.new_user.title": "Nuevo usuario",
     "page.new_user.title": "Nuevo usuario",

+ 0 - 1
internal/locale/translations/fi_FI.json

@@ -537,7 +537,6 @@
     "page.login.title": "Kirjaudu sisään",
     "page.login.title": "Kirjaudu sisään",
     "page.login.webauthn_login": "Kirjaudu sisään salasanalla",
     "page.login.webauthn_login": "Kirjaudu sisään salasanalla",
     "page.login.webauthn_login.error": "Ei voida kirjautua sisään salasanalla",
     "page.login.webauthn_login.error": "Ei voida kirjautua sisään salasanalla",
-    "page.login.webauthn_login.help": "Jos käytät turva-avainta, kirjoita käyttäjätunnus. Passkeytä käyttäessä tämä ei ole tarpeen.",
     "page.new_api_key.title": "Uusi API-avain",
     "page.new_api_key.title": "Uusi API-avain",
     "page.new_category.title": "Uusi kategoria",
     "page.new_category.title": "Uusi kategoria",
     "page.new_user.title": "Uusi käyttäjä",
     "page.new_user.title": "Uusi käyttäjä",

+ 0 - 1
internal/locale/translations/fr_FR.json

@@ -537,7 +537,6 @@
     "page.login.title": "Connexion",
     "page.login.title": "Connexion",
     "page.login.webauthn_login": "Se connecter avec une clé d’accès",
     "page.login.webauthn_login": "Se connecter avec une clé d’accès",
     "page.login.webauthn_login.error": "Impossible de se connecter avec la clé d’accès",
     "page.login.webauthn_login.error": "Impossible de se connecter avec la clé d’accès",
-    "page.login.webauthn_login.help": "Veuillez saisir votre nom d'utilisateur si vous utilisez une clé de sécurité. Cela n'est pas nécessaire si vous utilisez une clé d'accès (Passkey).",
     "page.new_api_key.title": "Nouvelle clé d'API",
     "page.new_api_key.title": "Nouvelle clé d'API",
     "page.new_category.title": "Nouvelle catégorie",
     "page.new_category.title": "Nouvelle catégorie",
     "page.new_user.title": "Nouvel Utilisateur",
     "page.new_user.title": "Nouvel Utilisateur",

+ 0 - 1
internal/locale/translations/gl_ES.json

@@ -537,7 +537,6 @@
     "page.login.title": "Acceder",
     "page.login.title": "Acceder",
     "page.login.webauthn_login": "Acceso con clave de paso",
     "page.login.webauthn_login": "Acceso con clave de paso",
     "page.login.webauthn_login.error": "Non se puido acceder coa clave de paso",
     "page.login.webauthn_login.error": "Non se puido acceder coa clave de paso",
-    "page.login.webauthn_login.help": "Por favor escribe o teu identificador se estás a usar unha chave de seguridade. Non se require isto se estás a usar unha «Clave de Paso» (credenciais descubribles).",
     "page.new_api_key.title": "Nova clave da API",
     "page.new_api_key.title": "Nova clave da API",
     "page.new_category.title": "Nova Categoría",
     "page.new_category.title": "Nova Categoría",
     "page.new_user.title": "Nova Usuaria",
     "page.new_user.title": "Nova Usuaria",

+ 0 - 1
internal/locale/translations/hi_IN.json

@@ -537,7 +537,6 @@
     "page.login.title": "साइन इन करें",
     "page.login.title": "साइन इन करें",
     "page.login.webauthn_login": "पासकी से लॉगिन करें",
     "page.login.webauthn_login": "पासकी से लॉगिन करें",
     "page.login.webauthn_login.error": "पासकी से लॉगिन करने में असमर्थ",
     "page.login.webauthn_login.error": "पासकी से लॉगिन करने में असमर्थ",
-    "page.login.webauthn_login.help": "यदि आप सुरक्षा कुंजी का उपयोग कर रहे हैं तो कृपया अपना उपयोगकर्ता नाम दर्ज करें। पासकी (discoverable credentials) के लिए यह आवश्यक नहीं है।",
     "page.new_api_key.title": "नई एपीआई कुंजी",
     "page.new_api_key.title": "नई एपीआई कुंजी",
     "page.new_category.title": "नया श्रेणी",
     "page.new_category.title": "नया श्रेणी",
     "page.new_user.title": "नया उपभोक्ता",
     "page.new_user.title": "नया उपभोक्ता",

+ 0 - 1
internal/locale/translations/id_ID.json

@@ -531,7 +531,6 @@
     "page.login.title": "Masuk",
     "page.login.title": "Masuk",
     "page.login.webauthn_login": "Masuk menggunakan passkey",
     "page.login.webauthn_login": "Masuk menggunakan passkey",
     "page.login.webauthn_login.error": "Tidak dapat masuk menggunakan passkey",
     "page.login.webauthn_login.error": "Tidak dapat masuk menggunakan passkey",
-    "page.login.webauthn_login.help": "Mohon untuk memasukkan nama pengguna Anda jika Anda menggunakan kunci keamanan. Tidak diperlukan jika anda menggunakan Passkey (kredensial dapat ditemukan).",
     "page.new_api_key.title": "Kunci API Baru",
     "page.new_api_key.title": "Kunci API Baru",
     "page.new_category.title": "Kategori Baru",
     "page.new_category.title": "Kategori Baru",
     "page.new_user.title": "Pengguna Baru",
     "page.new_user.title": "Pengguna Baru",

+ 0 - 1
internal/locale/translations/it_IT.json

@@ -537,7 +537,6 @@
     "page.login.title": "Accedi",
     "page.login.title": "Accedi",
     "page.login.webauthn_login": "Accedi con passkey",
     "page.login.webauthn_login": "Accedi con passkey",
     "page.login.webauthn_login.error": "Impossibile accedere con passkey",
     "page.login.webauthn_login.error": "Impossibile accedere con passkey",
-    "page.login.webauthn_login.help": "Inserisci il tuo nome utente se stai usando una chiave di sicurezza. Non è necessario con una Passkey (credenziali rilevabili).",
     "page.new_api_key.title": "Nuova chiave API",
     "page.new_api_key.title": "Nuova chiave API",
     "page.new_category.title": "Nuova categoria",
     "page.new_category.title": "Nuova categoria",
     "page.new_user.title": "Nuovo utente",
     "page.new_user.title": "Nuovo utente",

+ 0 - 1
internal/locale/translations/ja_JP.json

@@ -531,7 +531,6 @@
     "page.login.title": "ログイン",
     "page.login.title": "ログイン",
     "page.login.webauthn_login": "パスキーでログイン",
     "page.login.webauthn_login": "パスキーでログイン",
     "page.login.webauthn_login.error": "パスキーでログインできない",
     "page.login.webauthn_login.error": "パスキーでログインできない",
-    "page.login.webauthn_login.help": "セキュリティキーを使用する場合はユーザー名を入力してください。パスキー(検出可能な認証情報)の場合は不要です。",
     "page.new_api_key.title": "新しい API キー",
     "page.new_api_key.title": "新しい API キー",
     "page.new_category.title": "新規カテゴリ",
     "page.new_category.title": "新規カテゴリ",
     "page.new_user.title": "新規ユーザー",
     "page.new_user.title": "新規ユーザー",

+ 0 - 1
internal/locale/translations/nan_Latn_pehoeji.json

@@ -531,7 +531,6 @@
     "page.login.title": "teng-lo̍k",
     "page.login.title": "teng-lo̍k",
     "page.login.webauthn_login": "Sú-iōng bi̍t-bé teng-lo̍k",
     "page.login.webauthn_login": "Sú-iōng bi̍t-bé teng-lo̍k",
     "page.login.webauthn_login.error": "Bô-hoat-tō͘ iōng bi̍t-bé teng-lo̍k",
     "page.login.webauthn_login.error": "Bô-hoat-tō͘ iōng bi̍t-bé teng-lo̍k",
-    "page.login.webauthn_login.help": "Sú-iōng an-choân só-sî teng-lo̍k ê sî-chūn, chhiáⁿ su-li̍p kháu-chō miâ. Nā-sī iōng thang chhiau-chhē ê Passkey (discoverable credentials) tio̍h bián.",
     "page.new_api_key.title": "Sin ê API só-sî",
     "page.new_api_key.title": "Sin ê API só-sî",
     "page.new_category.title": "Sin lūi-pia̍t",
     "page.new_category.title": "Sin lūi-pia̍t",
     "page.new_user.title": "Sin sú-iōng-lâng",
     "page.new_user.title": "Sin sú-iōng-lâng",

+ 0 - 1
internal/locale/translations/nl_NL.json

@@ -537,7 +537,6 @@
     "page.login.title": "Inloggen",
     "page.login.title": "Inloggen",
     "page.login.webauthn_login": "Inloggen met passkey",
     "page.login.webauthn_login": "Inloggen met passkey",
     "page.login.webauthn_login.error": "Kan niet inloggen met passkey",
     "page.login.webauthn_login.error": "Kan niet inloggen met passkey",
-    "page.login.webauthn_login.help": "Voer je gebruikersnaam in als je een beveiligingssleutel gebruikt. Dit is niet nodig als je een Passkey (ontdekkingsbare referenties) gebruikt.",
     "page.new_api_key.title": "Nieuwe API-sleutel",
     "page.new_api_key.title": "Nieuwe API-sleutel",
     "page.new_category.title": "Nieuwe categorie",
     "page.new_category.title": "Nieuwe categorie",
     "page.new_user.title": "Nieuwe gebruiker",
     "page.new_user.title": "Nieuwe gebruiker",

+ 0 - 1
internal/locale/translations/pl_PL.json

@@ -543,7 +543,6 @@
     "page.login.title": "Zaloguj się",
     "page.login.title": "Zaloguj się",
     "page.login.webauthn_login": "Zaloguj się przez klucz dostępu",
     "page.login.webauthn_login": "Zaloguj się przez klucz dostępu",
     "page.login.webauthn_login.error": "Nie można zalogować się za pomocą klucza dostępu",
     "page.login.webauthn_login.error": "Nie można zalogować się za pomocą klucza dostępu",
-    "page.login.webauthn_login.help": "Wpisz swoją nazwę użytkownika, jeśli używasz klucza bezpieczeństwa. Nie jest to wymagane, jeśli używasz klucza dostępu (wykrywalnych danych uwierzytelniających).",
     "page.new_api_key.title": "Nowy klucz API",
     "page.new_api_key.title": "Nowy klucz API",
     "page.new_category.title": "Nowa kategoria",
     "page.new_category.title": "Nowa kategoria",
     "page.new_user.title": "Nowy użytkownik",
     "page.new_user.title": "Nowy użytkownik",

+ 0 - 1
internal/locale/translations/pt_BR.json

@@ -537,7 +537,6 @@
     "page.login.title": "Iniciar Sessão",
     "page.login.title": "Iniciar Sessão",
     "page.login.webauthn_login": "Entrar com senha",
     "page.login.webauthn_login": "Entrar com senha",
     "page.login.webauthn_login.error": "Não é possível fazer login com senha",
     "page.login.webauthn_login.error": "Não é possível fazer login com senha",
-    "page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
     "page.new_api_key.title": "Nova chave de API",
     "page.new_api_key.title": "Nova chave de API",
     "page.new_category.title": "Nova categoria",
     "page.new_category.title": "Nova categoria",
     "page.new_user.title": "Novo usuário",
     "page.new_user.title": "Novo usuário",

+ 0 - 1
internal/locale/translations/ro_RO.json

@@ -543,7 +543,6 @@
     "page.login.title": "Conectare",
     "page.login.title": "Conectare",
     "page.login.webauthn_login": "Conectare cu cheia de acces",
     "page.login.webauthn_login": "Conectare cu cheia de acces",
     "page.login.webauthn_login.error": "Eroare la conectarea cu cheia de acces",
     "page.login.webauthn_login.error": "Eroare la conectarea cu cheia de acces",
-    "page.login.webauthn_login.help": "Vă rog să introduceți numele utilizatorului dacă utilizați o cheie. Nu este necesară dacă utilizați o cheie de acces (credențiale descoperibile).",
     "page.new_api_key.title": "Cheie API Nouă",
     "page.new_api_key.title": "Cheie API Nouă",
     "page.new_category.title": "Categorie Nouă",
     "page.new_category.title": "Categorie Nouă",
     "page.new_user.title": "Utilizator Nou",
     "page.new_user.title": "Utilizator Nou",

+ 0 - 1
internal/locale/translations/ru_RU.json

@@ -543,7 +543,6 @@
     "page.login.title": "Войти",
     "page.login.title": "Войти",
     "page.login.webauthn_login": "Войти с паролем",
     "page.login.webauthn_login": "Войти с паролем",
     "page.login.webauthn_login.error": "Невозможно войти с паролем",
     "page.login.webauthn_login.error": "Невозможно войти с паролем",
-    "page.login.webauthn_login.help": "Пожалуйста, введите имя пользователя, если вы используете ключ безопасности. Это не требуется при использовании Passkey (обнаруживаемые учетные данные).",
     "page.new_api_key.title": "Новый API-ключ",
     "page.new_api_key.title": "Новый API-ключ",
     "page.new_category.title": "Новая категория",
     "page.new_category.title": "Новая категория",
     "page.new_user.title": "Новый пользователь",
     "page.new_user.title": "Новый пользователь",

+ 0 - 1
internal/locale/translations/tr_TR.json

@@ -537,7 +537,6 @@
     "page.login.title": "Oturum aç",
     "page.login.title": "Oturum aç",
     "page.login.webauthn_login": "Passkey ile giriş yap",
     "page.login.webauthn_login": "Passkey ile giriş yap",
     "page.login.webauthn_login.error": "Passkey ile giriş yapılamıyor",
     "page.login.webauthn_login.error": "Passkey ile giriş yapılamıyor",
-    "page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
     "page.new_api_key.title": "Yeni API Anahtarı",
     "page.new_api_key.title": "Yeni API Anahtarı",
     "page.new_category.title": "Yeni Kategori",
     "page.new_category.title": "Yeni Kategori",
     "page.new_user.title": "Yeni Kullanıcı",
     "page.new_user.title": "Yeni Kullanıcı",

+ 0 - 1
internal/locale/translations/uk_UA.json

@@ -543,7 +543,6 @@
     "page.login.title": "Вхід",
     "page.login.title": "Вхід",
     "page.login.webauthn_login": "Увійти за допомогою пароля",
     "page.login.webauthn_login": "Увійти за допомогою пароля",
     "page.login.webauthn_login.error": "Неможливо ввійти за допомогою ключа доступу",
     "page.login.webauthn_login.error": "Неможливо ввійти за допомогою ключа доступу",
-    "page.login.webauthn_login.help": "Якщо використовуєте ключ безпеки, введіть ім'я користувача. Для паролю-паскі це не потрібно.",
     "page.new_api_key.title": "Створити ключ API",
     "page.new_api_key.title": "Створити ключ API",
     "page.new_category.title": "Нова категорія",
     "page.new_category.title": "Нова категорія",
     "page.new_user.title": "Новий користувач",
     "page.new_user.title": "Новий користувач",

+ 0 - 1
internal/locale/translations/zh_CN.json

@@ -531,7 +531,6 @@
     "page.login.title": "登录",
     "page.login.title": "登录",
     "page.login.webauthn_login": "使用通行密钥登录",
     "page.login.webauthn_login": "使用通行密钥登录",
     "page.login.webauthn_login.error": "无法使用通行密钥登录",
     "page.login.webauthn_login.error": "无法使用通行密钥登录",
-    "page.login.webauthn_login.help": "如果您正在使用安全密钥,请输入您的用户名。如果您正在使用通行密钥(可发现凭证),则无需输入。",
     "page.new_api_key.title": "新的 API 密钥",
     "page.new_api_key.title": "新的 API 密钥",
     "page.new_category.title": "新建分类",
     "page.new_category.title": "新建分类",
     "page.new_user.title": "新建用户",
     "page.new_user.title": "新建用户",

+ 0 - 1
internal/locale/translations/zh_TW.json

@@ -531,7 +531,6 @@
     "page.login.title": "登入",
     "page.login.title": "登入",
     "page.login.webauthn_login": "使用密碼登入",
     "page.login.webauthn_login": "使用密碼登入",
     "page.login.webauthn_login.error": "無法使用密碼登入",
     "page.login.webauthn_login.error": "無法使用密碼登入",
-    "page.login.webauthn_login.help": "使用安全金鑰登入時,請輸入使用者名稱。若使用可探索式 Passkey 則無需輸入。",
     "page.new_api_key.title": "新的 API 金鑰",
     "page.new_api_key.title": "新的 API 金鑰",
     "page.new_category.title": "新分類",
     "page.new_category.title": "新分類",
     "page.new_user.title": "新使用者",
     "page.new_user.title": "新使用者",

+ 1 - 4
internal/template/templates/views/login.html

@@ -15,7 +15,7 @@
         {{ end }}
         {{ end }}
 
 
         <label for="form-username">{{ t "form.user.label.username" }}</label>
         <label for="form-username">{{ t "form.user.label.username" }}</label>
-        <input type="text" name="username" id="form-username" value="{{ .form.Username }}" autocomplete="username" required autofocus>
+        <input type="text" name="username" id="form-username" value="{{ .form.Username }}" autocomplete="username webauthn" required autofocus>
 
 
         <label for="form-password">{{ t "form.user.label.password" }}</label>
         <label for="form-password">{{ t "form.user.label.password" }}</label>
         <input type="password" name="password" id="form-password" value="{{ .form.Password }}" autocomplete="current-password" required>
         <input type="password" name="password" id="form-password" value="{{ .form.Password }}" autocomplete="current-password" required>
@@ -39,9 +39,6 @@
         <div class="buttons">
         <div class="buttons">
             <button class="button button-primary" id="webauthn-login" disabled>{{ t "page.login.webauthn_login" }}</button>
             <button class="button button-primary" id="webauthn-login" disabled>{{ t "page.login.webauthn_login" }}</button>
         </div>
         </div>
-        <div class="form-help">
-            <p>{{ t "page.login.webauthn_login.help" }}</p>
-        </div>
     </div>
     </div>
     {{ end }}
     {{ end }}
     {{ if and (.webAuthnEnabled) (or (hasOAuth2Provider "google") (hasOAuth2Provider "oidc")) }}
     {{ if and (.webAuthnEnabled) (or (hasOAuth2Provider "google") (hasOAuth2Provider "oidc")) }}

+ 1 - 1
internal/template/templates/views/webauthn_rename.html

@@ -15,7 +15,7 @@
     {{ end }}
     {{ end }}
 
 
     <label for="form-title">{{ t "page.settings.webauthn.passkey_name" }}</label>
     <label for="form-title">{{ t "page.settings.webauthn.passkey_name" }}</label>
-    <input type="text" name="name" id="form-title" value="{{ .form.Name }}" autofocus>
+    <input type="text" name="name" id="form-title" value="{{ .form.Name }}" maxlength="255" required autofocus>
 
 
     <div class="buttons">
     <div class="buttons">
         <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
         <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>

+ 12 - 1
internal/ui/form/webauthn.go

@@ -5,6 +5,9 @@ package form // import "miniflux.app/v2/internal/ui/form"
 
 
 import (
 import (
 	"net/http"
 	"net/http"
+	"strings"
+
+	"miniflux.app/v2/internal/locale"
 )
 )
 
 
 // WebauthnForm represents a credential rename form in the UI
 // WebauthnForm represents a credential rename form in the UI
@@ -12,9 +15,17 @@ type WebauthnForm struct {
 	Name string
 	Name string
 }
 }
 
 
+// Validate makes sure the form values are valid.
+func (f *WebauthnForm) Validate() *locale.LocalizedError {
+	if f.Name == "" {
+		return locale.NewLocalizedError("error.fields_mandatory")
+	}
+	return nil
+}
+
 // NewWebauthnForm returns a new WebnauthnForm.
 // NewWebauthnForm returns a new WebnauthnForm.
 func NewWebauthnForm(r *http.Request) *WebauthnForm {
 func NewWebauthnForm(r *http.Request) *WebauthnForm {
 	return &WebauthnForm{
 	return &WebauthnForm{
-		Name: r.FormValue("name"),
+		Name: strings.TrimSpace(r.FormValue("name")),
 	}
 	}
 }
 }

+ 1 - 1
internal/ui/static/js/app.js

@@ -1152,7 +1152,7 @@ function initializeWebAuthn() {
 
 
         onClick("#webauthn-login", () => {
         onClick("#webauthn-login", () => {
             abortController.abort();
             abortController.abort();
-            webauthnHandler.login(usernameField.value).catch(err => WebAuthnHandler.showErrorMessage(err));
+            webauthnHandler.login().catch(err => WebAuthnHandler.showErrorMessage(err));
         });
         });
 
 
         webauthnHandler.conditionalLogin(abortController).catch(err => WebAuthnHandler.showErrorMessage(err));
         webauthnHandler.conditionalLogin(abortController).catch(err => WebAuthnHandler.showErrorMessage(err));

+ 10 - 17
internal/ui/static/js/webauthn_handler.js

@@ -30,7 +30,7 @@ class WebAuthnHandler {
 
 
     async conditionalLogin(abortController) {
     async conditionalLogin(abortController) {
         if (await WebAuthnHandler.isConditionalLoginSupported()) {
         if (await WebAuthnHandler.isConditionalLoginSupported()) {
-            return this.login("", abortController);
+            return this.login(abortController);
         }
         }
     }
     }
 
 
@@ -45,26 +45,19 @@ class WebAuthnHandler {
             .replace(/=+$/g, "");
             .replace(/=+$/g, "");
     }
     }
 
 
-    async post(urlKey, username, data) {
-        let url = document.body.dataset[urlKey];
-        if (username) {
-            url += `?username=${encodeURIComponent(username)}`;
-        }
-
+    async post(urlKey, data) {
+        const url = document.body.dataset[urlKey];
         return sendPOSTRequest(url, data);
         return sendPOSTRequest(url, data);
     }
     }
 
 
-    async get(urlKey, username) {
-        let url = document.body.dataset[urlKey];
-        if (username) {
-            url += `?username=${encodeURIComponent(username)}`;
-        }
+    async get(urlKey) {
+        const url = document.body.dataset[urlKey];
         return fetch(url);
         return fetch(url);
     }
     }
 
 
     async removeAllCredentials() {
     async removeAllCredentials() {
         try {
         try {
-            await this.post("webauthnDeleteAllUrl", null, {});
+            await this.post("webauthnDeleteAllUrl", {});
         } catch (err) {
         } catch (err) {
             WebAuthnHandler.showErrorMessage(err);
             WebAuthnHandler.showErrorMessage(err);
             return;
             return;
@@ -108,7 +101,7 @@ class WebAuthnHandler {
 
 
         let registrationFinishResponse;
         let registrationFinishResponse;
         try {
         try {
-            registrationFinishResponse = await this.post("webauthnRegisterFinishUrl", null, {
+            registrationFinishResponse = await this.post("webauthnRegisterFinishUrl", {
                 id: attestation.id,
                 id: attestation.id,
                 rawId: this.encodeBuffer(attestation.rawId),
                 rawId: this.encodeBuffer(attestation.rawId),
                 type: attestation.type,
                 type: attestation.type,
@@ -130,10 +123,10 @@ class WebAuthnHandler {
         window.location.href = jsonData.redirect;
         window.location.href = jsonData.redirect;
     }
     }
 
 
-    async login(username, abortController) {
+    async login(abortController) {
         let loginBeginResponse;
         let loginBeginResponse;
         try {
         try {
-            loginBeginResponse = await this.get("webauthnLoginBeginUrl", username);
+            loginBeginResponse = await this.get("webauthnLoginBeginUrl");
         } catch (err) {
         } catch (err) {
             WebAuthnHandler.showErrorMessage(err);
             WebAuthnHandler.showErrorMessage(err);
             return;
             return;
@@ -179,7 +172,7 @@ class WebAuthnHandler {
 
 
         let loginFinishResponse;
         let loginFinishResponse;
         try {
         try {
-            loginFinishResponse = await this.post("webauthnLoginFinishUrl", username, {
+            loginFinishResponse = await this.post("webauthnLoginFinishUrl", {
                 id: assertion.id,
                 id: assertion.id,
                 rawId: this.encodeBuffer(assertion.rawId),
                 rawId: this.encodeBuffer(assertion.rawId),
                 type: assertion.type,
                 type: assertion.type,

+ 70 - 132
internal/ui/webauthn.go

@@ -4,7 +4,6 @@
 package ui // import "miniflux.app/v2/internal/ui"
 package ui // import "miniflux.app/v2/internal/ui"
 
 
 import (
 import (
-	"bytes"
 	"encoding/hex"
 	"encoding/hex"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
@@ -96,8 +95,7 @@ func (h *handler) beginRegistration(w http.ResponseWriter, r *http.Request) {
 			AuthnID: crypto.GenerateRandomBytes(32),
 			AuthnID: crypto.GenerateRandomBytes(32),
 		},
 		},
 		webauthn.WithExclusions(credentialDescriptors),
 		webauthn.WithExclusions(credentialDescriptors),
-		webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementPreferred),
-		webauthn.WithExtensions(protocol.AuthenticationExtensions{"credProps": true}),
+		webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {
@@ -150,35 +148,10 @@ func (h *handler) beginLogin(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	var user *model.User
-	username := request.QueryStringParam(r, "username", "")
-	if username != "" {
-		user, err = h.store.UserByUsername(username)
-		if err != nil {
-			response.JSONUnauthorized(w, r)
-			return
-		}
-	}
-
-	var assertion *protocol.CredentialAssertion
-	var sessionData *webauthn.SessionData
-	if user != nil {
-		credentials, err := h.store.WebAuthnCredentialsByUserID(user.ID)
-		if err != nil {
-			response.JSONServerError(w, r, err)
-			return
-		}
-		assertion, sessionData, err = web.BeginLogin(WebAuthnUser{User: user, Credentials: credentials})
-		if err != nil {
-			response.JSONServerError(w, r, err)
-			return
-		}
-	} else {
-		assertion, sessionData, err = web.BeginDiscoverableLogin()
-		if err != nil {
-			response.JSONServerError(w, r, err)
-			return
-		}
+	assertion, sessionData, err := web.BeginDiscoverableLogin()
+	if err != nil {
+		response.JSONServerError(w, r, err)
+		return
 	}
 	}
 
 
 	request.WebSession(r).SetWebAuthn(sessionData)
 	request.WebSession(r).SetWebAuthn(sessionData)
@@ -212,117 +185,52 @@ func (h *handler) finishLogin(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	var user *model.User
-	username := request.QueryStringParam(r, "username", "")
-	if username != "" {
-		user, err = h.store.UserByUsername(username)
+	var resolvedUser *model.User
+	var resolvedCredential *model.WebAuthnCredential
+
+	userByHandle := func(rawID, userHandle []byte) (webauthn.User, error) {
+		userID, credential, err := h.store.WebAuthnCredentialByHandle(userHandle)
 		if err != nil {
 		if err != nil {
-			response.JSONUnauthorized(w, r)
-			return
+			return nil, err
 		}
 		}
-	}
-
-	var matchingCredential *model.WebAuthnCredential
-	if user != nil {
-		storedCredentials, err := h.store.WebAuthnCredentialsByUserID(user.ID)
+		if userID == 0 || credential == nil {
+			return nil, fmt.Errorf("no user found for handle %x", userHandle)
+		}
+		loadedUser, err := h.store.UserByID(userID)
 		if err != nil {
 		if err != nil {
-			response.JSONServerError(w, r, err)
-			return
+			return nil, err
 		}
 		}
-
-		sessionData.UserID = parsedResponse.Response.UserHandle
-		webAuthnUser := WebAuthnUser{
-			User:        user,
-			AuthnID:     parsedResponse.Response.UserHandle,
-			Credentials: storedCredentials,
+		if loadedUser == nil {
+			return nil, fmt.Errorf("no user found for handle %x", userHandle)
 		}
 		}
 
 
 		// Since go-webauthn v0.11.0, the backup eligibility flag is strictly validated, but Miniflux does not store this flag.
 		// Since go-webauthn v0.11.0, the backup eligibility flag is strictly validated, but Miniflux does not store this flag.
 		// This workaround set the flag based on the parsed response, and avoid "BackupEligible flag inconsistency detected during login validation" error.
 		// This workaround set the flag based on the parsed response, and avoid "BackupEligible flag inconsistency detected during login validation" error.
 		// See https://github.com/go-webauthn/webauthn/pull/240
 		// See https://github.com/go-webauthn/webauthn/pull/240
-		for index := range webAuthnUser.Credentials {
-			webAuthnUser.Credentials[index].Credential.Flags.BackupEligible = parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible()
-		}
-
-		for _, cred := range webAuthnUser.WebAuthnCredentials() {
-			slog.Debug("WebAuthn: stored credential flags",
-				slog.Bool("user_present", cred.Flags.UserPresent),
-				slog.Bool("user_verified", cred.Flags.UserVerified),
-				slog.Bool("backup_eligible", cred.Flags.BackupEligible),
-				slog.Bool("backup_state", cred.Flags.BackupState),
-			)
-		}
+		credential.Credential.Flags.BackupEligible = parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible()
 
 
-		validatedCredential, err := web.ValidateLogin(webAuthnUser, *sessionData, parsedResponse)
-		if err != nil {
-			slog.Warn("WebAuthn: ValidateLogin failed",
-				slog.String("client_ip", request.ClientIP(r)),
-				slog.String("user_agent", r.UserAgent()),
-				slog.String("username", user.Username),
-				slog.Any("error", err),
-			)
-			response.JSONUnauthorized(w, r)
-			return
-		}
-
-		for index := range storedCredentials {
-			if bytes.Equal(validatedCredential.ID, storedCredentials[index].Credential.ID) {
-				matchingCredential = &storedCredentials[index]
-				break
-			}
-		}
-
-		if matchingCredential == nil {
-			response.JSONServerError(w, r, fmt.Errorf("no matching credential for %v", validatedCredential))
-			return
-		}
-	} else {
-		var resolvedUser *model.User
-		var resolvedCredential *model.WebAuthnCredential
-
-		userByHandle := func(rawID, userHandle []byte) (webauthn.User, error) {
-			userID, credential, err := h.store.WebAuthnCredentialByHandle(userHandle)
-			if err != nil {
-				return nil, err
-			}
-			if userID == 0 || credential == nil {
-				return nil, fmt.Errorf("no user found for handle %x", userHandle)
-			}
-			loadedUser, err := h.store.UserByID(userID)
-			if err != nil {
-				return nil, err
-			}
-			if loadedUser == nil {
-				return nil, fmt.Errorf("no user found for handle %x", userHandle)
-			}
-
-			// Since go-webauthn v0.11.0, the backup eligibility flag is strictly validated, but Miniflux does not store this flag.
-			// This workaround set the flag based on the parsed response, and avoid "BackupEligible flag inconsistency detected during login validation" error.
-			// See https://github.com/go-webauthn/webauthn/pull/240
-			credential.Credential.Flags.BackupEligible = parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible()
-
-			resolvedUser = loadedUser
-			resolvedCredential = credential
-			return WebAuthnUser{
-				User:        loadedUser,
-				AuthnID:     userHandle,
-				Credentials: []model.WebAuthnCredential{*credential},
-			}, nil
-		}
+		resolvedUser = loadedUser
+		resolvedCredential = credential
+		return WebAuthnUser{
+			User:        loadedUser,
+			AuthnID:     userHandle,
+			Credentials: []model.WebAuthnCredential{*credential},
+		}, nil
+	}
 
 
-		if _, err := web.ValidateDiscoverableLogin(userByHandle, *sessionData, parsedResponse); err != nil {
-			slog.Warn("WebAuthn: ValidateDiscoverableLogin failed",
-				slog.String("client_ip", request.ClientIP(r)),
-				slog.String("user_agent", r.UserAgent()),
-				slog.Any("error", err),
-			)
-			response.JSONUnauthorized(w, r)
-			return
-		}
-		user = resolvedUser
-		matchingCredential = resolvedCredential
+	if _, err := web.ValidateDiscoverableLogin(userByHandle, *sessionData, parsedResponse); err != nil {
+		slog.Warn("WebAuthn: ValidateDiscoverableLogin failed",
+			slog.String("client_ip", request.ClientIP(r)),
+			slog.String("user_agent", r.UserAgent()),
+			slog.Any("error", err),
+		)
+		response.JSONUnauthorized(w, r)
+		return
 	}
 	}
 
 
+	user := resolvedUser
+	matchingCredential := resolvedCredential
+
 	if err := h.store.WebAuthnSaveLogin(matchingCredential.Handle); err != nil {
 	if err := h.store.WebAuthnSaveLogin(matchingCredential.Handle); err != nil {
 		slog.Warn("WebAuthn: unable to update last seen date for credential",
 		slog.Warn("WebAuthn: unable to update last seen date for credential",
 			slog.Int64("user_id", user.ID),
 			slog.Int64("user_id", user.ID),
@@ -390,6 +298,12 @@ func (h *handler) renameCredential(w http.ResponseWriter, r *http.Request) {
 }
 }
 
 
 func (h *handler) saveCredential(w http.ResponseWriter, r *http.Request) {
 func (h *handler) saveCredential(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
+	if err != nil {
+		response.HTMLServerError(w, r, err)
+		return
+	}
+
 	credentialHandleEncoded := request.RouteStringParam(r, "credentialHandle")
 	credentialHandleEncoded := request.RouteStringParam(r, "credentialHandle")
 	credentialHandle, err := hex.DecodeString(credentialHandleEncoded)
 	credentialHandle, err := hex.DecodeString(credentialHandleEncoded)
 	if err != nil {
 	if err != nil {
@@ -397,8 +311,32 @@ func (h *handler) saveCredential(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	newName := r.FormValue("name")
-	rowsAffected, err := h.store.WebAuthnUpdateName(request.UserID(r), credentialHandle, newName)
+	credUserID, credential, err := h.store.WebAuthnCredentialByHandle(credentialHandle)
+	if err != nil {
+		response.HTMLServerError(w, r, err)
+		return
+	}
+	if credUserID != user.ID {
+		response.HTMLForbidden(w, r)
+		return
+	}
+
+	webauthnForm := form.NewWebauthnForm(r)
+	if validationErr := webauthnForm.Validate(); validationErr != nil {
+		v := view.New(h.tpl, r)
+		v.Set("form", webauthnForm)
+		v.Set("cred", credential)
+		v.Set("menu", "settings")
+		v.Set("user", user)
+		v.Set("errorMessage", validationErr.Translate(request.WebSession(r).Language()))
+		navMetadata, _ := h.store.GetNavMetadata(user.ID)
+		v.Set("countUnread", navMetadata.CountUnread)
+		v.Set("countErrorFeeds", navMetadata.CountErrorFeeds)
+		response.HTML(w, r, v.Render("webauthn_rename"))
+		return
+	}
+
+	rowsAffected, err := h.store.WebAuthnUpdateName(user.ID, credentialHandle, webauthnForm.Name)
 	if err != nil {
 	if err != nil {
 		response.HTMLServerError(w, r, err)
 		response.HTMLServerError(w, r, err)
 		return
 		return