Upgrade to laragear/webauthn v2 - Fixes #255

This commit is contained in:
Bubka 2024-03-29 09:21:00 +01:00
parent 4a8db39ab0
commit ca903b6fc0
13 changed files with 115 additions and 87 deletions

View File

@ -200,7 +200,8 @@ PROXY_LOGOUT_URL=null
WEBAUTHN_NAME=2FAuth WEBAUTHN_NAME=2FAuth
# Relying Party ID. If null, the device will fill it internally. # Relying Party ID, should equal the site domain (i.e 2fauth.example.com).
# If null, the device will fill it internally (recommended)
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#how-to-determine-the-relying-party-id # See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#how-to-determine-the-relying-party-id
WEBAUTHN_ID=null WEBAUTHN_ID=null

8
Dockerfile vendored
View File

@ -194,14 +194,12 @@ ENV \
# Custom logout URL to open when using an auth proxy. # Custom logout URL to open when using an auth proxy.
PROXY_LOGOUT_URL=null \ PROXY_LOGOUT_URL=null \
# WebAuthn settings # WebAuthn settings
# Relying Party name, aka the name of the application. If null, defaults to APP_NAME # Relying Party name, aka the name of the application. If blank, defaults to APP_NAME. Do not set to null.
WEBAUTHN_NAME=2FAuth \ WEBAUTHN_NAME=2FAuth \
# Relying Party ID. If null, the device will fill it internally. # Relying Party ID, should equal the site domain (i.e 2fauth.example.com).
# If null, the device will fill it internally (recommended)
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#how-to-determine-the-relying-party-id # See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#how-to-determine-the-relying-party-id
WEBAUTHN_ID=null \ WEBAUTHN_ID=null \
# Optional image data in BASE64 (128 bytes maximum) or an image url
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#relying-party-icon
WEBAUTHN_ICON=null \
# Use this setting to control how user verification behave during the # Use this setting to control how user verification behave during the
# WebAuthn authentication flow. # WebAuthn authentication flow.
# #

View File

@ -16,7 +16,7 @@ class WebauthnTwoFAuthUserProvider extends WebAuthnUserProvider
public function validateCredentials($user, array $credentials) : bool public function validateCredentials($user, array $credentials) : bool
{ {
if ($user instanceof WebAuthnAuthenticatable && $this->isSignedChallenge($credentials)) { if ($user instanceof WebAuthnAuthenticatable && $this->isSignedChallenge($credentials)) {
return $this->validateWebAuthn(); return $this->validateWebAuthn($user);
} }
// If the user disabled the fallback, we will validate the credential password. // If the user disabled the fallback, we will validate the credential password.

View File

@ -10,11 +10,10 @@
use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Lang; use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Laragear\WebAuthn\Enums\UserVerification;
use Laragear\WebAuthn\Http\Requests\AssertionRequest; use Laragear\WebAuthn\Http\Requests\AssertionRequest;
use Laragear\WebAuthn\WebAuthn;
class WebAuthnLoginController extends Controller class WebAuthnLoginController extends Controller
{ {
@ -44,10 +43,10 @@ class WebAuthnLoginController extends Controller
public function options(AssertionRequest $request) : Responsable|JsonResponse public function options(AssertionRequest $request) : Responsable|JsonResponse
{ {
switch (config('webauthn.user_verification')) { switch (config('webauthn.user_verification')) {
case WebAuthn::USER_VERIFICATION_DISCOURAGED: case UserVerification::DISCOURAGED:
$request = $request->fastLogin(); // Makes the authenticator to only check for user presence on registration $request = $request->fastLogin(); // Makes the authenticator to only check for user presence on registration
break; break;
case WebAuthn::USER_VERIFICATION_REQUIRED: case UserVerification::REQUIRED:
$request = $request->secureLogin(); // Makes the authenticator to always verify the user thoroughly on registration $request = $request->secureLogin(); // Makes the authenticator to always verify the user thoroughly on registration
break; break;
} }
@ -88,17 +87,6 @@ public function login(WebauthnAssertedRequest $request)
return $this->sendLockoutResponse($request); return $this->sendLockoutResponse($request);
} }
if ($request->has('response')) {
$response = $request->response;
// Some authenticators do not send a userHandle so we hack the response to be compliant
// with Laragear\WebAuthn implementation that waits for a userHandle
if (! Arr::exists($response, 'userHandle') || blank($response['userHandle'])) {
$response['userHandle'] = User::getFromCredentialId($request->id)?->userHandle();
$request->merge(['response' => $response]);
}
}
if ($this->attemptLogin($request)) { if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request); return $this->sendLoginResponse($request);
} }

View File

@ -6,9 +6,9 @@
use Illuminate\Contracts\Support\Responsable; use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Laragear\WebAuthn\Enums\UserVerification;
use Laragear\WebAuthn\Http\Requests\AttestationRequest; use Laragear\WebAuthn\Http\Requests\AttestationRequest;
use Laragear\WebAuthn\Http\Requests\AttestedRequest; use Laragear\WebAuthn\Http\Requests\AttestedRequest;
use Laragear\WebAuthn\WebAuthn;
class WebAuthnRegisterController extends Controller class WebAuthnRegisterController extends Controller
{ {
@ -18,10 +18,10 @@ class WebAuthnRegisterController extends Controller
public function options(AttestationRequest $request) : Responsable public function options(AttestationRequest $request) : Responsable
{ {
switch (config('webauthn.user_verification')) { switch (config('webauthn.user_verification')) {
case WebAuthn::USER_VERIFICATION_DISCOURAGED: case UserVerification::DISCOURAGED:
$request = $request->fastRegistration(); // Makes the authenticator to only check for user presence on registration $request = $request->fastRegistration(); // Makes the authenticator to only check for user presence on registration
break; break;
case WebAuthn::USER_VERIFICATION_REQUIRED: case UserVerification::REQUIRED:
$request = $request->secureRegistration(); // Makes the authenticator to always verify the user thoroughly on registration $request = $request->secureRegistration(); // Makes the authenticator to always verify the user thoroughly on registration
break; break;
} }

View File

@ -4,7 +4,6 @@
use App\Notifications\WebauthnRecoveryNotification; use App\Notifications\WebauthnRecoveryNotification;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
/** /**
* @see \App\Models\WebAuthnAuthenticatable * @see \App\Models\WebAuthnAuthenticatable
@ -12,20 +11,6 @@
*/ */
trait WebAuthnManageCredentials trait WebAuthnManageCredentials
{ {
/**
* Return the handle used to identify his credentials.
*/
public function userHandle() : string
{
// Laragear\WebAuthn uses Ramsey\Uuid\Uuid::fromString()->getHex()->toString()
// to obtain a UUID v4 with dashes removed and uses it as user_id (aka userHandle)
// see https://github.com/ramsey/uuid/blob/4.x/src/Uuid.php#L379
// and Laragear\WebAuthn\Assertion\Validator\Pipes\CheckCredentialIsForUser::validateId()
return $this->webAuthnCredentials()->value('user_id')
?? str_replace('-', '', Str::uuid()->toString());
}
/** /**
* Saves a new alias for a given WebAuthn credential. * Saves a new alias for a given WebAuthn credential.
*/ */

View File

@ -6,11 +6,6 @@
interface WebAuthnAuthenticatable extends Authenticatable interface WebAuthnAuthenticatable extends Authenticatable
{ {
/**
* Return the handle used to identify his credentials.
*/
public function userHandle() : string;
/** /**
* Saves a new alias for a given WebAuthn credential. * Saves a new alias for a given WebAuthn credential.
*/ */

View File

@ -27,7 +27,7 @@
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"jackiedo/dotenv-editor": "^2.1", "jackiedo/dotenv-editor": "^2.1",
"khanamiryan/qrcode-detector-decoder": "^2.0.2", "khanamiryan/qrcode-detector-decoder": "^2.0.2",
"laragear/webauthn": "^1.2.0", "laragear/webauthn": "^2.0",
"laravel/framework": "^10.10", "laravel/framework": "^10.10",
"laravel/passport": "^11.2", "laravel/passport": "^11.2",
"laravel/socialite": "^5.10", "laravel/socialite": "^5.10",

109
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "15022c60c2ef59e0821ae0064f477e50", "content-hash": "7c27f612b0b319b88f2b8b3f1d52a51d",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -1918,34 +1918,101 @@
"time": "2022-11-17T10:54:53+00:00" "time": "2022-11-17T10:54:53+00:00"
}, },
{ {
"name": "laragear/webauthn", "name": "laragear/meta-model",
"version": "v1.2.1", "version": "v1.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Laragear/WebAuthn.git", "url": "https://github.com/Laragear/MetaModel.git",
"reference": "e57ac258b0d76eee4ab77c1e1b465f5d8e0c46de" "reference": "86aa8bbd0e1b9d03467a0257f0cd5815b6836a34"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Laragear/WebAuthn/zipball/e57ac258b0d76eee4ab77c1e1b465f5d8e0c46de", "url": "https://api.github.com/repos/Laragear/MetaModel/zipball/86aa8bbd0e1b9d03467a0257f0cd5815b6836a34",
"reference": "e57ac258b0d76eee4ab77c1e1b465f5d8e0c46de", "reference": "86aa8bbd0e1b9d03467a0257f0cd5815b6836a34",
"shasum": ""
},
"require": {
"illuminate/database": "10.*|11.*",
"php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.6",
"phpunit/phpunit": "^10.5|11.*"
},
"type": "library",
"autoload": {
"psr-4": {
"Laragear\\MetaModel\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Italo Israel Baeza Cabrera",
"email": "DarkGhostHunter@Gmail.com",
"homepage": "https://github.com/sponsors/DarkGhostHunter",
"role": "Developer"
}
],
"description": "Let other developers customize your package model and migrations",
"keywords": [
"database",
"eloquent",
"laravel",
"model"
],
"support": {
"issues": "https://github.com/Laragear/MetaModel/issues",
"source": "https://github.com/Laragear/MetaModel"
},
"funding": [
{
"url": "https://github.com/sponsors/DarkGhostHunter",
"type": "Github Sponsorship"
},
{
"url": "https://paypal.me/darkghosthunter",
"type": "Paypal"
}
],
"time": "2024-03-15T23:27:56+00:00"
},
{
"name": "laragear/webauthn",
"version": "v2.0.3",
"source": {
"type": "git",
"url": "https://github.com/Laragear/WebAuthn.git",
"reference": "15b29db0edb0a12c0fa45c404e57b0d5f1789465"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Laragear/WebAuthn/zipball/15b29db0edb0a12c0fa45c404e57b0d5f1789465",
"reference": "15b29db0edb0a12c0fa45c404e57b0d5f1789465",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
"ext-openssl": "*", "ext-openssl": "*",
"illuminate/auth": "9.*|10.*", "illuminate/auth": "10.*|11.*",
"illuminate/config": "9.*|10.*", "illuminate/config": "10.*|11.*",
"illuminate/database": "9.*|10.*", "illuminate/database": "10.*|11.*",
"illuminate/encryption": "9.*|10.*", "illuminate/encryption": "10.*|11.*",
"illuminate/http": "9.*|10.*", "illuminate/http": "10.*|11.*",
"illuminate/session": "9.*|10.*", "illuminate/session": "10.*|11.*",
"illuminate/support": "9.*|10.*", "illuminate/support": "10.*|11.*",
"php": "8.*" "laragear/meta-model": "^1.1",
"php": "^8.1"
}, },
"require-dev": { "require-dev": {
"jetbrains/phpstorm-attributes": "*", "ext-sodium": "*",
"orchestra/testbench": "^7.22|8.*" "orchestra/testbench": "8.*|9.*"
},
"suggest": {
"paragonie/sodium_compat": "To enable EdDSA 25519 keys from authenticators, if `ext-sodium` is unavailable."
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -1976,7 +2043,7 @@
"role": "Developer" "role": "Developer"
} }
], ],
"description": "Authenticate your users with biometric data, devices or USB keys.", "description": "Authenticate users with Passkeys: fingerprints, patterns and biometric data.",
"homepage": "https://github.com/laragear/webauthn", "homepage": "https://github.com/laragear/webauthn",
"keywords": [ "keywords": [
"Authentication", "Authentication",
@ -1988,8 +2055,8 @@
"windows hello" "windows hello"
], ],
"support": { "support": {
"issues": "https://github.com/Laragear/TwoFactor/issues", "issues": "https://github.com/Laragear/WebAuthn/issues",
"source": "https://github.com/Laragear/TwoFactor" "source": "https://github.com/Laragear/WebAuthn"
}, },
"funding": [ "funding": [
{ {
@ -2001,7 +2068,7 @@
"type": "Paypal" "type": "Paypal"
} }
], ],
"time": "2023-03-09T18:38:16+00:00" "time": "2024-03-18T22:38:29+00:00"
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",

View File

@ -91,14 +91,12 @@ services:
# Custom logout URL to open when using an auth proxy. # Custom logout URL to open when using an auth proxy.
- PROXY_LOGOUT_URL=null - PROXY_LOGOUT_URL=null
# WebAuthn settings # WebAuthn settings
# Relying Party name, aka the name of the application. If null, defaults to APP_NAME # Relying Party name, aka the name of the application. If blank, defaults to APP_NAME. Do not set to null.
- WEBAUTHN_NAME=2FAuth - WEBAUTHN_NAME=2FAuth
# Relying Party ID. If null, the device will fill it internally. # Relying Party ID, should equal the site domain (i.e 2fauth.example.com).
# If null, the device will fill it internally (recommended)
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#how-to-determine-the-relying-party-id # See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#how-to-determine-the-relying-party-id
- WEBAUTHN_ID=null - WEBAUTHN_ID=null
# Optional image data in BASE64 (128 bytes maximum) or an image url
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#relying-party-icon
- WEBAUTHN_ICON=null
# Use this setting to control how user verification behave during the # Use this setting to control how user verification behave during the
# WebAuthn authentication flow. # WebAuthn authentication flow.
# #

View File

@ -8,7 +8,7 @@
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator; use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
use Laragear\WebAuthn\WebAuthn; use Laragear\WebAuthn\Enums\UserVerification;
use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversClass;
use Tests\FeatureTestCase; use Tests\FeatureTestCase;
@ -369,7 +369,7 @@ public function test_too_many_invalid_login_attempts_returns_too_many_request_er
*/ */
public function test_get_options_returns_success() public function test_get_options_returns_success()
{ {
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_PREFERRED); Config::set('webauthn.user_verification', UserVerification::PREFERRED);
$this->user = User::factory()->create(['email' => self::EMAIL]); $this->user = User::factory()->create(['email' => self::EMAIL]);
@ -409,7 +409,7 @@ public function test_get_options_returns_success()
*/ */
public function test_get_options_for_securelogin_returns_required_userVerification() public function test_get_options_for_securelogin_returns_required_userVerification()
{ {
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_REQUIRED); Config::set('webauthn.user_verification', UserVerification::REQUIRED);
$this->user = User::factory()->create(['email' => self::EMAIL]); $this->user = User::factory()->create(['email' => self::EMAIL]);
@ -451,7 +451,7 @@ public function test_get_options_for_securelogin_returns_required_userVerificati
*/ */
public function test_get_options_for_fastlogin_returns_discouraged_userVerification() public function test_get_options_for_fastlogin_returns_discouraged_userVerification()
{ {
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_DISCOURAGED); Config::set('webauthn.user_verification', UserVerification::DISCOURAGED);
$this->user = User::factory()->create(['email' => self::EMAIL]); $this->user = User::factory()->create(['email' => self::EMAIL]);

View File

@ -5,10 +5,10 @@
use App\Http\Controllers\Auth\WebAuthnRegisterController; use App\Http\Controllers\Auth\WebAuthnRegisterController;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Laragear\WebAuthn\Enums\UserVerification;
use Laragear\WebAuthn\Http\Requests\AttestationRequest; use Laragear\WebAuthn\Http\Requests\AttestationRequest;
use Laragear\WebAuthn\Http\Requests\AttestedRequest; use Laragear\WebAuthn\Http\Requests\AttestedRequest;
use Laragear\WebAuthn\JsonTransport; use Laragear\WebAuthn\JsonTransport;
use Laragear\WebAuthn\WebAuthn;
use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversClass;
use Tests\FeatureTestCase; use Tests\FeatureTestCase;
@ -38,7 +38,7 @@ public function setUp() : void
*/ */
public function test_uses_attestation_with_fastRegistration_request() : void public function test_uses_attestation_with_fastRegistration_request() : void
{ {
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_DISCOURAGED); Config::set('webauthn.user_verification', UserVerification::DISCOURAGED);
$request = $this->mock(AttestationRequest::class); $request = $this->mock(AttestationRequest::class);
@ -55,7 +55,7 @@ public function test_uses_attestation_with_fastRegistration_request() : void
*/ */
public function test_uses_attestation_with_secureRegistration_request() : void public function test_uses_attestation_with_secureRegistration_request() : void
{ {
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_REQUIRED); Config::set('webauthn.user_verification', UserVerification::REQUIRED);
$request = $this->mock(AttestationRequest::class); $request = $this->mock(AttestationRequest::class);

View File

@ -2,14 +2,10 @@
namespace Tests\Feature\Models; namespace Tests\Feature\Models;
use App\Extensions\WebauthnCredentialBroker;
use App\Models\Group; use App\Models\Group;
use App\Models\TwoFAccount; use App\Models\TwoFAccount;
use App\Models\User; use App\Models\User;
use Database\Factories\UserFactory;
use Illuminate\Auth\Events\PasswordReset; use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Testing\FileFactory;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
@ -52,7 +48,7 @@ public function test_admin_scope_returns_only_admin()
*/ */
public function test_isAdministrator_returns_correct_state() public function test_isAdministrator_returns_correct_state()
{ {
$user = User::factory()->create(); $user = User::factory()->create();
$admin = User::factory()->administrator()->create(); $admin = User::factory()->administrator()->create();
$this->assertEquals($user->isAdministrator(), false); $this->assertEquals($user->isAdministrator(), false);
@ -88,7 +84,7 @@ public function test_promoteToAdministrator_demote_administrator_status()
*/ */
public function test_resetPassword_resets_password_with_success() public function test_resetPassword_resets_password_with_success()
{ {
$user = User::factory()->create(); $user = User::factory()->create();
$oldPassword = $user->password; $oldPassword = $user->password;
$user->resetPassword(); $user->resetPassword();
@ -118,7 +114,7 @@ public function test_delete_removes_user_data()
$user = User::factory()->create(); $user = User::factory()->create();
TwoFAccount::factory()->for($user)->create(); TwoFAccount::factory()->for($user)->create();
Group::factory()->for($user)->create(); Group::factory()->for($user)->create();
DB::table('webauthn_credentials')->insert([ DB::table('webauthn_credentials')->insert([
'id' => '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg', 'id' => '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg',
'authenticatable_type' => \App\Models\User::class, 'authenticatable_type' => \App\Models\User::class,
@ -139,7 +135,7 @@ public function test_delete_removes_user_data()
Password::broker()->createToken($user); Password::broker()->createToken($user);
$user->delete(); $user->delete();
$this->assertDatabaseMissing('twofaccounts', [ $this->assertDatabaseMissing('twofaccounts', [
'user_id' => $user->id, 'user_id' => $user->id,
]); ]);