From 160f55fa6ba139865483863645923996712ae8bc Mon Sep 17 00:00:00 2001
From: indy koning
Date: Mon, 20 Nov 2023 23:25:36 +0100
Subject: [PATCH] Add support for an openid provider
---
.env.example | 10 +-
.../Controllers/Auth/SocialiteController.php | 44 ++++
app/Http/Controllers/SinglePageController.php | 2 +
app/Providers/EventServiceProvider.php | 5 +
app/Providers/Socialite/OpenId.php | 90 +++++++
app/Providers/Socialite/RegisterOpenId.php | 13 +
composer.json | 2 +
composer.lock | 222 +++++++++++++++++-
config/2fauth.php | 3 +-
config/services/openid.php | 11 +
docker/docker-compose.yml | 6 +
resources/js/api.js | 6 +-
resources/js/routes.js | 12 +-
resources/js/views/auth/Login.vue | 6 +-
routes/web.php | 4 +
15 files changed, 425 insertions(+), 11 deletions(-)
create mode 100644 app/Http/Controllers/Auth/SocialiteController.php
create mode 100644 app/Providers/Socialite/OpenId.php
create mode 100644 app/Providers/Socialite/RegisterOpenId.php
create mode 100644 config/services/openid.php
diff --git a/.env.example b/.env.example
index 85c3e7f4..3d1ead85 100644
--- a/.env.example
+++ b/.env.example
@@ -212,6 +212,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:
@@ -239,4 +247,4 @@ PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
-MIX_ENV=local
\ No newline at end of file
+MIX_ENV=local
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 ad02df0a..cf02bc00 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(),
'userPreferences' => $userPreferences,
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 f3320f8a..a971fff9 100644
--- a/config/2fauth.php
+++ b/config/2fauth.php
@@ -71,6 +71,7 @@ return [
'lastRadarScan' => 0,
'latestRelease' => false,
'disableRegistration' => false,
+ 'disableRegistrationSso' => false,
],
/*
@@ -103,4 +104,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 a081f250..5b322556 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -73,6 +73,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/api.js b/resources/js/api.js
index a0fbd480..4b6d9457 100644
--- a/resources/js/api.js
+++ b/resources/js/api.js
@@ -13,7 +13,7 @@ if (window.appConfig.subdirectory) {
}
Vue.axios.interceptors.response.use(response => response, error => {
-
+
// Whether or not the promise must be returned, if unauthenticated is received
// we update the auth state of the front-end
if ( error.response.status === 401 ) {
@@ -49,10 +49,10 @@ Vue.axios.interceptors.response.use(response => response, error => {
// so it returns a 418 response.
// We catch the 418 response and push the user to the autolock view
if ( error.response.status === 418 ) routeName = 'autolock'
-
+
if ( error.response.status === 404 ) routeName = '404'
router.push({ name: routeName, params: { err: error.response } })
return new Promise(() => {})
-})
\ No newline at end of file
+})
diff --git a/resources/js/routes.js b/resources/js/routes.js
index bde1c99b..20f1a1a8 100644
--- a/resources/js/routes.js
+++ b/resources/js/routes.js
@@ -68,12 +68,18 @@ const router = new Router({
],
});
+const urlParams = new URLSearchParams(window.location.search);
+
+if (urlParams.has('authenticated')) {
+ Vue.$storage.set('authenticated', true)
+}
+
let isFirstLoad = true;
router.beforeEach((to, from, next) => {
-
+
document.title = router.app.$options.i18n.t('titles.' + to.name)
-
+
if( to.name === 'accounts') {
to.params.isFirstLoad = isFirstLoad ? true : false
isFirstLoad = false;
@@ -124,4 +130,4 @@ router.afterEach(to => {
Vue.$storage.set('lastRoute', to.name)
});
-export default router
\ No newline at end of file
+export default router
diff --git a/resources/js/views/auth/Login.vue b/resources/js/views/auth/Login.vue
index 24d10b14..4b92e5cb 100644
--- a/resources/js/views/auth/Login.vue
+++ b/resources/js/views/auth/Login.vue
@@ -14,6 +14,7 @@
{{ $t('auth.sign_in_using') }}
{{ $t('auth.login_and_password') }}
+ {{ $t('auth.sign_in_using') }} OpenID
@@ -31,6 +32,7 @@
{{ $t('auth.webauthn.security_device') }}
{{ $t('auth.forms.dont_have_account_yet') }} {{ $t('auth.register') }}
+ {{ $t('auth.sign_in_using') }} OpenID
@@ -145,7 +147,7 @@
this.$storage.set('authenticated', false)
if( error.response.status === 401 ) {
-
+
this.$notify({ type: 'is-danger', text: this.$t('auth.forms.authentication_failed'), duration:-1 })
}
else if( error.response.status !== 422 ) {
@@ -184,4 +186,4 @@
next()
}
}
-
\ No newline at end of file
+
diff --git a/routes/web.php b/routes/web.php
index 56b9cdcf..435060d6 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -5,6 +5,7 @@ use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\Auth\ResetPasswordController;
+use App\Http\Controllers\Auth\SocialiteController;
use App\Http\Controllers\Auth\UserController;
use App\Http\Controllers\Auth\WebAuthnDeviceLostController;
use App\Http\Controllers\Auth\WebAuthnLoginController;
@@ -47,6 +48,9 @@ Route::group(['middleware' => ['rejectIfDemoMode', 'throttle:10,1']], function (
Route::group(['middleware' => ['SkipIfAuthenticated', 'throttle:10,1']], function () {
Route::post('user/login', [LoginController::class, 'login'])->name('user.login');
Route::post('webauthn/login', [WebAuthnLoginController::class, 'login'])->name('webauthn.login');
+
+ Route::get('/socialite/redirect/{driver}', [SocialiteController::class, 'redirect'])->name('socialite.redirect');
+ Route::get('/socialite/callback/{driver}', [SocialiteController::class, 'callback'])->name('socialite.callback');
});
/**