From c128d75d8ec0c265a10062e6313500315622261f Mon Sep 17 00:00:00 2001 From: antoine Date: Thu, 18 Jun 2026 17:07:17 +0200 Subject: [PATCH 1/8] wip --- resources/js/Pages/Auth/Login.vue | 33 ++-------- resources/js/Pages/Auth/usePasskeyLogin.ts | 34 ++++++++++ src/Auth/TwoFactor/Sharp2faPasskeyHandler.php | 65 +++++++++++++++++++ src/Config/SharpConfigBuilder.php | 12 ++++ src/SharpInternalServiceProvider.php | 2 + 5 files changed, 117 insertions(+), 29 deletions(-) create mode 100644 resources/js/Pages/Auth/usePasskeyLogin.ts create mode 100644 src/Auth/TwoFactor/Sharp2faPasskeyHandler.php diff --git a/resources/js/Pages/Auth/Login.vue b/resources/js/Pages/Auth/Login.vue index 3fa88e9b3..3155ef450 100644 --- a/resources/js/Pages/Auth/Login.vue +++ b/resources/js/Pages/Auth/Login.vue @@ -23,6 +23,7 @@ } from "@simplewebauthn/browser"; import { api } from "@/api/api"; import { Separator } from "@/components/ui/separator"; + import { usePasskeyLogin } from "@/Pages/Auth/usePasskeyLogin"; const props = defineProps<{ loginIsEmail: boolean, @@ -42,37 +43,11 @@ supports_passkeys: browserSupportsWebAuthn(), }); - const passkeyForm = useForm({ - remember: false, - start_authentication_response: '', - }); - - async function loginWithPasskey(autofill = false) { - try { - const response = await api.get(route('passkeys.authentication_options'), { - ignoreContentType: true, - }); - - const authenticationOptions = response.data; - const authenticationResponse = await startAuthentication({ - optionsJSON: authenticationOptions, - useBrowserAutofill: autofill, - }); - - console.log(authenticationResponse); - - passkeyForm.remember = form.remember; - passkeyForm.start_authentication_response = JSON.stringify(authenticationResponse); - - passkeyForm.post(route('passkeys.login')); - } catch (error) { - console.error(error); - } - } + const { loginWithPasskey } = usePasskeyLogin(); onMounted(async () => { if(config('sharp.auth.passkeys.enabled') && await browserSupportsWebAuthnAutofill()) { - await loginWithPasskey(true); + await loginWithPasskey({ autofill: true }); } }); @@ -161,7 +136,7 @@ type="button" variant="outline" class="w-full" - @click="loginWithPasskey()" + @click="loginWithPasskey({ remember: form.remember })" > {{ __('sharp::pages/auth/login.passkey_button') }} diff --git a/resources/js/Pages/Auth/usePasskeyLogin.ts b/resources/js/Pages/Auth/usePasskeyLogin.ts new file mode 100644 index 000000000..bd1c1704e --- /dev/null +++ b/resources/js/Pages/Auth/usePasskeyLogin.ts @@ -0,0 +1,34 @@ +import { useForm } from "@inertiajs/vue3"; +import { api } from "@/api/api"; +import { route } from "@/utils/url"; +import { startAuthentication } from "@simplewebauthn/browser"; + +export function usePasskeyLogin() { + const passkeyForm = useForm({ + remember: false, + start_authentication_response: '', + }); + + async function loginWithPasskey({ autofill = false, remember = false }) { + try { + const response = await api.get(route('passkeys.authentication_options'), { + ignoreContentType: true, + }); + + const authenticationOptions = response.data; + const authenticationResponse = await startAuthentication({ + optionsJSON: authenticationOptions, + useBrowserAutofill: autofill, + }); + + passkeyForm.remember = remember; + passkeyForm.start_authentication_response = JSON.stringify(authenticationResponse); + + passkeyForm.post(route('passkeys.login')); + } catch (error) { + console.error(error); + } + } + + return { loginWithPasskey } +} diff --git a/src/Auth/TwoFactor/Sharp2faPasskeyHandler.php b/src/Auth/TwoFactor/Sharp2faPasskeyHandler.php new file mode 100644 index 000000000..538d1ef72 --- /dev/null +++ b/src/Auth/TwoFactor/Sharp2faPasskeyHandler.php @@ -0,0 +1,65 @@ +getSessionKey(), + [ + 'user_id' => $this->user->id, + 'remember' => $remember, + ] + ); + } + + public function isExpectingLogin(): bool + { + return Session::has($this->getSessionKey()); + } + + public function setUser($user): self + { + $this->user = $user; + + return $this; + } + + public function isEnabledFor($user): bool + { + return true; + } + + public function checkCode(string $code): bool + { + return $this->isExpectingLogin() + && Hash::check($code, Session::get($this->getSessionKey())['code']); + } + + public function userId(): mixed + { + return Session::get($this->getSessionKey())['user_id'] ?? null; + } + + public function remember(): bool + { + return Session::get($this->getSessionKey())['remember'] ?? false; + } + + public function forgetCode(): void + { + Session::forget($this->getSessionKey()); + } + + protected function getSessionKey(): string + { + return 'sharp:2fa:prompt-passkey'; + } +} diff --git a/src/Config/SharpConfigBuilder.php b/src/Config/SharpConfigBuilder.php index 2a1141364..cfcece7b2 100644 --- a/src/Config/SharpConfigBuilder.php +++ b/src/Config/SharpConfigBuilder.php @@ -520,6 +520,18 @@ public function enable2faByTotp(): self return $this; } + public function enable2faByPasskey(): self + { + $this->config['auth']['2fa'] = [ + 'enabled' => true, + 'handler' => 'passkey', + ]; + + $this->enablePasskeys(); + + return $this; + } + public function enablePasskeys(bool $promptAfterLogin = true): self { $this->config['auth']['passkeys'] = [ diff --git a/src/SharpInternalServiceProvider.php b/src/SharpInternalServiceProvider.php index 535f31268..f11244a8a 100644 --- a/src/SharpInternalServiceProvider.php +++ b/src/SharpInternalServiceProvider.php @@ -10,6 +10,7 @@ use Code16\Sharp\Auth\TwoFactor\Sharp2faEloquentDefaultTotpHandler; use Code16\Sharp\Auth\TwoFactor\Sharp2faHandler; use Code16\Sharp\Auth\TwoFactor\Sharp2faNotificationHandler; +use Code16\Sharp\Auth\TwoFactor\Sharp2faPasskeyHandler; use Code16\Sharp\Config\SharpConfigBuilder; use Code16\Sharp\Config\SharpLegacyConfigBuilder; use Code16\Sharp\Console\DashboardMakeCommand; @@ -127,6 +128,7 @@ public function register() fn () => match (sharp()->config()->get('auth.2fa.handler')) { 'notification' => app(Sharp2faNotificationHandler::class), 'totp' => app(Sharp2faEloquentDefaultTotpHandler::class), + 'passkey' => app(Sharp2faPasskeyHandler::class), default => sharp()->config()->get('auth.2fa.handler'), } ); From 0d205a9e4fc6a3d963133e69b6304fa62f5d5f1a Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 6 Jul 2026 19:53:12 +0200 Subject: [PATCH 2/8] Refactor Passkey and Multi-Factor Authentication system integrations. --- resources/js/Pages/Auth/Login.vue | 12 +-- resources/js/Pages/Auth/Login2Fa.vue | 37 ++++--- resources/js/Pages/Auth/Passkeys/Create.vue | 99 ++++++++----------- resources/js/Pages/Auth/usePasskeyLogin.ts | 25 ++++- resources/js/Pages/Auth/usePasskeyRegister.ts | 41 ++++++++ resources/js/types/generated.d.ts | 2 +- .../Commands/UpdatePasskeyNameCommand.php | 6 +- src/Auth/Passkeys/Entity/PasskeyList.php | 21 ++-- src/Auth/Passkeys/PasskeyEventSubscriber.php | 17 ---- src/Auth/Passkeys/PasskeyManager.php | 41 ++++++++ src/Auth/Passkeys/SpatiePasskeyManager.php | 78 +++++++++++++++ src/Auth/TwoFactor/MultiFactorManager.php | 50 ++++++++++ src/Auth/TwoFactor/Sharp2faHandler.php | 4 + .../TwoFactor/Sharp2faNotificationHandler.php | 6 ++ src/Auth/TwoFactor/Sharp2faPasskeyHandler.php | 6 ++ src/Auth/TwoFactor/Sharp2faTotpHandler.php | 6 ++ src/Enums/MultiFactorMethod.php | 10 ++ .../Controllers/Auth/Login2faController.php | 26 +++-- src/Http/Controllers/Auth/LoginController.php | 16 ++- .../PasskeyAuthenticatedController.php | 27 +++++ .../Auth/Passkeys/PasskeyController.php | 68 +------------ .../Auth/Passkeys/SpatiePasskeyController.php | 73 ++++++++++++++ .../SharpAuthenticateOrInMultiFactor.php | 20 ++++ src/SharpInternalServiceProvider.php | 7 +- src/routes/auth/passkeys.php | 28 ++++-- 25 files changed, 513 insertions(+), 213 deletions(-) create mode 100644 resources/js/Pages/Auth/usePasskeyRegister.ts delete mode 100644 src/Auth/Passkeys/PasskeyEventSubscriber.php create mode 100644 src/Auth/Passkeys/PasskeyManager.php create mode 100644 src/Auth/Passkeys/SpatiePasskeyManager.php create mode 100644 src/Auth/TwoFactor/MultiFactorManager.php create mode 100644 src/Enums/MultiFactorMethod.php create mode 100644 src/Http/Controllers/Auth/Passkeys/PasskeyAuthenticatedController.php create mode 100644 src/Http/Controllers/Auth/Passkeys/SpatiePasskeyController.php create mode 100644 src/Http/Middleware/SharpAuthenticateOrInMultiFactor.php diff --git a/resources/js/Pages/Auth/Login.vue b/resources/js/Pages/Auth/Login.vue index 3155ef450..cf36e9494 100644 --- a/resources/js/Pages/Auth/Login.vue +++ b/resources/js/Pages/Auth/Login.vue @@ -15,13 +15,9 @@ import { FormItem } from "@/components/ui/form"; import { Check } from "lucide-vue-next"; import TemplateRenderer from "@/components/TemplateRenderer.vue"; - import { onMounted } from "vue"; import { - startAuthentication, browserSupportsWebAuthn, - browserSupportsWebAuthnAutofill } from "@simplewebauthn/browser"; - import { api } from "@/api/api"; import { Separator } from "@/components/ui/separator"; import { usePasskeyLogin } from "@/Pages/Auth/usePasskeyLogin"; @@ -43,12 +39,8 @@ supports_passkeys: browserSupportsWebAuthn(), }); - const { loginWithPasskey } = usePasskeyLogin(); - - onMounted(async () => { - if(config('sharp.auth.passkeys.enabled') && await browserSupportsWebAuthnAutofill()) { - await loginWithPasskey({ autofill: true }); - } + const { loginWithPasskey } = usePasskeyLogin({ + autofill: config('sharp.auth.passkeys.enabled'), }); diff --git a/resources/js/Pages/Auth/Login2Fa.vue b/resources/js/Pages/Auth/Login2Fa.vue index 447090b99..bb57e7980 100644 --- a/resources/js/Pages/Auth/Login2Fa.vue +++ b/resources/js/Pages/Auth/Login2Fa.vue @@ -10,14 +10,20 @@ import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { FormItem, FormMessage } from "@/components/ui/form"; + import { TwoFactorMode } from "@/types"; + import { usePasskeyLogin } from "@/Pages/Auth/usePasskeyLogin"; - defineProps<{ - helpText: string + const props = defineProps<{ + helpText: string, + mode: TwoFactorMode, + passkeyError: string, }>(); const form = useForm({ code: '', }); + + const { loginWithPasskey } = usePasskeyLogin({ autofill: props.mode === 'passkey' });