Add SSO E-Cosplay (Keycloak OIDC) and dynamic navbar active state
- Install knpuniversity/oauth2-client-bundle and stevenmaguire/oauth2-keycloak - Register KnpUOAuth2ClientBundle in bundles.php - Configure Keycloak OIDC client (realm e-cosplay, auth.esy-web.dev) - Add keycloakId field to User entity with migration - Create KeycloakAuthenticator with group-to-role mapping (/superadmin -> ROLE_ROOT) - Create OAuthController with SSO routes (/connection/sso/login, logout, check) - Add custom_authenticator to security firewall with form_login entry point - Add auth.esy-web.dev to nelmio external_redirects whitelist and CSP form-action - Add SSO button and error flash messages to login page - Make navbar active state dynamic based on current route (desktop + mobile) - Add Keycloak env vars to .env, .env.local, and ansible/env.local.j2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.env
7
.env
@@ -47,6 +47,13 @@ STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_MODE=test
|
||||
SMIME_PASSPHRASE='KLreLnyR07x5h#3$AC'
|
||||
|
||||
###> SSO E-Cosplay (Keycloak OIDC) ###
|
||||
OAUTH_KEYCLOAK_CLIENT_ID=e-ticket
|
||||
OAUTH_KEYCLOAK_CLIENT_SECRET=changeme
|
||||
OAUTH_KEYCLOAK_URL=https://auth.esy-web.dev
|
||||
OAUTH_KEYCLOAK_REALM=e-cosplay
|
||||
###< SSO E-Cosplay (Keycloak OIDC) ###
|
||||
|
||||
###> s3/minio ###
|
||||
S3_ENDPOINT=http://minio:9000
|
||||
S3_ACCESS_KEY=e-ticket
|
||||
|
||||
@@ -19,3 +19,7 @@ STRIPE_MODE=live
|
||||
SMIME_PASSPHRASE='{{ smime_passphrase }}'
|
||||
MEILISEARCH_URL=http://meilisearch:7700
|
||||
MEILISEARCH_API_KEY={{ meilisearch_api_key }}
|
||||
OAUTH_KEYCLOAK_CLIENT_ID=e-ticket
|
||||
OAUTH_KEYCLOAK_CLIENT_SECRET=1oLwbhJDNVmGH8CES1OdQtzR7dECOlII
|
||||
OAUTH_KEYCLOAK_URL=https://auth.esy-web.dev
|
||||
OAUTH_KEYCLOAK_REALM=e-cosplay
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"doctrine/orm": "^3.6",
|
||||
"dompdf/dompdf": "*",
|
||||
"endroid/qr-code-bundle": "*",
|
||||
"knpuniversity/oauth2-client-bundle": "^2.20",
|
||||
"league/flysystem-aws-s3-v3": "^3.32",
|
||||
"league/flysystem-bundle": "^3.6",
|
||||
"liip/imagine-bundle": "^2.17",
|
||||
@@ -19,6 +20,7 @@
|
||||
"nelmio/security-bundle": "^3.9",
|
||||
"phpdocumentor/reflection-docblock": "^6.0",
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
"stevenmaguire/oauth2-keycloak": "^6.1",
|
||||
"stripe/stripe-php": "*",
|
||||
"symfony/asset": "8.0.*",
|
||||
"symfony/console": "8.0.*",
|
||||
|
||||
252
composer.lock
generated
252
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "a708b901e0ade836caa13855beebd9d1",
|
||||
"content-hash": "ecb55cf346fc28e16c4caec521a016e2",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@@ -1872,6 +1872,69 @@
|
||||
],
|
||||
"time": "2025-12-01T22:03:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "firebase/php-jwt",
|
||||
"version": "v7.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/firebase/php-jwt.git",
|
||||
"reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
|
||||
"reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.4",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psr/cache": "^2.0||^3.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-sodium": "Support EdDSA (Ed25519) signatures",
|
||||
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Firebase\\JWT\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Neuman Vong",
|
||||
"email": "neuman+pear@twilio.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Anant Narayanan",
|
||||
"email": "anant@php.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
|
||||
"homepage": "https://github.com/firebase/php-jwt",
|
||||
"keywords": [
|
||||
"jwt",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/firebase/php-jwt/issues",
|
||||
"source": "https://github.com/firebase/php-jwt/tree/v7.0.3"
|
||||
},
|
||||
"time": "2026-02-25T22:16:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "7.10.0",
|
||||
@@ -2324,6 +2387,66 @@
|
||||
},
|
||||
"time": "2025-11-30T20:12:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "knpuniversity/oauth2-client-bundle",
|
||||
"version": "v2.20.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/knpuniversity/oauth2-client-bundle.git",
|
||||
"reference": "9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7",
|
||||
"reference": "9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"league/oauth2-client": "^2.0",
|
||||
"php": ">=8.1",
|
||||
"symfony/dependency-injection": "^6.4|^7.3|^8.0",
|
||||
"symfony/framework-bundle": "^6.4|^7.3|^8.0",
|
||||
"symfony/http-foundation": "^6.4|^7.3|^8.0",
|
||||
"symfony/routing": "^6.4|^7.3|^8.0",
|
||||
"symfony/security-core": "^6.4|^7.3|^8.0",
|
||||
"symfony/security-http": "^6.4|^7.3|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"league/oauth2-facebook": "^1.1|^2.0",
|
||||
"symfony/phpunit-bridge": "^7.3",
|
||||
"symfony/yaml": "^6.4|^7.3|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/security-guard": "For integration with Symfony's Guard Security layer"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"KnpU\\OAuth2ClientBundle\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ryan Weaver",
|
||||
"email": "ryan@symfonycasts.com"
|
||||
}
|
||||
],
|
||||
"description": "Integration with league/oauth2-client to provide services",
|
||||
"homepage": "https://symfonycasts.com",
|
||||
"keywords": [
|
||||
"oauth",
|
||||
"oauth2"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/knpuniversity/oauth2-client-bundle/issues",
|
||||
"source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.20.2"
|
||||
},
|
||||
"time": "2026-02-12T17:07:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "3.32.0",
|
||||
@@ -2637,6 +2760,71 @@
|
||||
],
|
||||
"time": "2024-09-21T08:32:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/oauth2-client",
|
||||
"version": "2.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/oauth2-client.git",
|
||||
"reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/26e8c5da4f3d78cede7021e09b1330a0fc093d5e",
|
||||
"reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
|
||||
"php": "^7.1 || >=8.0.0 <8.6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.3.5",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.4",
|
||||
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
|
||||
"squizlabs/php_codesniffer": "^3.11"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\OAuth2\\Client\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alex Bilbie",
|
||||
"email": "hello@alexbilbie.com",
|
||||
"homepage": "http://www.alexbilbie.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Woody Gilk",
|
||||
"homepage": "https://github.com/shadowhand",
|
||||
"role": "Contributor"
|
||||
}
|
||||
],
|
||||
"description": "OAuth 2.0 Client Library",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"SSO",
|
||||
"authorization",
|
||||
"identity",
|
||||
"idp",
|
||||
"oauth",
|
||||
"oauth2",
|
||||
"single sign on"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/oauth2-client/issues",
|
||||
"source": "https://github.com/thephpleague/oauth2-client/tree/2.9.0"
|
||||
},
|
||||
"time": "2025-11-25T22:17:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "liip/imagine-bundle",
|
||||
"version": "2.17.1",
|
||||
@@ -4036,6 +4224,68 @@
|
||||
},
|
||||
"time": "2026-03-03T17:31:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "stevenmaguire/oauth2-keycloak",
|
||||
"version": "6.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/stevenmaguire/oauth2-keycloak.git",
|
||||
"reference": "459cc58576d37f5823de2a677a7b17667b85ba7f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/stevenmaguire/oauth2-keycloak/zipball/459cc58576d37f5823de2a677a7b17667b85ba7f",
|
||||
"reference": "459cc58576d37f5823de2a677a7b17667b85ba7f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"firebase/php-jwt": "^7.0",
|
||||
"league/oauth2-client": "^2.8",
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.6",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpunit/phpunit": "~9.6.4",
|
||||
"squizlabs/php_codesniffer": "~3.7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Stevenmaguire\\OAuth2\\Client\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Steven Maguire",
|
||||
"email": "stevenmaguire@gmail.com",
|
||||
"homepage": "https://github.com/stevenmaguire"
|
||||
}
|
||||
],
|
||||
"description": "Keycloak OAuth 2.0 Client Provider for The PHP League OAuth2-Client",
|
||||
"keywords": [
|
||||
"authorisation",
|
||||
"authorization",
|
||||
"client",
|
||||
"keycloak",
|
||||
"oauth",
|
||||
"oauth2"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/stevenmaguire/oauth2-keycloak/issues",
|
||||
"source": "https://github.com/stevenmaguire/oauth2-keycloak/tree/6.1.0"
|
||||
},
|
||||
"time": "2026-03-03T11:50:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "stripe/stripe-php",
|
||||
"version": "v19.4.1",
|
||||
|
||||
@@ -16,4 +16,5 @@ return [
|
||||
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
|
||||
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
|
||||
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
|
||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
12
config/packages/knpu_oauth2_client.yaml
Normal file
12
config/packages/knpu_oauth2_client.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
knpu_oauth2_client:
|
||||
clients:
|
||||
keycloak:
|
||||
type: generic
|
||||
provider_class: Stevenmaguire\OAuth2\Client\Provider\Keycloak
|
||||
client_id: '%env(OAUTH_KEYCLOAK_CLIENT_ID)%'
|
||||
client_secret: '%env(OAUTH_KEYCLOAK_CLIENT_SECRET)%'
|
||||
redirect_route: app_oauth_keycloak_check
|
||||
redirect_params: {}
|
||||
provider_options:
|
||||
authServerUrl: '%env(OAUTH_KEYCLOAK_URL)%'
|
||||
realm: '%env(OAUTH_KEYCLOAK_REALM)%'
|
||||
@@ -51,6 +51,9 @@ nelmio_security:
|
||||
- 'https://fonts.gstatic.com'
|
||||
object-src:
|
||||
- 'none'
|
||||
form-action:
|
||||
- 'self'
|
||||
- 'https://auth.esy-web.dev'
|
||||
block-all-mixed-content: true
|
||||
|
||||
permissions_policy:
|
||||
@@ -71,3 +74,4 @@ nelmio_security:
|
||||
- stripe.com
|
||||
- checkout.stripe.com
|
||||
- hooks.stripe.com
|
||||
- auth.esy-web.dev
|
||||
|
||||
@@ -19,6 +19,9 @@ security:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
custom_authenticators:
|
||||
- App\Security\KeycloakAuthenticator
|
||||
entry_point: form_login
|
||||
logout:
|
||||
path: app_logout
|
||||
target: app_home
|
||||
|
||||
37
migrations/Version20260319093607.php
Normal file
37
migrations/Version20260319093607.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260319093607 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE messenger_log (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, message_class VARCHAR(255) NOT NULL, message_body TEXT DEFAULT NULL, status VARCHAR(20) NOT NULL, error_message TEXT DEFAULT NULL, stack_trace TEXT DEFAULT NULL, transport_name VARCHAR(255) DEFAULT NULL, retry_count INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, failed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
|
||||
$this->addSql('CREATE INDEX idx_messenger_log_status ON messenger_log (status)');
|
||||
$this->addSql('CREATE INDEX idx_messenger_log_created_at ON messenger_log (created_at)');
|
||||
$this->addSql('ALTER TABLE "user" ADD keycloak_id VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649491914B1 ON "user" (keycloak_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE messenger_log');
|
||||
$this->addSql('DROP INDEX UNIQ_8D93D649491914B1');
|
||||
$this->addSql('ALTER TABLE "user" DROP keycloak_id');
|
||||
}
|
||||
}
|
||||
29
src/Controller/OAuthController.php
Normal file
29
src/Controller/OAuthController.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class OAuthController extends AbstractController
|
||||
{
|
||||
#[Route('/connection/sso/login', name: 'app_oauth_keycloak')]
|
||||
public function connect(ClientRegistry $clientRegistry): RedirectResponse
|
||||
{
|
||||
return $clientRegistry->getClient('keycloak')->redirect(['openid', 'email', 'profile']);
|
||||
}
|
||||
|
||||
#[Route('/connection/sso/logout', name: 'app_oauth_keycloak_logout')]
|
||||
public function logout(): RedirectResponse
|
||||
{
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
#[Route('/connection/sso/check', name: 'app_oauth_keycloak_check')]
|
||||
public function check(): void
|
||||
{
|
||||
// Handled by KeycloakAuthenticator
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column]
|
||||
private ?string $password = null;
|
||||
|
||||
#[ORM\Column(length: 255, unique: true, nullable: true)]
|
||||
private ?string $keycloakId = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
@@ -122,6 +125,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getKeycloakId(): ?string
|
||||
{
|
||||
return $this->keycloakId;
|
||||
}
|
||||
|
||||
public function setKeycloakId(?string $keycloakId): static
|
||||
{
|
||||
$this->keycloakId = $keycloakId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// Required by UserInterface — no temporary credentials to clear
|
||||
|
||||
117
src/Security/KeycloakAuthenticator.php
Normal file
117
src/Security/KeycloakAuthenticator.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
|
||||
class KeycloakAuthenticator extends OAuth2Authenticator
|
||||
{
|
||||
public function __construct(
|
||||
private ClientRegistry $clientRegistry,
|
||||
private EntityManagerInterface $em,
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return 'app_oauth_keycloak_check' === $request->attributes->get('_route');
|
||||
}
|
||||
|
||||
private const GROUP_ROLE_MAP = [
|
||||
'/superadmin' => 'ROLE_ROOT',
|
||||
];
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$client = $this->clientRegistry->getClient('keycloak');
|
||||
$accessToken = $this->fetchAccessToken($client);
|
||||
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) {
|
||||
$keycloakUser = $client->fetchUserFromToken($accessToken);
|
||||
$keycloakId = $keycloakUser->getId();
|
||||
$email = $keycloakUser->getEmail();
|
||||
$data = $keycloakUser->toArray();
|
||||
$roles = $this->mapGroupsToRoles($data['groups'] ?? []);
|
||||
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['keycloakId' => $keycloakId]);
|
||||
|
||||
if ($user) {
|
||||
$user->setEmail($email);
|
||||
$user->setFirstName($data['given_name'] ?? $user->getFirstName());
|
||||
$user->setLastName($data['family_name'] ?? $user->getLastName());
|
||||
$user->setRoles($roles);
|
||||
$this->em->flush();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
$existingUser = $this->em->getRepository(User::class)->findOneBy(['email' => $email]);
|
||||
|
||||
if ($existingUser) {
|
||||
$existingUser->setKeycloakId($keycloakId);
|
||||
$existingUser->setRoles($roles);
|
||||
$this->em->flush();
|
||||
|
||||
return $existingUser;
|
||||
}
|
||||
|
||||
$newUser = new User();
|
||||
$newUser->setKeycloakId($keycloakId);
|
||||
$newUser->setEmail($email);
|
||||
$newUser->setFirstName($data['given_name'] ?? '');
|
||||
$newUser->setLastName($data['family_name'] ?? '');
|
||||
$newUser->setPassword('');
|
||||
$newUser->setRoles($roles);
|
||||
|
||||
$this->em->persist($newUser);
|
||||
$this->em->flush();
|
||||
|
||||
return $newUser;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $groups
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function mapGroupsToRoles(array $groups): array
|
||||
{
|
||||
$roles = [];
|
||||
|
||||
foreach ($groups as $group) {
|
||||
if (isset(self::GROUP_ROLE_MAP[$group])) {
|
||||
$roles[] = self::GROUP_ROLE_MAP[$group];
|
||||
}
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return new RedirectResponse($this->router->generate('app_account'));
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$request->getSession()->getFlashBag()->add('error', 'Echec de la connexion SSO E-Cosplay.');
|
||||
|
||||
return new RedirectResponse($this->router->generate('app_login'));
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,15 @@
|
||||
".php-cs-fixer.dist.php"
|
||||
]
|
||||
},
|
||||
"knpuniversity/oauth2-client-bundle": {
|
||||
"version": "2.20",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.20",
|
||||
"ref": "1ff300d8c030f55c99219cc55050b97a695af3f6"
|
||||
}
|
||||
},
|
||||
"league/flysystem-bundle": {
|
||||
"version": "3.6",
|
||||
"recipe": {
|
||||
|
||||
@@ -82,9 +82,10 @@
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex items-center space-x-1">
|
||||
<a href="{{ path('app_home') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]"><span itemprop="name">Accueil</span></a>
|
||||
{% set current_route = app.request.attributes.get('_route') %}
|
||||
<a href="{{ path('app_home') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_home' ? 'bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]' : 'hover:text-indigo-600' }}"><span itemprop="name">Accueil</span></a>
|
||||
<a href="#" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all hover:text-indigo-600"><span itemprop="name">Evenements</span></a>
|
||||
<a href="{{ path('app_contact') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all hover:text-indigo-600"><span itemprop="name">Contact</span></a>
|
||||
<a href="{{ path('app_contact') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_contact' ? 'bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]' : 'hover:text-indigo-600' }}"><span itemprop="name">Contact</span></a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4 border-l-4 border-gray-900 pl-6 h-full">
|
||||
@@ -110,9 +111,9 @@
|
||||
|
||||
<div id="mobile-menu" class="hidden lg:hidden border-t-4 border-gray-900 bg-white" role="menu">
|
||||
<div class="p-4 space-y-2 uppercase font-black italic">
|
||||
<a href="{{ path('app_home') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Accueil</a>
|
||||
<a href="{{ path('app_home') }}" class="block p-3 border-2 {{ current_route == 'app_home' ? 'border-gray-900 bg-yellow-400' : 'border-transparent hover:border-gray-900 hover:bg-gray-50' }}" role="menuitem">Accueil</a>
|
||||
<a href="#" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Evenements</a>
|
||||
<a href="{{ path('app_contact') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Contact</a>
|
||||
<a href="{{ path('app_contact') }}" class="block p-3 border-2 {{ current_route == 'app_contact' ? 'border-gray-900 bg-yellow-400' : 'border-transparent hover:border-gray-900 hover:bg-gray-50' }}" role="menuitem">Contact</a>
|
||||
{% if app.user %}
|
||||
<a href="{{ path('app_account') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Mon espace</a>
|
||||
{% else %}
|
||||
|
||||
@@ -1,7 +1,68 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Connexion - E-Ticket{% endblock %}
|
||||
{% block description %}Connectez-vous a votre compte E-Ticket pour gerer vos evenements et billets{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div style="max-width:28rem;margin:0 auto;padding:3rem 1rem;">
|
||||
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:0.5rem;">Connexion</h1>
|
||||
<p class="font-bold text-gray-600 italic" style="margin-bottom:2rem;">Accedez a votre espace.</p>
|
||||
|
||||
{% if error %}
|
||||
<div style="border:4px solid #111827;padding:1rem 1.5rem;margin-bottom:2rem;background:#fee2e2;box-shadow:4px 4px 0 rgba(0,0,0,1);">
|
||||
<p class="font-black text-sm">{{ error.messageKey|trans(error.messageData, 'security') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for message in app.flashes('error') %}
|
||||
<div style="border:4px solid #111827;padding:1rem 1.5rem;margin-bottom:2rem;background:#fee2e2;box-shadow:4px 4px 0 rgba(0,0,0,1);">
|
||||
<p class="font-black text-sm">{{ message }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<form method="post" action="{{ path('app_login') }}" style="display:flex;flex-direction:column;gap:1.5rem;">
|
||||
<div>
|
||||
<label for="login_email" class="text-xs font-black uppercase tracking-widest" style="display:block;margin-bottom:0.5rem;">Email</label>
|
||||
<input type="email" id="login_email" name="_username" required autofocus
|
||||
value="{{ last_username }}"
|
||||
style="width:100%;padding:0.75rem 1rem;border:3px solid #111827;font-weight:700;outline:none;"
|
||||
class="focus:border-indigo-600"
|
||||
placeholder="jean.dupont@exemple.fr">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="login_password" class="text-xs font-black uppercase tracking-widest" style="display:block;margin-bottom:0.5rem;">Mot de passe</label>
|
||||
<input type="password" id="login_password" name="_password" required
|
||||
style="width:100%;padding:0.75rem 1rem;border:3px solid #111827;font-weight:700;outline:none;"
|
||||
class="focus:border-indigo-600"
|
||||
placeholder="••••••••">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
|
||||
<div>
|
||||
<button type="submit"
|
||||
style="width:100%;padding:0.75rem 2rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);cursor:pointer;"
|
||||
class="bg-yellow-400 font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
|
||||
Se connecter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:1rem;margin-top:2rem;">
|
||||
<div style="flex:1;height:3px;background:#111827;"></div>
|
||||
<span class="text-xs font-black uppercase tracking-widest text-gray-500">ou</span>
|
||||
<div style="flex:1;height:3px;background:#111827;"></div>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('app_oauth_keycloak') }}"
|
||||
style="display:block;width:100%;padding:0.75rem 2rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);text-align:center;text-decoration:none;margin-top:1.5rem;"
|
||||
class="bg-gray-900 text-white font-black uppercase text-sm tracking-widest hover:bg-indigo-600 transition-all">
|
||||
Se connecter avec E-Cosplay
|
||||
</a>
|
||||
|
||||
<div style="margin-top:2rem;text-align:center;">
|
||||
<p class="text-sm font-bold text-gray-600">Pas encore de compte ? <a href="{{ path('app_register') }}" class="text-indigo-600 hover:underline font-black">Inscription</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user