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:
+4
-2
@@ -69,14 +69,16 @@ return [
|
|||||||
/*
|
/*
|
||||||
* WebAuthn Level 3 client hints. Modern Chromium based browsers (including
|
* WebAuthn Level 3 client hints. Modern Chromium based browsers (including
|
||||||
* Edge on Windows) use these to render a richer credential chooser that
|
* Edge on Windows) use these to render a richer credential chooser that
|
||||||
* lists Windows Hello, mobile (hybrid/QR) and security keys side by side.
|
* lists local browser/app passkeys and mobile (hybrid/QR) options first.
|
||||||
|
* Security keys are still allowed by WebAuthn, but are not the default hint
|
||||||
|
* because managed Windows devices can otherwise jump straight to the USB
|
||||||
|
* security-key prompt.
|
||||||
*
|
*
|
||||||
* Allowed values: "client-device", "hybrid", "security-key".
|
* Allowed values: "client-device", "hybrid", "security-key".
|
||||||
*/
|
*/
|
||||||
'hints' => [
|
'hints' => [
|
||||||
'client-device',
|
'client-device',
|
||||||
'hybrid',
|
'hybrid',
|
||||||
'security-key',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import Alpine from '@alpinejs/csp';
|
|||||||
|
|
||||||
Alpine.data('passkeyManage', () => ({
|
Alpine.data('passkeyManage', () => ({
|
||||||
supported: false,
|
supported: false,
|
||||||
|
platformAvailable: false,
|
||||||
|
platformChecked: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
name: '',
|
name: '',
|
||||||
error: '',
|
error: '',
|
||||||
@@ -14,6 +16,7 @@ Alpine.data('passkeyManage', () => ({
|
|||||||
init() {
|
init() {
|
||||||
this.supported = typeof window.browserSupportsWebAuthn === 'function'
|
this.supported = typeof window.browserSupportsWebAuthn === 'function'
|
||||||
&& window.browserSupportsWebAuthn();
|
&& window.browserSupportsWebAuthn();
|
||||||
|
void this.detectPlatformAuthenticator();
|
||||||
|
|
||||||
// Capture URLs from the root element's data attributes once, so we don't
|
// 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
|
// 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() {
|
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) {
|
if (!this.supported) {
|
||||||
this.error = 'Your browser does not support passkeys.';
|
this.error = 'Your browser does not support passkeys.';
|
||||||
return;
|
return;
|
||||||
@@ -60,7 +110,9 @@ Alpine.data('passkeyManage', () => ({
|
|||||||
});
|
});
|
||||||
const options = optionsResponse.data?.options;
|
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, {
|
const storeResponse = await window.axios.post(storeUrl, {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@@ -118,24 +170,54 @@ Alpine.data('passkeyManage', () => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async startRegistrationWithFallback(options) {
|
async startBrowserPasskeyRegistration(options) {
|
||||||
try {
|
return window.startRegistration({ optionsJSON: this.withBrowserPasskeyHints(options) });
|
||||||
return await window.startRegistration({ optionsJSON: options });
|
|
||||||
} catch (error) {
|
|
||||||
if (!this.shouldTryBrowserPasskeyRegistration(error)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.startRegistration({
|
|
||||||
optionsJSON: options,
|
|
||||||
useAutoRegister: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldTryBrowserPasskeyRegistration(error) {
|
withBrowserPasskeyHints(options) {
|
||||||
return error instanceof Error
|
const normalized = this.cloneOptions(options);
|
||||||
&& ['NotAllowedError', 'ConstraintError', 'UnknownError'].includes(error.name);
|
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) {
|
formatDate(value) {
|
||||||
|
|||||||
Vendored
+2
@@ -2,6 +2,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
browserSupportsWebAuthn,
|
browserSupportsWebAuthn,
|
||||||
browserSupportsWebAuthnAutofill,
|
browserSupportsWebAuthnAutofill,
|
||||||
|
platformAuthenticatorIsAvailable,
|
||||||
startAuthentication,
|
startAuthentication,
|
||||||
startRegistration,
|
startRegistration,
|
||||||
} from '@simplewebauthn/browser';
|
} from '@simplewebauthn/browser';
|
||||||
@@ -11,5 +12,6 @@ window.axios = axios;
|
|||||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
window.browserSupportsWebAuthn = browserSupportsWebAuthn;
|
window.browserSupportsWebAuthn = browserSupportsWebAuthn;
|
||||||
window.browserSupportsWebAuthnAutofill = browserSupportsWebAuthnAutofill;
|
window.browserSupportsWebAuthnAutofill = browserSupportsWebAuthnAutofill;
|
||||||
|
window.platformAuthenticatorIsAvailable = platformAuthenticatorIsAvailable;
|
||||||
window.startAuthentication = startAuthentication;
|
window.startAuthentication = startAuthentication;
|
||||||
window.startRegistration = startRegistration;
|
window.startRegistration = startRegistration;
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
data-passkeys='@json($passkeyPayload)'
|
data-passkeys='@json($passkeyPayload)'
|
||||||
>
|
>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
<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>
|
||||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<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>
|
</p>
|
||||||
|
|
||||||
<template x-if="!supported">
|
<template x-if="!supported">
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
<template x-if="supported">
|
<template x-if="supported">
|
||||||
<div class="mt-4 space-y-4">
|
<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">
|
<label for="passkey_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Passkey name
|
Passkey name
|
||||||
</label>
|
</label>
|
||||||
@@ -48,13 +48,24 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
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>
|
<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>
|
</button>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
|
|
||||||
<p x-show="error" x-text="error" class="text-sm text-red-600 dark:text-red-400"></p>
|
<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