mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update passkeys support
This commit is contained in:
@@ -2,6 +2,8 @@ import Alpine from '@alpinejs/csp';
|
||||
|
||||
Alpine.data('passkeyManage', () => ({
|
||||
supported: false,
|
||||
platformAvailable: false,
|
||||
platformChecked: false,
|
||||
busy: false,
|
||||
name: '',
|
||||
error: '',
|
||||
@@ -14,6 +16,7 @@ Alpine.data('passkeyManage', () => ({
|
||||
init() {
|
||||
this.supported = typeof window.browserSupportsWebAuthn === 'function'
|
||||
&& window.browserSupportsWebAuthn();
|
||||
void this.detectPlatformAuthenticator();
|
||||
|
||||
// Capture URLs from the root element's data attributes once, so we don't
|
||||
// depend on $el inside event handlers (which may resolve to the triggering
|
||||
@@ -41,7 +44,54 @@ Alpine.data('passkeyManage', () => ({
|
||||
}
|
||||
},
|
||||
|
||||
async detectPlatformAuthenticator() {
|
||||
this.platformAvailable = typeof window.platformAuthenticatorIsAvailable === 'function'
|
||||
&& await window.platformAuthenticatorIsAvailable();
|
||||
this.platformChecked = true;
|
||||
},
|
||||
|
||||
isOperaOnWindows() {
|
||||
const userAgent = window.navigator?.userAgent || '';
|
||||
const platform = window.navigator?.platform || '';
|
||||
|
||||
return (userAgent.includes('OPR/') || userAgent.includes('Opera'))
|
||||
&& (userAgent.includes('Windows') || platform.startsWith('Win'));
|
||||
},
|
||||
|
||||
async createPasskey() {
|
||||
return this.createBrowserPasskey();
|
||||
},
|
||||
|
||||
async createBrowserPasskey() {
|
||||
if (this.browserPasskeyBlocked()) {
|
||||
this.error = 'Opera on Windows is routing passkey creation through Windows Security and cannot create a browser/app passkey here. Use Edge/Chrome with Windows Hello or a password-manager passkey provider, or use the USB security key button.';
|
||||
return;
|
||||
}
|
||||
|
||||
return this.createPasskeyWithMode('browser');
|
||||
},
|
||||
|
||||
browserPasskeyBlocked() {
|
||||
return this.isOperaOnWindows();
|
||||
},
|
||||
|
||||
browserPasskeyNotice() {
|
||||
if (this.browserPasskeyBlocked()) {
|
||||
return 'Opera on Windows routes browser/app passkey creation through Windows Security on this setup. Use Edge/Chrome with Windows Hello or a password-manager passkey provider, or use the USB security key button.';
|
||||
}
|
||||
|
||||
if (this.platformChecked && !this.platformAvailable) {
|
||||
return 'This browser did not report a built-in platform passkey provider. You can still try browser/app passkey creation; if it fails, enable Windows Hello or a password-manager passkey provider, or use the USB security key button.';
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
async createSecurityKeyPasskey() {
|
||||
return this.createPasskeyWithMode('security-key');
|
||||
},
|
||||
|
||||
async createPasskeyWithMode(mode) {
|
||||
if (!this.supported) {
|
||||
this.error = 'Your browser does not support passkeys.';
|
||||
return;
|
||||
@@ -60,7 +110,9 @@ Alpine.data('passkeyManage', () => ({
|
||||
});
|
||||
const options = optionsResponse.data?.options;
|
||||
|
||||
const registration = await this.startRegistrationWithFallback(options);
|
||||
const registration = mode === 'security-key'
|
||||
? await window.startRegistration({ optionsJSON: this.withSecurityKeyHint(options) })
|
||||
: await this.startBrowserPasskeyRegistration(options);
|
||||
|
||||
const storeResponse = await window.axios.post(storeUrl, {
|
||||
name: this.name,
|
||||
@@ -118,24 +170,54 @@ Alpine.data('passkeyManage', () => ({
|
||||
}
|
||||
},
|
||||
|
||||
async startRegistrationWithFallback(options) {
|
||||
try {
|
||||
return await window.startRegistration({ optionsJSON: options });
|
||||
} catch (error) {
|
||||
if (!this.shouldTryBrowserPasskeyRegistration(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return window.startRegistration({
|
||||
optionsJSON: options,
|
||||
useAutoRegister: true,
|
||||
});
|
||||
}
|
||||
async startBrowserPasskeyRegistration(options) {
|
||||
return window.startRegistration({ optionsJSON: this.withBrowserPasskeyHints(options) });
|
||||
},
|
||||
|
||||
shouldTryBrowserPasskeyRegistration(error) {
|
||||
return error instanceof Error
|
||||
&& ['NotAllowedError', 'ConstraintError', 'UnknownError'].includes(error.name);
|
||||
withBrowserPasskeyHints(options) {
|
||||
const normalized = this.cloneOptions(options);
|
||||
normalized.hints = ['client-device'];
|
||||
normalized.authenticatorSelection = {
|
||||
...normalized.authenticatorSelection,
|
||||
authenticatorAttachment: 'platform',
|
||||
residentKey: normalized.authenticatorSelection?.residentKey || 'preferred',
|
||||
userVerification: normalized.authenticatorSelection?.userVerification || 'preferred',
|
||||
};
|
||||
|
||||
if (normalized.publicKey) {
|
||||
normalized.publicKey.hints = ['client-device'];
|
||||
normalized.publicKey.authenticatorSelection = {
|
||||
...normalized.publicKey.authenticatorSelection,
|
||||
authenticatorAttachment: 'platform',
|
||||
residentKey: normalized.publicKey.authenticatorSelection?.residentKey || 'preferred',
|
||||
userVerification: normalized.publicKey.authenticatorSelection?.userVerification || 'preferred',
|
||||
};
|
||||
}
|
||||
|
||||
return normalized;
|
||||
},
|
||||
|
||||
withSecurityKeyHint(options) {
|
||||
const normalized = this.cloneOptions(options);
|
||||
normalized.hints = ['security-key', 'client-device', 'hybrid'];
|
||||
normalized.authenticatorSelection = {
|
||||
...normalized.authenticatorSelection,
|
||||
authenticatorAttachment: 'cross-platform',
|
||||
};
|
||||
|
||||
if (normalized.publicKey) {
|
||||
normalized.publicKey.hints = ['security-key', 'client-device', 'hybrid'];
|
||||
normalized.publicKey.authenticatorSelection = {
|
||||
...normalized.publicKey.authenticatorSelection,
|
||||
authenticatorAttachment: 'cross-platform',
|
||||
};
|
||||
}
|
||||
|
||||
return normalized;
|
||||
},
|
||||
|
||||
cloneOptions(options) {
|
||||
return JSON.parse(JSON.stringify(options));
|
||||
},
|
||||
|
||||
formatDate(value) {
|
||||
|
||||
Vendored
+2
@@ -2,6 +2,7 @@ import axios from 'axios';
|
||||
import {
|
||||
browserSupportsWebAuthn,
|
||||
browserSupportsWebAuthnAutofill,
|
||||
platformAuthenticatorIsAvailable,
|
||||
startAuthentication,
|
||||
startRegistration,
|
||||
} from '@simplewebauthn/browser';
|
||||
@@ -11,5 +12,6 @@ window.axios = axios;
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
window.browserSupportsWebAuthn = browserSupportsWebAuthn;
|
||||
window.browserSupportsWebAuthnAutofill = browserSupportsWebAuthnAutofill;
|
||||
window.platformAuthenticatorIsAvailable = platformAuthenticatorIsAvailable;
|
||||
window.startAuthentication = startAuthentication;
|
||||
window.startRegistration = startRegistration;
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
data-passkeys='@json($passkeyPayload)'
|
||||
>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Register a passkey using Windows Hello, Touch ID / Face ID, your phone, a password manager, or a FIDO2 security key.
|
||||
Register a passkey using your browser, password manager, Windows Hello, Touch ID / Face ID, or your phone.
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
On a Windows domain / managed device the browser will show all available options. If Windows Hello is still disabled by group policy, you can fall back to a hardware security key or scan a QR code with your phone.
|
||||
Hardware security keys are available separately when you want to use a USB/FIDO2 key.
|
||||
</p>
|
||||
|
||||
<template x-if="!supported">
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<template x-if="supported">
|
||||
<div class="mt-4 space-y-4">
|
||||
<form @submit.prevent="createPasskey()" class="space-y-3">
|
||||
<form @submit.prevent="createBrowserPasskey()" class="space-y-3">
|
||||
<label for="passkey_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Passkey name
|
||||
</label>
|
||||
@@ -48,13 +48,24 @@
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="busy"
|
||||
:disabled="busy || browserPasskeyBlocked()"
|
||||
class="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-primary-700 dark:hover:bg-primary-800"
|
||||
>
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
<span x-text="busy ? 'Creating...' : 'Create passkey'"></span>
|
||||
<span x-text="busy ? 'Creating...' : 'Create browser/app passkey'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="browserPasskeyNotice()" x-text="browserPasskeyNotice()" x-cloak class="text-xs text-yellow-700 dark:text-yellow-300">
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
@click="createSecurityKeyPasskey()"
|
||||
:disabled="busy || !name"
|
||||
class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
<i class="fas fa-key mr-2"></i>
|
||||
Use USB security key
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p x-show="error" x-text="error" class="text-sm text-red-600 dark:text-red-400"></p>
|
||||
|
||||
Reference in New Issue
Block a user