Add remember me check for passkeys

This commit is contained in:
DariusIII
2026-04-25 08:56:39 +02:00
parent 24d0a8190a
commit 3987655051
4 changed files with 47 additions and 1 deletions
Generated
+1 -1
View File
@@ -15371,5 +15371,5 @@
"ext-zlib": "*" "ext-zlib": "*"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.9.0"
} }
@@ -4,6 +4,7 @@ Alpine.data('passkeyLogin', () => ({
supported: false, supported: false,
busy: false, busy: false,
error: '', error: '',
remember: false,
showCreateHint: false, showCreateHint: false,
hasAutoPrompted: false, hasAutoPrompted: false,
@@ -14,6 +15,7 @@ Alpine.data('passkeyLogin', () => ({
// If backend already reported an invalid passkey login attempt, // If backend already reported an invalid passkey login attempt,
// immediately show the "sign in first, then create passkey" guidance. // immediately show the "sign in first, then create passkey" guidance.
this.showCreateHint = this.$el.dataset.serverPasskeyError === '1'; this.showCreateHint = this.$el.dataset.serverPasskeyError === '1';
this.remember = this.$el.dataset.rememberDefault === '1';
const shouldAutoPrompt = this.$el.dataset.autoPrompt === '1'; const shouldAutoPrompt = this.$el.dataset.autoPrompt === '1';
if (this.supported && shouldAutoPrompt && !this.showCreateHint) { if (this.supported && shouldAutoPrompt && !this.showCreateHint) {
@@ -57,6 +59,7 @@ Alpine.data('passkeyLogin', () => ({
optionsJSON: optionsJson, optionsJSON: optionsJson,
}); });
this.$refs.remember.value = this.remember ? '1' : '0';
this.$refs.response.value = JSON.stringify(startAuthenticationResponse); this.$refs.response.value = JSON.stringify(startAuthenticationResponse);
document.getElementById('passkey-login-form')?.submit(); document.getElementById('passkey-login-form')?.submit();
} catch (error) { } catch (error) {
@@ -4,6 +4,7 @@
data-options-url="{{ route('passkeys.authentication_options') }}" data-options-url="{{ route('passkeys.authentication_options') }}"
data-server-passkey-error="{{ session('authenticatePasskey::reason') === 'invalid_passkey' ? '1' : '0' }}" data-server-passkey-error="{{ session('authenticatePasskey::reason') === 'invalid_passkey' ? '1' : '0' }}"
data-auto-prompt="{{ ($autoPromptPasskey ?? true) ? '1' : '0' }}" data-auto-prompt="{{ ($autoPromptPasskey ?? true) ? '1' : '0' }}"
data-remember-default="{{ old('rememberme') ? '1' : '0' }}"
data-captcha-enabled="{{ \App\Support\CaptchaHelper::isEnabled() ? '1' : '0' }}" data-captcha-enabled="{{ \App\Support\CaptchaHelper::isEnabled() ? '1' : '0' }}"
data-captcha-field="{{ \App\Support\CaptchaHelper::isEnabled() ? \App\Support\CaptchaHelper::getResponseFieldName() : '' }}" data-captcha-field="{{ \App\Support\CaptchaHelper::isEnabled() ? \App\Support\CaptchaHelper::getResponseFieldName() : '' }}"
class="mt-6" class="mt-6"
@@ -26,6 +27,18 @@
<input type="hidden" name="g-recaptcha-response" x-ref="recaptchaResponse" value=""> <input type="hidden" name="g-recaptcha-response" x-ref="recaptchaResponse" value="">
</form> </form>
<div class="mt-4 flex items-center">
<input
id="passkey-remember"
x-model="remember"
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:text-blue-400"
>
<label for="passkey-remember" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">
Remember me
</label>
</div>
<button <button
type="button" type="button"
@click="authenticate()" @click="authenticate()"
@@ -70,6 +70,36 @@ class PasskeyAuthenticationTest extends TestCase
Event::assertDispatched(UserLoggedIn::class); Event::assertDispatched(UserLoggedIn::class);
} }
public function test_passkey_authentication_with_remember_sets_remember_token(): void
{
Event::fake([UserLoggedIn::class]);
$user = $this->createUser('passkey-remember@example.test');
$passkey = new Passkey;
$passkey->setRawAttributes([
'id' => 3,
'authenticatable_id' => $user->id,
'name' => 'Desktop',
'credential_id' => 'credential-3',
'data' => '{}',
], true);
$passkey->setRelation('authenticatable', $user);
FakeFindPasskeyAction::$passkey = $passkey;
config()->set('passkeys.actions.find_passkey', FakeFindPasskeyAction::class);
$this
->withSession(['passkey-authentication-options' => '{}'])
->post(route('passkeys.login'), [
'start_authentication_response' => json_encode(['id' => 'credential-3'], JSON_THROW_ON_ERROR),
'remember' => true,
])
->assertRedirect('/');
$this->assertAuthenticatedAs($user);
$this->assertNotNull($user->fresh()?->remember_token);
}
public function test_unverified_users_cannot_authenticate_with_passkeys(): void public function test_unverified_users_cannot_authenticate_with_passkeys(): void
{ {
$user = $this->createUser('unverified@example.test', false); $user = $this->createUser('unverified@example.test', false);