champs-libres/persona-user-bundle
Installation
composer require champs-libres/persona-user-bundle
Add to config/bundles.php:
return [
// ...
ChampsLibres\PersonaUserBundle\ChampsLibresPersonaUserBundle::class => ['all' => true],
];
Configuration Publish the default config:
php bin/console champs-libres:persona-user:install
Update config/packages/champs_libres_persona_user.yaml:
champs_libres_persona_user:
persona_api_endpoint: 'https://verifier.login.persona.org/verify'
user_class: App\Entity\User
user_provider: 'app.user_provider.persona'
First Use Case
Add the login button to a template (e.g., templates/base.html.twig):
<button id="login-button" data-persona-verifier-url="{{ app.config.persona_api_endpoint }}">Login with Persona</button>
Include the JS bundle:
{{ encore_entry_script_tags('app') }}
Ensure webpack.config.js includes:
Encore
.addEntry('app', './assets/js/persona-login.js')
.enableReactPreset()
.copyFiles({
from: './assets/js/persona-login.js',
to: 'build/[name].js'
});
Frontend Flow
// assets/js/persona-login.js
document.getElementById('login-button').addEventListener('click', () => {
navigator.id.request({
onlogin: (assertion) => {
fetch('/login/persona', {
method: 'POST',
body: JSON.stringify({ assertion }),
headers: { 'Content-Type': 'application/json' }
})
.then(response => response.json())
.then(data => {
if (data.authenticated) window.location.reload();
});
}
});
});
Backend Handling
Create a controller (src/Controller/PersonaAuthController.php):
use ChampsLibres\PersonaUserBundle\PersonaAuthenticator;
class PersonaAuthController extends AbstractController {
public function login(PersonaAuthenticator $authenticator, Request $request) {
$assertion = json_decode($request->getContent(), true)['assertion'];
$user = $authenticator->authenticate($assertion);
$this->loginManager->authenticateUser($user);
return new JsonResponse(['authenticated' => true]);
}
}
User Creation
Extend the default user provider (src/Security/UserProvider/PersonaUserProvider.php):
use ChampsLibres\PersonaUserBundle\Security\User\PersonaUser;
class PersonaUserProvider extends AbstractPersonaUserProvider {
public function loadUserByPersonaAssertion($assertion) {
$email = $this->extractEmailFromAssertion($assertion);
$user = $this->userRepository->findOneBy(['email' => $email]);
if (!$user) {
$user = new PersonaUser($email, $this->passwordEncoder);
$user->setRoles(['ROLE_USER']);
$this->userRepository->save($user, true);
}
return $user;
}
}
Symfony Security
Configure security.yaml:
firewalls:
main:
pattern: ^/
persona_login:
provider: app.user_provider.persona
providers:
app.user_provider.persona:
id: app.user_provider.persona
User Entity
Extend PersonaUser in your User entity:
use ChampsLibres\PersonaUserBundle\Security\User\PersonaUser as BasePersonaUser;
class User extends BasePersonaUser {
// Custom fields/methods
}
Event Listeners
Subscribe to PersonaUserEvents for post-login actions:
use ChampsLibres\PersonaUserBundle\Event\PersonaUserEvent;
class PostLoginHandler {
public function onPersonaLogin(PersonaUserEvent $event) {
// e.g., log activity, update last_login_at
}
}
Register in services.yaml:
services:
App\EventListener\PostLoginHandler:
tags:
- { name: kernel.event_listener, event: persona.user.post_login, method: onPersonaLogin }
CORS Issues
persona_api_endpoint to include CORS headers or use a proxy.Assertion Validation
monolog for PersonaAssertionException.User Deduplication
email + persona_assertion_hash) or implement a merge strategy.Session Management
Log Assertions
Enable debug logging in config/packages/monolog.yaml:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
Check logs for PersonaAssertion payloads during failed logins.
Test Locally Use the Persona test server for development:
champs_libres_persona_user:
persona_api_endpoint: 'https://verifier.login.testmk2.mozilla.org/verify'
Custom Assertion Handling
Override PersonaAuthenticator to add logic:
class CustomPersonaAuthenticator extends PersonaAuthenticator {
protected function validateAssertion($assertion) {
// Add custom validation (e.g., check email domain)
return parent::validateAssertion($assertion);
}
}
Register in services.yaml:
services:
ChampsLibres\PersonaUserBundle\PersonaAuthenticator:
class: App\Security\CustomPersonaAuthenticator
Post-Login Redirects
Extend the PersonaUserEvent listener to redirect users:
public function onPersonaLogin(PersonaUserEvent $event) {
if ($event->getUser()->getEmail() === '[email protected]') {
$event->setResponse(new RedirectResponse('/admin'));
}
}
Multi-Factor Authentication (MFA)
Combine with Symfony’s Security component to add MFA post-Persona login:
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
class PersonaMFASuccessHandler implements AuthenticationSuccessHandlerInterface {
public function onAuthenticationSuccess(Request $request, TokenInterface $token) {
if (!$this->isMFAEnabled($token)) {
return new RedirectResponse('/mfa/setup');
}
return new RedirectResponse('/dashboard');
}
}
User Class
Ensure your user_class implements PersonaUserInterface or extends PersonaUser. Missing methods (e.g., getPersonaAssertion()) will throw BadMethodCallException.
API Endpoint
The persona_api_endpoint must support POST requests with application/json content type. Avoid GET endpoints or custom formats.
Rate Limiting Mozilla’s Persona API has rate limits. For high-traffic sites, implement client-side rate limiting or use a queue (e.g., Symfony Messenger) for assertion processing.
How can I help you explore Laravel packages today?