diff --git a/.env.example b/.env.example index 01fa345c..efdc0755 100644 --- a/.env.example +++ b/.env.example @@ -221,6 +221,14 @@ WEBAUTHN_ID=null WEBAUTHN_USER_VERIFICATION=preferred +### OpenID settings ### + +# OPENID_AUTHORIZE_URL= +# OPENID_TOKEN_URL= +# OPENID_USERINFO_URL= +# OPENID_CLIENT_ID= +# OPENID_CLIENT_SECRET= + # Use this setting to declare trusted proxied. # Supported: diff --git a/app/Http/Controllers/Auth/SocialiteController.php b/app/Http/Controllers/Auth/SocialiteController.php new file mode 100644 index 00000000..1600f978 --- /dev/null +++ b/app/Http/Controllers/Auth/SocialiteController.php @@ -0,0 +1,44 @@ +redirect(); + } + + public function callback(Request $request, $driver) + { + $socialiteUser = Socialite::driver($driver)->user(); + + /** @var User $user */ + $user = User::firstOrNew([ + 'email' => $socialiteUser->getEmail(), + ], [ + 'name' => $socialiteUser->getName(), + 'password' => bcrypt(Str::random()), + ]); + + if (!$user->exists && Settings::get('disableRegistrationSso')) { + return response(401); + } + + $user->last_seen_at = Carbon::now()->format('Y-m-d H:i:s'); + $user->save(); + + Auth::guard()->login($user, true); + + return redirect('/accounts?authenticated'); + } +} diff --git a/app/Http/Controllers/SinglePageController.php b/app/Http/Controllers/SinglePageController.php index 94796a1c..5cd510a7 100644 --- a/app/Http/Controllers/SinglePageController.php +++ b/app/Http/Controllers/SinglePageController.php @@ -27,6 +27,7 @@ class SinglePageController extends Controller $isTestingApp = config('2fauth.config.isTestingApp') ? 'true' : 'false'; $lang = App::getLocale(); $locales = collect(config('2fauth.locales'))->toJson(); /** @phpstan-ignore-line */ + $openidAuth = config('services.openid.client_secret') ? true : false; // if (Auth::user()->preferences) @@ -35,6 +36,7 @@ class SinglePageController extends Controller 'appConfig' => collect([ 'proxyAuth' => $proxyAuth, 'proxyLogoutUrl' => $proxyLogoutUrl, + 'openidAuth' => $openidAuth, 'subdirectory' => $subdir, ])->toJson(), 'defaultPreferences' => $defaultPreferences, diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 63e77535..53cc3d48 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -10,9 +10,11 @@ use App\Listeners\CleanIconStorage; use App\Listeners\DissociateTwofaccountFromGroup; use App\Listeners\ReleaseRadar; use App\Listeners\ResetUsersPreference; +use App\Providers\Socialite\RegisterOpenId; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; +use SocialiteProviders\Manager\SocialiteWasCalled; class EventServiceProvider extends ServiceProvider { @@ -37,6 +39,9 @@ class EventServiceProvider extends ServiceProvider ScanForNewReleaseCalled::class => [ ReleaseRadar::class, ], + SocialiteWasCalled::class => [ + RegisterOpenId::class, + ], ]; /** diff --git a/app/Providers/Socialite/OpenId.php b/app/Providers/Socialite/OpenId.php new file mode 100644 index 00000000..23ba5c4e --- /dev/null +++ b/app/Providers/Socialite/OpenId.php @@ -0,0 +1,90 @@ +buildAuthUrlFromBase($this->getConfig('authorize_url'), $state); + } + + /** + * {@inheritdoc} + */ + protected function getTokenUrl() + { + return $this->getConfig('token_url'); + } + + /** + * {@inheritdoc} + */ + protected function getUserByToken($token) + { + $response = $this->getHttpClient()->get($this->getConfig('userinfo_url'), [ + RequestOptions::HEADERS => [ + 'Authorization' => 'Bearer '.$token, + ], + ]); + + return json_decode((string) $response->getBody(), true); + } + + /** + * {@inheritdoc} + */ + protected function refreshToken($refreshToken) + { + return $this->getHttpClient()->post($this->getTokenUrl(), [ + RequestOptions::FORM_PARAMS => [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + ], + ]); + } + + /** + * {@inheritdoc} + */ + protected function mapUserToObject(array $user) + { + return (new User())->setRaw($user)->map([ + 'email' => $user['email'] ?? null, + 'email_verified' => $user['email_verified'] ?? null, + 'name' => $user['name'] ?? null, + 'given_name' => $user['given_name'] ?? null, + 'family_name' => $user['family_name'] ?? null, + 'preferred_username' => $user['preferred_username'] ?? null, + 'nickname' => $user['nickname'] ?? null, + 'groups' => $user['groups'] ?? null, + 'id' => $user['sub'], + ]); + } +} diff --git a/app/Providers/Socialite/RegisterOpenId.php b/app/Providers/Socialite/RegisterOpenId.php new file mode 100644 index 00000000..3dc9d545 --- /dev/null +++ b/app/Providers/Socialite/RegisterOpenId.php @@ -0,0 +1,13 @@ +extendSocialite('openid', OpenId::class); + } +} diff --git a/composer.json b/composer.json index bba0d943..7f73382c 100644 --- a/composer.json +++ b/composer.json @@ -30,9 +30,11 @@ "laragear/webauthn": "^1.2.0", "laravel/framework": "^10.10", "laravel/passport": "^11.2", + "laravel/socialite": "^5.10", "laravel/tinker": "^2.8", "laravel/ui": "^4.2", "paragonie/constant_time_encoding": "^2.6", + "socialiteproviders/manager": "^4.4", "spatie/eloquent-sortable": "^4.0.1", "spomky-labs/otphp": "^11.0" }, diff --git a/composer.lock b/composer.lock index 4854a630..8608cbef 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": "88245443209e7afe41d4a37b26f07c65", + "content-hash": "8ab04001cb3cb872bf0848236af3fc20", "packages": [ { "name": "brick/math", @@ -2310,6 +2310,76 @@ }, "time": "2023-07-14T13:56:28+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.10.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "f376b6eda9084899e37ac08bafd64a95edf9c6c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/f376b6eda9084899e37ac08bafd64a95edf9c6c0", + "reference": "f376b6eda9084899e37ac08bafd64a95edf9c6c0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.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|^8.0", + "phpstan/phpstan": "^1.10", + "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": "2023-10-30T22:09:58+00:00" + }, { "name": "laravel/tinker", "version": "v2.8.2", @@ -3028,6 +3098,82 @@ ], "time": "2023-08-05T12:09:49+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/oauth2-server", "version": "8.5.4", @@ -4930,6 +5076,80 @@ ], "time": "2023-04-15T23:01:58+00:00" }, + { + "name": "socialiteproviders/manager", + "version": "v4.4.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Manager.git", + "reference": "df5e45b53d918ec3d689f014d98a6c838b98ed96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/df5e45b53d918ec3d689f014d98a6c838b98ed96", + "reference": "df5e45b53d918ec3d689f014d98a6c838b98ed96", + "shasum": "" + }, + "require": { + "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0", + "laravel/socialite": "~5.0", + "php": "^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": "2023-08-27T23:46:34+00:00" + }, { "name": "spatie/eloquent-sortable", "version": "4.0.2", diff --git a/config/2fauth.php b/config/2fauth.php index 0eddbc69..ec4f814c 100644 --- a/config/2fauth.php +++ b/config/2fauth.php @@ -71,6 +71,7 @@ return [ 'lastRadarScan' => 0, 'latestRelease' => false, 'disableRegistration' => false, + 'disableRegistrationSso' => false, ], /* @@ -104,4 +105,4 @@ return [ 'getOtpOnRequest' => true, ], -]; \ No newline at end of file +]; diff --git a/config/services/openid.php b/config/services/openid.php new file mode 100644 index 00000000..7f753fdd --- /dev/null +++ b/config/services/openid.php @@ -0,0 +1,11 @@ + env('OPENID_TOKEN_URL'), + 'authorize_url' => env('OPENID_AUTHORIZE_URL'), + 'userinfo_url' => env('OPENID_USERINFO_URL'), + + 'client_id' => env('OPENID_CLIENT_ID'), + 'client_secret' => env('OPENID_CLIENT_SECRET'), + 'redirect' => '/socialite/callback/openid', +]; diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 790e1b1a..2f751829 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -78,6 +78,12 @@ services: # authentication checks. That means your proxy is fully responsible of the authentication process, 2FAuth will # trust him as long as headers are presents. - AUTHENTICATION_GUARD=web-guard + # OpenId settings + # - OPENID_AUTHORIZE_URL= + # - OPENID_TOKEN_URL= + # - OPENID_USERINFO_URL= + # - OPENID_CLIENT_ID= + # - OPENID_CLIENT_SECRET= # Name of the HTTP headers sent by the reverse proxy that identifies the authenticated user at proxy level. # Check your proxy documentation to find out how these headers are named (i.e 'REMOTE_USER', 'REMOTE_EMAIL', etc...) # (only relevant when AUTHENTICATION_GUARD is set to 'reverse-proxy-guard') diff --git a/resources/js/views/auth/Login.vue b/resources/js/views/auth/Login.vue index e27c1b33..58d154de 100644 --- a/resources/js/views/auth/Login.vue +++ b/resources/js/views/auth/Login.vue @@ -115,6 +115,11 @@ {{ $t('auth.login_and_password') }}
+{{ $t('auth.sign_in_using') }} + + OpenID + +
{{ $t('auth.forms.dont_have_account_yet') }}
{{ $t('auth.sign_in_using') }} + + OpenID + +
{{ $t('auth.forms.dont_have_account_yet') }}