Skip to content

Commit

Permalink
[3.x] Adds Login Callbacks (#95)
Browse files Browse the repository at this point in the history
* [3.x] Adds Login Callbacks

* Apply fixes from StyleCI

[ci skip] [skip ci]

* Fixes guard throw with config default

* Should fix server error test.

* Apply fixes from StyleCI

[ci skip] [skip ci]

* Should fix static analysis

* Apply fixes from StyleCI

[ci skip] [skip ci]

---------

Co-authored-by: Italo Israel Baeza Cabrera <[email protected]>
Co-authored-by: Italo <[email protected]>
  • Loading branch information
3 people authored Aug 15, 2024
1 parent 7a0c742 commit 5136b8f
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 5 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,22 @@ public function createChallenge(AssertedRequest $request)
}
```

You can also use the `login()` method will callbacks, which will be passed to the [`attemptWhen()`](https://laravel.com/docs/11.x/authentication#specifying-additional-conditions) method of the Session Guard.

```php
// app\Http\Controllers\WebAuthn\WebAuthnLoginController.php
use Laragear\WebAuthn\Http\Requests\AssertedRequest;

public function createChallenge(AssertedRequest $request)
{
$user = $request->login(callbacks: fn ($user) => $user->isNotBanned());

return $user
? response("Welcome back, $user->name!");
: response('Something went wrong, try again!');
}
```

If you need greater control on the Assertion procedure, you may want to [Assert manually](#manually-attesting-and-asserting).

### Assertion User Verification
Expand Down Expand Up @@ -449,7 +465,7 @@ The following events are fired by this package, which you can [listen to in your

## Manually Attesting and Asserting

If you want to manually Attest and Assert users, for example to create users at the same time they register (attest) a device, you may instance their respective pipelines used for both WebAuthn Ceremonies:
If you want to manually Attest and Assert users, for example to create users at the same time they register (attest) a device, you may instance their respective pipelines used for both WebAuthn Ceremonies:

| Pipeline | Description |
|------------------------|------------------------------------------------------|
Expand All @@ -462,7 +478,7 @@ If you want to manually Attest and Assert users, for example to create users at
>
> The `AttestationValidator` instances a storable credential, it doesn't save it. This way you have the chance to alter the model with additional data before persisting.
Compared to prior versions, the validation data to pass through `AttestationValidator` and `AssertionValidator` no longer require the current Request instance. Instead, these only need the JSON array.
Compared to prior versions, the validation data to pass through `AttestationValidator` and `AssertionValidator` no longer require the current Request instance. Instead, these only need the JSON array of data.

If you prefer, you can still use the `fromRequest()` helper, which will extract the required WebAuthn data from the current or issued Request instance, or manually instance a `Laragear\WebAuthn\JsonTransport` with the required data.

Expand Down
29 changes: 27 additions & 2 deletions src/Http/Requests/AssertedRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

use Illuminate\Foundation\Http\FormRequest;
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable;
use UnexpectedValueException;

use function auth;
use function config;
use function method_exists;

class AssertedRequest extends FormRequest
{
Expand Down Expand Up @@ -49,12 +52,34 @@ public function hasRemember(): bool
public function login(
string $guard = null,
bool $remember = null,
bool $destroySession = false
bool $destroySession = false,
callable|array $callbacks = null
): ?WebAuthnAuthenticatable {
/** @var \Illuminate\Contracts\Auth\StatefulGuard $auth */
$auth = auth()->guard($guard);

if ($auth->attempt($this->validated(), $remember ?? $this->hasRemember())) {
$remember ??= $this->hasRemember();

// If the developer is using a callback or an array of callbacks, we will try to use
// the "attemptWhen" method of the Session Guard. Since these callback are expected
// to run, we will fail miserably if the guard does not support attempt callbacks.
if ($callbacks !== null) {
if (! method_exists($auth, 'attemptWhen')) {
$guard ??= config('auth.defaults.guard');
throw new UnexpectedValueException("The [$guard] guard does not support attempt callbacks.");
}

if ($auth->attemptWhen($this->validated(), $callbacks, $remember)) {
$this->session()->regenerate($destroySession);

// @phpstan-ignore-next-line
return $auth->user();
}

return null;
}

if ($auth->attempt($this->validated(), $remember)) {
$this->session()->regenerate($destroySession);

// @phpstan-ignore-next-line
Expand Down
63 changes: 62 additions & 1 deletion tests/Http/Requests/AssertedRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace Tests\Http\Requests;

use Illuminate\Auth\Events\Login;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Session\Session as SessionContract;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
Expand Down Expand Up @@ -262,7 +265,7 @@ public function test_destroy_session_on_regeneration(): void
$request->login(destroySession: true);
});

$session = Mockery::mock(\Illuminate\Contracts\Session\Session::class);
$session = Mockery::mock(SessionContract::class);

$session->expects('regenerate')->with(true)->andReturn();

Expand All @@ -276,4 +279,62 @@ public function test_destroy_session_on_regeneration(): void

$this->postJson('custom', FakeAuthenticator::assertionResponse())->assertOk();
}

public function test_logins_with_callbacks(): void
{
Route::middleware('web')->post('custom-false', function (AssertedRequest $request) {
$request->login(callbacks: function ($user): bool {
static::assertInstanceOf(WebAuthnAuthenticatableUser::class, $user);

return false;
});
});

Route::middleware('web')->post('custom-true', function (AssertedRequest $request) {
$request->login(callbacks: function ($user): bool {
static::assertInstanceOf(WebAuthnAuthenticatableUser::class, $user);

return true;
});
});

$session = Mockery::mock(SessionContract::class);

// Expect it only once. The second callback doesn't reach a second execution since it fails.
$session->expects('regenerate')->with(false)->andReturn();

$this->app->resolving(AssertedRequest::class, function (AssertedRequest $request) use ($session): void {
$request->setLaravelSession($session);
});

$this->mock(AssertionValidator::class)
->expects('send->thenReturn')
->twice()
->andReturn();

$this->postJson('custom-false', FakeAuthenticator::assertionResponse())->assertOk();

$this->assertGuest();

$this->postJson('custom-true', FakeAuthenticator::assertionResponse())->assertOk();

$this->assertAuthenticated();
}

public function test_login_callback_fails_if_session_guard_does_not_supports_callbacks(): void
{
Route::middleware('web')->post('custom', function (AssertedRequest $request) {
$request->login(callbacks: fn (): bool => true);
});

$guard = Mockery::mock(Guard::class);
$guard->expects('attempt')->never();
$guard->expects('attemptWhen')->never();

$this->mock(AuthFactory::class)->expects('guard')->with(null)->andReturn($guard);

$this->postJson('custom', FakeAuthenticator::assertionResponse())
->assertJsonPath('message', 'The [web] guard does not support attempt callbacks.')
->assertServerError();
}
}

0 comments on commit 5136b8f

Please sign in to comment.