diff --git a/app/Actions/AttemptToAuthenticateSocialite.php b/app/Actions/AttemptToAuthenticateSocialite.php new file mode 100644 index 00000000000..f034d4e5670 --- /dev/null +++ b/app/Actions/AttemptToAuthenticateSocialite.php @@ -0,0 +1,265 @@ +guard = $guard; + $this->limiter = $limiter; + } + + /** + * Handle the incoming request. + * + * @param \Illuminate\Http\Request $request + * @param callable $next + * @return mixed + */ + public function handle(Request $request, callable $next) + { + $driver = $request->route()->parameter('driver'); + + $provider = $this->getSocialiteProvider($driver); + $user = $this->authenticateUser($request, $driver, $provider->user()); + + if ((optional($user)->two_factor_secret && ! is_null(optional($user)->two_factor_confirmed_at)) + || Webauthn::enabled($user)) { + return $this->twoFactorChallengeResponse($request, $user); + } + + $this->guard->login($user, $request->session()->pull('login.remember', false)); + + return $next($request); + } + + /** + * Get the provider. + * + * @param string $driver + * @return \Laravel\Socialite\Contracts\Provider + */ + private function getSocialiteProvider(string $driver): Provider + { + $provider = Socialite::driver($driver); + + if (App::environment('local') && $provider instanceof AbstractProvider) { + $provider->setHttpClient(new \GuzzleHttp\Client(['verify' => false])); + } + + return $provider; + } + + /** + * Authenticate the user. + * + * @param \Illuminate\Http\Request $request + * @param string $driver + * @param \Laravel\Socialite\Contracts\User $socialite + * @return User + */ + private function authenticateUser(Request $request, string $driver, SocialiteUser $socialite): User + { + if ($userToken = UserToken::where([ + 'driver_id' => $socialite->getId(), + 'driver' => $driver, + ])->first()) { + // Association already exist + + $user = $userToken->user; + + $this->checkUserAssociation($request, $user, $driver); + } else { + // New association: create user or add token to existing user + $user = tap($this->getUserOrCreate($socialite), function ($user) use ($driver, $socialite) { + $this->createUserToken($user, $driver, $socialite); + }); + } + + return $user; + } + + /** + * Check if the user is logged in and if the user is the same as the one. + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\User $user + * @param string $driver + * @return void + */ + private function checkUserAssociation(Request $request, User $user, string $driver) + { + if (($userId = Auth::id()) && $userId !== $user->id) { + $this->throwFailedAuthenticationException($request, $driver, __('This provider is already associated with another account')); + } + } + + /** + * Get authenticated user. + * + * @param SocialiteUser $socialite + * @return User + */ + private function getUserOrCreate(SocialiteUser $socialite): User + { + if ($user = Auth::user()) { + return $user; + } + + return $this->createUser($socialite); + } + + /** + * Create new user. + * + * @param SocialiteUser $socialite + * @return User + */ + private function createUser(SocialiteUser $socialite): User + { + $names = Str::of($socialite->getName())->split('/ /', 2); + $data = [ + 'email' => $socialite->getEmail(), + 'first_name' => $names[0], + 'last_name' => $names[1] ?? $names[0], + 'locale' => App::getLocale(), + 'terms' => true, + ]; + + event(new Registered($user = app(CreateNewUser::class)->create($data))); + + return $user; + } + + /** + * Create the user token register. + * + * @param User $user + * @param string $driver + * @param SocialiteUser $socialite + * @return UserToken + */ + private function createUserToken(User $user, string $driver, SocialiteUser $socialite): UserToken + { + $token = [ + 'driver' => $driver, + 'driver_id' => $socialite->getId(), + 'user_id' => $user->id, + 'email' => $socialite->getEmail(), + ]; + + if ($socialite instanceof OAuth1User) { + $token['token'] = $socialite->token; + $token['token_secret'] = $socialite->tokenSecret; + $token['format'] = 'oauth1'; + } elseif ($socialite instanceof OAuth2User) { + $token['token'] = $socialite->token; + $token['refresh_token'] = $socialite->refreshToken; + $token['expires_in'] = $socialite->expiresIn; + $token['format'] = 'oauth2'; + } else { + throw new \UnexpectedValueException('authentication format not supported'); + } + + return UserToken::create($token); + } + + /** + * Throw a failed authentication validation exception. + * + * @param \Illuminate\Http\Request $request + * @param string $driver + * @param string $message + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + protected function throwFailedAuthenticationException(Request $request, string $driver, string $message = null) + { + $this->fireFailedEvent($request, Auth::user()); + + $this->limiter->increment($request); + + throw ValidationException::withMessages([ + $driver => [empty($message) ? trans('auth.failed') : $message], + ]); + } + + /** + * Fire the failed authentication attempt event with the given arguments. + * + * @param \Illuminate\Http\Request $request + * @param User|null $user + * @return void + */ + protected function fireFailedEvent(Request $request, ?User $user = null) + { + event(new Failed('web', $user, [ + 'email' => $request->email, + ])); + } + + /** + * Get the two factor authentication enabled response. + * + * @param \Illuminate\Http\Request $request + * @param mixed $user + * @return \Symfony\Component\HttpFoundation\Response + */ + protected function twoFactorChallengeResponse($request, $user) + { + $request->session()->put([ + 'login.id' => $user->getKey(), + 'login.remember' => $request->session()->pull('login.remember', $request->filled('remember')), + ]); + + TwoFactorAuthenticationChallenged::dispatch($user); + + return $request->wantsJson() + ? response()->json(['two_factor' => true]) + : redirect()->route('two-factor.login'); + } +} diff --git a/app/Actions/AttemptToAuthenticateWebauthn.php b/app/Actions/AttemptToAuthenticateWebauthn.php new file mode 100644 index 00000000000..6dfc58ea330 --- /dev/null +++ b/app/Actions/AttemptToAuthenticateWebauthn.php @@ -0,0 +1,144 @@ +guard = $guard; + $this->limiter = $limiter; + } + + /** + * Handle the incoming request. + * + * @param \Illuminate\Http\Request $request + * @param callable $next + * @return mixed + */ + public function handle(Request $request, $next) + { + if ($this->attemptValidateAssertion($request) + || $this->attemptLogin($this->filterCredentials($request), $request->boolean('remember'))) { + return $next($request); + } + + $this->throwFailedAuthenticationException($request); + + return null; + } + + /** + * Attempt to log the user into the application. + * + * @param array $challenge + * @param bool $remember + * @return bool + */ + protected function attemptLogin(array $challenge, bool $remember = false): bool + { + return $this->guard->attempt($challenge, $remember); + } + + /** + * Attempt to validate assertion for authenticated user. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + protected function attemptValidateAssertion(Request $request): bool + { + $user = $request->user(); + + if ($user === null) { + return false; + } + + $result = WebauthnFacade::validateAssertion($user, $this->filterCredentials($request)); + + if (! $result) { + $this->fireFailedEvent($request, $user); + + $this->throwFailedAuthenticationException($request); + + return false; // @codeCoverageIgnore + } + + return true; + } + + /** + * Throw a failed authentication validation exception. + * + * @param \Illuminate\Http\Request $request + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + protected function throwFailedAuthenticationException(Request $request) + { + $this->limiter->increment($request); + + throw ValidationException::withMessages([ + Webauthn::username() => [trans('webauthn::errors.login_failed')], + ]); + } + + /** + * Fire the failed authentication attempt event with the given arguments. + * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @return void + */ + protected function fireFailedEvent(Request $request, ?Authenticatable $user = null) + { + event(new Failed(config('webauthn.guard'), $user, [ + Webauthn::username() => $user !== null + ? $user->{Webauthn::username()} + : $request->{Webauthn::username()}, + ])); + } + + /** + * Get array of webauthn credentials. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + protected function filterCredentials(Request $request): array + { + return $request->only(['id', 'rawId', 'response', 'type']); + } +} diff --git a/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php b/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php new file mode 100644 index 00000000000..2e1c664f6fd --- /dev/null +++ b/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php @@ -0,0 +1,130 @@ +guard = $guard; + $this->limiter = $limiter; + } + + /** + * Handle the incoming request. + * + * @param \Illuminate\Http\Request $request + * @param callable $next + * @return mixed + */ + public function handle($request, $next) + { + $user = $this->validateCredentials($request); + + if ((optional($user)->two_factor_secret && ! is_null(optional($user)->two_factor_confirmed_at)) + || Webauthn::enabled($user)) { + return $this->twoFactorChallengeResponse($request, $user); + } + + return $next($request); + } + + /** + * Attempt to validate the incoming credentials. + * + * @param \Illuminate\Http\Request $request + * @return mixed + */ + protected function validateCredentials($request) + { + return tap(User::where('email', $request->email)->first(), function ($user) use ($request) { + if (! $user || ! $this->guard->getProvider()->validateCredentials($user, ['password' => $request->password])) { + $this->fireFailedEvent($request, $user); + + $this->throwFailedAuthenticationException($request); + } + }); + } + + /** + * Throw a failed authentication validation exception. + * + * @param \Illuminate\Http\Request $request + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + protected function throwFailedAuthenticationException($request) + { + $this->limiter->increment($request); + + throw ValidationException::withMessages([ + 'email' => [trans('auth.failed')], + ]); + } + + /** + * Fire the failed authentication attempt event with the given arguments. + * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @return void + */ + protected function fireFailedEvent($request, $user = null) + { + event(new Failed(config('fortify.guard'), $user, [ + 'email' => $request->email, + 'password' => $request->password, + ])); + } + + /** + * Get the two factor authentication enabled response. + * + * @param \Illuminate\Http\Request $request + * @param mixed $user + * @return \Symfony\Component\HttpFoundation\Response + */ + protected function twoFactorChallengeResponse($request, $user) + { + $request->session()->put([ + 'login.id' => $user->getKey(), + 'login.remember' => $request->filled('remember'), + ]); + + TwoFactorAuthenticationChallenged::dispatch($user); + + return $request->wantsJson() + ? response()->json(['two_factor' => true]) + : redirect()->route('two-factor.login'); + } +} diff --git a/app/Actions/Fortify/TwoFactorChallengeView.php b/app/Actions/Fortify/TwoFactorChallengeView.php index 2f8931a15cd..e8af5bc2326 100644 --- a/app/Actions/Fortify/TwoFactorChallengeView.php +++ b/app/Actions/Fortify/TwoFactorChallengeView.php @@ -5,6 +5,7 @@ use App\Models\User; use Inertia\Inertia; use Laravel\Fortify\Contracts\TwoFactorChallengeViewResponse as TwoFactorChallengeViewContract; +use LaravelWebauthn\Facades\Webauthn; class TwoFactorChallengeView implements TwoFactorChallengeViewContract { @@ -19,7 +20,12 @@ public function toResponse($request) $userId = $request->session()->get('login.id'); $user = User::find($userId); - return Inertia::render('Auth/TwoFactorChallenge', [ + $data = []; + if ($user !== null) { + $data['publicKey'] = Webauthn::prepareAssertion($user); + } + + return Inertia::render('Auth/TwoFactorChallenge', $data + [ 'two_factor' => optional($user)->two_factor_secret && ! is_null(optional($user)->two_factor_confirmed_at), 'remember' => $request->session()->get('login.remember'), ])->toResponse($request); diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php index da4ba348311..af10427ebc6 100644 --- a/app/Actions/Fortify/UpdateUserProfileInformation.php +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -33,7 +33,6 @@ public function update($user, array $input) 'first_name' => $input['first_name'], 'last_name' => $input['last_name'], 'email' => $input['email'], - 'locale' => $input['locale'], ])->save(); // @codeCoverageIgnoreEnd } diff --git a/app/Actions/Jetstream/UserProfile.php b/app/Actions/Jetstream/UserProfile.php new file mode 100644 index 00000000000..775392e1b3b --- /dev/null +++ b/app/Actions/Jetstream/UserProfile.php @@ -0,0 +1,52 @@ +filter(fn ($provider) => ! empty($provider)); + $providersName = $providers->mapWithKeys(function ($provider) { + return [ + $provider => config("services.$provider.name") ?? __("auth.login_provider_{$provider}"), + ]; + }); + + $webauthnKeys = $request->user()->webauthnKeys() + ->get() + ->map(function ($key) { + return [ + 'id' => $key->id, + 'name' => $key->name, + 'type' => $key->type, + 'last_active' => $key->updated_at->diffForHumans(), + ]; + }) + ->toArray(); + + $data['providers'] = $providers; + $data['providersName'] = $providersName; + $data['userTokens'] = $request->user()->userTokens()->get(); + $data['webauthnKeys'] = $webauthnKeys; + + $data['locales'] = collect(config('lang-detector.languages'))->map(fn ($locale) => [ + 'id' => $locale, + 'name' => __('auth.lang', [], $locale), + ]); + + $data['layoutData'] = VaultIndexViewHelper::layoutData(); + + return $data; + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index a2ebda406ec..dd0953b6f30 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -4,10 +4,13 @@ use App\Helpers\WallpaperHelper; use App\Http\Controllers\Controller; +use App\Models\User; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cookie; use Illuminate\Support\Facades\Route; use Inertia\Inertia; use Inertia\Response; +use LaravelWebauthn\Facades\Webauthn; class LoginController extends Controller { @@ -19,10 +22,28 @@ class LoginController extends Controller */ public function __invoke(Request $request): Response { - return Inertia::render('Auth/Login', [ + $providers = collect(config('auth.login_providers'))->reject(fn ($provider) => empty($provider)); + $providersName = $providers->mapWithKeys(fn ($provider) => [ + $provider => config("services.$provider.name") ?? __("auth.login_provider_{$provider}"), + ]); + + $data = []; + + if ($webauthnRemember = $request->cookie('webauthn_remember')) { + if (($user = User::find($webauthnRemember)) && $user->webauthnKeys()->count() > 0) { + $data['publicKey'] = Webauthn::prepareAssertion($user); + $data['userName'] = $user->name; + } else { + Cookie::expire('webauthn_remember'); + } + } + + return Inertia::render('Auth/Login', $data + [ 'canResetPassword' => Route::has('password.request'), 'status' => session('status'), 'wallpaperUrl' => WallpaperHelper::getRandomWallpaper(), + 'providers' => $providers, + 'providersName' => $providersName, ]); } } diff --git a/app/Http/Controllers/Auth/SocialiteCallbackController.php b/app/Http/Controllers/Auth/SocialiteCallbackController.php new file mode 100644 index 00000000000..0aa127b7d1c --- /dev/null +++ b/app/Http/Controllers/Auth/SocialiteCallbackController.php @@ -0,0 +1,106 @@ +checkProvider($driver); + + $redirect = $request->input('redirect'); + if ($redirect && Str::of($redirect)->startsWith($request->getSchemeAndHttpHost())) { + Redirect::setIntendedUrl($redirect); + } + + if ($request->boolean('remember')) { + $request->session()->put('login.remember', true); + } + + return Inertia::location(Socialite::driver($driver)->redirect()); + } + + /** + * Handle socalite callback. + * + * @param \Illuminate\Http\Request $request + * @param string $driver + * @return \Illuminate\Http\RedirectResponse + */ + public function callback(Request $request, string $driver): RedirectResponse + { + try { + $this->checkProvider($driver); + $this->checkForErrors($request, $driver); + + return $this->loginPipeline($request)->then(function (/*$request*/) { + return Redirect::intended(route('vault.index')); + }); + } catch (ValidationException $e) { + throw $e->redirectTo(Redirect::intended(route('home'))->getTargetUrl()); + } + } + + /** + * Get the authentication pipeline instance. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Pipeline\Pipeline + */ + protected function loginPipeline(Request $request) + { + return (new Pipeline(app()))->send($request)->through([ + AttemptToAuthenticateSocialite::class, + PrepareAuthenticatedSession::class, + ]); + } + + /** + * Check for errors. + * + * @param \Illuminate\Http\Request $request + * @param string $driver + * @return void + */ + private function checkForErrors(Request $request, string $driver): void + { + if ($request->filled('error')) { + throw ValidationException::withMessages([ + $driver => [$request->input('error_description')], + ]); + } + } + + /** + * Check if the driver is activated. + * + * @param string $driver + */ + private function checkProvider(string $driver): void + { + if (! collect(config('auth.login_providers'))->contains($driver)) { + throw ValidationException::withMessages([ + $driver => ['This provider does not exist.'], + ]); + } + } +} diff --git a/app/Http/Controllers/Profile/UserTokenController.php b/app/Http/Controllers/Profile/UserTokenController.php new file mode 100644 index 00000000000..c92157522ca --- /dev/null +++ b/app/Http/Controllers/Profile/UserTokenController.php @@ -0,0 +1,25 @@ +user()->userTokens() + ->where('driver', $driver) + ->delete(); + + return redirect()->route('profile.show'); + } +} diff --git a/app/Http/Controllers/Profile/WebauthnDestroyResponse.php b/app/Http/Controllers/Profile/WebauthnDestroyResponse.php new file mode 100644 index 00000000000..0e5d5497909 --- /dev/null +++ b/app/Http/Controllers/Profile/WebauthnDestroyResponse.php @@ -0,0 +1,24 @@ +wantsJson() + ? Response::noContent() + : Redirect::intended(Webauthn::redirects('register')); + } +} diff --git a/app/Http/Controllers/Profile/WebauthnUpdateResponse.php b/app/Http/Controllers/Profile/WebauthnUpdateResponse.php new file mode 100644 index 00000000000..8f3476f0cff --- /dev/null +++ b/app/Http/Controllers/Profile/WebauthnUpdateResponse.php @@ -0,0 +1,24 @@ +wantsJson() + ? Response::noContent() + : Redirect::intended(Webauthn::redirects('register')); + } +} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php index a539eb876bc..940a2d4119d 100644 --- a/app/Http/Requests/Auth/LoginRequest.php +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -49,7 +49,7 @@ public function authenticate() RateLimiter::hit($this->throttleKey()); throw ValidationException::withMessages([ - 'email' => __('auth.failed'), + 'email' => trans('auth.failed'), ]); } diff --git a/app/Listeners/LoginListener.php b/app/Listeners/LoginListener.php new file mode 100644 index 00000000000..c1732a2e638 --- /dev/null +++ b/app/Listeners/LoginListener.php @@ -0,0 +1,23 @@ +user) && $event->remember) { + Cookie::queue('webauthn_remember', $event->user->getAuthIdentifier(), 60 * 24 * 30); + } + } +} diff --git a/app/Models/User.php b/app/Models/User.php index fa80b937ce5..dc858047dbe 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -13,6 +13,7 @@ use Illuminate\Support\Facades\DB; use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; +use LaravelWebauthn\WebauthnAuthenticatable; class User extends Authenticatable implements MustVerifyEmail { @@ -20,6 +21,7 @@ class User extends Authenticatable implements MustVerifyEmail use HasFactory; use HasApiTokens; use TwoFactorAuthenticatable; + use WebauthnAuthenticatable; /** * Possible number format types. @@ -175,4 +177,14 @@ public function getContactInVault(Vault $vault): ?Contact return $contact; } + + /** + * Get the user tokens for external login providers. + * + * @return HasMany + */ + public function userTokens() + { + return $this->hasMany(UserToken::class); + } } diff --git a/app/Models/UserToken.php b/app/Models/UserToken.php new file mode 100644 index 00000000000..188bd00b29e --- /dev/null +++ b/app/Models/UserToken.php @@ -0,0 +1,61 @@ + + */ + protected $fillable = [ + 'user_id', + 'driver', + 'driver_id', + 'email', + 'format', + 'token', + 'token_secret', + 'refresh_token', + 'expires_in', + ]; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'token', + 'token_secret', + 'refresh_token', + ]; + + /** + * The attributes that should be visible in serialization. + * + * @var array + */ + protected $visible = [ + 'driver', + 'email', + 'format', + ]; + + /** + * Get the user record associated with the company. + * + * @return BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ee8ca5bcd8f..5fb7e91f933 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,13 @@ namespace App\Providers; +use App\Http\Controllers\Profile\WebauthnDestroyResponse; +use App\Http\Controllers\Profile\WebauthnUpdateResponse; +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; +use LaravelWebauthn\Facades\Webauthn; class AppServiceProvider extends ServiceProvider { @@ -23,6 +29,11 @@ public function register() */ public function boot() { - // + RateLimiter::for('oauth2-socialite', function (Request $request) { + return Limit::perMinute(5)->by(optional($request->user())->id ?: $request->ip()); + }); + + Webauthn::updateViewResponseUsing(WebauthnUpdateResponse::class); + Webauthn::destroyViewResponseUsing(WebauthnDestroyResponse::class); } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 9d2f333ea81..1bc533d730c 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -4,24 +4,34 @@ use App\Contact\ManageDocuments\Events\FileDeleted; use App\Contact\ManageDocuments\Listeners\DeleteFileInStorage; +use App\Listeners\LoginListener; +use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; +use SocialiteProviders\GitHub\GitHubExtendSocialite; +use SocialiteProviders\Manager\SocialiteWasCalled; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * - * @var array + * @var array> */ protected $listen = [ - Registered::class => [ - SendEmailVerificationNotification::class, + Login::class => [ + LoginListener::class, ], FileDeleted::class => [ DeleteFileInStorage::class, ], + Registered::class => [ + SendEmailVerificationNotification::class, + ], + SocialiteWasCalled::class => [ + GitHubExtendSocialite::class, + ], ]; /** diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 8ba35368de8..e21cd0bc4c5 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Actions\Fortify\CreateNewUser; +use App\Actions\Fortify\RedirectIfTwoFactorAuthenticatable; use App\Actions\Fortify\ResetUserPassword; use App\Actions\Fortify\TwoFactorChallengeView; use App\Actions\Fortify\UpdateUserPassword; @@ -13,6 +14,8 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; +use Laravel\Fortify\Actions\AttemptToAuthenticate; +use Laravel\Fortify\Actions\PrepareAuthenticatedSession; use Laravel\Fortify\Fortify; class FortifyServiceProvider extends ServiceProvider @@ -34,11 +37,19 @@ public function register() */ public function boot() { + Fortify::authenticateThrough(fn () => [ + RedirectIfTwoFactorAuthenticatable::class, + AttemptToAuthenticate::class, + PrepareAuthenticatedSession::class, + ]); + Fortify::loginView(fn ($request) => (new LoginController())($request)); - Fortify::confirmPasswordsUsing(fn ($user, ?string $password = null) => $user->password ? app(StatefulGuard::class)->validate([ - 'email' => $user->email, - 'password' => $password, - ]) : true + Fortify::confirmPasswordsUsing(fn ($user, ?string $password = null) => $user->password + ? app(StatefulGuard::class)->validate([ + 'email' => $user->email, + 'password' => $password, + ]) + : true ); Fortify::createUsersUsing(CreateNewUser::class); @@ -53,7 +64,6 @@ public function boot() return Limit::perMinute(5)->by($email.$request->ip()); }); - RateLimiter::for('two-factor', fn (Request $request) => Limit::perMinute(5)->by($request->session()->get('login.id')) - ); + RateLimiter::for('two-factor', fn (Request $request) => Limit::perMinute(5)->by($request->session()->get('login.id'))); } } diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index f193ba24dda..d10041e257d 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Actions\Jetstream\DeleteUser; +use App\Actions\Jetstream\UserProfile; use Illuminate\Support\ServiceProvider; use Laravel\Jetstream\Jetstream; @@ -28,6 +29,7 @@ public function boot() $this->configurePermissions(); Jetstream::deleteUsersUsing(DeleteUser::class); + Jetstream::inertia()->whenRendering('Profile/Show', new UserProfile()); } /** diff --git a/composer.json b/composer.json index d8445cb556e..fd2142c797c 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "license": "MIT", "require": { "php": "^8.1", + "asbiin/laravel-webauthn": "^3.2", "fruitcake/laravel-cors": "^2.2", "guzzlehttp/guzzle": "^7.4.5", "http-interop/http-factory-guzzle": "^1.2", @@ -21,10 +22,12 @@ "laravel/jetstream": "^2.10", "laravel/sanctum": "^2.15.1", "laravel/scout": "^9.4.10", + "laravel/socialite": "^5.5", "laravel/tinker": "^2.7.2", "meilisearch/meilisearch-php": "^0.23.3", "naugrim/laravel-sentry-tunnel": "^1.0", "sentry/sentry-laravel": "^2.13", + "socialiteproviders/github": "^4.1", "tightenco/ziggy": "^1.4.6", "uploadcare/uploadcare-php": "^3.2.4", "vluzrmos/language-detector": "^2.3.3" diff --git a/composer.lock b/composer.lock index 740d05520d4..1c93b5157e7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,96 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d122d4dfbaf95c1eca838a2ca0c9595e", + "content-hash": "a9e8bc1c5b08157f5d1e071a57050320", "packages": [ + { + "name": "asbiin/laravel-webauthn", + "version": "3.2.3", + "source": { + "type": "git", + "url": "https://github.com/asbiin/laravel-webauthn.git", + "reference": "155ae8cd2fea1cee5dd0fc0f28ca1d539d80392f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/asbiin/laravel-webauthn/zipball/155ae8cd2fea1cee5dd0fc0f28ca1d539d80392f", + "reference": "155ae8cd2fea1cee5dd0fc0f28ca1d539d80392f", + "shasum": "" + }, + "require": { + "illuminate/support": "^9.0", + "php": ">=8.1", + "psr/http-factory-implementation": "1.0", + "thecodingmachine/safe": "^2.0", + "web-auth/cose-lib": "^4.0", + "web-auth/webauthn-lib": "^4.0", + "web-token/jwt-signature": "^3.0" + }, + "require-dev": { + "ext-sqlite3": "*", + "guzzlehttp/psr7": "^2.1", + "laravel/legacy-factories": "^1.0", + "nunomaduro/larastan": "^2.0", + "ocramius/package-versions": "^2.0", + "orchestra/testbench": "^7.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.0", + "thecodingmachine/phpstan-safe-rule": "^1.0" + }, + "suggest": { + "guzzlehttp/psr7": "To provide a psr/http-factory-implementation implementation", + "php-http/discovery": "To find a psr/http-factory-implementation implementation", + "psr/http-client-implementation": "Required for the AndroidSafetyNet Attestation Statement support", + "symfony/psr-http-message-bridge": "To find a psr/http-factory-implementation implementation", + "web-token/jwt-key-mgmt": "Required for the AndroidSafetyNet Attestation Statement support", + "web-token/jwt-signature-algorithm-ecdsa": "Required for the AndroidSafetyNet Attestation Statement support", + "web-token/jwt-signature-algorithm-eddsa": "Required for the AndroidSafetyNet Attestation Statement support", + "web-token/jwt-signature-algorithm-rsa": "Required for the AndroidSafetyNet Attestation Statement support" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "LaravelWebauthn\\WebauthnServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "LaravelWebauthn\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexis Saettler", + "email": "alexis@saettler.org" + } + ], + "description": "Laravel Webauthn support", + "keywords": [ + "laravel", + "php", + "security", + "webauthn" + ], + "support": { + "issues": "https://github.com/asbiin/laravel-webauthn/issues", + "source": "https://github.com/asbiin/laravel-webauthn" + }, + "funding": [ + { + "url": "https://github.com/asbiin", + "type": "github" + } + ], + "time": "2022-09-06T11:14:30+00:00" + }, { "name": "asm89/stack-cors", "version": "v2.1.1", @@ -116,6 +204,73 @@ }, "time": "2022-03-14T02:02:36+00:00" }, + { + "name": "beberlei/assert", + "version": "v3.3.2", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/cb70015c04be1baee6f5f5c953703347c0ac1655", + "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7.0 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=6.0.0", + "yoast/phpunit-polyfills": "^0.1.0" + }, + "suggest": { + "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Assert/functions.php" + ], + "psr-4": { + "Assert\\": "lib/Assert" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "support": { + "issues": "https://github.com/beberlei/assert/issues", + "source": "https://github.com/beberlei/assert/tree/v3.3.2" + }, + "time": "2021-12-16T21:41:27+00:00" + }, { "name": "brick/math", "version": "0.10.2", @@ -656,6 +811,81 @@ ], "time": "2022-06-18T20:57:19+00:00" }, + { + "name": "fgrosse/phpasn1", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/fgrosse/PHPASN1.git", + "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/eef488991d53e58e60c9554b09b1201ca5ba9296", + "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296", + "shasum": "" + }, + "require": { + "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "~2.0", + "phpunit/phpunit": "^6.3 || ^7.0 || ^8.0" + }, + "suggest": { + "ext-bcmath": "BCmath is the fallback extension for big integer calculations", + "ext-curl": "For loading OID information from the web if they have not bee defined statically", + "ext-gmp": "GMP is the preferred extension for big integer calculations", + "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "FG\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Friedrich Große", + "email": "friedrich.grosse@gmail.com", + "homepage": "https://github.com/FGrosse", + "role": "Author" + }, + { + "name": "All contributors", + "homepage": "https://github.com/FGrosse/PHPASN1/contributors" + } + ], + "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", + "homepage": "https://github.com/FGrosse/PHPASN1", + "keywords": [ + "DER", + "asn.1", + "asn1", + "ber", + "binary", + "decoding", + "encoding", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/fgrosse/PHPASN1/issues", + "source": "https://github.com/fgrosse/PHPASN1/tree/v2.4.0" + }, + "time": "2021-12-11T12:41:06+00:00" + }, { "name": "fruitcake/laravel-cors", "version": "v2.2.0", @@ -2096,6 +2326,75 @@ }, "time": "2022-08-26T15:25:27+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.5.5", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "ce8b2f967eead5a6bae74449e207be6f8046edc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/ce8b2f967eead5a6bae74449e207be6f8046edc3", + "reference": "ce8b2f967eead5a6bae74449e207be6f8046edc3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0", + "league/oauth1-client": "^1.10.1", + "php": "^7.2|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0", + "phpunit/phpunit": "^8.0|^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ], + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + } + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2022-08-20T21:32:07+00:00" + }, { "name": "laravel/tinker", "version": "v2.7.2", @@ -2164,6 +2463,67 @@ }, "time": "2022-03-23T12:38:24+00:00" }, + { + "name": "lcobucci/clock", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "fb533e093fd61321bfcbac08b131ce805fe183d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/fb533e093fd61321bfcbac08b131ce805fe183d3", + "reference": "fb533e093fd61321bfcbac08b131ce805fe183d3", + "shasum": "" + }, + "require": { + "php": "^8.0", + "stella-maris/clock": "^0.1.4" + }, + "require-dev": { + "infection/infection": "^0.26", + "lcobucci/coding-standard": "^8.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2022-04-19T19:34:17+00:00" + }, { "name": "league/commonmark", "version": "2.3.5", @@ -2499,39 +2859,115 @@ "time": "2022-04-17T13:12:02+00:00" }, { - "name": "meilisearch/meilisearch-php", - "version": "v0.23.3", + "name": "league/oauth1-client", + "version": "v1.10.1", "source": { "type": "git", - "url": "https://github.com/meilisearch/meilisearch-php.git", - "reference": "e993256ff7c5be7fe760392145830c9defab8214" + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/meilisearch/meilisearch-php/zipball/e993256ff7c5be7fe760392145830c9defab8214", - "reference": "e993256ff7c5be7fe760392145830c9defab8214", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167", "shasum": "" }, "require": { "ext-json": "*", - "php": "^7.4 || ^8.0", - "php-http/client-common": "^2.0", - "php-http/discovery": "^1.7", - "php-http/httplug": "^2.1" + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.0", - "guzzlehttp/guzzle": "^7.1", - "http-interop/http-factory-guzzle": "^1.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^9.5" + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" }, "suggest": { - "guzzlehttp/guzzle": "Use Guzzle ^7 as HTTP client", + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1" + }, + "time": "2022-04-15T14:02:14+00:00" + }, + { + "name": "meilisearch/meilisearch-php", + "version": "v0.23.3", + "source": { + "type": "git", + "url": "https://github.com/meilisearch/meilisearch-php.git", + "reference": "e993256ff7c5be7fe760392145830c9defab8214" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/meilisearch/meilisearch-php/zipball/e993256ff7c5be7fe760392145830c9defab8214", + "reference": "e993256ff7c5be7fe760392145830c9defab8214", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "php-http/client-common": "^2.0", + "php-http/discovery": "^1.7", + "php-http/httplug": "^2.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "guzzlehttp/guzzle": "^7.1", + "http-interop/http-factory-guzzle": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^9.5" + }, + "suggest": { + "guzzlehttp/guzzle": "Use Guzzle ^7 as HTTP client", "http-interop/http-factory-guzzle": "Factory for guzzlehttp/guzzle" }, "type": "library", @@ -4735,6 +5171,344 @@ ], "time": "2022-07-15T11:36:23+00:00" }, + { + "name": "socialiteproviders/github", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/GitHub.git", + "reference": "25fc481721d74b829b43bff3519e7103d5339e19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/GitHub/zipball/25fc481721d74b829b43bff3519e7103d5339e19", + "reference": "25fc481721d74b829b43bff3519e7103d5339e19", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2 || ^8.0", + "socialiteproviders/manager": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\GitHub\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Komarev", + "email": "ell@cybercog.su" + } + ], + "description": "GitHub OAuth2 Provider for Laravel Socialite", + "support": { + "source": "https://github.com/SocialiteProviders/GitHub/tree/4.1.0" + }, + "time": "2020-12-01T23:10:59+00:00" + }, + { + "name": "socialiteproviders/manager", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Manager.git", + "reference": "738276dfbc2b68a9145db7b3df1588d53db528a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/738276dfbc2b68a9145db7b3df1588d53db528a1", + "reference": "738276dfbc2b68a9145db7b3df1588d53db528a1", + "shasum": "" + }, + "require": { + "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0", + "laravel/socialite": "~5.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "phpunit/phpunit": "^6.0 || ^9.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "SocialiteProviders\\Manager\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "SocialiteProviders\\Manager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andy Wendt", + "email": "andy@awendt.com" + }, + { + "name": "Anton Komarev", + "email": "a.komarev@cybercog.su" + }, + { + "name": "Miguel Piedrafita", + "email": "soy@miguelpiedrafita.com" + }, + { + "name": "atymic", + "email": "atymicq@gmail.com", + "homepage": "https://atymic.dev" + } + ], + "description": "Easily add new or override built-in providers in Laravel Socialite.", + "homepage": "https://socialiteproviders.com", + "keywords": [ + "laravel", + "manager", + "oauth", + "providers", + "socialite" + ], + "support": { + "issues": "https://github.com/socialiteproviders/manager/issues", + "source": "https://github.com/socialiteproviders/manager" + }, + "time": "2022-09-02T10:20:10+00:00" + }, + { + "name": "spomky-labs/cbor-php", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "de06c1be866bade5270f4cf5ef3b3746ba3f26eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/de06c1be866bade5270f4cf5ef3b3746ba3f26eb", + "reference": "de06c1be866bade5270f4cf5ef3b3746ba3f26eb", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10", + "ext-mbstring": "*", + "php": ">=8.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-json": "*", + "infection/infection": "^0.26", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13", + "roave/security-advisories": "dev-latest", + "symfony/var-dumper": "^6.0", + "symplify/easy-coding-standard": "^11.0" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", + "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "CBOR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + } + ], + "description": "CBOR Encoder/Decoder for PHP", + "keywords": [ + "Concise Binary Object Representation", + "RFC7049", + "cbor" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/cbor-php/issues", + "source": "https://github.com/Spomky-Labs/cbor-php/tree/v3.0.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-06-26T07:20:40+00:00" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "4dc75ffcdaad63b3512c30bdae8a8d862cf1b2cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/4dc75ffcdaad63b3512c30bdae8a8d862cf1b2cb", + "reference": "4dc75ffcdaad63b3512c30bdae8a8d862cf1b2cb", + "shasum": "" + }, + "require": { + "brick/math": "^0.10", + "ext-mbstring": "*", + "php": ">=8.1" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-gmp": "*", + "infection/infection": "^0.26", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^9.5.5", + "rector/rector": "^0.14", + "roave/security-advisories": "dev-latest", + "symfony/phpunit-bridge": "^6.1", + "symfony/var-dumper": "^6.1", + "symplify/easy-coding-standard": "^11.1", + "thecodingmachine/phpstan-safe-rule": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "SpomkyLabs\\Pki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" + } + ], + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-08-22T11:26:16+00:00" + }, + { + "name": "stella-maris/clock", + "version": "0.1.5", + "source": { + "type": "git", + "url": "git@gitlab.com:stella-maris/clock.git", + "reference": "447879c53ca0b2a762cdbfba5e76ccf4deca9158" + }, + "dist": { + "type": "zip", + "url": "https://gitlab.com/api/v4/projects/stella-maris%2Fclock/repository/archive.zip?sha=447879c53ca0b2a762cdbfba5e76ccf4deca9158", + "reference": "447879c53ca0b2a762cdbfba5e76ccf4deca9158", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "StellaMaris\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Heigl", + "role": "Maintainer" + } + ], + "description": "A pre-release of the proposed PSR-20 Clock-Interface", + "homepage": "https://gitlab.com/stella-maris/clock", + "keywords": [ + "clock", + "datetime", + "point in time", + "psr20" + ], + "time": "2022-08-05T07:21:25+00:00" + }, { "name": "symfony/console", "version": "v6.1.4", @@ -7154,11 +7928,85 @@ "time": "2022-06-27T17:24:16+00:00" }, { - "name": "symfony/var-dumper", + "name": "symfony/uid", "version": "v6.1.3", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", + "url": "https://github.com/symfony/uid.git", + "reference": "ea2ccf0fdb88c83e626105b68e5bab5c132d812b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/ea2ccf0fdb88c83e626105b68e5bab5c132d812b", + "reference": "ea2ccf0fdb88c83e626105b68e5bab5c132d812b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T13:46:29+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", "reference": "d5a5e44a2260c5eb5e746bf4f1fbd12ee6ceb427" }, "dist": { @@ -7241,6 +8089,144 @@ ], "time": "2022-07-20T13:46:29+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v2.2.3", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "e454a3142d2197694d1fda291a4462ccd3f66e12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/e454a3142d2197694d1fda291a4462ccd3f66e12", + "reference": "e454a3142d2197694d1fda291a4462ccd3f66e12", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", + "deprecated/libevent.php", + "deprecated/password.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "deprecated/strings.php", + "lib/special_cases.php", + "deprecated/mysqli.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "deprecated/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v2.2.3" + }, + "time": "2022-08-04T14:05:49+00:00" + }, { "name": "tightenco/ziggy", "version": "v1.4.6", @@ -7644,6 +8630,398 @@ ], "time": "2022-03-08T17:03:00+00:00" }, + { + "name": "web-auth/cose-lib", + "version": "v4.0.6", + "source": { + "type": "git", + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "d72032843c97ba40edb682dfcec0b787b25cbe6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/d72032843c97ba40edb682dfcec0b787b25cbe6f", + "reference": "d72032843c97ba40edb682dfcec0b787b25cbe6f", + "shasum": "" + }, + "require": { + "beberlei/assert": "^3.2", + "brick/math": "^0.9|^0.10", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "fgrosse/phpasn1": "^2.1", + "php": ">=8.1", + "thecodingmachine/safe": "^1.0|^2.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.26.12", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.7", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.6", + "symfony/phpunit-bridge": "^6.1", + "symplify/easy-coding-standard": "^11.0", + "thecodingmachine/phpstan-safe-rule": "^1.2" + }, + "suggest": { + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cose\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/cose/contributors" + } + ], + "description": "CBOR Object Signing and Encryption (COSE) For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "COSE", + "RFC8152" + ], + "support": { + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/v4.0.6" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-08-16T14:04:27+00:00" + }, + { + "name": "web-auth/metadata-service", + "version": "4.1.4", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-metadata-service.git", + "reference": "a0fee00f92da22d1bb7e01727ff33c8f58192fcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-metadata-service/zipball/a0fee00f92da22d1bb7e01727ff33c8f58192fcd", + "reference": "a0fee00f92da22d1bb7e01727ff33c8f58192fcd", + "shasum": "" + }, + "require": { + "beberlei/assert": "^3.2", + "ext-json": "*", + "lcobucci/clock": "^2.2", + "paragonie/constant_time_encoding": "^2.4", + "php": ">=8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/log": "^2.0|^3.0", + "spomky-labs/pki-framework": "^1.0", + "thecodingmachine/safe": "^2.0" + }, + "suggest": { + "psr/log-implementation": "Recommended to receive logs from the library", + "web-token/jwt-key-mgmt": "Mandatory for fetching Metadata Statement from distant sources", + "web-token/jwt-signature-algorithm-ecdsa": "Mandatory for fetching Metadata Statement from distant sources" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webauthn\\MetadataService\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/metadata-service/contributors" + } + ], + "description": "Metadata Service for FIDO2/Webauthn", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-metadata-service/tree/4.1.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-08-29T17:23:55+00:00" + }, + { + "name": "web-auth/webauthn-lib", + "version": "4.1.4", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "8fad67be1448d4271fce992b7bcca62ae371d3b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/8fad67be1448d4271fce992b7bcca62ae371d3b9", + "reference": "8fad67be1448d4271fce992b7bcca62ae371d3b9", + "shasum": "" + }, + "require": { + "beberlei/assert": "^3.2", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "fgrosse/phpasn1": "^2.1", + "paragonie/constant_time_encoding": "^2.4", + "php": ">=8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "psr/log": "^2.0|^3.0", + "spomky-labs/cbor-php": "^3.0", + "symfony/uid": "^6.0", + "thecodingmachine/safe": "^2.0", + "web-auth/cose-lib": "^4.0.6", + "web-auth/metadata-service": "self.version" + }, + "suggest": { + "psr/log-implementation": "Recommended to receive logs from the library", + "web-token/jwt-key-mgmt": "Mandatory for the AndroidSafetyNet Attestation Statement support", + "web-token/jwt-signature-algorithm-ecdsa": "Recommended for the AndroidSafetyNet Attestation Statement support", + "web-token/jwt-signature-algorithm-eddsa": "Recommended for the AndroidSafetyNet Attestation Statement support", + "web-token/jwt-signature-algorithm-rsa": "Mandatory for the AndroidSafetyNet Attestation Statement support" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webauthn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/webauthn-library/contributors" + } + ], + "description": "FIDO2/Webauthn Support For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-lib/tree/4.1.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-08-29T20:33:38+00:00" + }, + { + "name": "web-token/jwt-core", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-core.git", + "reference": "4d956e786a4e35d54c74787ebff840a0311c5e83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-core/zipball/4d956e786a4e35d54c74787ebff840a0311c5e83", + "reference": "4d956e786a4e35d54c74787ebff840a0311c5e83", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10", + "ext-json": "*", + "ext-mbstring": "*", + "fgrosse/phpasn1": "^2.0", + "paragonie/constant_time_encoding": "^2.4", + "php": ">=8.1" + }, + "conflict": { + "spomky-labs/jose": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Core\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "Core component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-core/tree/3.1.2" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-08-04T21:04:09+00:00" + }, + { + "name": "web-token/jwt-signature", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature.git", + "reference": "53dd6c8ad7b2d679721fe0d5b2b0b88c68c3dbce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature/zipball/53dd6c8ad7b2d679721fe0d5b2b0b88c68c3dbce", + "reference": "53dd6c8ad7b2d679721fe0d5b2b0b88c68c3dbce", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "web-token/jwt-core": "^3.0" + }, + "suggest": { + "web-token/jwt-signature-algorithm-ecdsa": "ECDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-eddsa": "EdDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-experimental": "Experimental Signature Algorithms", + "web-token/jwt-signature-algorithm-hmac": "HMAC Based Signature Algorithms", + "web-token/jwt-signature-algorithm-none": "None Signature Algorithm", + "web-token/jwt-signature-algorithm-rsa": "RSA Based Signature Algorithms" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-signature/contributors" + } + ], + "description": "Signature component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature/tree/3.1.2" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-08-04T21:04:09+00:00" + }, { "name": "webmozart/assert", "version": "1.11.0", diff --git a/config/app.php b/config/app.php index 2991bbb70cc..e8313177547 100644 --- a/config/app.php +++ b/config/app.php @@ -177,6 +177,7 @@ /* * Package Service Providers... */ + SocialiteProviders\Manager\ServiceProvider::class, /* * Application Service Providers... diff --git a/config/auth.php b/config/auth.php index e29a3f7dc0b..8709676fd06 100644 --- a/config/auth.php +++ b/config/auth.php @@ -61,14 +61,9 @@ 'providers' => [ 'users' => [ - 'driver' => 'eloquent', + 'driver' => 'webauthn', 'model' => App\Models\User::class, ], - - // 'users' => [ - // 'driver' => 'database', - // 'table' => 'users', - // ], ], /* @@ -108,4 +103,18 @@ 'password_timeout' => 10800, + /* + |-------------------------------------------------------------------------- + | Login providers + |-------------------------------------------------------------------------- + | + | List of available login providers, coma separated. This uses socialite + | packages. + | + | Supported: azure, github, google, linkedin, monica, saml2, slack, twitter. + | + */ + + 'login_providers' => explode(',', env('LOGIN_PROVIDERS', '')), + ]; diff --git a/config/lang-detector.php b/config/lang-detector.php index 5858b8d67b4..4fdb5af43cf 100644 --- a/config/lang-detector.php +++ b/config/lang-detector.php @@ -28,6 +28,7 @@ 'languages' => parse_langs_to_array( env('LANG_DETECTOR_LANGUAGES', [ 'en', + 'fr', ]) ), diff --git a/config/services.php b/config/services.php index bbac0acbecd..ebd86fb0f44 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,14 @@ 'private_key' => env('UPLOADCARE_PRIVATE_KEY', null), ], + /* + * Socialite providers + */ + + 'github' => [ + 'client_id' => env('GITHUB_CLIENT_ID'), + 'client_secret' => env('GITHUB_CLIENT_SECRET'), + 'redirect' => env('GITHUB_REDIRECT_URI', '/auth/github/callback'), + ], + ]; diff --git a/config/webauthn.php b/config/webauthn.php new file mode 100644 index 00000000000..91078fe726c --- /dev/null +++ b/config/webauthn.php @@ -0,0 +1,288 @@ + true, + + /* + |-------------------------------------------------------------------------- + | Webauthn Guard + |-------------------------------------------------------------------------- + | + | Here you may specify which authentication guard Webauthn will use while + | authenticating users. This value should correspond with one of your + | guards that is already present in your "auth" configuration file. + | + */ + + 'guard' => 'web', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + */ + + 'username' => 'email', + + /* + |-------------------------------------------------------------------------- + | Webauthn Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Webauthn will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Webauthn routes will be available. + | + */ + + 'prefix' => 'webauthn', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Webauthn Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Webauthn will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Webauthn key model + |-------------------------------------------------------------------------- + | + | Here you may specify the model used to create Webauthn keys. + | + */ + + 'model' => WebauthnKey::class, + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Laravel Webauthn will throttle logins to five requests per + | minute for every email and IP address combination. However, if you would + | like to specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + ], + + /* + |-------------------------------------------------------------------------- + | Redirect routes + |-------------------------------------------------------------------------- + | + | When using navigation, redirects to these url on success: + | - login: after a successfull login. + | - register: after a successfull Webauthn key creation. + | + | Redirects are not used in case of application/json requests. + | + */ + + 'redirects' => [ + 'login' => '/vaults', + 'register' => '/user/profile', + ], + + /* + |-------------------------------------------------------------------------- + | View to load after middleware login request. + |-------------------------------------------------------------------------- + | + | The name of blade template to load: + | - authenticate: when a user login, and has to validate Webauthn 2nd factor. + | - register: when a user request to create a Webauthn key. + | + | If the views are empty or null, then the route will not be registered. + | + */ + + 'views' => [ + 'authenticate' => null, + 'register' => null, + ], + + /* + |-------------------------------------------------------------------------- + | Session name + |-------------------------------------------------------------------------- + | + | Name of the session parameter to store the successful login. + | + */ + + 'session_name' => 'webauthn_auth', + + /* + |-------------------------------------------------------------------------- + | Webauthn challenge length + |-------------------------------------------------------------------------- + | + | Length of the random string used in the challenge request. + | + */ + + 'challenge_length' => 32, + + /* + |-------------------------------------------------------------------------- + | Webauthn timeout (milliseconds) + |-------------------------------------------------------------------------- + | + | Time that the caller is willing to wait for the call to complete. + | + */ + + 'timeout' => 60000, + + /* + |-------------------------------------------------------------------------- + | Webauthn extension client input + |-------------------------------------------------------------------------- + | + | Optional authentication extension. + | See https://www.w3.org/TR/webauthn/#client-extension-input + | + */ + + 'extensions' => [], + + /* + |-------------------------------------------------------------------------- + | Webauthn icon + |-------------------------------------------------------------------------- + | + | Url which resolves to an image associated with the entity. + | See https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-icon + | + */ + + 'icon' => env('WEBAUTHN_ICON'), + + /* + |-------------------------------------------------------------------------- + | Webauthn Attestation Conveyance + |-------------------------------------------------------------------------- + | + | This parameter specify the preference regarding the attestation conveyance + | during credential generation. + | See https://www.w3.org/TR/webauthn/#enum-attestation-convey + | + | Supported: "none", "indirect", "direct", "enterprise". + */ + + 'attestation_conveyance' => 'none', + + /* + |-------------------------------------------------------------------------- + | Google Safetynet ApiKey + |-------------------------------------------------------------------------- + | + | Api key to use Google Safetynet. + | See https://developer.android.com/training/safetynet/attestation + | + */ + + 'google_safetynet_api_key' => env('GOOGLE_SAFETYNET_API_KEY'), + + /* + |-------------------------------------------------------------------------- + | Webauthn Public Key Credential Parameters + |-------------------------------------------------------------------------- + | + | List of allowed Cryptographic Algorithm Identifier. + | See https://www.w3.org/TR/webauthn/#sctn-alg-identifier + | + */ + + 'public_key_credential_parameters' => [ + \Cose\Algorithms::COSE_ALGORITHM_ES256, // ECDSA with SHA-256 + \Cose\Algorithms::COSE_ALGORITHM_ES512, // ECDSA with SHA-512 + \Cose\Algorithms::COSE_ALGORITHM_RS256, // RSASSA-PKCS1-v1_5 with SHA-256 + \Cose\Algorithms::COSE_ALGORITHM_EdDSA, // EdDSA + \Cose\Algorithms::COSE_ALGORITHM_ES384, // ECDSA with SHA-384 + ], + + /* + |-------------------------------------------------------------------------- + | Credentials Attachment. + |-------------------------------------------------------------------------- + | + | Authentication can be tied to the current device (like when using Windows + | Hello or Touch ID) or a cross-platform device (like USB Key). When this + | is "null" the user will decide where to store his authentication info. + | + | See https://www.w3.org/TR/webauthn/#enum-attachment + | + | Supported: "null", "cross-platform", "platform". + | + */ + + 'attachment_mode' => null, + + /* + |-------------------------------------------------------------------------- + | User presence and verification + |-------------------------------------------------------------------------- + | + | Most authenticators and smartphones will ask the user to actively verify + | themselves for log in. Use "required" to always ask verify, "preferred" + | to ask when possible, and "discouraged" to just ask for user presence. + | + | See https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement + | + | Supported: "required", "preferred", "discouraged". + | + */ + + 'user_verification' => 'preferred', + + /* + |-------------------------------------------------------------------------- + | Userless (One touch, Typeless) login + |-------------------------------------------------------------------------- + | + | By default, users must input their email to receive a list of credentials + | ID to use for authentication, but they can also login without specifying + | one if the device can remember them, allowing for true one-touch login. + | + | If required or preferred, login verification will be always required. + | + | See https://www.w3.org/TR/webauthn/#enum-residentKeyRequirement + | + | Supported: "null", "required", "preferred", "discouraged". + | + */ + + 'userless' => null, + +]; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index a9e530b008e..1967e3ecc91 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -9,6 +9,13 @@ class UserFactory extends Factory { + /** + * The name of the factory's corresponding model. + * + * @var class-string<\Illuminate\Database\Eloquent\Model> + */ + protected $model = User::class; + /** * Define the model's default state. * diff --git a/database/factories/UserTokenFactory.php b/database/factories/UserTokenFactory.php new file mode 100644 index 00000000000..f0b123d32cf --- /dev/null +++ b/database/factories/UserTokenFactory.php @@ -0,0 +1,33 @@ + + */ + protected $model = UserToken::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + 'user_id' => User::factory(), + 'driver' => 'github', + 'driver_id' => $this->faker->uuid, + 'format' => 'oauth2', + 'token' => $this->faker->word, + ]; + } +} diff --git a/database/migrations/2019_03_29_163611_add_webauthn.php b/database/migrations/2019_03_29_163611_add_webauthn.php new file mode 100644 index 00000000000..e3f804f72f2 --- /dev/null +++ b/database/migrations/2019_03_29_163611_add_webauthn.php @@ -0,0 +1,45 @@ +id(); + $table->bigInteger('user_id')->unsigned(); + + $table->string('name')->default('key'); + $table->string('credentialId', 768); + $table->string('type', 255); + $table->text('transports'); + $table->string('attestationType', 255); + $table->text('trustPath'); + $table->text('aaguid'); + $table->text('credentialPublicKey'); + $table->bigInteger('counter')->unsigned(); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->index('credentialId'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('webauthn_keys'); + } +}; diff --git a/database/migrations/2021_07_06_065356_user_token_socialite.php b/database/migrations/2021_07_06_065356_user_token_socialite.php new file mode 100644 index 00000000000..9c98892b3da --- /dev/null +++ b/database/migrations/2021_07_06_065356_user_token_socialite.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedBigInteger('user_id'); + $table->string('driver_id', 256); + $table->string('driver', 50); + $table->char('format', 6); + $table->string('email', 1024)->nullable(); + $table->string('token', 2048); + $table->string('token_secret', 2048)->nullable(); + $table->string('refresh_token', 2048)->nullable(); + $table->unsignedBigInteger('expires_in')->nullable(); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->index(['driver', 'driver_id']); + }); + } +}; diff --git a/lang/en/auth.php b/lang/en/auth.php index f023d2eec4f..ef0272b4c25 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -14,4 +14,6 @@ 'login_password' => 'Your password', 'login_cta' => 'Login →', 'login_invalid_credentials' => '😳 Invalid credentials', + + 'login_provider_github' => 'GitHub', ]; diff --git a/public/img/auth/github.svg b/public/img/auth/github.svg new file mode 100644 index 00000000000..7d2cf98ad62 --- /dev/null +++ b/public/img/auth/github.svg @@ -0,0 +1 @@ + diff --git a/resources/js/Components/ApplicationLogo.vue b/resources/js/Components/ApplicationLogo.vue index e69fb5acc6a..0471496de17 100644 --- a/resources/js/Components/ApplicationLogo.vue +++ b/resources/js/Components/ApplicationLogo.vue @@ -1,6 +1,53 @@ diff --git a/resources/js/Components/Jetstream/AuthenticationCardLogo.vue b/resources/js/Components/AuthenticationCardLogo.vue similarity index 50% rename from resources/js/Components/Jetstream/AuthenticationCardLogo.vue rename to resources/js/Components/AuthenticationCardLogo.vue index d71db97e680..981bddbf1d9 100644 --- a/resources/js/Components/Jetstream/AuthenticationCardLogo.vue +++ b/resources/js/Components/AuthenticationCardLogo.vue @@ -1,14 +1,12 @@ diff --git a/resources/js/Components/Breadcrumb.vue b/resources/js/Components/Breadcrumb.vue new file mode 100644 index 00000000000..9403a41d383 --- /dev/null +++ b/resources/js/Components/Breadcrumb.vue @@ -0,0 +1,36 @@ + + + diff --git a/resources/js/Components/Button.vue b/resources/js/Components/Button.vue index d0b20dc8e21..9227a0103ce 100644 --- a/resources/js/Components/Button.vue +++ b/resources/js/Components/Button.vue @@ -1,18 +1,16 @@ + + - - diff --git a/resources/js/Components/Checkbox.vue b/resources/js/Components/Checkbox.vue index 132ed78774a..14507208120 100644 --- a/resources/js/Components/Checkbox.vue +++ b/resources/js/Components/Checkbox.vue @@ -1,3 +1,30 @@ + + - - diff --git a/resources/js/Components/Dropdown.vue b/resources/js/Components/Dropdown.vue index df2e9de537b..dfd8567ffbe 100644 --- a/resources/js/Components/Dropdown.vue +++ b/resources/js/Components/Dropdown.vue @@ -1,3 +1,51 @@ + + - - diff --git a/resources/js/Components/DropdownLink.vue b/resources/js/Components/DropdownLink.vue index 59c9e642c34..11ea98de44a 100644 --- a/resources/js/Components/DropdownLink.vue +++ b/resources/js/Components/DropdownLink.vue @@ -1,12 +1,33 @@ - + + + diff --git a/resources/js/Components/Input.vue b/resources/js/Components/Input.vue index a0574358957..6c3e7062a4f 100644 --- a/resources/js/Components/Input.vue +++ b/resources/js/Components/Input.vue @@ -1,26 +1,27 @@ + + - - diff --git a/resources/js/Components/InputError.vue b/resources/js/Components/InputError.vue index b9fdb046f4b..4c32b94c03d 100644 --- a/resources/js/Components/InputError.vue +++ b/resources/js/Components/InputError.vue @@ -1,3 +1,9 @@ + + - - diff --git a/resources/js/Components/Jetstream/ActionSection.vue b/resources/js/Components/Jetstream/ActionSection.vue index 8e17a950e4e..16d6c42b359 100644 --- a/resources/js/Components/Jetstream/ActionSection.vue +++ b/resources/js/Components/Jetstream/ActionSection.vue @@ -1,6 +1,7 @@ diff --git a/resources/js/Pages/Webauthn/Partials/UpdateKey.vue b/resources/js/Pages/Webauthn/Partials/UpdateKey.vue new file mode 100644 index 00000000000..e63a5f8dd08 --- /dev/null +++ b/resources/js/Pages/Webauthn/Partials/UpdateKey.vue @@ -0,0 +1,72 @@ + + + diff --git a/resources/js/Pages/Webauthn/Partials/WaitForKey.vue b/resources/js/Pages/Webauthn/Partials/WaitForKey.vue new file mode 100644 index 00000000000..d28f2c5f371 --- /dev/null +++ b/resources/js/Pages/Webauthn/Partials/WaitForKey.vue @@ -0,0 +1,82 @@ + + + diff --git a/resources/js/Pages/Webauthn/WebauthnKeys.vue b/resources/js/Pages/Webauthn/WebauthnKeys.vue new file mode 100644 index 00000000000..b460dc19256 --- /dev/null +++ b/resources/js/Pages/Webauthn/WebauthnKeys.vue @@ -0,0 +1,215 @@ + + + diff --git a/resources/js/Pages/Webauthn/WebauthnLogin.vue b/resources/js/Pages/Webauthn/WebauthnLogin.vue new file mode 100644 index 00000000000..b4cd22fb28a --- /dev/null +++ b/resources/js/Pages/Webauthn/WebauthnLogin.vue @@ -0,0 +1,122 @@ + + + diff --git a/resources/js/Shared/Form/Dropdown.vue b/resources/js/Shared/Form/Dropdown.vue index 618e135d24a..bc255596a8c 100644 --- a/resources/js/Shared/Form/Dropdown.vue +++ b/resources/js/Shared/Form/Dropdown.vue @@ -34,10 +34,8 @@ export default { default: 'dropdown-', }, data: { - type: Array, - default() { - return []; - }, + type: Object, + default: null, }, dropdownClass: { type: String, diff --git a/resources/js/Shared/Form/PrettyLink.vue b/resources/js/Shared/Form/PrettyLink.vue index 85062d060f6..092c2289e28 100644 --- a/resources/js/Shared/Form/PrettyLink.vue +++ b/resources/js/Shared/Form/PrettyLink.vue @@ -1,8 +1,8 @@ - - diff --git a/resources/js/Shared/Layout.vue b/resources/js/Shared/Layout.vue index 859cbb71b34..ce37a42c325 100644 --- a/resources/js/Shared/Layout.vue +++ b/resources/js/Shared/Layout.vue @@ -1,299 +1,300 @@