소스 검색

Let the WebAuthn form log in any user

Bubka 2 년 전
부모
커밋
5c83e17752

+ 4 - 2
app/Http/Controllers/Auth/LoginController.php

@@ -71,8 +71,10 @@ class LoginController extends Controller
      */
      */
     public function logout(Request $request)
     public function logout(Request $request)
     {
     {
+        $user = $request->user();
         Auth::logout();
         Auth::logout();
-        Log::info('User logged out');
+
+        Log::info(sprintf('User id #%s logged out', $user->id));
 
 
         return response()->json(['message' => 'signed out'], Response::HTTP_OK);
         return response()->json(['message' => 'signed out'], Response::HTTP_OK);
     }
     }
@@ -151,6 +153,6 @@ class LoginController extends Controller
         $user->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
         $user->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
         $user->save();
         $user->save();
 
 
-        Log::info('User authenticated');
+        Log::info(sprintf('User id #%s authenticated using login & pwd', $user->id));
     }
     }
 }
 }

+ 9 - 12
app/Http/Controllers/Auth/WebAuthnLoginController.php

@@ -43,16 +43,13 @@ class WebAuthnLoginController extends Controller
                 break;
                 break;
         }
         }
 
 
-        // Since 2FAuth is single user designed we fetch the user instance.
-        // This lets Larapass validate the request without the need to ask
-        // the visitor for an email address.
-        $user = User::first();
-
-        return $user
-            ? $request->toVerify($user)
-            : response()->json([
-                'message' => 'no registered user',
-            ], 400);
+        return $request->toVerify($request->validate([
+            'email' => [
+                'required',
+                'email',
+                new \App\Rules\CaseInsensitiveEmailExists
+            ]
+        ]));
     }
     }
 
 
     /**
     /**
@@ -69,7 +66,7 @@ class WebAuthnLoginController extends Controller
             $response = $request->response;
             $response = $request->response;
 
 
             // Some authenticators do not send a userHandle so we hack the response to be compliant
             // Some authenticators do not send a userHandle so we hack the response to be compliant
-            // with Larapass/webauthn-lib implementation that waits for a userHandle
+            // with Laragear\WebAuthn implementation that waits for a userHandle
             if (! Arr::exists($response, 'userHandle') || blank($response['userHandle'])) {
             if (! Arr::exists($response, 'userHandle') || blank($response['userHandle'])) {
                 $response['userHandle'] = User::getFromCredentialId($request->id)?->userHandle();
                 $response['userHandle'] = User::getFromCredentialId($request->id)?->userHandle();
                 $request->merge(['response' => $response]);
                 $request->merge(['response' => $response]);
@@ -98,6 +95,6 @@ class WebAuthnLoginController extends Controller
         $user->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
         $user->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
         $user->save();
         $user->save();
 
 
-        Log::info('User authenticated via webauthn');
+        Log::info(sprintf('User id #%s authenticated using webauthn', $user->id));
     }
     }
 }
 }

+ 21 - 5
resources/js/views/auth/Login.vue

@@ -5,9 +5,10 @@
             <div class="field">
             <div class="field">
                 {{ $t('auth.webauthn.use_security_device_to_sign_in') }}
                 {{ $t('auth.webauthn.use_security_device_to_sign_in') }}
             </div>
             </div>
-            <div class="control">
-                <button id="btnContinue" type="button" class="button is-link" @click="webauthnLogin">{{ $t('commons.continue') }}</button>
-            </div>
+            <form id="frmWebauthnLogin" @submit.prevent="webauthnLogin" @keydown="form.onKeydown($event)">
+                <form-field :form="form" fieldName="email" inputType="email" :label="$t('auth.forms.email')" autofocus />
+                <form-buttons :isBusy="form.isBusy" :caption="$t('commons.continue')" :submitId="'btnContinue'"/>
+            </form>
             <div class="nav-links">
             <div class="nav-links">
                 <p>{{ $t('auth.webauthn.lost_your_device') }}&nbsp;<router-link id="lnkRecoverAccount" :to="{ name: 'webauthn.lost' }" class="is-link">{{ $t('auth.webauthn.recover_your_account') }}</router-link></p>
                 <p>{{ $t('auth.webauthn.lost_your_device') }}&nbsp;<router-link id="lnkRecoverAccount" :to="{ name: 'webauthn.lost' }" class="is-link">{{ $t('auth.webauthn.recover_your_account') }}</router-link></p>
                 <p v-if="!this.$root.userPreferences.useWebauthnOnly">{{ $t('auth.sign_in_using') }}&nbsp;
                 <p v-if="!this.$root.userPreferences.useWebauthnOnly">{{ $t('auth.sign_in_using') }}&nbsp;
@@ -103,11 +104,26 @@
                     return false
                     return false
                 }
                 }
 
 
-                const loginOptions = await this.axios.post('/webauthn/login/options').then(res => res.data)
+                const loginOptions = await this.form.post('/webauthn/login/options').then(res => res.data)
                 const publicKey = this.webauthn.parseIncomingServerOptions(loginOptions)
                 const publicKey = this.webauthn.parseIncomingServerOptions(loginOptions)
                 const credentials = await navigator.credentials.get({ publicKey: publicKey })
                 const credentials = await navigator.credentials.get({ publicKey: publicKey })
                 .catch(error => {
                 .catch(error => {
-                    this.$notify({ type: 'is-danger', text: this.$t('auth.webauthn.unknown_device') })
+                    if (error.name == 'AbortError') {
+                        this.$notify({ type: 'is-warning', text: this.$t('errors.aborted_by_user') })
+                    }
+                    else if (error.name == 'SecurityError') {
+                        this.$notify({ type: 'is-danger', text: this.$t('errors.security_error_check_rpid') })
+                    }
+                    else if (error.name == 'NotAllowedError') {
+                        this.$notify({ type: 'is-danger', text: this.$t('errors.not_allowed_operation') })
+                    }
+                    else if (error.name == 'NotSupportedError') {
+                        this.$notify({ type: 'is-danger', text: this.$t('errors.unsupported_operation') })
+                    }
+                    else if (error.name == 'InvalidStateError') {
+                        this.$notify({ type: 'is-danger', text: this.$t('auth.webauthn.unknown_device') })
+                    }
+                    else this.$notify({ type: 'is-danger', text: this.$t('errors.unknown_error') })
                 })
                 })
 
 
                 if (!credentials) return false
                 if (!credentials) return false

+ 3 - 0
resources/js/views/settings/WebAuthn.vue

@@ -167,6 +167,9 @@
                     else if (error.name == 'NotAllowedError') {
                     else if (error.name == 'NotAllowedError') {
                         this.$notify({ type: 'is-danger', text: this.$t('errors.not_allowed_operation') })
                         this.$notify({ type: 'is-danger', text: this.$t('errors.not_allowed_operation') })
                     }
                     }
+                    else if (error.name == 'NotSupportedError') {
+                        this.$notify({ type: 'is-danger', text: this.$t('errors.unsupported_operation') })
+                    }
                     return false
                     return false
                 }
                 }
 
 

+ 2 - 0
resources/lang/en/errors.php

@@ -35,6 +35,8 @@ return [
     'aborted_by_user' => 'Aborted by user',
     'aborted_by_user' => 'Aborted by user',
     'security_device_unsupported' => 'Unsupported or in use device',
     'security_device_unsupported' => 'Unsupported or in use device',
     'not_allowed_operation' => 'Operation not allowed',
     'not_allowed_operation' => 'Operation not allowed',
+    'unsupported_operation' => 'Unsupported operation',
+    'unknown_error' => 'Unknown error',
     'security_error_check_rpid' => 'Security error<br/>Check your WEBAUTHN_ID env var',
     'security_error_check_rpid' => 'Security error<br/>Check your WEBAUTHN_ID env var',
     'unsupported_with_reverseproxy' => 'Not applicable when using an auth proxy',
     'unsupported_with_reverseproxy' => 'Not applicable when using an auth proxy',
     'user_deletion_failed' => 'User account deletion failed, no data have been deleted',
     'user_deletion_failed' => 'User account deletion failed, no data have been deleted',