From 446432bb5c2cb38e6c642a4f7b1be33966d63b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Viguier?= Date: Tue, 25 Jun 2024 17:34:12 +0200 Subject: [PATCH] Add ability to create user on the fly on Oauth auth step (#2475) --- app/Actions/User/Create.php | 3 +- .../Commands/UserManagment/CreateUser.php | 6 +- .../Administration/UsersController.php | 6 +- app/Http/Controllers/Oauth.php | 97 ++++++++++++++++--- app/Livewire/Components/Pages/Users.php | 8 +- ...247_create_user_if_not_exists_on_oauth.php | 37 +++++++ phpstan.neon | 1 + 7 files changed, 139 insertions(+), 19 deletions(-) create mode 100644 database/migrations/2024_06_21_154247_create_user_if_not_exists_on_oauth.php diff --git a/app/Actions/User/Create.php b/app/Actions/User/Create.php index 23272e870d..555cc3225a 100644 --- a/app/Actions/User/Create.php +++ b/app/Actions/User/Create.php @@ -14,7 +14,7 @@ class Create * @throws InvalidPropertyException * @throws ModelDBException */ - public function do(string $username, string $password, bool $mayUpload, bool $mayEditOwnSettings): User + public function do(string $username, string $password, ?string $email = null, bool $mayUpload = false, bool $mayEditOwnSettings = false): User { if (User::query()->where('username', '=', $username)->count() !== 0) { throw new ConflictingPropertyException('Username already exists'); @@ -24,6 +24,7 @@ public function do(string $username, string $password, bool $mayUpload, bool $ma $user->may_edit_own_settings = $mayEditOwnSettings; $user->may_administrate = false; $user->username = $username; + $user->email = $email; $user->password = Hash::make($password); $user->save(); diff --git a/app/Console/Commands/UserManagment/CreateUser.php b/app/Console/Commands/UserManagment/CreateUser.php index 59e443d617..7d275eccf8 100644 --- a/app/Console/Commands/UserManagment/CreateUser.php +++ b/app/Console/Commands/UserManagment/CreateUser.php @@ -58,7 +58,11 @@ public function handle(): int $mayEditOwnSettings = $mayAdministrate || $this->option('may-edit-own-settings') === true; $mayUpload = $mayAdministrate || $this->option('may-upload') === true; - $user = $this->create->do($username, $password, $mayUpload, $mayEditOwnSettings); + $user = $this->create->do( + username: $username, + password: $password, + mayUpload: $mayUpload, + mayEditOwnSettings: $mayEditOwnSettings); $user->may_administrate = $mayAdministrate; $user->save(); diff --git a/app/Http/Controllers/Administration/UsersController.php b/app/Http/Controllers/Administration/UsersController.php index f27418029b..06a5d20bb6 100644 --- a/app/Http/Controllers/Administration/UsersController.php +++ b/app/Http/Controllers/Administration/UsersController.php @@ -90,7 +90,11 @@ public function delete(DeleteUserRequest $request): void */ public function create(AddUserRequest $request, Create $create): UserManagementResource { - $user = $create->do($request->username(), $request->password(), $request->mayUpload(), $request->mayEditOwnSettings()); + $user = $create->do( + username: $request->username(), + password: $request->password(), + mayUpload: $request->mayUpload(), + mayEditOwnSettings: $request->mayEditOwnSettings()); return UserManagementResource::make($user)->setStatus(201); } diff --git a/app/Http/Controllers/Oauth.php b/app/Http/Controllers/Oauth.php index 0c90affe53..6e8c5ce12a 100644 --- a/app/Http/Controllers/Oauth.php +++ b/app/Http/Controllers/Oauth.php @@ -2,11 +2,13 @@ namespace App\Http\Controllers; +use App\Actions\User\Create; use App\Enum\OauthProvidersType; use App\Exceptions\Internal\LycheeInvalidArgumentException; use App\Exceptions\Internal\LycheeLogicException; use App\Exceptions\UnauthenticatedException; use App\Exceptions\UnauthorizedException; +use App\Models\Configs; use App\Models\OauthCredential; use App\Models\User; use Illuminate\Http\RedirectResponse; @@ -15,6 +17,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Session; +use Laravel\Socialite\Contracts\User as ContractsUser; use Laravel\Socialite\Facades\Socialite; use Symfony\Component\HttpFoundation\RedirectResponse as HttpFoundationRedirectResponse; @@ -109,23 +112,76 @@ public function register(string $provider) */ private function authenticateOrDie(OauthProvidersType $provider) { - $user = Socialite::driver($provider->value)->user(); + /** @var ContractsUser */ + $user = $this->getUserFromOauth($provider); - $credential = OauthCredential::query() - ->with(['user']) - ->where('token_id', '=', $user->getId()) - ->where('provider', '=', $provider) - ->first(); + $credential = $this->fetchAssociatedUserFromDB($provider, $user->getId()); + + if ($credential !== null) { + Auth::login($credential->user); - if ($credential === null) { + return redirect(route('livewire-gallery')); + } + + if (!Configs::getValueAsBool('oauth_create_user_on_first_attempt')) { throw new UnauthorizedException('User not found!'); } - Auth::login($credential->user); + if (User::query()->where('username', '=', $user->getName() ?? $user->getEmail() ?? $user->getId()) + ->when( + $user->getEmail() !== null && $user->getEmail() !== '', + fn ($q) => $q->orWhere('email', '=', $user->getEmail()) + )->exists()) { + throw new UnauthorizedException('User already exists!'); + } + + $create = resolve(Create::class); + $new_user = $create->do( + username: $user->getName() ?? $user->getEmail() ?? $user->getId(), + email: $user->getEmail(), + password: strtr(base64_encode(random_bytes(8)), '+/', '-_'), + mayUpload: Configs::getValueAsBool('oauth_grant_new_user_upload_rights'), + mayEditOwnSettings: Configs::getValueAsBool('oauth_grant_new_user_modification_rights')); + + Auth::login($new_user); + + $this->saveOauth( + provider: $provider, + authedUser_id: $new_user->id, + oauth_id: $user->getId()); return redirect(route('livewire-gallery')); } + /** + * Get the user from the driver. + * + * @param OauthProvidersType $provider + * + * @return ContractsUser + */ + private function getUserFromOauth(OauthProvidersType $provider): ContractsUser + { + return Socialite::driver($provider->value)->user(); + } + + /** + * Fetch the Oauth credential and user associated. + * + * @param OauthProvidersType $provider Oauth provider + * @param string $user_id to fetch with + * + * @return OauthCredential|null credential if found + */ + private function fetchAssociatedUserFromDB(OauthProvidersType $provider, string $user_id): OauthCredential|null + { + return OauthCredential::query() + ->with(['user']) + ->where('token_id', '=', $user_id) + ->where('provider', '=', $provider) + ->first(); + } + /** * Authenticate and redirect. * @@ -152,13 +208,30 @@ private function registerOrDie(OauthProvidersType $provider) throw new LycheeLogicException('Oauth credential for that provider already exists.'); } + $this->saveOauth( + provider: $provider, + authedUser_id: $authedUser->id, + oauth_id: $user->getId()); + + return redirect(route('profile')); + } + + /** + * Save a credential for a user. + * + * @param OauthProvidersType $provider of credential + * @param int $authedUser_id user ID already existing in the database + * @param string $oauth_id oauth id on the Oauth server side + * + * @return void + */ + private function saveOauth(OauthProvidersType $provider, int $authedUser_id, string $oauth_id): void + { $credential = OauthCredential::create([ 'provider' => $provider, - 'user_id' => $authedUser->id, - 'token_id' => $user->getId(), + 'user_id' => $authedUser_id, + 'token_id' => $oauth_id, ]); $credential->save(); - - return redirect(route('profile')); } } \ No newline at end of file diff --git a/app/Livewire/Components/Pages/Users.php b/app/Livewire/Components/Pages/Users.php index 90567acfba..d825832b7e 100644 --- a/app/Livewire/Components/Pages/Users.php +++ b/app/Livewire/Components/Pages/Users.php @@ -81,10 +81,10 @@ public function create(): void // Create user $this->create->do( - $this->username, - $this->password, - $this->may_upload, - $this->may_edit_own_settings); + username: $this->username, + password: $this->password, + mayUpload: $this->may_upload, + mayEditOwnSettings: $this->may_edit_own_settings); // reset attributes and reload user list (triggers refresh) $this->username = ''; diff --git a/database/migrations/2024_06_21_154247_create_user_if_not_exists_on_oauth.php b/database/migrations/2024_06_21_154247_create_user_if_not_exists_on_oauth.php new file mode 100644 index 0000000000..5deb815542 --- /dev/null +++ b/database/migrations/2024_06_21_154247_create_user_if_not_exists_on_oauth.php @@ -0,0 +1,37 @@ + 'oauth_create_user_on_first_attempt', + 'value' => '0', + 'is_secret' => true, + 'cat' => self::OAUTH, + 'type_range' => '0|1', + 'description' => 'Allow user creation when oauth id does not exist.', + ], + [ + 'key' => 'oauth_grant_new_user_upload_rights', + 'value' => '0', + 'is_secret' => true, + 'cat' => self::OAUTH, + 'type_range' => '0|1', + 'description' => 'Newly created user are allowed to upload content.', + ], + [ + 'key' => 'oauth_grant_new_user_modification_rights', + 'value' => '0', + 'is_secret' => true, + 'cat' => self::OAUTH, + 'type_range' => '0|1', + 'description' => 'Newly created user are allowed to edit their profile.', + ], + ]; + } +}; \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index 5256b55166..72592b60c1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -47,6 +47,7 @@ parameters: - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::latest\(\).#' - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::first\(\).#' - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::skip\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::exists\(\).#' - '#Dynamic call to static method App\\Models\\Builders\\.*::orderByDesc\(\).#' - '#Dynamic call to static method App\\Models\\Builders\\.*::selectRaw\(\).#' - '#Call to an undefined method Illuminate\\Database\\Eloquent\\.*::with(Only)?\(\)#'