Implement OAuth and SAML2 support (#13764)

* Implement OAuth and SAML2 support via Socialite

* Add socialite docs

* fixes

* Additional information added

* wip

* 22.3.0 targeted version

* Allow mysql auth as long as there is a password saved

Co-authored-by: laf <gh+n@laf.io>
Co-authored-by: Tony Murray <murraytony@gmail.com>
This commit is contained in:
Jellyfrog
2022-02-20 22:05:51 +01:00
committed by GitHub
parent 2e5b343731
commit 09929bd686
42 changed files with 1105 additions and 31 deletions

View File

@@ -18,7 +18,7 @@ class MysqlAuthorizer extends AuthorizerBase
$username = $credentials['username'] ?? null; $username = $credentials['username'] ?? null;
$password = $credentials['password'] ?? null; $password = $credentials['password'] ?? null;
$user_data = User::thisAuth()->firstWhere(['username' => $username]); $user_data = User::whereNotNull('password')->firstWhere(['username' => $username]);
$hash = $user_data->password; $hash = $user_data->password;
$enabled = $user_data->enabled; $enabled = $user_data->enabled;
@@ -76,7 +76,7 @@ class MysqlAuthorizer extends AuthorizerBase
$user_id = $new_user->user_id; $user_id = $new_user->user_id;
// set auth_id // set auth_id
$new_user->auth_id = $this->getUserid($username); $new_user->auth_id = (string) $this->getUserid($username);
$new_user->save(); $new_user->save();
if ($user_id) { if ($user_id) {

View File

@@ -83,6 +83,18 @@ class DynamicConfigItem implements \ArrayAccess
return filter_var($value, FILTER_VALIDATE_EMAIL); return filter_var($value, FILTER_VALIDATE_EMAIL);
} elseif ($this->type == 'array') { } elseif ($this->type == 'array') {
return is_array($value); // this should probably have more complex validation via validator rules return is_array($value); // this should probably have more complex validation via validator rules
} elseif ($this->type == 'array-sub-keyed') {
if (! is_array($value)) {
return false;
}
foreach ($value as $v) {
if (! is_array($v)) {
return false;
}
}
return true;
} elseif ($this->type == 'color') { } elseif ($this->type == 'color') {
return (bool) preg_match('/^#?[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/', $value); return (bool) preg_match('/^#?[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/', $value);
} elseif (in_array($this->type, ['text', 'password'])) { } elseif (in_array($this->type, ['text', 'password'])) {

View File

@@ -97,7 +97,7 @@ class AddUserCommand extends LnmsCommand
$user->setPassword($password); $user->setPassword($password);
$user->save(); $user->save();
$user->auth_id = LegacyAuth::get()->getUserid($user->username) ?: $user->user_id; $user->auth_id = (string) LegacyAuth::get()->getUserid($user->username) ?: $user->user_id;
$user->save(); $user->save();
$this->info(__('commands.user:add.success', ['username' => $user->username])); $this->info(__('commands.user:add.success', ['username' => $user->username]));

View File

@@ -41,13 +41,21 @@ class LoginController extends Controller
$this->middleware('guest')->except('logout'); $this->middleware('guest')->except('logout');
} }
public function username() public function username(): string
{ {
return 'username'; return 'username';
} }
public function showLoginForm() /**
* @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function showLoginForm(Request $request)
{ {
// Check if we want to redirect users to the socialite provider directly
if (! $request->has('redirect') && Config::get('auth.socialite.redirect') && array_key_first(Config::get('auth.socialite.configs', []))) {
return (new SocialiteController)->redirect($request, array_key_first(Config::get('auth.socialite.configs', [])));
}
if (Config::get('public_status')) { if (Config::get('public_status')) {
$devices = Device::isActive()->with('location')->get(); $devices = Device::isActive()->with('location')->get();
@@ -57,7 +65,7 @@ class LoginController extends Controller
return view('auth.login'); return view('auth.login');
} }
protected function loggedOut(Request $request) protected function loggedOut(Request $request): \Illuminate\Http\RedirectResponse
{ {
return redirect(Config::get('auth_logout_handler', $this->redirectTo)); return redirect(Config::get('auth_logout_handler', $this->redirectTo));
} }

View File

@@ -0,0 +1,223 @@
<?php
/**
* SocialiateController.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Config;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Laravel\Socialite\Contracts\User as SocialiteUser;
use Laravel\Socialite\Facades\Socialite;
use LibreNMS\Config as LibreNMSConfig;
use LibreNMS\Exceptions\AuthenticationException;
use Log;
class SocialiteController extends Controller
{
/** @var SocialiteUser */
private $socialite_user;
public function __construct()
{
$this->injectConfig();
}
public static function registerEventListeners(): void
{
foreach (LibreNMSConfig::get('auth.socialite.configs', []) as $provider => $config) {
// Treat not set as "disabled"
if (! isset($config['listener'])) {
continue;
}
$listener = $config['listener'];
if (class_exists($listener)) {
Event::listen(\SocialiteProviders\Manager\SocialiteWasCalled::class, "$listener@handle");
} else {
Log::error("Wrong value for auth.socialite.configs.$provider.listener set, class: '$listener' does not exist!");
}
}
}
/**
* @return RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function redirect(Request $request, string $provider)
{
// Re-store target url since it will be forgotten after the redirect
$request->session()->put('url.intended', redirect()->intended()->getTargetUrl());
return Socialite::driver($provider)->redirect();
}
public function callback(Request $request, string $provider): RedirectResponse
{
$this->socialite_user = Socialite::driver($provider)->user();
// If we already have a valid session, user is trying to pair their account
if (Auth::user()) {
return $this->pairUser($provider);
}
$this->register($provider);
return $this->login($provider);
}
/**
* Metadata endpoint used in SAML
*/
public function metadata(Request $request, string $provider): \Illuminate\Http\Response
{
$socialite = Socialite::driver($provider);
if (method_exists($socialite, 'getServiceProviderMetadata')) {
return $socialite->getServiceProviderMetadata();
}
return abort(404);
}
private function login(string $provider): RedirectResponse
{
$user = User::where('auth_type', "socialite_$provider")
->where('auth_id', $this->socialite_user->getId())
->first();
try {
if (! $user) {
throw new AuthenticationException();
}
Auth::login($user);
return redirect()->intended();
} catch (AuthenticationException $e) {
flash()->addError($e->getMessage());
}
return redirect()->route('login');
}
private function register(string $provider): void
{
if (! LibreNMSConfig::get('auth.socialite.register', false)) {
return;
}
$user = User::firstOrNew([
'auth_type' => "socialite_$provider",
'auth_id' => $this->socialite_user->getId(),
]);
if ($user->user_id) {
return;
}
$user->username = $this->buildUsername();
$user->email = $this->socialite_user->getEmail();
$user->realname = $this->buildRealName();
$user->save();
}
private function pairUser(string $provider): RedirectResponse
{
$user = Auth::user();
$user->auth_type = "socialite_$provider";
$user->auth_id = $this->socialite_user->getId();
$user->save();
return redirect()->route('preferences.index');
}
private function buildUsername(): string
{
return $this->socialite_user->getNickname()
?: $this->socialite_user->getEmail()
?: $this->buildRealName();
}
private function buildRealName(): string
{
$name = '';
// These methods only exist for a few providers
if (method_exists($this->socialite_user, 'getFirstName')) {
$name = $this->socialite_user->getFirstName();
}
if (method_exists($this->socialite_user, 'getLastName')) {
$name = trim($name . ' ' . $this->socialite_user->getLastName());
}
if (empty($name)) {
$name = $this->socialite_user->getName();
}
return ! empty($name) ? $name : '';
}
/**
* Take the config from Librenms Config, and insert it into Laravel Config
*/
private function injectConfig(): void
{
foreach (LibreNMSConfig::get('auth.socialite.configs', []) as $provider => $config) {
Config::set("services.$provider", $config);
// Inject redirect URL automatically if not set
if (! Config::has("services.$provider.redirect")) {
Config::set("services.$provider.redirect",
route('socialite.callback', [$provider])
);
}
// Inject SAML redirect url automatically
$this->injectSAML2Config($provider);
}
}
private function injectSAML2Config(string $provider): void
{
if ($provider !== 'saml2') {
return;
}
if (! Config::has("services.$provider.sp_acs")) {
Config::set("services.$provider.sp_acs", route('socialite.callback', [$provider]));
}
if (! Config::has("services.$provider.client_id")) {
Config::set("services.$provider.client_id", '');
}
if (! Config::has("services.$provider.client_secret")) {
Config::set("services.$provider.client_secret", '');
}
}
}

View File

@@ -98,7 +98,7 @@ class UserController extends Controller
$user = User::create($user); $user = User::create($user);
$user->setPassword($request->new_password); $user->setPassword($request->new_password);
$user->auth_id = LegacyAuth::get()->getUserid($user->username) ?: $user->user_id; $user->auth_id = (string) LegacyAuth::get()->getUserid($user->username) ?: $user->user_id;
$this->updateDashboard($user, $request->get('dashboard')); $this->updateDashboard($user, $request->get('dashboard'));
if ($user->save()) { if ($user->save()) {

View File

@@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware
* @var array * @var array
*/ */
protected $except = [ protected $except = [
// '*', // FIXME: CSRF completely disabled! '/auth/*/callback',
]; ];
} }

View File

@@ -53,6 +53,7 @@ class AppServiceProvider extends ServiceProvider
$this->app->booted('\LibreNMS\DB\Eloquent::initLegacyListeners'); $this->app->booted('\LibreNMS\DB\Eloquent::initLegacyListeners');
$this->app->booted('\LibreNMS\Config::load'); $this->app->booted('\LibreNMS\Config::load');
$this->app->booted('\App\Http\Controllers\Auth\SocialiteController::registerEventListeners');
$this->bootCustomBladeDirectives(); $this->bootCustomBladeDirectives();
$this->bootCustomValidators(); $this->bootCustomValidators();
@@ -174,5 +175,23 @@ class AppServiceProvider extends ServiceProvider
return $validator->passes(); return $validator->passes();
}, trans('validation.exists')); }, trans('validation.exists'));
Validator::extend('url_or_xml', function ($attribute, $value): bool {
if (! is_string($value)) {
return false;
}
if (filter_var($value, FILTER_VALIDATE_URL) !== false) {
return true;
}
libxml_use_internal_errors(true);
$xml = simplexml_load_string($value);
if ($xml !== false) {
return true;
}
return false;
});
} }
} }

View File

@@ -205,7 +205,7 @@ class LegacyUserProvider implements UserProvider
/** @var User $user */ /** @var User $user */
$user->fill($new_user); // fill all attributes $user->fill($new_user); // fill all attributes
$user->auth_type = $type; // doing this here in case it was null (legacy) $user->auth_type = $type; // doing this here in case it was null (legacy)
$user->auth_id = $auth_id; $user->auth_id = (string) $auth_id;
$user->save(); $user->save();
return $user; return $user;

View File

@@ -49,6 +49,7 @@
"php-flasher/flasher-laravel": "^0.9", "php-flasher/flasher-laravel": "^0.9",
"phpmailer/phpmailer": "~6.0", "phpmailer/phpmailer": "~6.0",
"predis/predis": "^1.1", "predis/predis": "^1.1",
"socialiteproviders/manager": "^4.1",
"symfony/yaml": "^4.0", "symfony/yaml": "^4.0",
"tecnickcom/tcpdf": "^6.4", "tecnickcom/tcpdf": "^6.4",
"tightenco/ziggy": "^0.9" "tightenco/ziggy": "^0.9"

221
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": "17d76cfe55a8adb13cb6df7c52662b86", "content-hash": "b4fa65afc5e0f75e6f171f432932cb75",
"packages": [ "packages": [
{ {
"name": "amenadiel/jpgraph", "name": "amenadiel/jpgraph",
@@ -2188,6 +2188,75 @@
}, },
"time": "2021-11-30T15:53:04+00:00" "time": "2021-11-30T15:53:04+00:00"
}, },
{
"name": "laravel/socialite",
"version": "v5.5.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "cb5b5538c207efa19aa5d7f46cd76acb03ec3055"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/cb5b5538c207efa19aa5d7f46cd76acb03ec3055",
"reference": "cb5b5538c207efa19aa5d7f46cd76acb03ec3055",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"illuminate/http": "^6.0|^7.0|^8.0|^9.0",
"illuminate/support": "^6.0|^7.0|^8.0|^9.0",
"league/oauth1-client": "^1.0",
"php": "^7.2|^8.0"
},
"require-dev": {
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0",
"mockery/mockery": "^1.0",
"orchestra/testbench": "^4.0|^5.0|^6.0|^7.0",
"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": "2022-02-01T16:31:36+00:00"
},
{ {
"name": "laravel/tinker", "name": "laravel/tinker",
"version": "v2.7.0", "version": "v2.7.0",
@@ -2560,6 +2629,82 @@
], ],
"time": "2021-11-21T11:48:40+00:00" "time": "2021-11-21T11:48:40+00:00"
}, },
{
"name": "league/oauth1-client",
"version": "v1.10.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/oauth1-client.git",
"reference": "88dd16b0cff68eb9167bfc849707d2c40ad91ddc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/88dd16b0cff68eb9167bfc849707d2c40ad91ddc",
"reference": "88dd16b0cff68eb9167bfc849707d2c40ad91ddc",
"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.0"
},
"time": "2021-08-15T23:05:49+00:00"
},
{ {
"name": "librenms/laravel-vue-i18n-generator", "name": "librenms/laravel-vue-i18n-generator",
"version": "0.1.47", "version": "0.1.47",
@@ -4657,6 +4802,80 @@
], ],
"time": "2021-09-25T23:10:38+00:00" "time": "2021-09-25T23:10:38+00:00"
}, },
{
"name": "socialiteproviders/manager",
"version": "v4.1.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "4e63afbd26dc45ff263591de2a0970436a6a0bf9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/4e63afbd26dc45ff263591de2a0970436a6a0bf9",
"reference": "4e63afbd26dc45ff263591de2a0970436a6a0bf9",
"shasum": ""
},
"require": {
"illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0",
"laravel/socialite": "~4.0 || ~5.0",
"php": "^7.2 || ^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": "2022-01-23T22:40:23+00:00"
},
{ {
"name": "spomky-labs/base64url", "name": "spomky-labs/base64url",
"version": "v2.0.4", "version": "v2.0.4",

View File

@@ -170,6 +170,11 @@ return [
Illuminate\Validation\ValidationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class, Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
\SocialiteProviders\Manager\ServiceProvider::class,
/* /*
* Application Service Providers... * Application Service Providers...
*/ */

View File

@@ -204,6 +204,6 @@ return [
| |
*/ */
'same_site' => 'lax', 'same_site' => env('SESSION_SAME_SITE_COOKIE', 'lax'),
]; ];

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class IncreaseAuthIdLength extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('auth_id')->nullable()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->integer('auth_id')->nullable()->change();
});
}
}

View File

@@ -0,0 +1,352 @@
# OAuth and SAML Support
## Introduction
LibreNMS has support for [Laravel Socialite](https://github.com/laravel/socialite) to try and simplify the use of OAuth 1 or 2 providers such as using GitHub, Microsoft, Twitter + many more and SAML.
[Socialite Providers](https://socialiteproviders.com) supports more than 100+ 3rd parties so you will most likely find support for the SAML or OAuth provider you need without too much trouble.
Please do note however, these providers are not maintained by LibreNMS so we cannot add support for new ones and we can only provide you basic help with general configuration.
See the Socialite Providers website for more information on adding a new OAuth provider.
Below we will guide you on how to install SAML or some of these OAth providers, you should be able to use these as a guide on how to install any others you may need but **please, please, ensure you read the Socialite Providers documentation carefully**.
[GitHub Provider](https://socialiteproviders.com/GitHub/)
[Microsoft Provider](https://socialiteproviders.com/Microsoft/)
[SAML2](https://socialiteproviders.com/Saml2/)
## Requirements
LibreNMS version 22.3.0 or later.
Please ensure you set `APP_URL` within your `.env` file so that callback URLs work correctly with the identify provider.
!!! note
Once you have configured your OAuth or SAML2 provider, please ensure you check the [Post configuration settings](#post-configration-settings) section at the end.
## GitHub and Microsoft Examples
### Install plugin
!!! note
First we need to install the plugin itself. The plugin name can be slightly different so be sure to check the Socialite Providers documentation and look for this line, `composer require socialiteproviders/github` which will give you the name you need for the command, i.e: `socialiteproviders/github`.
=== "GitHub"
`lnms plugin:add socialiteproviders/github`
=== "Microsoft"
`lnms plugin:add socialiteproviders/microsoft`
### Find the provider name
Next we need to find the provider name and writing it down
!!! note
It's almost always the name of the provider in lowercase but can be different so check the Socialite Providers documentation and look for this line, `github => [` which will give you the name you need for the above command: `github`.
=== "GitHub"
For GitHub we can find the line:
```php
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('GITHUB_REDIRECT_URI')
],
```
So our provider name is `github`, write this down.
=== "Microsoft"
For Microsoft we can find the line:
```php
'microsoft' => [
'client_id' => env('MICROSOFT_CLIENT_ID'),
'client_secret' => env('MICROSOFT_CLIENT_SECRET'),
'redirect' => env('MICROSOFT_REDIRECT_URI')
],
```
So our provider name is `microsoft`, write this down.
### Register OAuth application
#### Register a new application
Now we need some values from the OAuth provider itself, in most cases you need to register a new "OAuth application" at the providers site. This will vary from provider to provider but the process itself should be similar to the examples below.
!!! note
The callback URL is always: https://*your-librenms-url*/auth/*provider*/callback
It doesn't need to be a public available site, but it almost always needs to support TLS (https)!
=== "GitHub"
For our example with GitHub we go to [GitHub Developer Settings](https://github.com/settings/developers) and press "Register a new application":
![socialite-github-1](/img/socialite-github-1.png)
Fill out the form accordingly (with your own values):
![socialite-github-2](/img/socialite-github-2.png)
=== "Microsoft"
For our example with Microsoft we go to ["Azure Active Directory" > "App registrations"](https://aad.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) and press "New registration"
![socialite-1](/img/socialite-microsoft-1.png)
Fill out the form accordingly using your own values):
![socialite-2](/img/socialite-microsoft-2.png)
Copy the value of the **Application (client) ID** and **Directory (tenant) ID** and save them, you will need them in the next step.
![socialite-2](/img/socialite-microsoft-3.png)
#### Generate a new client secret
=== "GitHub"
Press 'Generate a new client secret' to get a new client secret.
![socialite-github-3](/img/socialite-github-3.png)
Copy the **Client ID** and **Client secret**
In the example above it is:
**Client ID**: 7a41f1d8215640ca6b00
**Client secret**: ea03957288edd0e590be202b239e4f0ff26b8047
=== "Microsoft"
Select Certificates & secrets under Manage.
Select the 'New client secret' button.
Enter a value in Description and select one of the options for Expires and select 'Add'.
![socialite-2](/img/socialite-microsoft-6.png)
Copy the client secret **Value** (not Secret ID!) before you leave this page. You will need it in the next step.
![socialite-2](/img/socialite-microsoft-5.png)
### Saving configuration
Now we need to set the configuration options for your provider within LibreNMS itself. Please replace the values in the examples below with the values you collected earlier:
The format of the configuration string is `auth.socialite.configs.*provider name*.*value*`
=== "GitHub"
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.github.client_id 7a41f1d8215640ca6b00
lnms config:set auth.socialite.configs.github.client_secret ea03957288edd0e590be202b239e4f0ff26b8047
```
=== "Microsoft"
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.microsoft.client_id 7983ac13-c955-40e9-9b85-5ba27be52a52
lnms config:set auth.socialite.configs.microsoft.client_secret J9P7Q~K2F5C.L243sqzbGj.cOOcjTBgAPak_l
lnms config:set auth.socialite.configs.microsoft.tenant a15edc05-152d-4eb4-973c-14f1fdc57d8b
```
### Add provider event listener
The final step is to now add an event listener.
!!! note
It's important to copy exactly the right value here,
It should begin with a `\` and end before the `::class.'@handle'`
=== "GitHub"
Find the section looking like:
```php
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
// ... other providers
\SocialiteProviders\GitHub\GitHubExtendSocialite::class.'@handle',
],
];
```
Copy the part: `\SocialiteProviders\GitHub\GitHubExtendSocialite` and run;
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.github.listener "\SocialiteProviders\GitHub\GitHubExtendSocialite"
```
Don't forget the initial backslash (\\) !
=== "Microsoft"
Find the section looking like:
```php
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
// ... other providers
\SocialiteProviders\Microsoft\MicrosoftExtendSocialite::class.'@handle',
],
];
```
Copy the part: `\SocialiteProviders\Microsoft\MicrosoftExtendSocialite` and run;
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.microsoft.listener "\SocialiteProviders\Microsoft\MicrosoftExtendSocialite"
```
Don't forget the initial backslash (\\) !
Now you are done with setting up the OAuth provider!
If it doesn't work, please double check your configuration values by using the `config:get` command below.
!!! setting "auth/socialite"
```bash
lnms config:get auth.socialite
```
## SAML2 Example
### Install plugin
The first step is to install the plugin itself.
```bash
lnms plugin:add socialiteproviders/saml2
```
### Add configuration
Depending on what your identity provider (Google, Azure, ...) supports, the configuration could look different from what you see next so please use this as a rough guide.
It is up the IdP to provide the relevant details that you will need for configuration.
=== "Google"
Go to [https://admin.google.com/ac/apps/unified](https://admin.google.com/ac/apps/unified)
![socialite-saml-google-1](/img/socialite-saml-google-1.png)
![socialite-saml-google-2](/img/socialite-saml-google-2.png)
Press "DOWNLOAD METADATA" and save the file somewhere accessible by your LibreNMS server
![socialite-saml-google-3](/img/socialite-saml-google-3.png)
ACS URL = https://*your-librenms-url*/auth/saml2/callback
Entity ID = https://*your-librenms-url*/auth/saml2
Name ID format = PERSISTANT
Name ID = Basic Information > Primary email
![socialite-saml-google-4](/img/socialite-saml-google-4.png)
First name = http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
Last name = http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname
Primary email = http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
![socialite-saml-google-5](/img/socialite-saml-google-5.png)
![socialite-saml-google-6](/img/socialite-saml-google-6.png)
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.saml2.metadata "$(cat /tmp/GoogleIDPMetadata.xml)"
```
Alternatively, you can copy the content of the file and run it like so, this will result in the exact same result as above.
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.saml2.metadata '''<?xml version="1.0" encoding
...
...
</md:EntityDescriptor>'''
```
#### Using an Identity Provider metadata URL
!!! note
This is the prefered and easiest way, if your IdP supports it!
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.saml2.metadata https://idp.co/metadata/xml
```
#### Using an Identity Provider metadata XML file
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.saml2.metadata "$(cat GoogleIDPMetadata.xml)"
```
#### Manually configuring the Identity Provider with a certificate string
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.saml2.acs https://idp.co/auth/acs
lnms config:set auth.socialite.configs.saml2.entityid http://saml.to/trust
lnms config:set auth.socialite.configs.saml2.certificate MIIC4jCCAcqgAwIBAgIQbDO5YO....
```
#### Manually configuring the Identity Provider with a certificate file
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.saml2.acs https://idp.co/auth/acs
lnms config:set auth.socialite.configs.saml2.entityid http://saml.to/trust
lnms config:set auth.socialite.configs.saml2.certificate "$(cat /path/to/certificate.pem)"
```
### Add provider event listener
Now we just need to define the listener service within LibreNMS:
!!! setting "auth/socialite"
```bash
lnms config:set auth.socialite.configs.saml2.listener "\SocialiteProviders\Saml2\Saml2ExtendSocialite"
```
### SESSION_SAME_SITE_COOKIE
You most likely will need to set `SESSION_SAME_SITE_COOKIE=none` in `.env` if you use SAML2!
!!! note
Don't forget to run `lnms config:clear` after you modify `.env` to flush the config cache
### Service provider metadata
Your identify provider might ask you for your Service Provider (SP) metadata.
LibreNMS exposes all of this information from your [LibreNMS install](https://*your-librenms-url*/auth/saml2/metadata)
## Troubleshooting
If it doesn't work, please double check your configuration values by using the `config:get` command below.
!!! setting "auth/socialite"
```bash
lnms config:get auth.socialite
```
### Redirect URL
If you have a need to, then you can override redirect url with the following commands:
=== "OAuth"
Replace `github` and the relevant URL below with your identity provider details.
`lnms config:set auth.socialite.configs.github.redirect https://demo.librenms.org/auth/github/callback`
=== "SAML2"
`lnms config:set auth.socialite.configs.saml2.sp_acs auth/saml2/callback`
## Post configuration settings
!!! setting "auth/socialite"
From here you can configure the settings for any identity providers you have configured along with some bespoke options.
Redirect Login page: This setting will skip your LibreNMS login and take the end user straight to the first idP you configured.
Allow registration via provider: If this setting is disabled, new users signing in via the idP will not be authenticated. This setting allows a local user to be automatically created which permits their login.

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,11 @@
{ {
"/js/app.js": "/js/app.js?id=50f82aa2aac679191fac", "/js/app.js": "/js/app.js?id=dba3e37f44ce826e96d0",
"/js/manifest.js": "/js/manifest.js?id=2951ae529be231f05a93", "/js/manifest.js": "/js/manifest.js?id=2951ae529be231f05a93",
"/css/vendor.css": "/css/vendor.css?id=2568831af31dbfc3128a", "/css/vendor.css": "/css/vendor.css?id=2568831af31dbfc3128a",
"/css/app.css": "/css/app.css?id=936fe619dcf1bac0a33f", "/css/app.css": "/css/app.css?id=936fe619dcf1bac0a33f",
"/js/vendor.js": "/js/vendor.js?id=c5fd3d75a63757080dbb", "/js/vendor.js": "/js/vendor.js?id=c5fd3d75a63757080dbb",
"/js/lang/de.js": "/js/lang/de.js?id=1aedfce25e3daad3046a", "/js/lang/de.js": "/js/lang/de.js?id=1aedfce25e3daad3046a",
"/js/lang/en.js": "/js/lang/en.js?id=3617cad4c8bcf97221a6", "/js/lang/en.js": "/js/lang/en.js?id=f16225e77f5dbe2541ac",
"/js/lang/fr.js": "/js/lang/fr.js?id=a20c4c78eb5f9f4a374b", "/js/lang/fr.js": "/js/lang/fr.js?id=a20c4c78eb5f9f4a374b",
"/js/lang/it.js": "/js/lang/it.js?id=6b0bdf3be6dc3bf0a167", "/js/lang/it.js": "/js/lang/it.js?id=6b0bdf3be6dc3bf0a167",
"/js/lang/ru.js": "/js/lang/ru.js?id=f6b7c078755312a0907c", "/js/lang/ru.js": "/js/lang/ru.js?id=f6b7c078755312a0907c",

View File

@@ -372,6 +372,38 @@
"order": 1, "order": 1,
"type": "text" "type": "text"
}, },
"auth.socialite.redirect": {
"group": "auth",
"section": "socialite",
"order": 1,
"type": "boolean",
"default": false
},
"auth.socialite.register": {
"group": "auth",
"section": "socialite",
"order": 2,
"type": "boolean",
"default": false
},
"auth.socialite.configs": {
"group": "auth",
"section": "socialite",
"order": 3,
"type": "array-sub-keyed",
"validate": {
"value": "array",
"value.*": "array",
"value.*.listener": ["not_regex:/[:|@]/"],
"value.*.listener": ["regex:/^\\\\SocialiteProviders\\\\[^\\\\]+\\\\[^\\\\]+ExtendSocialite$/"],
"value.*.redirect": "url",
"value.saml.metadata": "url_or_xml",
"value.saml.acs": "url",
"value.saml.entityid": "url"
}
},
"auth_ad_check_certificates": { "auth_ad_check_certificates": {
"default": false, "default": false,
"group": "auth", "group": "auth",

View File

@@ -105,6 +105,7 @@
"text", "text",
"boolean", "boolean",
"array", "array",
"array-sub-keyed",
"password", "password",
"email", "email",
"color", "color",

View File

@@ -2039,7 +2039,7 @@ users:
Columns: Columns:
- { Field: user_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment } - { Field: user_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
- { Field: auth_type, Type: varchar(32), 'Null': true, Extra: '' } - { Field: auth_type, Type: varchar(32), 'Null': true, Extra: '' }
- { Field: auth_id, Type: int, 'Null': true, Extra: '' } - { Field: auth_id, Type: varchar(255), 'Null': true, Extra: '' }
- { Field: username, Type: varchar(255), 'Null': false, Extra: '' } - { Field: username, Type: varchar(255), 'Null': false, Extra: '' }
- { Field: password, Type: varchar(255), 'Null': true, Extra: '' } - { Field: password, Type: varchar(255), 'Null': true, Extra: '' }
- { Field: realname, Type: varchar(64), 'Null': false, Extra: '' } - { Field: realname, Type: varchar(64), 'Null': false, Extra: '' }

View File

@@ -134,6 +134,7 @@ nav:
- Galera Database Cluster: Extensions/Galera-Cluster.md - Galera Database Cluster: Extensions/Galera-Cluster.md
- IRC Bot Extensions: Extensions/IRC-Bot-Extensions.md - IRC Bot Extensions: Extensions/IRC-Bot-Extensions.md
- IRC Bot: Extensions/IRC-Bot.md - IRC Bot: Extensions/IRC-Bot.md
- Oauth/SAML support: Extensions/OAuth-SAML.md
- RRDCached: Extensions/RRDCached.md - RRDCached: Extensions/RRDCached.md
- RRDTune: Extensions/RRDTune.md - RRDTune: Extensions/RRDTune.md
- Scaling LibreNMS: Extensions/Distributed-Poller.md - Scaling LibreNMS: Extensions/Distributed-Poller.md

View File

@@ -7615,21 +7615,6 @@ parameters:
count: 1 count: 1
path: app/Http/Controllers/Ajax/RipeNccApiController.php path: app/Http/Controllers/Ajax/RipeNccApiController.php
-
message: "#^Method App\\\\Http\\\\Controllers\\\\Auth\\\\LoginController\\:\\:loggedOut\\(\\) has no return type specified\\.$#"
count: 1
path: app/Http/Controllers/Auth/LoginController.php
-
message: "#^Method App\\\\Http\\\\Controllers\\\\Auth\\\\LoginController\\:\\:showLoginForm\\(\\) has no return type specified\\.$#"
count: 1
path: app/Http/Controllers/Auth/LoginController.php
-
message: "#^Method App\\\\Http\\\\Controllers\\\\Auth\\\\LoginController\\:\\:username\\(\\) has no return type specified\\.$#"
count: 1
path: app/Http/Controllers/Auth/LoginController.php
- -
message: "#^Method App\\\\Http\\\\Controllers\\\\Auth\\\\TwoFactorController\\:\\:showTwoFactorForm\\(\\) has no return type specified\\.$#" message: "#^Method App\\\\Http\\\\Controllers\\\\Auth\\\\TwoFactorController\\:\\:showTwoFactorForm\\(\\) has no return type specified\\.$#"
count: 1 count: 1

View File

@@ -0,0 +1,139 @@
<!--
- SettingArraySubKeyed.vue
-
- Description-
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
- @package LibreNMS
- @link https://www.librenms.org
-->
<template>
<div v-tooltip="disabled ? $t('settings.readonly') : false">
<div v-for="(item, index) in localList">
<b>{{ index }}</b>
<div v-for="(item, subindex) in item" class="input-group">
<span :class="['input-group-addon', disabled ? 'disabled' : '']">{{ subindex }}</span>
<input type="text"
class="form-control"
:value="item"
:readonly="disabled"
@blur="updateSubItem(index, subindex, $event.target.value)"
@keyup.enter="updateSubItem(index, subindex, $event.target.value)"
>
<span class="input-group-btn">
<button v-if="!disabled" @click="removeSubItem(index, subindex)" type="button" class="btn btn-danger"><i class="fa fa-minus-circle"></i></button>
</span>
</div>
<div v-if="!disabled">
<div class="row">
<div class="col-lg-4">
<div class="input-group">
<input type="text" v-model="newSubItemKey[index]" class="form-control" placeholder="Key">
</div>
</div>
<div class="col-lg-8">
<div class="input-group">
<input type="text" v-model="newSubItemValue[index]" @keyup.enter="addSubItem(index)" class="form-control" placeholder="Value">
<span class="input-group-btn">
<button @click="addSubItem(index)" type="button" class="btn btn-primary"><i class="fa fa-plus-circle"></i></button>
</span>
</div>
</div>
</div>
</div>
<hr/>
</div>
<div v-if="!disabled">
<div class="input-group">
<input type="text" v-model="newSubArray" @keyup.enter="addSubArray" class="form-control">
<span class="input-group-btn">
<button @click="addSubArray" type="button" class="btn btn-primary"><i class="fa fa-plus-circle"></i></button>
</span>
</div>
</div>
</div>
</template>
<script>
import BaseSetting from "./BaseSetting";
export default {
name: "SettingArraySubKeyed",
mixins: [BaseSetting],
data() {
return {
localList: this.value ?? new Object(),
newSubItemKey: {},
newSubItemValue: {},
newSubArray: ""
}
},
methods: {
addSubItem(index) {
if (this.disabled) return;
var obj = {};
obj[this.newSubItemKey[index]] = this.newSubItemValue[index];
if (Object.keys(this.localList[index]).length === 0) {
this.localList[index] = new Object();
}
Object.assign(this.localList[index], obj);
this.$emit('input', this.localList);
this.newSubItemValue[index] = "";
this.newSubItemKey[index] = "";
},
removeSubItem(index, subindex) {
if (this.disabled) return;
delete this.localList[index][subindex];
if (Object.keys(this.localList[index]).length === 0) {
delete this.localList[index];
}
this.$emit('input', this.localList);
},
updateSubItem(index, subindex, value) {
if (this.disabled || this.localList[index][subindex] === value) return;
this.localList[index][subindex] = value;
this.$emit('input', this.localList);
},
addSubArray() {
if (this.disabled) return;
this.localList[this.newSubArray] = new Object();
this.$emit('input', this.localList);
this.newSubArray = "";
},
},
watch: {
value(updated) {
// careful to avoid loops with this
this.localList = updated;
}
}
}
</script>
<style scoped>
.input-group {
margin-bottom: 3px;
}
.input-group-addon:not(.disabled) {
cursor: move;
}
</style>

View File

@@ -30,6 +30,7 @@ return [
'general' => ['name' => 'General Authentication Settings'], 'general' => ['name' => 'General Authentication Settings'],
'ad' => ['name' => 'Active Directory Settings'], 'ad' => ['name' => 'Active Directory Settings'],
'ldap' => ['name' => 'LDAP Settings'], 'ldap' => ['name' => 'LDAP Settings'],
'socialite' => ['name' => 'Socialite Settings'],
], ],
'authorization' => [ 'authorization' => [
'device-group' => ['name' => 'Device Group Settings'], 'device-group' => ['name' => 'Device Group Settings'],
@@ -258,6 +259,20 @@ return [
'astext' => [ 'astext' => [
'description' => 'Key to hold cache of autonomous systems descriptions', 'description' => 'Key to hold cache of autonomous systems descriptions',
], ],
'auth' => [
'socialite' => [
'redirect' => [
'description' => 'Redirect Login page',
'help' => 'Login page should redirect immediately to the first defined provider.<br><br>TIPS: You can prevent it by appending ?redirect=0 in the url',
],
'register' => [
'description' => 'Allow registration via provider',
],
'configs' => [
'description' => 'Provider configs',
],
],
],
'auth_ad_base_dn' => [ 'auth_ad_base_dn' => [
'description' => 'Base DN', 'description' => 'Base DN',
'help' => 'groups and users must be under this dn. Example: dc=example,dc=com', 'help' => 'groups and users must be under this dn. Example: dc=example,dc=com',

View File

@@ -46,8 +46,18 @@
<button type="submit" id="login" class="btn btn-primary btn-block" name="submit"> <button type="submit" id="login" class="btn btn-primary btn-block" name="submit">
<i class="fa fa-btn fa-sign-in"></i> {{ __('Login') }} <i class="fa fa-btn fa-sign-in"></i> {{ __('Login') }}
</button> </button>
</form>
@foreach (\LibreNMS\Config::get('auth.socialite.configs', []) as $provider => $config)
<br>
<form role="form" action="{{ route('socialite.redirect', $provider) }}" method="post">
{{ csrf_field() }}
<button type="submit" id="login" class="btn btn-success btn-block">
<i class="fab fa-btn fa-{{ $provider }}"></i> {{ __('Login with') }} {{ ucfirst($provider) }}
</button>
</form>
@endforeach
</div> </div>
</div> </div>
</form>
</div> </div>
</x-panel> </x-panel>

View File

@@ -101,6 +101,19 @@
</form> </form>
</x-panel> </x-panel>
@config('auth.socialite.configs')
<x-panel title="{{ __('OAuth/SAML Authentication') }}">
@foreach (\LibreNMS\Config::get('auth.socialite.configs', []) as $provider => $config)
<form role="form" action="{{ route('socialite.redirect', $provider) }}" method="post">
{{ csrf_field() }}
<button type="submit" id="login" class="btn btn-success btn-block">
<i class="fab fa-btn fa-{{ $provider }}"></i> {{ __('Register with') }} {{ ucfirst($provider) }}
</button>
</form>
@endforeach
</x-panel>
@endconfig
@config('twofactor') @config('twofactor')
<x-panel title="{{ __('Two-Factor Authentication') }}"> <x-panel title="{{ __('Two-Factor Authentication') }}">
@if($twofactor) @if($twofactor)

View File

@@ -16,6 +16,13 @@ use Illuminate\Support\Facades\Route;
// Auth // Auth
Auth::routes(['register' => false, 'reset' => false, 'verify' => false]); Auth::routes(['register' => false, 'reset' => false, 'verify' => false]);
// Socialite
Route::prefix('auth')->name('socialite.')->group(function () {
Route::post('{provider}/redirect', [\App\Http\Controllers\Auth\SocialiteController::class, 'redirect'])->name('redirect');
Route::match(['get', 'post'], '{provider}/callback', [\App\Http\Controllers\Auth\SocialiteController::class, 'callback'])->name('callback');
Route::get('{provider}/metadata', [\App\Http\Controllers\Auth\SocialiteController::class, 'metadata'])->name('metadata');
});
// WebUI // WebUI
Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () { Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () {