Skip to content

Commit df9f299

Browse files
authored
feat: allow true userless login (#501)
1 parent 9b68ae9 commit df9f299

22 files changed

+124
-168
lines changed

config/webauthn.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@
101101
|--------------------------------------------------------------------------
102102
|
103103
| When using navigation, redirects to these url on success:
104-
| - login: after a successfull login.
105-
| - register: after a successfull Webauthn key creation.
104+
| - login: after a successful login.
105+
| - register: after a successful Webauthn key creation.
106106
|
107107
| Redirects are not used in case of application/json requests.
108108
|
@@ -264,26 +264,39 @@
264264
| See https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement
265265
|
266266
| Supported: "required", "preferred", "discouraged".
267+
| Forced to "required" when userless is true.
267268
|
268269
*/
269270

270271
'user_verification' => 'preferred',
271272

272273
/*
273274
|--------------------------------------------------------------------------
274-
| Userless (One touch, Typeless) login
275+
| The resident key
275276
|--------------------------------------------------------------------------
276277
|
277-
| By default, users must input their email to receive a list of credentials
278-
| ID to use for authentication, but they can also login without specifying
279-
| one if the device can remember them, allowing for true one-touch login.
278+
| When userless is set to 'preferred' or 'required', the resident key will be
279+
| forced to be 'required' automatically.
280280
|
281281
| See https://www.w3.org/TR/webauthn/#enum-residentKeyRequirement
282282
|
283283
| Supported: "null", "required", "preferred", "discouraged".
284+
| Forced to "required" when userless is true.
285+
|
286+
*/
287+
288+
'resident_key' => 'preferred',
289+
290+
/*
291+
|--------------------------------------------------------------------------
292+
| Userless (One touch, Typeless) login
293+
|--------------------------------------------------------------------------
294+
|
295+
| This activates userless login, also known as one-touch login or typeless
296+
| login for devices when they're being registered.
284297
|
285298
*/
286299

287-
'userless' => null,
300+
'userless' => (bool) env('WEBAUTHN_USERLESS', false),
288301

289302
];

src/Actions/PrepareAssertionData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class PrepareAssertionData
1111
/**
1212
* Get data to authenticate a user.
1313
*/
14-
public function __invoke(User $user): PublicKeyCredentialRequestOptions
14+
public function __invoke(?User $user): PublicKeyCredentialRequestOptions
1515
{
1616
return Webauthn::prepareAssertion($user);
1717
}

src/Facades/Webauthn.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* @method static bool hasKey(\Illuminate\Contracts\Auth\Authenticatable $user)
2020
* @method static string redirects(string $redirect, $default = null)
2121
* @method static string model()
22+
* @method static bool userless()
2223
*
2324
* @see \LaravelWebauthn\Webauthn
2425
*/

src/Http/Controllers/AuthenticateController.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ class AuthenticateController extends Controller
2323
public function create(WebauthnLoginAttemptRequest $request): LoginViewResponse
2424
{
2525
$user = $this->createPipeline($request)->then(function ($request) {
26+
if (Webauthn::userless() || config('webauthn.user_verification') === 'discouraged') {
27+
return null;
28+
}
29+
2630
return app(LoginUserRetrieval::class)($request);
2731
});
2832

src/Services/Webauthn.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,4 +272,11 @@ public static function ignoreRoutes(): void
272272
{
273273
static::$registersRoutes = false;
274274
}
275+
276+
public static function userless(): bool
277+
{
278+
$userless = config('webauthn.userless');
279+
280+
return $userless === true || $userless === 'true' || $userless === 'preferred' || $userless === 'required';
281+
}
275282
}

src/Services/Webauthn/CredentialAssertionValidator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ protected function pullPublicKey(?User $user): PublicKeyCredentialRequestOptions
5656
try {
5757
$value = $this->cache->pull($this->cacheKey($user));
5858

59-
if ($value === null && in_array(config('webauthn.userless'), ['required', 'preferred'], true)) {
59+
if ($value === null && Webauthn::userless()) {
6060
$value = $this->cache->pull($this->cacheKey(null));
6161
}
6262

src/WebauthnServiceProvider.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public function register(): void
8181
$this->app->singleton(WebauthnFacade::class, Webauthn::class);
8282

8383
$this->registerResponseBindings();
84+
$this->overrideConfiguration();
8485
$this->bindWebAuthnPackage();
8586
$this->bindPsrInterfaces();
8687

@@ -121,6 +122,17 @@ public function registerResponseBindings(): void
121122
$this->app->singleton(UpdateResponseContract::class, UpdateResponse::class);
122123
}
123124

125+
/**
126+
* Override the configuration for userless WebAuthn.
127+
*/
128+
protected function overrideConfiguration(): void
129+
{
130+
if (Webauthn::userless()) {
131+
$this->app['config']->set('webauthn.user_verification', 'required');
132+
$this->app['config']->set('webauthn.resident_key', 'required');
133+
}
134+
}
135+
124136
/**
125137
* Bind all the WebAuthn package services to the Service Container.
126138
*/
@@ -192,9 +204,9 @@ protected function bindWebAuthnPackage(): void
192204
$this->app->bind(
193205
AuthenticatorSelectionCriteria::class,
194206
fn ($app) => new AuthenticatorSelectionCriteria(
195-
authenticatorAttachment: $app['config']->get('webauthn.attachment_mode', 'null'),
207+
authenticatorAttachment: $app['config']->get('webauthn.attachment_mode'),
196208
userVerification: $app['config']->get('webauthn.user_verification', 'preferred'),
197-
residentKey: $app['config']->get('webauthn.userless')
209+
residentKey: $app['config']->get('webauthn.resident_key', 'preferred')
198210
)
199211
);
200212
$this->app->bind(

tests/Unit/Actions/AttemptToAuthenticateTest.php

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use LaravelWebauthn\Facades\Webauthn;
1010
use LaravelWebauthn\Services\Webauthn as WebauthnService;
1111
use LaravelWebauthn\Tests\FeatureTestCase;
12+
use PHPUnit\Framework\Attributes\Test;
1213

1314
class AttemptToAuthenticateTest extends FeatureTestCase
1415
{
@@ -20,9 +21,7 @@ protected function tearDown(): void
2021
WebauthnService::$authenticateUsingCallback = null;
2122
}
2223

23-
/**
24-
* @test
25-
*/
24+
#[Test]
2625
public function it_get_user_request()
2726
{
2827
$user = $this->user();
@@ -36,9 +35,7 @@ public function it_get_user_request()
3635
$this->assertEquals(1, $result);
3736
}
3837

39-
/**
40-
* @test
41-
*/
38+
#[Test]
4239
public function it_fails_with_request()
4340
{
4441
$user = $this->user();
@@ -51,9 +48,7 @@ public function it_fails_with_request()
5148
app(AttemptToAuthenticate::class)->handle($request, fn () => 1);
5249
}
5350

54-
/**
55-
* @test
56-
*/
51+
#[Test]
5752
public function it_get_user_with_callback()
5853
{
5954
$user = $this->user();
@@ -72,9 +67,7 @@ public function it_get_user_with_callback()
7267
$this->assertEquals(1, $result);
7368
}
7469

75-
/**
76-
* @test
77-
*/
70+
#[Test]
7871
public function it_fails_with_callback()
7972
{
8073
$user = $this->user();

tests/Unit/Actions/DeleteKeyTest.php

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77
use LaravelWebauthn\Actions\DeleteKey;
88
use LaravelWebauthn\Models\WebauthnKey;
99
use LaravelWebauthn\Tests\FeatureTestCase;
10+
use PHPUnit\Framework\Attributes\Test;
1011

1112
class DeleteKeyTest extends FeatureTestCase
1213
{
1314
use DatabaseTransactions;
1415

15-
/**
16-
* @test
17-
*/
16+
#[Test]
1817
public function it_delete_key()
1918
{
2019
$user = $this->user();
@@ -29,9 +28,7 @@ public function it_delete_key()
2928
]);
3029
}
3130

32-
/**
33-
* @test
34-
*/
31+
#[Test]
3532
public function it_fails_if_wrong_user()
3633
{
3734
$user = $this->user();
@@ -41,9 +38,7 @@ public function it_fails_if_wrong_user()
4138
app(DeleteKey::class)($user, $webauthnKey->id);
4239
}
4340

44-
/**
45-
* @test
46-
*/
41+
#[Test]
4742
public function it_fails_if_wrong_id()
4843
{
4944
$user = $this->user();

tests/Unit/Actions/LoginUserRetrievalTest.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77
use Illuminate\Validation\ValidationException;
88
use LaravelWebauthn\Actions\LoginUserRetrieval;
99
use LaravelWebauthn\Tests\FeatureTestCase;
10+
use PHPUnit\Framework\Attributes\Test;
1011

1112
class LoginUserRetrievalTest extends FeatureTestCase
1213
{
1314
use DatabaseTransactions;
1415

15-
/**
16-
* @test
17-
*/
16+
#[Test]
1817
public function it_get_user_request()
1918
{
2019
$user = $this->user();
@@ -26,9 +25,7 @@ public function it_get_user_request()
2625
$this->assertEquals($user, $result);
2726
}
2827

29-
/**
30-
* @test
31-
*/
28+
#[Test]
3229
public function it_get_user_fail()
3330
{
3431
$request = $this->app->make(Request::class);

0 commit comments

Comments
 (0)