I'm using knpuniversity's oauthbundle in symfony 5.4 and I have a fairly straightforward implementation of the AzureAuthenticator.
When I fetch the user from Microsoft365 I would like to also retrieve the group memberships of that user. It seems this requires a separate API call, or is there a smoother way to do this?
Can I control in the azure portal what gets passed in the callback? Currently it only holds email, name and a few other values, nothing related to groups.
<?php
namespace App\Security;
use App\Entity\WebtoolsUser as User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use KnpU\OAuth2ClientBundle\Client\Provider\AzureClient;
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\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class AzureAuthenticator extends OAuth2Authenticator
{
private $clientRegistry;
private $em;
private $router;
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
{
$this->clientRegistry = $clientRegistry;
$this->em = $em;
$this->router = $router;
}
public function supports(Request $request): ?bool
{
// continue ONLY if the current ROUTE matches the check ROUTE
return $request->attributes->get('_route') === 'login_microsoft_check';
}
public function authenticate(Request $request): PassportInterface
{
$client = $this->clientRegistry->getClient('azure');
$accessToken = $this->fetchAccessToken($client);
return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) {
$azureUser = $client->fetchUserFromToken($accessToken);
IS SEPARATE CALL NEEDED HERE? ------->
$email = $azureUser->claim('unique_name');
// 1) have they logged in with a Microsoft account ? Easy!
$existingUser = $this->em->getRepository(User::class)->findOneBy(['azureId' => $azureUser->getId()]);
Related
By using Symfony 5 and after user registration, the system should send an automatic email to the user to activate a signed URL. In EmailVerifer.php, the sendMailConfirmation method does not recognize the getId and getEmail methods. However after registration the confirmation email is anyway sent by the system to the user but without a signed URL to activate it. I think the issue is related to the implementation of serialization and deserialization methods in user class, as it worked before Serialization implementation. I'm trying to implement serializable methods in EmailVerifier class but it doesn't work. Thanks for the help.
<?php
namespace App\Security;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
class EmailVerifier
{
private $verifyEmailHelper;
private $mailer;
private $entityManager;
public function __construct(VerifyEmailHelperInterface $helper, MailerInterface $mailer, EntityManagerInterface $manager)
{
$this->verifyEmailHelper = $helper;
$this->mailer = $mailer;
$this->entityManager = $manager;
}
public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
{
$signatureComponents = $this->verifyEmailHelper->generateSignature(
$verifyEmailRouteName,
$user->getId(),
$user->getEmail()
);
$context = $email->getContext();
$context['signedUrl'] = $signatureComponents->getSignedUrl();
$context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
$context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
$email->context($context);
$this->mailer->send($email);
}
/**
* #throws VerifyEmailExceptionInterface
*/
public function handleEmailConfirmation(Request $request, UserInterface $user): void
{
$this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getEmail());
$user->setIsVerified(true);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
}
I want to redirect user conditionnaly from my event subscriber on kernel.controller event and change it if user can't acces the one asked.
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
class WebPageAccessSuscriber implements EventSubscriberInterface
{
/**
* #var SessionInterface
*/
private $session;
/**
* Contient l'ensemble des parametres de config
*
* #var ParameterBagInterface
*/
private $params;
private $router;
public function __construct(SessionInterface $session, ParameterBagInterface $params, RouterInterface $router)
{
// Retrieve the client session
$this->session = $session;
// Retrieve configuration variable
$this->params = $params;
$this->router = $router;
}
public function onKernelController(ControllerEvent $event)
{
error_log(__METHOD__);
$controller = $event->getController();
if(!is_array($controller)) return;
if ($this->params->get('visitor_access') === false && $controller[0] instanceof \App\Controller\restictedController) {
$event->setController(function() use ($event) {
return new RedirectResponse($this->router->generate('login_' . $event->getRequest()->getLocale()));
});
}
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::CONTROLLER => [['onKernelController']]
];
}
}
It works but symfony web profiler doesn't shows anymore.
I mean it does but his content is the one from my login template
Why symfony is redirecting webprofiler to login page ? How can prevent this behavior ? Should I use another way to achieve this ?
If you want to protect a whole section of your application use the symfony security component to create a firewall with its own authentication.
If you want to protect specific pages you could use a Security Voter https://symfony.com/doc/current/security/voters.html
Edit: After denying access via a Voter you can redirect the user with a https://symfony.com/doc/current/security/access_denied_handler.html
Alright so what am I trying to do is that I check if user status is "pending" and if so, I'd redirect him to "/pending" page.
Now I need this check on almost the entire website.
I tried with the decision manager but was unable to redirect, any other way to do this?
This should be called only for logged users
security.yaml
access_decision_manager:
service: App\Security\StatusAuthenticator
And the StatusAuthenticator
<?php
namespace App\Security;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class StatusAuthenticator implements AccessDecisionManagerInterface
{
/**
* #param TokenInterface $token
* #param array $attributes
* #param null $object
* #return bool|void
*/
public function decide(TokenInterface $token, array $attributes, $object = null)
{
if($token->getUser()->getStatus() == User::USER_STATUS_PENDING) {
// Needs to be redirected to /pending
return false;
}
return true;
}
}
Since you need to "check this on almost the entire website", you can use an EventListener that will fire on every request and there you can check if you have an authenticated user and their status.
// src/EventListener/PendingUserListener.php
namespace App\EventListener;
use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class PendingUserListener implements EventSubscriberInterface
{
/**
* #var Security
*/
private $security;
/**
* #var UrlGeneratorInterface
*/
private $urlGenerator;
public function __construct(Security $security, UrlGeneratorInterface $urlGenerator)
{
$this->security = $security;
$this->urlGenerator = $urlGenerator;
}
public static function getSubscribedEvents()
{
return [ KernelEvents::REQUEST => 'onKernelRequest' ];
}
public function onKernelRequest(RequestEvent $event)
{
$pending_route = 'pending';
$user = $this->security->getUser();
if (!$event->isMasterRequest()) {
return;
}
if (!$user instanceof UserInterface) {
return;
}
// Check if the requested page is 'pending', prevent redirect loops
if ($pending_route === $event->getRequest()->get('_route')) {
return;
}
// RedirectResponse expects a full url, generate from route name
if (User::USER_STATUS_PENDING == $user->getStatus()) {
$event->setResponse(
new RedirectResponse($this->urlGenerator->generate($pending_route))
);
}
}
}
I have a file "AuthenticationHandler.php" which is using "onAuthenticationFailure()" function to be able to lock an account after 5 failed attempts. If an account is locked, an email is sent to this user with an unlock link to his account... Everything is working fine here.
The problem is when I try to use "render()" instead of simply adding a sentence in "setBody("something", 'text/html')"
I have tried injecting #twig/#templating/#service_container in my AuthenticationHandler but none of them worked. When I fail on purpose to log in, I'm not receiving any error nor any flashbag saying "Bad Credential". It simply does nothing. It goes back to the login page without any message or mail.
I know my imports(use) are good because if I ommit them, I receive some common error where it says that I forgot a use statement.
I have tried putting a try/catch around "render()", but it simply ignores the rest of my code the moment I reach "$this->template->render('email:myTestPage.html.twig')
Again, every other service I inject works fine, If I simply don't use the "render()" function, an email is sent correctly. Is there any other way to capture an error message? Or at least something to put me on the right track!
service.yml:
authentication_handler:
class: Dbm\UserBundle\Handler\AuthenticationHandler
arguments: ["#session", "#router", "#security.context", "#doctrine.orm.entity_manager", "#mailer", "#twig"]
AuthenticationHandler.php
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Routing\Router;
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
class AuthenticationHandler implements AuthenticationSuccessHandlerInterface, AuthenticationFailureHandlerInterface
{
private $session;
private $security;
private $router;
private $em;
private $mailer;
private $twig;
public function __construct(Session $session, Router $router, SecurityContext $security, EntityManager $em, \Swift_Mailer $mailer, \Twig_Environment $twig)
{
$this->session = $session;
$this->security = $security;
$this->router = $router;
$this->em = $em;
$this->mailer = $mailer;
$this->twig = $twig;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
...
$message = (new \Swift_Message())
->setSubject('Unlock Account')
->setFrom('foobar#foobar.com')
->setTo($mail)
->setBody($this->twig->render('Email:new-unexpire-account.html.twig', array('hash' => $hash)), 'text/html');
$this->mailer->send($message);
}
}
I have also tried with $foo = $this->twig->render('Email:new-unexpire-account.html.twig', array('hash' => $hash)), 'text/html') alone and I'm not getting anything either. It stops there.
based on this question
I have implemented an automatic logout of users after a certain period of inactivity (like in question above). This works fine, but I need to make a log entry for this event.
The problem is that when logout fires, I get multiple records in my log file instead of 1 record. I guess I need to listen to some other request, instead of onKernelRequest. Any ideas how to do that? My code is as follows:
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Exception\CredentialsExpiredException;
class RequestListener{
protected $session;
protected $securityToken;
protected $router;
protected $logger;
protected $maxIdleTime;
public function __construct(Session $session, TokenStorage $securityToken, RouterInterface $router, $logger, $maxIdleTime)
{
$this->session = $session;
$this->securityToken = $securityToken;
$this->router = $router;
$this->logger = $logger;
$this->maxIdleTime = $maxIdleTime;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
return;
}
if ($this->maxIdleTime > 0) {
$lapse = time() - $this->session->getMetadataBag()->getCreated();
if ($lapse > $this->maxIdleTime) {
$username = $this->securityToken->getToken()->getUser();
if ($username !== 'anon.'){
$username = $username->getUsername();
}
$this->securityToken->setToken(null);
$this->session->getFlashBag()->set('error', 'Your session expired, you need to login again');
$this->session->invalidate();
$this->logger->makelog(//I get multiple log entries here instead of 1
0,
'Session timeout',
$username
);
$event->setResponse(new RedirectResponse($this->router->generate('login')));
}
}
}
}
UPD_1
I have already created a logout listener, but it listens only for logout event when the Logout button is pressed and this action is logged with different log entry. In my code above I use $this->session->invalidate() in order to logout the user. My code for logout listener is as follows:
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
use Doctrine\ORM\EntityManager;
class LogoutListener implements LogoutHandlerInterface
{
protected $securityContext;
protected $entityManager;
protected $logger;
public function __construct(TokenStorage $securityContext, EntityManager $entityManager, $logger)
{
$this->securityContext = $securityContext;
$this->entityManager = $entityManager;
$this->logger = $logger;
}
public function logout(Request $Request, Response $Response, TokenInterface $Token)
{
$em = $this->entityManager;
$user = $this->securityContext->getToken()->getUser();
$this->logger->makelog(1, 'Logout action, logout button', $user);
}
}