Add OAuth backend (#386)

* add socialite backend

* fix redirect url

* small cleanup

* fix "oauth" type

* changes from review
This commit is contained in:
Boy132 2024-06-13 21:06:31 +02:00 committed by GitHub
parent 6916b89638
commit 5a3c606627
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 505 additions and 3 deletions

View File

@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\RedirectResponse;
use Laravel\Socialite\Facades\Socialite;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\Users\UserUpdateService;
use Exception;
use Illuminate\Http\Request;
class OAuthController extends Controller
{
/**
* OAuthController constructor.
*/
public function __construct(
private AuthManager $auth,
private UserUpdateService $updateService
) {
}
/**
* Redirect user to the OAuth provider
*/
protected function redirect(string $driver): RedirectResponse
{
return Socialite::with($driver)->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('/');
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Base;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Laravel\Socialite\Facades\Socialite;
use App\Http\Controllers\Controller;
use App\Services\Users\UserUpdateService;
use Illuminate\Http\Response;
class OAuthController extends Controller
{
/**
* OAuthController constructor.
*/
public function __construct(
private UserUpdateService $updateService
) {
}
/**
* Link a new OAuth
*/
protected function link(Request $request): RedirectResponse
{
$driver = $request->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);
}
}

View File

@ -42,6 +42,7 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
* @property bool $use_totp * @property bool $use_totp
* @property string|null $totp_secret * @property string|null $totp_secret
* @property \Illuminate\Support\Carbon|null $totp_authenticated_at * @property \Illuminate\Support\Carbon|null $totp_authenticated_at
* @property array $oauth
* @property bool $gravatar * @property bool $gravatar
* @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $updated_at
@ -127,12 +128,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'totp_authenticated_at', 'totp_authenticated_at',
'gravatar', 'gravatar',
'root_admin', 'root_admin',
'oauth',
]; ];
/** /**
* The attributes excluded from the model's JSON form. * 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. * Default values for specific fields in the database.
@ -145,6 +147,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'totp_secret' => null, 'totp_secret' => null,
'name_first' => '', 'name_first' => '',
'name_last' => '', 'name_last' => '',
'oauth' => '[]',
]; ];
/** /**
@ -162,6 +165,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'language' => 'string', 'language' => 'string',
'use_totp' => 'boolean', 'use_totp' => 'boolean',
'totp_secret' => 'nullable|string', 'totp_secret' => 'nullable|string',
'oauth' => 'array',
]; ];
protected function casts(): array protected function casts(): array
@ -172,6 +176,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'gravatar' => 'boolean', 'gravatar' => 'boolean',
'totp_authenticated_at' => 'datetime', 'totp_authenticated_at' => 'datetime',
'totp_secret' => 'encrypted', 'totp_secret' => 'encrypted',
'oauth' => 'array',
]; ];
} }

View File

@ -13,6 +13,7 @@ use Dedoc\Scramble\Support\Generator\SecurityScheme;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\Paginator; use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema; 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('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']]);
Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens); 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); 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);
});
} }
/** /**

View File

@ -8,4 +8,6 @@ return [
App\Providers\Filament\AdminPanelProvider::class, App\Providers\Filament\AdminPanelProvider::class,
App\Providers\RouteServiceProvider::class, App\Providers\RouteServiceProvider::class,
App\Providers\ViewComposerServiceProvider::class, App\Providers\ViewComposerServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class,
]; ];

View File

@ -20,6 +20,7 @@
"laravel/framework": "^11.7", "laravel/framework": "^11.7",
"laravel/helpers": "^1.7", "laravel/helpers": "^1.7",
"laravel/sanctum": "^4.0.2", "laravel/sanctum": "^4.0.2",
"laravel/socialite": "^5.14",
"laravel/tinker": "^2.9", "laravel/tinker": "^2.9",
"laravel/ui": "^4.5.1", "laravel/ui": "^4.5.1",
"lcobucci/jwt": "~4.3.0", "lcobucci/jwt": "~4.3.0",
@ -31,6 +32,7 @@
"prologue/alerts": "^1.2", "prologue/alerts": "^1.2",
"ryangjchandler/blade-tabler-icons": "^2.3", "ryangjchandler/blade-tabler-icons": "^2.3",
"s1lentium/iptools": "~1.2.0", "s1lentium/iptools": "~1.2.0",
"socialiteproviders/discord": "^4.2",
"spatie/laravel-fractal": "^6.2", "spatie/laravel-fractal": "^6.2",
"spatie/laravel-query-builder": "^5.8.1", "spatie/laravel-query-builder": "^5.8.1",
"spatie/temporary-directory": "^2.2", "spatie/temporary-directory": "^2.2",

339
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "bf44faee3aae2b1d4c1b57893c1aba98", "content-hash": "443ec1d95b892b261af5481f27b31083",
"packages": [ "packages": [
{ {
"name": "abdelhamiderrahmouni/filament-monaco-editor", "name": "abdelhamiderrahmouni/filament-monaco-editor",
@ -2069,6 +2069,69 @@
}, },
"time": "2024-06-05T09:38:52+00:00" "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", "name": "fruitcake/php-cors",
"version": "v1.3.0", "version": "v1.3.0",
@ -3180,6 +3243,78 @@
}, },
"time": "2023-11-08T14:08:06+00:00" "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", "name": "laravel/tinker",
"version": "v2.9.0", "version": "v2.9.0",
@ -4114,6 +4249,82 @@
], ],
"time": "2024-01-28T23:22:08+00:00" "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", "name": "league/uri",
"version": "7.4.1", "version": "7.4.1",
@ -6579,6 +6790,130 @@
}, },
"time": "2022-08-17T14:28:59+00:00" "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", "name": "spatie/color",
"version": "1.5.3", "version": "1.5.3",
@ -13141,5 +13476,5 @@
"ext-zip": "*" "ext-zip": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }

View File

@ -9,4 +9,16 @@ return [
'scheme' => 'https', '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',
],
]; ];

View File

@ -35,6 +35,7 @@ class UserFactory extends Factory
'language' => 'en', 'language' => 'en',
'root_admin' => false, 'root_admin' => false,
'use_totp' => false, 'use_totp' => false,
'oauth' => [],
'created_at' => Carbon::now(), 'created_at' => Carbon::now(),
'updated_at' => Carbon::now(), 'updated_at' => Carbon::now(),
]; ];

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->json('oauth')->after('totp_authenticated_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('oauth');
});
}
};

View File

@ -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', [Auth\LoginController::class, 'index'])->name('auth.forgot-password');
Route::get('/password/reset/{token}', [Auth\LoginController::class, 'index'])->name('auth.reset'); 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 // Apply a throttle to authentication action endpoints, in addition to the
// recaptcha endpoints to slow down manual attack spammers even more. 🤷‍ // recaptcha endpoints to slow down manual attack spammers even more. 🤷‍
// //

View File

@ -9,6 +9,9 @@ Route::get('/account', [Base\IndexController::class, 'index'])
->withoutMiddleware(RequireTwoFactorAuthentication::class) ->withoutMiddleware(RequireTwoFactorAuthentication::class)
->name('account'); ->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) Route::get('/locales/locale.json', Base\LocaleController::class)
->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class]) ->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class])
->where('namespace', '.*'); ->where('namespace', '.*');