diff --git a/.env.example b/.env.example index 4908720..caafd59 100644 --- a/.env.example +++ b/.env.example @@ -45,3 +45,10 @@ GOOGLE_RECAPTCHA_VERSION= # LDC Web Admin Secure LDC_WEB_API_KEY="MY_KEY" + +# SAML +SAML2_DEFAULT_TENANT_UUID= +SAML2_SP_CERT_x509= +SAML2_SP_CERT_PRIVATEKEY= +SAML2_CONTACT_TECHNICAL_EMAIL= +SAML2_CONTACT_SUPPORT_EMAIL= diff --git a/app/Console/Commands/CreateAdmin.php b/app/Console/Commands/CreateAdmin.php deleted file mode 100644 index 5b29f0d..0000000 --- a/app/Console/Commands/CreateAdmin.php +++ /dev/null @@ -1,177 +0,0 @@ -firstname = null; - $this->addFirstname(); - - $this->lastname = null; - $this->addLastname(); - - $this->email = null; - $this->addEmail(); - - $this->password = null; - $this->addPassword(); - - $user = new User([ - 'firstname' => $this->firstname, - 'lastname' => $this->lastname, - 'email' => $this->email, - 'password' => $this->password, - ]); - $user->saveOrFail(); - - $this->info('User created 👌'); - - return 0; - } - - /** - * Add a firstname to the user. - * - * @return void - */ - private function addFirstname(): void - { - while (is_null($this->firstname)) { - try { - $tmp = $this->ask('Type the wanted user firstname', 'Admin'); - $validator = Validator::make(['firstname' => $tmp], [ - 'firstname' => 'required|min:3|max:255' - ]); - $validator->validate(); - $this->firstname = $tmp; - } catch (ValidationException $e) { - $this->error(sprintf( - 'Please provide a valid firstname %s', - \implode(',', collect($e->errors())->flatten()->all()) - )); - continue; - } //end try - }; //end while - } - - /** - * Add a lastname to the user. - * - * @return void - */ - private function addLastname(): void - { - while (is_null($this->lastname)) { - try { - $tmp = $this->ask('Type the wanted user lastname', 'Saïtama'); - $validator = Validator::make(['lastname' => $tmp], [ - 'lastname' => 'required|min:3|max:255' - ]); - $validator->validate(); - $this->lastname = $tmp; - } catch (ValidationException $e) { - $this->error(sprintf( - 'Please provide a valid lastname %s', - \implode(',', collect($e->errors())->flatten()->all()) - )); - continue; - } //end try - }; //end while - } - - /** - * Add an email to the user. - * - * @return void - */ - private function addEmail(): void - { - while (is_null($this->email)) { - try { - $tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr'); - $validator = Validator::make(['email' => $tmp], [ - 'email' => 'required|unique:users,email|email:rfc,dns' - ]); - $validator->validate(); - $this->email = $tmp; - } catch (ValidationException $e) { - $this->error(sprintf( - 'Please provide a valid email %s', - \implode(',', collect($e->errors())->flatten()->all()) - )); - continue; - } //end try - }; //end while - } - - /** - * Add a password to the user. - * - * @return void - */ - private function addPassword(): void - { - while ( - ($this->password = $this->secret( - (!\is_null($this->password) ? 'Failed to confirm, ' : '') . 'Type the wanted user password' - )) !== $this->secret('Confirm the password') - ) { - continue; - }; //end while - } -} diff --git a/app/Console/Commands/UserCreate.php b/app/Console/Commands/UserCreate.php new file mode 100644 index 0000000..936849c --- /dev/null +++ b/app/Console/Commands/UserCreate.php @@ -0,0 +1,204 @@ +first_name = null; + $this->addFirstName(); + + $this->last_name = null; + $this->addLastName(); + + $this->email = null; + $this->addEmail(); + + $this->role = null; + $this->addRole(); + + $user = new User([ + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'email' => $this->email, + 'role' => $this->role, + 'published' => true, + ]); + $user->saveOrFail(); + + $this->info('User created 👌'); + } + + /** + * Add a first name to the user. + * + * @return void + */ + private function addFirstName(): void + { + while (is_null($this->first_name)) { + try { + $tmp = $this->ask('Type the wanted user first name', 'Admin'); + $validator = Validator::make(['first_name' => $tmp], [ + 'first_name' => 'required|min:3|max:255' + ]); + $validator->validate(); + $this->first_name = $tmp; + } catch (ValidationException $e) { + $this->error(sprintf( + 'Please provide a valid first name %s', + \implode(',', collect($e->errors())->flatten()->all()) + )); + continue; + } //end try + }; //end while + } + + /** + * Add a last name to the user. + * + * @return void + */ + private function addLastName(): void + { + while (is_null($this->last_name)) { + try { + $tmp = $this->ask('Type the wanted user last name', 'Saïtama'); + $validator = Validator::make(['last_name' => $tmp], [ + 'last_name' => 'required|min:3|max:255' + ]); + $validator->validate(); + $this->last_name = $tmp; + } catch (ValidationException $e) { + $this->error(sprintf( + 'Please provide a valid last_name %s', + \implode(',', collect($e->errors())->flatten()->all()) + )); + continue; + } //end try + }; //end while + } + + /** + * Add an email to the user. + * + * @return void + */ + private function addEmail(): void + { + while (is_null($this->email)) { + try { + $tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr'); + $validator = Validator::make(['email' => $tmp], [ + 'email' => 'required|unique:users,email|email:rfc,dns' + ]); + $validator->validate(); + $this->email = $tmp; + } catch (ValidationException $e) { + $this->error(sprintf( + 'Please provide a valid email %s', + \implode(',', collect($e->errors())->flatten()->all()) + )); + continue; + } //end try + }; //end while + } + + /** + * Add a role to the user. + * + * @return void + */ + private function addRole(): void + { + while (is_null($this->role)) { + try { + $tmp = $this->choice( + 'Select his role', + [ + Str::ucFirst(RoleEnum::conceptor->label()), + Str::ucFirst(RoleEnum::admin->label()), + Str::ucFirst(RoleEnum::visitor->label()) + ], + Str::ucFirst(RoleEnum::visitor->label()), + ); + if ($tmp === Str::ucFirst(RoleEnum::conceptor->label())) { + $tmp = RoleEnum::conceptor; + } + if ($tmp === Str::ucFirst(RoleEnum::admin->label())) { + $tmp = RoleEnum::admin; + } + if ($tmp === Str::ucFirst(RoleEnum::visitor->label())) { + $tmp = RoleEnum::visitor; + } + $validator = Validator::make(['role' => $tmp], [ + 'role' => 'required' + ]); + $validator->validate(); + $this->role = $tmp; + } catch (ValidationException $e) { + $this->error(sprintf( + 'Please choose from the selection %s', + \implode(',', collect($e->errors())->flatten()->all()) + )); + continue; + } //end try + }; //end while + } +} diff --git a/app/Enums/Users/RoleEnum.php b/app/Enums/Users/RoleEnum.php new file mode 100644 index 0000000..999bb6a --- /dev/null +++ b/app/Enums/Users/RoleEnum.php @@ -0,0 +1,25 @@ +name => 'enums.user-role.conceptor', + self::admin->name => 'enums.user-role.admin', + self::visitor->name => 'enums.user-role.visitor', + ]; +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 7023839..7dde76d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -40,6 +40,12 @@ class Kernel extends HttpKernel \App\Http\Middleware\SecureHeaders::class ], + 'saml' => [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + ], + 'api' => [ \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api', \Illuminate\Routing\Middleware\SubstituteBindings::class, diff --git a/app/Models/User.php b/app/Models/User.php index 9b9a177..3a3737a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,14 +2,29 @@ namespace App\Models; +use App\Enums\Users\RoleEnum; +use App\Traits\Models\ActivityLog; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Str; +/** + * @property string $first_name Firstname. + * @property string $last_name Lastname. + * @property string $email Email. + * @property string $remember_token Remember_token. + * @property \App\Enums\Users\RoleEnum $role Role. + * @property \Illuminate\Support\Carbon $last_connection Last_connection. + * @property \Illuminate\Support\Carbon $published Published status. + * @property-read \Illuminate\Support\Carbon $created_at Created date. + * @property-read \Illuminate\Support\Carbon $updated_at Updated date. + * + * @property-read \App\Models\ActivityLog $activityLogs Activities logs HasMany relationship. + */ class User extends Authenticatable { + use ActivityLog; use HasFactory; use Notifiable; @@ -19,10 +34,11 @@ class User extends Authenticatable * @var array */ protected $fillable = [ - 'firstname', - 'lastname', + 'first_name', + 'last_name', 'email', - 'password', + 'role', + 'published', ]; /** @@ -31,7 +47,6 @@ class User extends Authenticatable * @var array */ protected $hidden = [ - 'password', 'remember_token', ]; @@ -41,8 +56,9 @@ class User extends Authenticatable * @var array */ protected $casts = [ - 'email_verified_at' => 'datetime', - 'last_connection' => 'datetime', + 'role' => RoleEnum::class, + 'last_connection' => 'date', + 'published' => 'boolean', ]; /** @@ -50,20 +66,28 @@ class User extends Authenticatable * * @return void */ - protected static function booted() + protected static function booted(): void { - // * EVENT CLOSURES. - static::creating(function (self $user) { - if (Hash::needsRehash($user->password)) { - $user->password = Hash::make($user->password); - } - $user->remember_token = Str::random(10); + static::creating(function (self $model) { + // * Prevent user elevation privileges + throw_if( + auth('backend')->user() and auth('backend')->user()->role->value() > $model->role->value(), + AuthorizationException::class + ); }); - static::updating(function (self $user) { - if (Hash::needsRehash($user->password)) { - $user->password = Hash::make($user->password); + static::updating(function (self $model) { + if (optional(auth('backend')->user())->getKey() === $model->getKey()) { + \validator( + ['published' => $model->published], + ['published' => 'required|boolean|accepted'], + ['published.accepted' => trans('crud.messages.user_unpublished')], + )->validate(); } - $user->remember_token = Str::random(10); + // * Prevent user elevation privileges + throw_if( + auth('backend')->user() and auth('backend')->user()->role->value() > $model->role->value(), + AuthorizationException::class + ); }); } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 8eaf20c..35745bf 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -5,6 +5,13 @@ namespace App\Providers; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; +use Illuminate\Support\Facades\Event; +use App\Models\User; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Session; +use Slides\Saml2\Events\SignedIn; +use Slides\Saml2\Events\SignedOut; class EventServiceProvider extends ServiceProvider { @@ -19,15 +26,6 @@ class EventServiceProvider extends ServiceProvider ], ]; - /** - * Register any events for your application. - * - * @return void - */ - public function boot(): void - { - } - /** * Determine if events and listeners should be automatically discovered. * @@ -37,4 +35,49 @@ class EventServiceProvider extends ServiceProvider { return false; } + + /** + * Register any events for your application. + * + * @return void + */ + public function boot() + { + Event::listen('Slides\Saml2\Events\SignedIn', function (SignedIn $event) { + + $messageId = $event->getAuth()->getLastMessageId(); + + $fromCache = Cache::get(config('cache.prefix') . "_saml_message_id_" . $messageId); + if ($fromCache) { + logger()->error( + 'SAML MESSAGE ID HANDLER', + ['error' => 'Message ID : ' . $messageId . " replayed !"] + ); + abort(403); + } else { + Cache::put(config('cache.prefix') . "_saml_message_id_" . $messageId, $messageId, 28800); + // Cache for 8h. + } + + $user = $event->getSaml2User(); + $email = $user->getNameId(); + + $lUser = User::where('email', '=', $email)->first(); + + if (!$lUser) { + abort(403); + } + + Auth::guard('backend')->login($lUser); + session(['sessionIndex' => $user->getSessionIndex()]); + $lUser->last_connection = now(); + $lUser->saveQuietly(); + }); + + //phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + Event::listen('Slides\Saml2\Events\SignedOut', function (SignedOut $event) { + Auth::guard('backend')->logout(); + Session::save(); + }); + } } diff --git a/composer.json b/composer.json index de6805b..2d7f647 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "description": "Site de la marque Lionor", "require": { "php": "^8.3", + "24slides/laravel-saml2": "^2.4", "axlon/laravel-postal-code-validation": "^3.3", "carlos-meneses/laravel-mpdf": "^2.1", "diglactic/laravel-breadcrumbs": "^9.0", diff --git a/composer.lock b/composer.lock index 2b62fa0..fe97f4c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,83 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5fa930c4320067f6816f00faa0a3a5f8", + "content-hash": "5808ffd0b5badfc5737bf3ff38631a77", "packages": [ + { + "name": "24slides/laravel-saml2", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/24Slides/laravel-saml2.git", + "reference": "d3d8ac6cc456a2bd3e24e0d05c2dc3303c2938c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/24Slides/laravel-saml2/zipball/d3d8ac6cc456a2bd3e24e0d05c2dc3303c2938c4", + "reference": "d3d8ac6cc456a2bd3e24e0d05c2dc3303c2938c4", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "illuminate/console": "~5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/database": "~5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "~5.4|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "onelogin/php-saml": "^3.0|^4.0", + "php": ">=7.1", + "ramsey/uuid": "^3.8|^4.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.9", + "phpunit/phpunit": "^7.5|^9.0", + "squizlabs/php_codesniffer": "^2.3" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Saml2": "Slides\\Saml2\\Facades\\Auth" + }, + "providers": [ + "Slides\\Saml2\\ServiceProvider" + ] + }, + "branch-aliases": { + "dev-master": "2.0.8-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Slides\\Saml2\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Artem Brezhnev", + "email": "brezzhnev@gmail.com" + } + ], + "description": "SAML2 Service Provider integration to your Laravel 5.4+ application, based on OneLogin toolkit", + "homepage": "https://github.com/24slides/laravel-saml2", + "keywords": [ + "SAML2", + "SSO", + "laravel", + "onelogin", + "saml" + ], + "support": { + "issues": "https://github.com/24Slides/laravel-saml2/issues", + "source": "https://github.com/24Slides/laravel-saml2/tree/2.4.1" + }, + "time": "2025-02-04T13:32:39+00:00" + }, { "name": "axlon/laravel-postal-code-validation", "version": "v3.8.0", @@ -3897,6 +3972,70 @@ ], "time": "2024-09-09T07:06:30+00:00" }, + { + "name": "onelogin/php-saml", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SAML-Toolkits/php-saml.git", + "reference": "d3b5172f137db2f412239432d77253ceaaa1e939" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SAML-Toolkits/php-saml/zipball/d3b5172f137db2f412239432d77253ceaaa1e939", + "reference": "d3b5172f137db2f412239432d77253ceaaa1e939", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "robrichards/xmlseclibs": "^3.1" + }, + "require-dev": { + "pdepend/pdepend": "^2.8.0", + "php-coveralls/php-coveralls": "^2.0", + "phploc/phploc": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "phpunit/phpunit": "^9.5", + "sebastian/phpcpd": "^4.0 || ^5.0 || ^6.0 ", + "squizlabs/php_codesniffer": "^3.5.8" + }, + "suggest": { + "ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs", + "ext-dom": "Install xml lib", + "ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)", + "ext-zlib": "Install zlib" + }, + "type": "library", + "autoload": { + "psr-4": { + "OneLogin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP SAML Toolkit", + "homepage": "https://github.com/SAML-Toolkits/php-saml", + "keywords": [ + "Federation", + "SAML2", + "SSO", + "identity", + "saml" + ], + "support": { + "email": "sixto.martin.garcia@gmail.com", + "issues": "https://github.com/onelogin/SAML-Toolkits/issues", + "source": "https://github.com/onelogin/SAML-Toolkits/" + }, + "funding": [ + { + "url": "https://github.com/SAML-Toolkits", + "type": "github" + } + ], + "time": "2024-05-30T15:10:40+00:00" + }, { "name": "paragonie/random_compat", "version": "v9.99.100", @@ -4906,6 +5045,48 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "robrichards/xmlseclibs", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/robrichards/xmlseclibs.git", + "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/2bdfd742624d739dfadbd415f00181b4a77aaf07", + "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">= 5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "RobRichards\\XMLSecLibs\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "A PHP library for XML Security", + "homepage": "https://github.com/robrichards/xmlseclibs", + "keywords": [ + "security", + "signature", + "xml", + "xmldsig" + ], + "support": { + "issues": "https://github.com/robrichards/xmlseclibs/issues", + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.3" + }, + "time": "2024-11-20T21:13:56+00:00" + }, { "name": "sentry/sentry", "version": "4.10.0", @@ -12050,12 +12231,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.3" }, - "platform-dev": {}, - "plugin-api-version": "2.6.0" + "platform-dev": [], + "plugin-api-version": "2.3.0" } diff --git a/config/app.php b/config/app.php index 4937fa8..69dcc10 100644 --- a/config/app.php +++ b/config/app.php @@ -56,8 +56,6 @@ return [ 'url' => env('APP_URL', 'http://localhost'), - 'asset_url' => env('ASSET_URL'), - /* |-------------------------------------------------------------------------- | Application Timezone diff --git a/config/breadcrumbs.php b/config/breadcrumbs.php index aefdaad..69594ae 100644 --- a/config/breadcrumbs.php +++ b/config/breadcrumbs.php @@ -23,7 +23,7 @@ return [ | */ - 'view' => 'modules/breadcrumb/breadcrumb-layout', + 'view' => 'modules.breadcrumbs.breadcrumb-layout', /* |-------------------------------------------------------------------------- diff --git a/config/saml2.php b/config/saml2.php new file mode 100644 index 0000000..7687a41 --- /dev/null +++ b/config/saml2.php @@ -0,0 +1,397 @@ + \Slides\Saml2\Models\Tenant::class, + + /* + |-------------------------------------------------------------------------- + | Use built-in routes + |-------------------------------------------------------------------------- + | + | If "useRoutes" set to true, the package defines five new routes: + | + | Method | URI | Name + | -------|---------------------------------|------------------ + | POST | {routesPrefix}/{uuid}/acs | saml.acs + | GET | {routesPrefix}/{uuid}/login | saml.login + | GET | {routesPrefix}/{uuid}/logout | saml.logout + | GET | {routesPrefix}/{uuid}/metadata | saml.metadata + | GET | {routesPrefix}/{uuid}/sls | saml.sls + | + */ + + 'useRoutes' => true, + + /* + |-------------------------------------------------------------------------- + | Built-in routes prefix + |-------------------------------------------------------------------------- + | + | Here you may define the prefix for built-in routes. + | + */ + + 'routesPrefix' => '/saml2', + + /* + |-------------------------------------------------------------------------- + | Middle groups to use for the SAML routes + |-------------------------------------------------------------------------- + | + | Note, Laravel 5.2 requires a group which includes StartSession + | + */ + + 'routesMiddleware' => ['saml'], + + /* + |-------------------------------------------------------------------------- + | Signature validation + |-------------------------------------------------------------------------- + | + | Set to true if you want to use parameters from $_SERVER to validate the signature. + | + */ + + 'retrieveParametersFromServer' => false, + + /* + |-------------------------------------------------------------------------- + | Login redirection URL. + |-------------------------------------------------------------------------- + | + | The redirection URL after successful login. + | + */ + + 'loginRoute' => '/bo', + + /* + |-------------------------------------------------------------------------- + | Logout redirection URL. + |-------------------------------------------------------------------------- + | + | The redirection URL after successful logout. + | + */ + + 'logoutRoute' => '/bo', + + + /* + |-------------------------------------------------------------------------- + | Login error redirection URL. + |-------------------------------------------------------------------------- + | + | The redirection URL after login failing. + | + */ + + 'errorRoute' => '/bo/login-error', + + /* + |-------------------------------------------------------------------------- + | Strict mode. + |-------------------------------------------------------------------------- + | + | If 'strict' is True, then the PHP Toolkit will reject unsigned + | or unencrypted messages if it expects them signed or encrypted + | Also will reject the messages if not strictly follow the SAML + | standard: Destination, NameId, Conditions... are validated too. + | + */ + + 'strict' => true, + + /* + |-------------------------------------------------------------------------- + | Debug mode. + |-------------------------------------------------------------------------- + | + | When enabled, errors must be printed. + | + */ + + 'debug' => env('SAML2_DEBUG', env('APP_DEBUG', false)), + + /* + |-------------------------------------------------------------------------- + | Whether to use `X-Forwarded-*` headers to determine port/domain/protocol. + |-------------------------------------------------------------------------- + | + | If 'proxyVars' is True, then the Saml lib will trust proxy headers + | e.g X-Forwarded-Proto / HTTP_X_FORWARDED_PROTO. This is useful if + | your application is running behind a load balancer which terminates SSL. + | + */ + + 'proxyVars' => true, + + /* + |-------------------------------------------------------------------------- + | Service Provider configuration. + |-------------------------------------------------------------------------- + | + | General setting of the service provider. + | + */ + + 'sp' => [ + + /* + |-------------------------------------------------------------------------- + | NameID format. + |-------------------------------------------------------------------------- + | + | Specifies constraints on the name identifier to be used to + | represent the requested subject. + | + */ + + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + + /* + |-------------------------------------------------------------------------- + | SP Certificates. + |-------------------------------------------------------------------------- + | + | Usually x509cert and privateKey of the SP are provided by files placed at + | the certs folder. But we can also provide them with the following parameters. + | + */ + + 'x509cert' => env('SAML2_SP_CERT_x509', ''), + 'privateKey' => env('SAML2_SP_CERT_PRIVATEKEY', ''), + + /* + |-------------------------------------------------------------------------- + | Identifier (URI) of the SP entity. + |-------------------------------------------------------------------------- + | + | Leave blank to use the 'saml.metadata' route. + | + */ + + 'entityId' => env('SAML2_SP_ENTITYID', ''), + + /* + |-------------------------------------------------------------------------- + | The Assertion Consumer Service (ACS) URL. + |-------------------------------------------------------------------------- + | + | URL Location where the from the IdP will be returned, using HTTP-POST binding. + | Leave blank to use the 'saml.acs' route. + | + */ + + 'assertionConsumerService' => [ + 'url' => '', + ], + + /* + |-------------------------------------------------------------------------- + | The Single Logout Service URL. + |-------------------------------------------------------------------------- + | + | Specifies info about where and how the message MUST be + | returned to the requester, in this case our SP. + | + | URL Location where the from the IdP will be returned, using HTTP-Redirect binding. + | Leave blank to use the 'saml.sls' route. + | + */ + + 'singleLogoutService' => [ + 'url' => '' + ], + ], + + /* + |-------------------------------------------------------------------------- + | OneLogin security settings. + |-------------------------------------------------------------------------- + | + | + | + */ + + 'security' => [ + + 'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', + 'digestAlgorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256', + + /* + |-------------------------------------------------------------------------- + | NameId encryption + |-------------------------------------------------------------------------- + | + | Indicates that the nameID of the sent by this SP + | will be encrypted. + | + */ + + 'nameIdEncrypted' => true, + + /* + |-------------------------------------------------------------------------- + | AuthnRequest signage + |-------------------------------------------------------------------------- + | + | Indicates whether the messages sent by + | this SP will be signed. The Metadata of the SP will offer this info + | + */ + + 'authnRequestsSigned' => true, + + /* + |-------------------------------------------------------------------------- + | Logout request signage + |-------------------------------------------------------------------------- + | + | Indicates whether the messages sent by this SP + | will be signed. + | + */ + + 'logoutRequestSigned' => true, + + /* + |-------------------------------------------------------------------------- + | Logout response signage + |-------------------------------------------------------------------------- + | + | Indicates whether the messages sent by this SP + | will be signed. + | + */ + + 'logoutResponseSigned' => true, + + /* + |-------------------------------------------------------------------------- + | Whether need to sign metadata. + |-------------------------------------------------------------------------- + | + | The possible values: + | - false + | - true (use certs) + | - array: + | ``` + | [ + | 'keyFileName' => 'metadata.key', + | 'certFileName' => 'metadata.crt' + | ] + | ``` + | + */ + + 'signMetadata' => true, + + /* + |-------------------------------------------------------------------------- + | Requirement to sign messages. + |-------------------------------------------------------------------------- + | + | Indicates a requirement for the , and + | elements received by this SP to be signed. + | + */ + + 'wantMessagesSigned' => true, + + /* + |-------------------------------------------------------------------------- + | Requirement to sign assertion elements. + |-------------------------------------------------------------------------- + | + | Indicates a requirement for the elements received by + | this SP to be signed. + | + */ + + 'wantAssertionsSigned' => true, + + /* + |-------------------------------------------------------------------------- + | Requirement to encrypt NameID. + |-------------------------------------------------------------------------- + | + | Indicates a requirement for the NameID received by this SP to be encrypted. + | + */ + + 'wantNameIdEncrypted' => false, + + /* + |-------------------------------------------------------------------------- + | Authentication context. + |-------------------------------------------------------------------------- + | + | Set to false and no AuthContext will be sent in the AuthNRequest, + | + | Set true or don't present this parameter and you will get an + | AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + | + | Set an array with the possible auth context values: + | ['urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'] + | + */ + + 'requestedAuthnContext' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Contact information. + |-------------------------------------------------------------------------- + | + | It is recommended to supply a technical and support contacts. + | + */ + + 'contactPerson' => [ + 'technical' => [ + 'givenName' => env('SAML2_CONTACT_TECHNICAL_NAME', env('APP_NAME', 'Technical')), + 'emailAddress' => env('SAML2_CONTACT_TECHNICAL_EMAIL', 'no@reply.com') + ], + 'support' => [ + 'givenName' => env('SAML2_CONTACT_SUPPORT_NAME', env('APP_NAME', 'Support')), + 'emailAddress' => env('SAML2_CONTACT_SUPPORT_EMAIL', 'no@reply.com') + ], + ], + + /* + |-------------------------------------------------------------------------- + | Organization information. + |-------------------------------------------------------------------------- + | + | The info in en_US lang is recommended, add more if required. + | + */ + + 'organization' => [ + 'en-US' => [ + 'name' => env('SAML2_ORGANIZATION_NAME', env('APP_NAME', 'Name')), + 'displayname' => env('SAML2_ORGANIZATION_NAME', env('APP_NAME', 'Name')), + 'url' => env('SAML2_ORGANIZATION_URL', env('APP_URL', 'http://url')) + ], + ], + + /** + * Default tenant UUID + */ + + 'default_tenant_uuid' => env('SAML2_DEFAULT_TENANT_UUID', null) + +]; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 2ae68c4..2ea8002 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -15,8 +15,8 @@ class UserFactory extends Factory public function definition() { return [ - 'firstname' => $this->faker->firstName(), - 'lastname' => $this->faker->lastName(), + 'first_name' => $this->faker->firstName(), + 'last_name' => $this->faker->lastName(), 'email' => $this->faker->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', diff --git a/database/migrations/2025_03_07_094600_alter_users_for_saml.php b/database/migrations/2025_03_07_094600_alter_users_for_saml.php new file mode 100644 index 0000000..7e45507 --- /dev/null +++ b/database/migrations/2025_03_07_094600_alter_users_for_saml.php @@ -0,0 +1,50 @@ +dropColumn('email_verified_at'); + $table->dropColumn('password'); + // Rename. + $table->renameColumn('firstname', 'first_name'); + $table->renameColumn('lastname', 'last_name'); + // New columns. + $table->unsignedTinyInteger('role')->after('remember_token') + ->comment('Role of the user who defines the rights/access.'); + $table->boolean('published')->after('role') + ->comment('Is the user published ?'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + // New columns. + $table->timestamp('email_verified_at')->nullable()->after('email'); + $table->string('password')->after('email_verified_at'); + // Rename. + $table->renameColumn('first_name', 'firstname'); + $table->renameColumn('last_name', 'lastname'); + // Drop. + $table->dropColumn('role'); + $table->dropColumn('published'); + }); + } +}; diff --git a/manifest.yaml b/manifest.yaml index 6070697..3087509 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -28,6 +28,12 @@ data: GOOGLE_RECAPTCHA_VERSION: "${APP_GOOGLE_RECAPTCHA_VERSION}" SENTRY_LARAVEL_DSN: "${APP_SENTRY_LARAVEL_DSN}" MATOMO_SITE_ID: "${APP_MATOMO_SITE_ID}" + SAML2_SP_CERT_x509: "${APP_SAML2_SP_CERT_x509}" + SAML2_SP_CERT_PRIVATEKEY: "${APP_SAML2_SP_CERT_PRIVATEKEY}" + SAML2_CONTACT_TECHNICAL_EMAIL: "${APP_SAML2_CONTACT_TECHNICAL_EMAIL}" + SAML2_CONTACT_SUPPORT_EMAIL: "${APP_SAML2_CONTACT_SUPPORT_EMAIL}" + SAML2_DEFAULT_TENANT_UUID: "${APP_SAML2_DEFAULT_TENANT_UUID}" + --- apiVersion: v1 diff --git a/resources/lang/en/enums.php b/resources/lang/en/enums.php index 0b67a5f..aa29bed 100644 --- a/resources/lang/en/enums.php +++ b/resources/lang/en/enums.php @@ -1,3 +1,9 @@ [ + "conceptor" => "conceptor", + "admin" => "admin", + "visitor" => "visitor", + ] +]; diff --git a/resources/lang/fr/enums.php b/resources/lang/fr/enums.php index 0b67a5f..117eb10 100644 --- a/resources/lang/fr/enums.php +++ b/resources/lang/fr/enums.php @@ -1,3 +1,9 @@ [ + "conceptor" => "concepteur", + "admin" => "administrateur", + "visitor" => "visiteur", + ], +]; diff --git a/resources/ts/bo/utilities/fontawesome.ts b/resources/ts/bo/utilities/fontawesome.ts index d4b9149..658784a 100644 --- a/resources/ts/bo/utilities/fontawesome.ts +++ b/resources/ts/bo/utilities/fontawesome.ts @@ -8,6 +8,7 @@ import { faArrowRight, faArrowRightFromBracket, faArrowUp, + faArrowUpRightFromSquare, faArrowsAltH, faArrowsAltV, faBackspace, @@ -81,6 +82,7 @@ library.add(faBan); library.add(faCircleExclamation); library.add(faCircleInfo); library.add(faPaintRoller); +library.add(faArrowUpRightFromSquare); // Front library.add(faChevronLeft); diff --git a/resources/views/bo/auth/login.blade.php b/resources/views/bo/auth/login.blade.php index a55d338..b9fd7af 100644 --- a/resources/views/bo/auth/login.blade.php +++ b/resources/views/bo/auth/login.blade.php @@ -3,65 +3,23 @@ @section('description', __('Formulaire d\'authentification au back-office.')) @section('content') -
-
-
-
-
{{ __('Connexion') }}
-
-
- @csrf -
- -
- - @error('email') - - {{ $message }} - - @enderror -
-
-
- -
- - @error('password') - - {{ $message }} - - @enderror -
-
-
-
-
- - -
-
-
- -
+
- @endsection diff --git a/resources/views/bo/layout.blade.php b/resources/views/bo/layout.blade.php index 0766c29..151d3fd 100644 --- a/resources/views/bo/layout.blade.php +++ b/resources/views/bo/layout.blade.php @@ -40,7 +40,7 @@