From 016b695af45fd3e01e8b6fbfbdf691babe0a5781 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Fri, 13 Jan 2023 13:31:16 -0500 Subject: [PATCH] wip --- app/Auth/CredentialSourceRepository.php | 14 ------ .../Controllers/AuthenticationController.php | 27 +++++++++-- .../Controllers/RegistrationController.php | 43 +++++++++++++---- app/Models/User.php | 14 +----- .../2014_10_12_000000_create_users_table.php | 5 +- ..._12_151412_create_authenticators_table.php | 5 -- resources/js/app.js | 30 +++++------- resources/views/authed.blade.php | 48 +++++++++++++++++++ routes/web.php | 13 +++++ 9 files changed, 130 insertions(+), 69 deletions(-) create mode 100644 resources/views/authed.blade.php diff --git a/app/Auth/CredentialSourceRepository.php b/app/Auth/CredentialSourceRepository.php index b3e1934..14c3482 100644 --- a/app/Auth/CredentialSourceRepository.php +++ b/app/Auth/CredentialSourceRepository.php @@ -12,36 +12,22 @@ class CredentialSourceRepository implements PublicKeyCredentialSourceRepository { public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource { - ray('findOneByCredentialId'); - ray($publicKeyCredentialId); - ray(base64_encode($publicKeyCredentialId)); - $authenticator = Authenticator::where('credential_id', base64_encode($publicKeyCredentialId))->first(); if (!$authenticator) { return null; } - ray($authenticator); - return PublicKeyCredentialSource::createFromArray($authenticator->public_key); } public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array { - ray('findAllForUserEntity'); - ray($publicKeyCredentialUserEntity); - ray($publicKeyCredentialUserEntity->getName()); - ray($publicKeyCredentialUserEntity->getId()); - return User::with('authenticators')->where('id', $publicKeyCredentialUserEntity->getId())->first()->authenticators->toArray(); } public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void { - ray('saveCredentialSource'); - ray($publicKeyCredentialSource); - $user = User::where('id', $publicKeyCredentialSource->getUserHandle())->firstOrFail(); $user->authenticators()->save(new Authenticator([ diff --git a/app/Http/Controllers/AuthenticationController.php b/app/Http/Controllers/AuthenticationController.php index eacb7e8..ad76a3d 100644 --- a/app/Http/Controllers/AuthenticationController.php +++ b/app/Http/Controllers/AuthenticationController.php @@ -17,7 +17,10 @@ use Cose\Algorithm\Signature\RSA\RS256; use Cose\Algorithm\Signature\RSA\RS384; use Cose\Algorithm\Signature\RSA\RS512; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Validation\ValidationException; use Psr\Http\Message\ServerRequestInterface; use Webauthn\AttestationStatement\AttestationObjectLoader; use Webauthn\AttestationStatement\AttestationStatementSupportManager; @@ -36,12 +39,18 @@ class AuthenticationController extends Controller { public function generateOptions(Request $request) { - $user = User::where('email', $request->input('username'))->firstOrFail(); + try { + $user = User::where('username', $request->input('username'))->firstOrFail(); + } catch (ModelNotFoundException $e) { + throw ValidationException::withMessages([ + 'username' => 'User not found', + ]); + } $userEntity = PublicKeyCredentialUserEntity::create( - $user->email, + $user->username, (string) $user->id, - $user->name, + $user->username, null, ); @@ -117,7 +126,9 @@ public function verify(Request $request, ServerRequestInterface $serverRequest) $authenticatorAssertionResponse = $publicKeyCredential->getResponse(); if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) { - abort(403, 'Invalid response type'); + throw ValidationException::withMessages([ + 'username' => 'Invalid response type', + ]); } $publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check( @@ -130,6 +141,12 @@ public function verify(Request $request, ServerRequestInterface $serverRequest) $request->session()->forget('publicKeyCredentialRequestOptions'); - ray('log user in now!!!!'); + $user = User::where('id', $publicKeyCredentialSource->getUserHandle())->firstOrFail(); + + Auth::login($user); + + return [ + 'verified' => true, + ]; } } diff --git a/app/Http/Controllers/RegistrationController.php b/app/Http/Controllers/RegistrationController.php index 43ee1fe..686e92d 100644 --- a/app/Http/Controllers/RegistrationController.php +++ b/app/Http/Controllers/RegistrationController.php @@ -5,8 +5,11 @@ use App\Auth\CredentialSourceRepository; use App\Models\User; use Cose\Algorithms; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; use Psr\Http\Message\ServerRequestInterface; use Webauthn\AttestationStatement\AttestationObjectLoader; use Webauthn\AttestationStatement\AttestationStatementSupportManager; @@ -24,6 +27,8 @@ class RegistrationController extends Controller { + const CREDENTIAL_CREATION_OPTIONS_SESSION_KEY = 'publicKeyCredentialCreationOptions'; + public function generateOptions(Request $request) { $rpEntity = PublicKeyCredentialRpEntity::create( @@ -33,19 +38,17 @@ public function generateOptions(Request $request) ); $user = User::firstOrNew([ - 'email' => $request->input('username'), + 'username' => $request->input('username'), ]); if (!$user->exists) { - $user->name = $request->input('username'); - $user->password = Str::random(32); $user->save(); } $userEntity = PublicKeyCredentialUserEntity::create( - $user->email, + $user->username, $user->id, - $user->name, + $user->username, null, ); @@ -94,7 +97,10 @@ public function generateOptions(Request $request) $serializedPublicKeyCredentialCreationOptions['excludeCredentials'] = []; } - $request->session()->put('publicKeyCredentialCreationOptions', $serializedPublicKeyCredentialCreationOptions); + $request->session()->put( + self::CREDENTIAL_CREATION_OPTIONS_SESSION_KEY, + $serializedPublicKeyCredentialCreationOptions + ); return $serializedPublicKeyCredentialCreationOptions; } @@ -130,17 +136,34 @@ public function verify(Request $request, ServerRequestInterface $serverRequest) $authenticatorAttestationResponse = $publicKeyCredential->getResponse(); if (!$authenticatorAttestationResponse instanceof AuthenticatorAttestationResponse) { - abort(403, 'Invalid response type'); + throw ValidationException::withMessages([ + 'username' => 'Invalid response type', + ]); } $publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check( $authenticatorAttestationResponse, - PublicKeyCredentialCreationOptions::createFromArray(session('publicKeyCredentialCreationOptions')), + PublicKeyCredentialCreationOptions::createFromArray( + session(self::CREDENTIAL_CREATION_OPTIONS_SESSION_KEY) + ), $serverRequest ); - $request->session()->forget('publicKeyCredentialCreationOptions'); + $request->session()->forget(self::CREDENTIAL_CREATION_OPTIONS_SESSION_KEY); + + try { + $publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource); + $user = User::where('id', $publicKeyCredentialSource->getUserHandle())->firstOrFail(); + } catch (ModelNotFoundException $e) { + throw ValidationException::withMessages([ + 'username' => 'User not found', + ]); + } + + Auth::login($user); - $publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource); + return [ + 'verified' => true, + ]; } } diff --git a/app/Models/User.php b/app/Models/User.php index 2dab4e1..976fbe3 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -18,9 +18,7 @@ class User extends Authenticatable * @var array */ protected $fillable = [ - 'name', - 'email', - 'password', + 'username', ]; /** @@ -29,19 +27,9 @@ class User extends Authenticatable * @var array */ protected $hidden = [ - 'password', 'remember_token', ]; - /** - * The attributes that should be cast. - * - * @var array - */ - protected $casts = [ - 'email_verified_at' => 'datetime', - ]; - public function authenticators() { return $this->hasMany(Authenticator::class); diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index cf6b776..83fff12 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -15,10 +15,7 @@ public function up() { Schema::create('users', function (Blueprint $table) { $table->id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); + $table->string('username')->unique(); $table->rememberToken(); $table->timestamps(); }); diff --git a/database/migrations/2022_12_12_151412_create_authenticators_table.php b/database/migrations/2022_12_12_151412_create_authenticators_table.php index 0927e75..e1d536a 100644 --- a/database/migrations/2022_12_12_151412_create_authenticators_table.php +++ b/database/migrations/2022_12_12_151412_create_authenticators_table.php @@ -18,11 +18,6 @@ public function up() $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->text('credential_id'); $table->text('public_key'); - // $table->binary('public_key'); - // $table->unsignedBigInteger('counter'); - // $table->string('device_type'); - // $table->boolean('backed_up'); - // $table->string('transports'); $table->timestamps(); }); } diff --git a/resources/js/app.js b/resources/js/app.js index 02e1138..ba6fb41 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -26,21 +26,18 @@ document.addEventListener('alpine:init', () => { .post('/registration/options', { username: this.username, }) - // Pass the options to the authenticator and wait for a response .then((response) => startRegistration(response.data)) .then((attResp) => axios.post('/registration/verify', attResp)) .then((verificationResponse) => { - if ( - verificationResponse.data && - verificationResponse.data.verified - ) { - console.log('success!'); - } else { - console.log('invalid?', verificationResponse.data); + if (verificationResponse.data?.verified) { + return window.location.reload(); } + + this.error = + 'Something went wrong verifying the registration.'; }) .catch((error) => { - this.error = error; + this.error = error?.response?.data?.message || error; }); }, submitLogin() { @@ -48,23 +45,20 @@ document.addEventListener('alpine:init', () => { .post('/authentication/options', { username: this.username, }) - // Pass the options to the authenticator and wait for a response .then((response) => startAuthentication(response.data)) .then((attResp) => axios.post('/authentication/verify', attResp), ) .then((verificationResponse) => { - if ( - verificationResponse.data && - verificationResponse.data.verified - ) { - console.log('success!'); - } else { - console.log('invalid?', verificationResponse.data); + if (verificationResponse.data?.verified) { + return window.location.reload(); } + + this.error = + 'Something went wrong verifying the authentication.'; }) .catch((error) => { - this.error = error; + this.error = error?.response?.data?.message || error; }); }, })); diff --git a/resources/views/authed.blade.php b/resources/views/authed.blade.php new file mode 100644 index 0000000..399ba8e --- /dev/null +++ b/resources/views/authed.blade.php @@ -0,0 +1,48 @@ + + + + + + + + Laravel Passkey Authentication Demo | Joe Codes + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + +
+
+ + + +

Laravel Passkeys Demo

+

+ Something here, probably a link to the post and a note that the database deletes info every 30 minutes +

+
+ +
+
+

👋 Well hey there, {{ $user->username }}!

+
+
+ @csrf + +
+
+
+
+
+ + + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 8010531..7e48024 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,12 +2,25 @@ use App\Http\Controllers\AuthenticationController; use App\Http\Controllers\RegistrationController; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; Route::get('/', function () { + if (Auth::check()) { + return view('authed', [ + 'user' => Auth::user(), + ]); + } + return view('home'); }); +Route::post('logout', function () { + Auth::logout(); + + return redirect('/'); +}); + Route::prefix('registration')->controller(RegistrationController::class)->group(function () { Route::post('/options', 'generateOptions'); Route::post('/verify', 'verify');