Add support for an openid provider

This commit is contained in:
indy koning 2023-11-20 23:25:36 +01:00
parent 296691beee
commit 160f55fa6b
15 changed files with 425 additions and 11 deletions

View File

@ -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
MIX_ENV=local

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Facades\Settings;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
class SocialiteController extends Controller
{
public function redirect(Request $request, $driver)
{
return Socialite::driver($driver)->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');
}
}

View File

@ -27,6 +27,7 @@ public function index()
$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 @@ public function index()
'appConfig' => collect([
'proxyAuth' => $proxyAuth,
'proxyLogoutUrl' => $proxyLogoutUrl,
'openidAuth' => $openidAuth,
'subdirectory' => $subdir,
])->toJson(),
'userPreferences' => $userPreferences,

View File

@ -10,9 +10,11 @@
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,
],
];
/**

View File

@ -0,0 +1,90 @@
<?php
namespace App\Providers\Socialite;
use GuzzleHttp\RequestOptions;
use InvalidArgumentException;
use SocialiteProviders\Manager\OAuth2\AbstractProvider;
use SocialiteProviders\Manager\OAuth2\User;
use SocialiteProviders\Manager\SocialiteWasCalled;
class OpenId extends AbstractProvider
{
public const IDENTIFIER = 'OPENID';
/**
* {@inheritdoc}
*/
protected $scopes = ['openid profile email'];
/**
* {@inheritdoc}
*/
public static function additionalConfigKeys()
{
return ['token_url', 'authorize_url', 'userinfo_url'];
}
/**
* {@inheritdoc}
*/
protected function getAuthUrl($state)
{
return $this->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'],
]);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Providers\Socialite;
use SocialiteProviders\Manager\SocialiteWasCalled;
class RegisterOpenId
{
public function __invoke(SocialiteWasCalled $socialiteWasCalled)
{
$socialiteWasCalled->extendSocialite('openid', OpenId::class);
}
}

View File

@ -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"
},

222
composer.lock generated
View File

@ -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",

View File

@ -71,6 +71,7 @@
'lastRadarScan' => 0,
'latestRelease' => false,
'disableRegistration' => false,
'disableRegistrationSso' => false,
],
/*
@ -103,4 +104,4 @@
'getOtpOnRequest' => true,
],
];
];

View File

@ -0,0 +1,11 @@
<?php
return [
'token_url' => 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',
];

View File

@ -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')

6
resources/js/api.js vendored
View File

@ -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(() => {})
})
})

View File

@ -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
export default router

View File

@ -14,6 +14,7 @@
<p v-if="!this.$root.userPreferences.useWebauthnOnly">{{ $t('auth.sign_in_using') }}&nbsp;
<a id="lnkSignWithLegacy" role="button" class="is-link" @keyup.enter="toggleForm" @click="toggleForm" tabindex="0">{{ $t('auth.login_and_password') }}</a>
</p>
<p v-if="this.$root.appConfig.openidAuth"><a id="lnkSignWithOpenID" class="is-link" href="/socialite/redirect/openid">{{ $t('auth.sign_in_using') }} OpenID</a></p>
</div>
</form-wrapper>
<!-- login/password legacy form -->
@ -31,6 +32,7 @@
<a id="lnkSignWithWebauthn" role="button" class="is-link" @keyup.enter="toggleForm" @click="toggleForm" tabindex="0" :aria-label="$t('auth.sign_in_using_security_device')">{{ $t('auth.webauthn.security_device') }}</a>
</p>
<p v-if="this.$root.appSettings.disableRegistration == false" class="mt-4">{{ $t('auth.forms.dont_have_account_yet') }}&nbsp;<router-link id="lnkRegister" :to="{ name: 'register' }" class="is-link">{{ $t('auth.register') }}</router-link></p>
<p v-if="this.$root.appConfig.openidAuth"><a id="lnkSignWithOpenID" class="is-link" href="/socialite/redirect/openid">{{ $t('auth.sign_in_using') }} OpenID</a></p>
</div>
</form-wrapper>
<!-- footer -->
@ -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()
}
}
</script>
</script>

View File

@ -5,6 +5,7 @@
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' => ['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');
});
/**