diff --git a/app/Http/Controllers/RemoteAuthController.php b/app/Http/Controllers/RemoteAuthController.php index 73d4c5592..5f559761a 100644 --- a/app/Http/Controllers/RemoteAuthController.php +++ b/app/Http/Controllers/RemoteAuthController.php @@ -14,6 +14,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; +use App\Rules\PixelfedUsername; use InvalidArgumentException; use Purify; @@ -359,37 +360,7 @@ class RemoteAuthController extends Controller 'required', 'min:2', 'max:30', - function ($attribute, $value, $fail) { - $dash = substr_count($value, '-'); - $underscore = substr_count($value, '_'); - $period = substr_count($value, '.'); - - if (ends_with($value, ['.php', '.js', '.css'])) { - return $fail('Username is invalid.'); - } - - if (($dash + $underscore + $period) > 1) { - return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); - } - - if (! ctype_alnum($value[0])) { - return $fail('Username is invalid. Must start with a letter or number.'); - } - - if (! ctype_alnum($value[strlen($value) - 1])) { - return $fail('Username is invalid. Must end with a letter or number.'); - } - - $val = str_replace(['_', '.', '-'], '', $value); - if (! ctype_alnum($val)) { - return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); - } - - $restricted = RestrictedNames::get(); - if (in_array(strtolower($value), array_map('strtolower', $restricted))) { - return $fail('Username cannot be used.'); - } - }, + new PixelfedUsername(), ], ]); $username = strtolower($request->input('username')); diff --git a/app/Http/Controllers/RemoteOidcController.php b/app/Http/Controllers/RemoteOidcController.php new file mode 100644 index 000000000..e04fcfc9e --- /dev/null +++ b/app/Http/Controllers/RemoteOidcController.php @@ -0,0 +1,121 @@ +user()) { + return redirect('/'); + } + + $url = $provider->getAuthorizationUrl([ + 'scope' => $provider->getDefaultScopes(), + ]); + + $request->session()->put('oauth2state', $provider->getState()); + + return redirect($url); + } + + public function handleCallback(UserOidcService $provider, Request $request) + { + abort_unless(config('remote-auth.oidc.enabled'), 404); + + if ($request->user()) { + return redirect('/'); + } + + abort_unless($request->input("state"), 400); + abort_unless($request->input("code"), 400); + + abort_unless(hash_equals($request->session()->pull('oauth2state'), $request->input("state")), 400, "invalid state"); + + $accessToken = $provider->getAccessToken('authorization_code', [ + 'code' => $request->get('code') + ]); + + $userInfo = $provider->getResourceOwner($accessToken); + $userInfoId = $userInfo->getId(); + $userInfoData = $userInfo->toArray(); + + $mappedUser = UserOidcMapping::where('oidc_id', $userInfoId)->first(); + if ($mappedUser) { + $this->guarder()->login($mappedUser->user); + return redirect('/'); + } + + abort_if(EmailService::isBanned($userInfoData["email"]), 400, 'Banned email.'); + + $user = $this->createUser([ + 'username' => $userInfoData[config('remote-auth.oidc.field_username')], + 'name' => $userInfoData["name"] ?? $userInfoData["display_name"] ?? $userInfoData[config('remote-auth.oidc.field_username')] ?? null, + 'email' => $userInfoData["email"], + ]); + + UserOidcMapping::create([ + 'user_id' => $user->id, + 'oidc_id' => $userInfoId, + ]); + + return redirect('/'); + } + + protected function createUser($data) + { + $this->validate(new Request($data), [ + 'email' => [ + 'required', + 'string', + 'email:strict,filter_unicode,dns,spoof', + 'max:255', + 'unique:users', + new EmailNotBanned(), + ], + 'username' => [ + 'required', + 'min:2', + 'max:30', + 'unique:users,username', + new PixelfedUsername(), + ], + 'name' => 'nullable|max:30', + ]); + + event(new Registered($user = User::create([ + 'name' => Purify::clean($data['name']), + 'username' => $data['username'], + 'email' => $data['email'], + 'password' => Hash::make(Str::password()), + 'email_verified_at' => now(), + 'app_register_ip' => request()->ip(), + 'register_source' => 'oidc', + ]))); + + $this->guarder()->login($user); + + return $user; + } + + protected function guarder() + { + return Auth::guard(); + } +} diff --git a/app/Models/UserOidcMapping.php b/app/Models/UserOidcMapping.php new file mode 100644 index 000000000..8932ff617 --- /dev/null +++ b/app/Models/UserOidcMapping.php @@ -0,0 +1,25 @@ +belongsTo(User::class); + } + +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 9c5fdb7fa..c9eeba627 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -21,6 +21,7 @@ use App\Observers\UserFilterObserver; use App\Observers\UserObserver; use App\Profile; use App\Services\AccountService; +use App\Services\UserOidcService; use App\Status; use App\StatusHashtag; use App\User; @@ -112,6 +113,8 @@ class AppServiceProvider extends ServiceProvider */ public function register() { - // + $this->app->bind(UserOidcService::class, function() { + return UserOidcService::build(); + }); } } diff --git a/app/Rules/EmailNotBanned.php b/app/Rules/EmailNotBanned.php new file mode 100644 index 000000000..3d637ada8 --- /dev/null +++ b/app/Rules/EmailNotBanned.php @@ -0,0 +1,25 @@ + 1) { + $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); + return; + } + + if (! ctype_alnum($value[0])) { + $fail('Username is invalid. Must start with a letter or number.'); + return; + } + + if (! ctype_alnum($value[strlen($value) - 1])) { + $fail('Username is invalid. Must end with a letter or number.'); + return; + } + + $val = str_replace(['_', '.', '-'], '', $value); + if (! ctype_alnum($val)) { + $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); + return; + } + + $restricted = RestrictedNames::get(); + if (in_array(strtolower($value), array_map('strtolower', $restricted))) { + $fail('Username cannot be used.'); + return; + } + } +} diff --git a/app/Services/UserOidcService.php b/app/Services/UserOidcService.php new file mode 100644 index 000000000..c89975935 --- /dev/null +++ b/app/Services/UserOidcService.php @@ -0,0 +1,21 @@ + config('remote-auth.oidc.clientId'), + 'clientSecret' => config('remote-auth.oidc.clientSecret'), + 'redirectUri' => url('auth/oidc/callback'), + 'urlAuthorize' => config('remote-auth.oidc.authorizeURL'), + 'urlAccessToken' => config('remote-auth.oidc.tokenURL'), + 'urlResourceOwnerDetails' => config('remote-auth.oidc.profileURL'), + 'scopes' => config('remote-auth.oidc.scopes'), + 'responseResourceOwnerId' => config('remote-auth.oidc.field_id'), + ]); + } +} diff --git a/composer.json b/composer.json index b9a11b462..07f7a61e2 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "laravel/ui": "^4.2", "league/flysystem-aws-s3-v3": "^3.0", "league/iso3166": "^2.1|^4.0", + "league/oauth2-client": "^2.8", "league/uri": "^7.4", "pbmedia/laravel-ffmpeg": "^8.0", "phpseclib/phpseclib": "~2.0", diff --git a/composer.lock b/composer.lock index 19440079d..098e0d7d5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a011d3030ab0153865ef4cd6a7b615a3", + "content-hash": "ac363dfc5037ce5d118b7b4a8e75bffe", "packages": [ { "name": "aws/aws-crt-php", @@ -3872,6 +3872,71 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "league/oauth2-client", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.5.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1" + }, + "time": "2025-02-26T04:37:30+00:00" + }, { "name": "league/oauth2-server", "version": "8.5.5", @@ -12680,7 +12745,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -12693,6 +12758,6 @@ "ext-mbstring": "*", "ext-openssl": "*" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/config/remote-auth.php b/config/remote-auth.php index 182bb99a7..c421929fc 100644 --- a/config/remote-auth.php +++ b/config/remote-auth.php @@ -54,4 +54,16 @@ return [ 'limit' => env('PF_LOGIN_WITH_MASTODON_MAX_USES_LIMIT', 3) ] ], + 'oidc' => [ + 'enabled' => env('PF_OIDC_ENABLED', false), + 'clientId' => env('PF_OIDC_CLIENT_ID', false), + 'clientSecret' => env('PF_OIDC_CLIENT_SECRET', false), + 'scopes' => env('PF_OIDC_SCOPES', 'openid profile email'), + 'authorizeURL' => env('PF_OIDC_AUTHORIZE_URL', ''), + 'tokenURL' => env('PF_OIDC_TOKEN_URL', ''), + 'profileURL' => env('PF_OIDC_PROFILE_URL', ''), + 'logoutURL' => env('PF_OIDC_LOGOUT_URL', ''), + 'field_username' => env('PF_OIDC_USERNAME_FIELD', "preferred_username"), + 'field_id' => env('PF_OIDC_FIELD_ID', 'sub'), + ], ]; diff --git a/database/migrations/2025_01_30_061434_create_user_oidc_mapping_table.php b/database/migrations/2025_01_30_061434_create_user_oidc_mapping_table.php new file mode 100644 index 000000000..0986d9aa9 --- /dev/null +++ b/database/migrations/2025_01_30_061434_create_user_oidc_mapping_table.php @@ -0,0 +1,30 @@ +bigIncrements('id'); + $table->bigInteger('user_id')->unsigned()->index(); + $table->string('oidc_id')->unique()->index(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_oidc_mappings'); + } +}; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 0f77f778e..fce2344dc 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -111,6 +111,17 @@ @endif + @if( config('remote-auth.oidc.enabled') ) +