feat: Added SAML authentication

This commit is contained in:
Noah Lancien 2025-03-06 16:06:54 +01:00
parent ab85b33052
commit ecaff7a068
24 changed files with 798 additions and 215 deletions

View file

@ -47,3 +47,10 @@ GOOGLE_RECAPTCHA_VERSION=
# Admin secure
LDC_WEB_API_KEY=
# SAML
SAML2_DEFAULT_TENANT_UUID=
SAML2_SP_CERT_x509=
SAML2_SP_CERT_PRIVATEKEY=
SAML2_CONTACT_TECHNICAL_EMAIL=
SAML2_CONTACT_SUPPORT_EMAIL=

View file

@ -46,13 +46,6 @@ class UserCreate extends Command
*/
protected $email;
/**
* The password of the new user.
*
* @var string|null
*/
protected $password;
/**
* The role of the new user.
*
@ -76,9 +69,6 @@ class UserCreate extends Command
$this->email = null;
$this->addEmail();
$this->password = null;
$this->addPassword();
$this->role = null;
$this->addRole();
@ -86,7 +76,6 @@ class UserCreate extends Command
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email' => $this->email,
'password' => $this->password,
'role' => $this->role,
'published' => true,
'published_at' => Carbon::now(),
@ -173,22 +162,6 @@ class UserCreate extends Command
}; //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
}
/**
* Add a role to the user.
*
@ -198,12 +171,10 @@ class UserCreate extends Command
{
while (is_null($this->role)) {
try {
$tmp = $this->choice(
$tmp = $this->choice(
'Select his role',
[RoleEnum::conceptor->label(), RoleEnum::admin->label(), RoleEnum::visitor->label()],
RoleEnum::visitor->label(),
$maxAttempts = null,
$allowMultipleSelections = false
);
if ($tmp === RoleEnum::conceptor->label()) {
$tmp = RoleEnum::conceptor;

View file

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

View file

@ -7,7 +7,6 @@ use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\Password;
class StoreUserRequest extends FormRequest
{
@ -44,14 +43,6 @@ class StoreUserRequest extends FormRequest
'first_name' => 'required|string|min:3|max:255',
'last_name' => 'required|string|min:3|max:255',
'email' => 'required|email:rfc,strict,dns,spoof,filter|unique:users,email',
'password' => [
'required', 'required_with:password_confirmation', 'confirmed', 'max:255', Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
'role' => ['required', new Enum(RoleEnum::class)],
'published' => 'required|boolean',
];
@ -68,7 +59,6 @@ class StoreUserRequest extends FormRequest
'first_name' => trans('validation.attributes.first_name'),
'last_name' => trans('validation.attributes.last_name'),
'email' => trans('validation.attributes.email'),
'password' => trans('validation.attributes.password'),
'role' => trans('validation.attributes.role'),
'published' => trans('validation.custom.publication')
];

View file

@ -4,7 +4,6 @@ namespace App\Http\Requests\Bo\Users;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
class UpdateUserRequest extends StoreUserRequest
{
@ -26,11 +25,7 @@ class UpdateUserRequest extends StoreUserRequest
protected function prepareForValidation(): void
{
$this->merge([
'published' => $this->boolean('published'),
'password' => $this->input('password') ??
\optional($this->route('user'))->password,
'password_confirmation' => $this->input('password_confirmation') ??
\optional($this->route('user'))->password
'published' => $this->boolean('published'),
]);
}
@ -47,14 +42,6 @@ class UpdateUserRequest extends StoreUserRequest
'email:rfc,strict,dns,spoof,filter',
Rule::unique('users', 'email')->ignore(\optional($this->route('user'))->getRouteKey())
],
'password' => [
'required', 'required_with:password_confirmation', 'confirmed', 'max:255', Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
]);
}
}

View file

@ -8,13 +8,11 @@ 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;
/**
* @property string $first_name Firstname.
* @property string $last_name Lastname.
* @property string $email Email.
* @property string $password Password.
* @property \App\Enums\Users\RoleEnum $role Role.
* @property \Illuminate\Support\Carbon $published Publication status.
* @property \Illuminate\Support\Carbon $published_at Publication status update date.
@ -38,7 +36,6 @@ class User extends Authenticatable
'first_name',
'last_name',
'email',
'password',
'role',
'published',
'published_at',
@ -50,7 +47,6 @@ class User extends Authenticatable
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
@ -60,9 +56,9 @@ class User extends Authenticatable
* @var array<string, string>
*/
protected $casts = [
'role' => RoleEnum::class,
'email_verified_at' => 'datetime',
'published' => 'boolean',
'role' => RoleEnum::class,
'last_connection' => 'date',
'published' => 'boolean',
];
/**
@ -73,7 +69,6 @@ class User extends Authenticatable
protected static function booted(): void
{
static::creating(function (self $model) {
self::updatePassword($model);
// * Prevent user elevation privileges
throw_if(
auth('backend')->user() and auth('backend')->user()->role->value() > $model->role->value(),
@ -81,7 +76,6 @@ class User extends Authenticatable
);
});
static::updating(function (self $model) {
self::updatePassword($model);
if (optional(auth('backend')->user())->getKey() === $model->getKey()) {
\validator(
['published' => $model->published],
@ -96,31 +90,4 @@ class User extends Authenticatable
);
});
}
// * METHODS
/**
* Update password.
*
* @param self $model
* @return void
*/
private static function updatePassword(self $model): void
{
if (Hash::needsRehash($model->password)) {
$model->password = Hash::make($model->password);
}
}
// * RELATIONS
/**
* Activities logs One-to-many relationship.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function activityLogs(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ActivityLog::class);
}
}

View file

@ -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
{
@ -28,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();
});
}
}

View file

@ -2,6 +2,7 @@
namespace App\Traits\Models;
use App\Models\ActivityLog as ModelsActivityLog;
use App\Observers\ModelObserver;
/**
@ -20,4 +21,16 @@ trait ActivityLog
{
static::observe(ModelObserver::class);
}
// * RELATIONS
/**
* Activities logs One-to-many relationship.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function activityLogs(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ModelsActivityLog::class);
}
}

View file

@ -5,6 +5,7 @@
"description": "Agis website.",
"require": {
"php": "^8.3",
"24slides/laravel-saml2": "^2.4",
"diglactic/laravel-breadcrumbs": "^9.0",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^11.0",

183
composer.lock generated
View file

@ -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": "65ad4d664d2bf0c3d17b4058bff1a66e",
"content-hash": "d40ecb6782259f44460e61322fe17b65",
"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": "brick/math",
"version": "0.12.3",
@ -3188,6 +3263,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": "phpoption/phpoption",
"version": "1.9.3",
@ -4076,6 +4215,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",

View file

@ -56,8 +56,6 @@ return [
'url' => env('APP_URL', 'http://localhost'),
'asset_url' => env('ASSET_URL'),
/*
|--------------------------------------------------------------------------
| Application Timezone

397
config/saml2.php Normal file
View file

@ -0,0 +1,397 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Tenant Model
|--------------------------------------------------------------------------
|
| This will allow you to override the tenant model with your own.
|
*/
'tenantModel' => \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 <Response> 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 <Logout Response> message MUST be
| returned to the requester, in this case our SP.
|
| URL Location where the <Response> 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 <samlp:logoutRequest> sent by this SP
| will be encrypted.
|
*/
'nameIdEncrypted' => true,
/*
|--------------------------------------------------------------------------
| AuthnRequest signage
|--------------------------------------------------------------------------
|
| Indicates whether the <samlp:AuthnRequest> 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 <samlp:logoutRequest> messages sent by this SP
| will be signed.
|
*/
'logoutRequestSigned' => true,
/*
|--------------------------------------------------------------------------
| Logout response signage
|--------------------------------------------------------------------------
|
| Indicates whether the <samlp:logoutResponse> 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 <samlp:Response>, <samlp:LogoutRequest> and
| <samlp:LogoutResponse> elements received by this SP to be signed.
|
*/
'wantMessagesSigned' => true,
/*
|--------------------------------------------------------------------------
| Requirement to sign assertion elements.
|--------------------------------------------------------------------------
|
| Indicates a requirement for the <saml:Assertion> 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)
];

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('email_verified_at');
$table->dropColumn('password');
$table->date('last_connection')->nullable()->after('role');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->timestamp('email_verified_at')->nullable()->after('email');
$table->string('password')->after('email_verified_at');
$table->dropColumn('last_connection');
});
}
};

View file

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

View file

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

View file

@ -9,57 +9,14 @@
<div class="card">
<div class="card-header">{{ __('Connexion') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('bo.login') }}">
@csrf
<div class="row mb-3">
<label class="col-md-4 col-form-label text-md-end" for="email">{{ __('Adresse Email') }}</label>
<div class="col-md-6">
<input class="form-control @error('email') is-invalid @enderror" id="email" name="email"
type="email" value="{{ old('email') }}" autocomplete="email" autofocus required>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<div class="row">
<div class="col text-center">
<a class="btn btn-primary" href="{{ route('saml.login', ['uuid' => config('saml2.default_tenant_uuid')]) }}">
<span>{{ __('Connexion avec UWIL Support') }}</span>
<i class="fa-solid fa-arrow-right-from-bracket"></i>
</a>
</div>
<div class="row mb-3">
<label class="col-md-4 col-form-label text-md-end" for="password">{{ __('Mot de passe') }}</label>
<div class="col-md-6">
<input class="form-control @error('password') is-invalid @enderror" id="password" name="password"
type="password" autocomplete="current-password" required>
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<div class="col-md-6 offset-md-4">
<div class="form-check">
<input class="form-check-input" id="remember" name="remember" type="checkbox"
{{ old('remember') ? 'checked' : '' }}>
<label class="form-check-label user-select-none" for="remember">
{{ __('Se souvenir de moi') }}
</label>
</div>
</div>
</div>
<div class="row mb-0">
<div class="col-md-8 offset-md-4">
<button class="btn btn-primary" type="submit">
<span>{{ __('Connexion') }}</span>
<i class="fa-solid fa-arrow-right-from-bracket"></i>
</button>
@if (Route::has('bo.password.request'))
<a class="btn btn-link" href="{{ route('bo.password.request') }}">
{{ __('Mot de passe oublié ?') }}
</a>
@endif
</div>
</div>
</form>
</div>
</div>
</div>
</div>

View file

@ -37,7 +37,7 @@
@vite(['resources/sass/bo/bo.scss'])
@if (config('app.debug'))
<script nonce="{{ $nonce }}">
<script nonce="{{ $nonce ?? '' }}">
window.vueDebug = true;
</script>
@endif

View file

@ -12,19 +12,29 @@
<h1 class="h2 fw-bold">{{ __('Dashboard') }}</h1>
</div>
<div class="card-body">
<h5 class="card-title">
<h5 class="card-title fw-bold py-2">
{{ __('Bonjour :first_name :last_name !', [
'first_name' => auth('backend')->user()->first_name,
'last_name' => auth('backend')->user()->last_name,
]) }}
</h5>
<p class="card-text m-0">
<p class="card-text">
{{ __('Bienvenue dans le back-office, où vous pouvez gérer votre application web.') }}
</p>
<p class="card-text">
{{ __('Pour accéder à vos statistiques') }} :
<a class="btn btn-primary btn-sm" data-bs-toggle="tooltip" href="https://report.uwil.fr"
title="{{ __('Accéder à notre outil de ticketing') }}" target="blank">
{{ __('UWIL Report') }}
<i class="fa-solid fa-arrow-up-right-from-square ms-1"></i>
</a>
<br>
{{ __('Pour tout problème ou évolution souhaitée, veuillez nous contacter via notre outil : ') }}
<a data-bs-toggle="tooltip" href="https://support.uwil.fr/" title="{{ __('Accéder à notre outil de ticketing') }}"
target="blank">https://support.uwil.fr/</a>.
target="blank">
{{ __('UWIL Support') }}
<i class="fa-solid fa-arrow-up-right-from-square ms-1"></i>
</a>
</p>
</div>
</div>

View file

@ -0,0 +1,22 @@
@extends('fo.layout')
@section('title', __('Une erreur est survenue lors de la connexion.'))
@section('description', __('Il semble qu\'il y ait des erreurs pour répondre à votre demande.'))
@section('breadcrumb', 'no')
@section('bodyClass', 'bg-white-wood')
@section('content')
<div class="container my-5 py-5">
<div class="row">
<div class="col text-center">
<h1>{{ str(__('Une erreur est survenue lors de la connexion.'))->upper() }}</h1>
<h4 class="h4 my-4">
{{ __('Veuillez contacter le support.') }}
</h4>
<a class="btn btn-secondary" href="{{ route('fo.home.show') }}" title="{{ __('Accéder à la page d\'accueil') }}">
{{ str(__('retour à la page d\'accueil'))->upper() }}
</a>
</div>
</div>
</div>
@endsection

View file

@ -89,39 +89,6 @@
</div>
</fieldset>
</div>
{{-- PASSWORD --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ __('Mot de passe') }}</legend>
<div class="row">
<div class="col-12 col-md-6 mb-3">
@include('bo.partials.inputs.varchar-password', [
'id' => 'userPassword',
'label' => __('Mot de passe de l\':model', ['model' => __('models.classes.user')]),
'name' => 'password',
'hasWordCounter' => true,
'required' => !$userModel->exists,
'model' => null,
'helper' => __(
'Doît respecter certaines contraintes (8 caractères minimum, lettres, chiffres, caractères spéciaux).'),
])
</div>
<div class="col-12 col-md-6 mb-3">
@include('bo.partials.inputs.varchar-password', [
'id' => 'userPassword_confirmation',
'label' => __('Confirmation du mot de passe de l\':model', [
'model' => __('models.classes.user'),
]),
'name' => 'password_confirmation',
'hasWordCounter' => true,
'required' => !$userModel->exists,
'model' => null,
'helper' => __('Doît correspondre au mot de passe saisi précédemment.'),
])
</div>
</div>
</fieldset>
</div>
{{-- STATUS --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
@ -156,17 +123,6 @@
event.preventDefault();
submit.click('');
})
// If we edit the password, the confirmation is required.
const passwordInput = document.getElementById('userPassword');
const confirmPasswordInput = document.getElementById('userPassword_confirmation');
passwordInput.addEventListener('input', () => {
if (passwordInput.value !== '') {
confirmPasswordInput.required = true;
} else {
confirmPasswordInput.required = false;
}
})
});
</script>
@endpush

View file

@ -31,7 +31,7 @@
<!-- Others -->
<title>{{ config('app.name') }} - @yield('title')</title>
@if (config('app.debug'))
<script nonce="{{ $nonce }}">
<script nonce="{{ $nonce ?? '' }}">
window.vueDebug = true;
</script>
@endif

View file

@ -1,4 +1,4 @@
<script @if (isset($nonce)) nonce="{{ $nonce }}" @endif>
<script nonce="{{ $nonce ?? '' }}">
if (!window.__SYSTEM) {
window.__SYSTEM = {};
}

View file

@ -1,5 +1,5 @@
@include('modules.script-constants')
<script @if (isset($nonce)) nonce="{{ $nonce }}" @endif>
<script nonce="{{ $nonce ?? '' }}">
if (!window.__SYSTEM._routes) {
window.__SYSTEM._routes = {};
}

View file

@ -35,6 +35,9 @@ Route::prefix('bo')
'verify' => false,
]
);
Route::get('/login-error', function () {
return view('bo.pages.login-error');
})->name('login-error');
});
Route::middleware('admin-secure', 'auth:backend')->group(function () {
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
@ -42,22 +45,27 @@ Route::prefix('bo')
// * USERS.
Route::resource('users', UsersController::class)->except('show');
Route::patch('/users/{user}/change-published', [
UsersController::class, 'changePublished'
UsersController::class,
'changePublished'
])->name('users.change-published');
Route::get('/users/{user}/duplicate', [
UsersController::class, 'duplicate'
UsersController::class,
'duplicate'
])->name('users.duplicate');
// * COMMITMENTS
Route::resource('commitments', CommitmentController::class)->except(['show']);
Route::get('/commitments/{commitment}/duplicate', [
CommitmentController::class, 'duplicate'
CommitmentController::class,
'duplicate'
])->name('commitments.duplicate');
Route::patch('/commitments/{commitment}/change-order/{direction}', [
CommitmentController::class, 'changeOrder'
CommitmentController::class,
'changeOrder'
])->where('direction', 'up|down')->name('commitments.change-order');
Route::patch('/commitments/{commitment}/change-published', [
CommitmentController::class, 'changePublished'
CommitmentController::class,
'changePublished'
])->name('commitments.change-published');
// * STATIC_PAGES
@ -77,13 +85,16 @@ Route::prefix('bo')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:description');
Route::patch('/ranges/{range}/change-published', [
RangeController::class, 'changePublished'
RangeController::class,
'changePublished'
])->name('ranges.change-published');
Route::patch('/ranges/{range}/change-order/{direction}', [
RangeController::class, 'changeOrder'
RangeController::class,
'changeOrder'
])->where('direction', 'up|down')->name('ranges.change-order');
Route::get('/ranges/{range}/duplicate', [
RangeController::class, 'duplicate'
RangeController::class,
'duplicate'
])->name('ranges.duplicate');
// * NEWS.
@ -91,13 +102,16 @@ Route::prefix('bo')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content');
Route::patch('/news/{news}/change-published', [
NewsController::class, 'changePublished'
NewsController::class,
'changePublished'
])->name('news.change-published');
Route::patch('/news/{news}/change-order/{direction}', [
RangeController::class, 'changeOrder'
RangeController::class,
'changeOrder'
])->where('direction', 'up|down')->name('news.change-order');
Route::get('/news/{news}/duplicate', [
NewsController::class, 'duplicate'
NewsController::class,
'duplicate'
])->name('news.duplicate');
// * SEASONAL SECTIONS.
@ -106,10 +120,12 @@ Route::prefix('bo')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:text');
Route::patch('/seasonalsections/{seasonal_section}/change-published', [
SeasonalSectionController::class, 'changePublished'
SeasonalSectionController::class,
'changePublished'
])->name('seasonalsections.change-published');
Route::get('/seasonalsections/{seasonal_section}/duplicate', [
SeasonalSectionController::class, 'duplicate'
SeasonalSectionController::class,
'duplicate'
])->name('seasonalsections.duplicate');
// * HOMEPAGE
@ -123,13 +139,16 @@ Route::prefix('bo')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:description');
Route::patch('/stamps/{stamp}/change-published', [
StampController::class, 'changePublished'
StampController::class,
'changePublished'
])->name('stamps.change-published');
Route::patch('/stamps/{stamp}/change-order/{direction}', [
StampController::class, 'changeOrder'
StampController::class,
'changeOrder'
])->where('direction', 'up|down')->name('stamps.change-order');
Route::get('/stamps/{stamp}/duplicate', [
StampController::class, 'duplicate'
StampController::class,
'duplicate'
])->name('stamps.duplicate');
// * EXPLORERPAGES
@ -143,13 +162,16 @@ Route::prefix('bo')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:description,cooking_advice,ingredients');
Route::patch('/products/{product}/change-published', [
ProductController::class, 'changePublished'
ProductController::class,
'changePublished'
])->name('products.change-published');
Route::patch('/products/{product}/change-order/{direction}', [
ProductController::class, 'changeOrder'
ProductController::class,
'changeOrder'
])->where('direction', 'up|down')->name('products.change-order');
Route::get('/products/{product}/duplicate', [
ProductController::class, 'duplicate'
ProductController::class,
'duplicate'
])->name('products.duplicate');
// * CONVICTIONPAGES
@ -167,10 +189,12 @@ Route::prefix('bo')
// * TESTIMONIALS
Route::resource('testimonials', TestimonialController::class)->except(['show']);
Route::get('/testimonials/{testimonial}/duplicate', [
TestimonialController::class, 'duplicate'
TestimonialController::class,
'duplicate'
])->name('testimonials.duplicate');
Route::patch('/testimonials/{testimonial}/change-order/{direction}', [
TestimonialController::class, 'changeOrder'
TestimonialController::class,
'changeOrder'
])->where('direction', 'up|down')->name('testimonials.change-order');
Route::middleware('can:isConceptor')->group(function () {