From ecef28abd48aac2cfd37efce97933298a7a2e899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Viguier?= Date: Sat, 7 Jan 2023 12:41:30 +0100 Subject: [PATCH] Fixes #1630 - 2FA not working (#1691) --- .../WebAuthn/WebAuthnLoginController.php | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/WebAuthn/WebAuthnLoginController.php b/app/Http/Controllers/WebAuthn/WebAuthnLoginController.php index e5200049121..c7e51efe9bf 100644 --- a/app/Http/Controllers/WebAuthn/WebAuthnLoginController.php +++ b/app/Http/Controllers/WebAuthn/WebAuthnLoginController.php @@ -2,8 +2,13 @@ namespace App\Http\Controllers\WebAuthn; +use App\Exceptions\UnauthenticatedException; +use App\Models\User; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Support\Responsable; +use Illuminate\Support\Facades\Auth; +use Laragear\WebAuthn\Assertion\Validator\AssertionValidation; +use Laragear\WebAuthn\Assertion\Validator\AssertionValidator; use Laragear\WebAuthn\Http\Requests\AssertedRequest; use Laragear\WebAuthn\Http\Requests\AssertionRequest; use Symfony\Component\HttpFoundation\Response; @@ -28,15 +33,69 @@ public function options(AssertionRequest $request): Responsable /** * Log the user in. * + * 1. We retrieve the credentials candidate + * 2. Double check the challenge is signed. + * 3. Retrieve the User from the credential ID, we will use it to validate later (otherwise keys like yubikey4 are not working). + * 4. Validate the credentials + * 5. Log in on success + * * @param AssertedRequest $request * * @return void */ - public function login(AssertedRequest $request): void + public function login(AssertedRequest $request, AssertionValidator $validator): void { - $user = $request->login(); - if ($user === null) { - throw new HttpException(Response::HTTP_UNPROCESSABLE_ENTITY); + $credentials = $request->validated(); + + if (!$this->isSignedChallenge($credentials)) { + throw new HttpException(Response::HTTP_UNPROCESSABLE_ENTITY, 'Response is not signed.'); + } + $associatedUser = $this->retrieveByCredentials($credentials); + + if ($associatedUser === null) { + throw new HttpException(Response::HTTP_UNPROCESSABLE_ENTITY, 'Associated user does not exists.'); + } + + $credential = $validator + ->send(new AssertionValidation($request, $associatedUser)) + ->thenReturn() + ->credential; + + if ($credential === null) { + throw new UnauthenticatedException('Invalid credentials'); } + + /** @var \Illuminate\Contracts\Auth\Authenticatable $authenticatable */ + $authenticatable = $credential->authenticatable; + Auth::login($authenticatable); + } + + /** + * Check if the credentials are for a public key signed challenge. + * + * @param array $credentials + * + * @return bool + */ + private function isSignedChallenge(array $credentials): bool + { + return isset($credentials['id'], $credentials['rawId'], $credentials['response'], $credentials['type']); + } + + /** + * Retrieve a user by the given credentials. + * + * @param array $credentials + * + * @return User|null + */ + public function retrieveByCredentials(array $credentials): User|null + { + /** @var User|null $user */ + $user = User::whereHas('webAuthnCredentials', + fn ($query) => $query->whereKey($credentials['id'])->whereEnabled() + )->first(); + + return $user; } }