diff --git a/app/Http/Controllers/Auth/OAuthController.php b/app/Http/Controllers/Auth/OAuthController.php new file mode 100644 index 000000000..ed24b0f23 --- /dev/null +++ b/app/Http/Controllers/Auth/OAuthController.php @@ -0,0 +1,61 @@ +redirect(); + } + + /** + * Callback from OAuth provider. + */ + protected function callback(Request $request, string $driver): RedirectResponse + { + $oauthUser = Socialite::driver($driver)->user(); + + // User is already logged in and wants to link a new OAuth Provider + if ($request->user()) { + $oauth = $request->user()->oauth; + $oauth[$driver] = $oauthUser->getId(); + + $this->updateService->handle($request->user(), ['oauth' => json_encode($oauth)]); + + return redirect()->route('account'); + } + + try { + $user = User::query()->whereJsonContains('oauth->'. $driver, $oauthUser->getId())->firstOrFail(); + + $this->auth->guard()->login($user, true); + } catch (Exception $e) { + // No user found - redirect to normal login + return redirect()->route('auth.login'); + } + + return redirect('/'); + } +} diff --git a/app/Http/Controllers/Base/OauthController.php b/app/Http/Controllers/Base/OauthController.php new file mode 100644 index 000000000..8b2632b6e --- /dev/null +++ b/app/Http/Controllers/Base/OauthController.php @@ -0,0 +1,44 @@ +get('driver'); + + return Socialite::with($driver)->redirect(); + } + + /** + * Remove a OAuth link + */ + protected function unlink(Request $request): Response + { + $oauth = $request->user()->oauth; + unset($oauth[$request->get('driver')]); + + $this->updateService->handle($request->user(), ['oauth' => json_encode($oauth)]); + + return new Response('', Response::HTTP_NO_CONTENT); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index f089aba07..ef9084d82 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -42,6 +42,7 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification; * @property bool $use_totp * @property string|null $totp_secret * @property \Illuminate\Support\Carbon|null $totp_authenticated_at + * @property array $oauth * @property bool $gravatar * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at @@ -127,12 +128,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'totp_authenticated_at', 'gravatar', 'root_admin', + 'oauth', ]; /** * The attributes excluded from the model's JSON form. */ - protected $hidden = ['password', 'remember_token', 'totp_secret', 'totp_authenticated_at']; + protected $hidden = ['password', 'remember_token', 'totp_secret', 'totp_authenticated_at', 'oauth']; /** * Default values for specific fields in the database. @@ -145,6 +147,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'totp_secret' => null, 'name_first' => '', 'name_last' => '', + 'oauth' => '[]', ]; /** @@ -162,6 +165,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'language' => 'string', 'use_totp' => 'boolean', 'totp_secret' => 'nullable|string', + 'oauth' => 'array', ]; protected function casts(): array @@ -172,6 +176,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'gravatar' => 'boolean', 'totp_authenticated_at' => 'datetime', 'totp_secret' => 'encrypted', + 'oauth' => 'array', ]; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 5817890b1..c2473e87d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -13,6 +13,7 @@ use Dedoc\Scramble\Support\Generator\SecurityScheme; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Broadcast; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Schema; @@ -78,6 +79,10 @@ class AppServiceProvider extends ServiceProvider Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']]); Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens); Scramble::registerApi('remote', ['api_path' => 'api/remote', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens); + + Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) { + $event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class); + }); } /** diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 80be25c8e..8c37bb7b5 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -8,4 +8,6 @@ return [ App\Providers\Filament\AdminPanelProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\ViewComposerServiceProvider::class, + + SocialiteProviders\Manager\ServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 481fa145d..aaa3e9bc5 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "laravel/framework": "^11.7", "laravel/helpers": "^1.7", "laravel/sanctum": "^4.0.2", + "laravel/socialite": "^5.14", "laravel/tinker": "^2.9", "laravel/ui": "^4.5.1", "lcobucci/jwt": "~4.3.0", @@ -31,6 +32,7 @@ "prologue/alerts": "^1.2", "ryangjchandler/blade-tabler-icons": "^2.3", "s1lentium/iptools": "~1.2.0", + "socialiteproviders/discord": "^4.2", "spatie/laravel-fractal": "^6.2", "spatie/laravel-query-builder": "^5.8.1", "spatie/temporary-directory": "^2.2", diff --git a/composer.lock b/composer.lock index ba784fff9..22543ba07 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": "bf44faee3aae2b1d4c1b57893c1aba98", + "content-hash": "443ec1d95b892b261af5481f27b31083", "packages": [ { "name": "abdelhamiderrahmouni/filament-monaco-editor", @@ -2069,6 +2069,69 @@ }, "time": "2024-06-05T09:38:52+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v6.10.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "500501c2ce893c824c801da135d02661199f60c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", + "reference": "500501c2ce893c824c801da135d02661199f60c5", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" + }, + "time": "2024-05-18T18:05:11+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -3180,6 +3243,78 @@ }, "time": "2023-11-08T14:08:06+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.14.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/c7b0193a3753a29aff8ce80aa2f511917e6ed68a", + "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "league/oauth1-client": "^1.10.1", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.0|^9.3|^10.4" + }, + "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": "2024-05-03T20:31:38+00:00" + }, { "name": "laravel/tinker", "version": "v2.9.0", @@ -4114,6 +4249,82 @@ ], "time": "2024-01-28T23:22:08+00:00" }, + { + "name": "league/oauth1-client", + "version": "v1.10.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "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": { + "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": "league/uri", "version": "7.4.1", @@ -6579,6 +6790,130 @@ }, "time": "2022-08-17T14:28:59+00:00" }, + { + "name": "socialiteproviders/discord", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Discord.git", + "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/c71c379acfdca5ba4aa65a3db5ae5222852a919c", + "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "socialiteproviders/manager": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Discord\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Eklund", + "email": "eklundchristopher@gmail.com" + } + ], + "description": "Discord OAuth2 Provider for Laravel Socialite", + "keywords": [ + "discord", + "laravel", + "oauth", + "provider", + "socialite" + ], + "support": { + "docs": "https://socialiteproviders.com/discord", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2023-07-24T23:28:47+00:00" + }, + { + "name": "socialiteproviders/manager", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Manager.git", + "reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/dea5190981c31b89e52259da9ab1ca4e2b258b21", + "reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21", + "shasum": "" + }, + "require": { + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "laravel/socialite": "^5.5", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "phpunit/phpunit": "^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": "2024-05-04T07:57:39+00:00" + }, { "name": "spatie/color", "version": "1.5.3", @@ -13141,5 +13476,5 @@ "ext-zip": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/services.php b/config/services.php index 62e0a08a8..7311fdc16 100644 --- a/config/services.php +++ b/config/services.php @@ -9,4 +9,16 @@ return [ 'scheme' => 'https', ], + 'github' => [ + 'client_id' => env('OAUTH_GITHUB_CLIENT_ID'), + 'client_secret' => env('OAUTH_GITHUB_CLIENT_SECRET'), + 'redirect' => '/auth/oauth/callback/github', + ], + + 'discord' => [ + 'client_id' => env('OAUTH_DISCORD_CLIENT_ID'), + 'client_secret' => env('OAUTH_DISCORD_CLIENT_SECRET'), + 'redirect' => '/auth/oauth/callback/discord', + ], + ]; diff --git a/database/Factories/UserFactory.php b/database/Factories/UserFactory.php index 74d2cdbb4..510edaef1 100644 --- a/database/Factories/UserFactory.php +++ b/database/Factories/UserFactory.php @@ -35,6 +35,7 @@ class UserFactory extends Factory 'language' => 'en', 'root_admin' => false, 'use_totp' => false, + 'oauth' => [], 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; diff --git a/database/migrations/2024_06_13_120409_add_oauth_column_to_users.php b/database/migrations/2024_06_13_120409_add_oauth_column_to_users.php new file mode 100644 index 000000000..22273db94 --- /dev/null +++ b/database/migrations/2024_06_13_120409_add_oauth_column_to_users.php @@ -0,0 +1,28 @@ +json('oauth')->after('totp_authenticated_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('oauth'); + }); + } +}; diff --git a/routes/auth.php b/routes/auth.php index 517afb742..7c572c91b 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -18,6 +18,10 @@ Route::get('/login', [Auth\LoginController::class, 'index'])->name('auth.login') Route::get('/password', [Auth\LoginController::class, 'index'])->name('auth.forgot-password'); Route::get('/password/reset/{token}', [Auth\LoginController::class, 'index'])->name('auth.reset'); +// Endpoints for OAuth +Route::get('/oauth/redirect/{driver}', [Auth\OAuthController::class, 'redirect'])->name('auth.oauth.redirect'); +Route::get('/oauth/callback/{driver}', [Auth\OAuthController::class, 'callback'])->name('auth.oauth.callback'); + // Apply a throttle to authentication action endpoints, in addition to the // recaptcha endpoints to slow down manual attack spammers even more. 🤷‍ // diff --git a/routes/base.php b/routes/base.php index d1dbf7fb4..6fbc41ae8 100644 --- a/routes/base.php +++ b/routes/base.php @@ -9,6 +9,9 @@ Route::get('/account', [Base\IndexController::class, 'index']) ->withoutMiddleware(RequireTwoFactorAuthentication::class) ->name('account'); +Route::get('/account/oauth/link', [Base\OAuthController::class, 'link'])->name('account.oauth.link'); +Route::get('/account/oauth/unlink', [Base\OAuthController::class, 'unlink'])->name('account.oauth.unlink'); + Route::get('/locales/locale.json', Base\LocaleController::class) ->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class]) ->where('namespace', '.*');