I'm trying to develop an API with an authentication system.
But the response of my default route / always display the source code of the Symfony default welcome page instead of no route found or authentication required. What's wrong with my config.
my security.yaml file :
security:
encoders:
App\Entity\User:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: true
json_login:
check_path: /api/login
logout:
path: /api/logout
success_handler: api.logout.success.listener
access_control:
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
My SecurityController :
/**
* #Route("/api")
*/
class SecurityController extends AbstractController
{
/**
* #Route("/", name="api", methods={"GET"})
* #param Request $request
* #return JsonResponse
*/
public function test(Request $request)
{
return $this->json([
'message' => 'test'
]);
}
/**
* #Route("/login", name="login", methods={"POST"})
* #param Request $request
* #return JsonResponse
*/
public function login(Request $request)
{
$user = $this->getUser();
return $this->json([
'username' => $user->getUsername(),
'roles' => $user->getRoles(),
]);
}
/**
* #Route("/logout", name="logout", methods={"GET"})
* #throws Exception
*/
public function logout()
{
// controller can be blank: it will never be executed!
throw new Exception('Don\'t forget to activate logout in security.yaml');
}
}
Problem resolved, I finally added a DefaultController with default route "/" that throw a NotFoundException.
Related
I want to implement the following authentication scenario in symfony 5:
User sends a login form with username and password, authentication is processed against an LDAP server
if authentication against the LDAP server is successful :
if there is an instance of my App\Entity\User that as the same username as the ldap matching entry, refresh some of its attributes from the ldap server and return this entity
if there is no instance create a new instance of my App\Entity\User and return it
I have implemented a guard authenticator which authenticates well against the LDAP server but it's returning me an instance of Symfony\Component\Ldap\Security\LdapUser and I don't know how to use this object to make relation with others entities!
For instance, let's say I have a Car entity with an owner property that must be a reference to an user.
How can I manage that ?
Here is the code of my security.yaml file:
security:
encoders:
App\Entity\User:
algorithm: auto
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
my_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: "%env(LDAP_BASE_DN)%"
search_dn: "%env(LDAP_SEARCH_DN)%"
search_password: "%env(LDAP_SEARCH_PASSWORD)%"
default_roles: ROLE_USER
uid_key: uid
extra_fields: ['mail']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: my_ldap
guard:
authenticators:
- App\Security\LdapFormAuthenticator
I finally found a good working solution.
The missing piece was a custom user provider.
This user provider has the responsibility to authenticate user against ldap and to return the matching App\Entity\User entity. This is done in getUserEntityCheckedFromLdap method of LdapUserProvider class.
If there is no instance of App\Entity\User saved in the database, the custom user provider will instantiate one and persist it. This is the first user connection use case.
Full code is available in this public github repository.
You will find below, the detailed steps I follow to make the ldap connection work.
So, let's declare the custom user provider in security.yaml.
security.yaml:
providers:
ldap_user_provider:
id: App\Security\LdapUserProvider
Now, configure it as a service, to pass some ldap usefull string arguments in services.yaml.
Note since we are going to autowire the Symfony\Component\Ldap\Ldap service, let's add this service configuration too:
services.yaml:
#see https://symfony.com/doc/current/security/ldap.html
Symfony\Component\Ldap\Ldap:
arguments: ['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: ldap
port: 389
# encryption: tls
options:
protocol_version: 3
referrals: false
App\Security\LdapUserProvider:
arguments:
$ldapBaseDn: '%env(LDAP_BASE_DN)%'
$ldapSearchDn: '%env(LDAP_SEARCH_DN)%'
$ldapSearchPassword: '%env(LDAP_SEARCH_PASSWORD)%'
$ldapSearchDnString: '%env(LDAP_SEARCH_DN_STRING)%'
Note the arguments of the App\Security\LdapUserProvider come from env vars.
.env:
LDAP_URL=ldap://ldap:389
LDAP_BASE_DN=dc=mycorp,dc=com
LDAP_SEARCH_DN=cn=admin,dc=mycorp,dc=com
LDAP_SEARCH_PASSWORD=s3cr3tpassw0rd
LDAP_SEARCH_DN_STRING='uid=%s,ou=People,dc=mycorp,dc=com'
Implement the custom user provider :
App\Security\LdapUserProvider:
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class LdapUserProvider implements UserProviderInterface
{
/**
* #var Ldap
*/
private $ldap;
/**
* #var EntityManager
*/
private $entityManager;
/**
* #var string
*/
private $ldapSearchDn;
/**
* #var string
*/
private $ldapSearchPassword;
/**
* #var string
*/
private $ldapBaseDn;
/**
* #var string
*/
private $ldapSearchDnString;
public function __construct(EntityManagerInterface $entityManager, Ldap $ldap, string $ldapSearchDn, string $ldapSearchPassword, string $ldapBaseDn, string $ldapSearchDnString)
{
$this->ldap = $ldap;
$this->entityManager = $entityManager;
$this->ldapSearchDn = $ldapSearchDn;
$this->ldapSearchPassword = $ldapSearchPassword;
$this->ldapBaseDn = $ldapBaseDn;
$this->ldapSearchDnString = $ldapSearchDnString;
}
/**
* #param string $username
* #return UserInterface|void
* #see getUserEntityCheckedFromLdap(string $username, string $password)
*/
public function loadUserByUsername($username)
{
// must be present because UserProviders must implement UserProviderInterface
}
/**
* search user against ldap and returns the matching App\Entity\User. The $user entity will be created if not exists.
* #param string $username
* #param string $password
* #return User|object|null
*/
public function getUserEntityCheckedFromLdap(string $username, string $password)
{
$this->ldap->bind(sprintf($this->ldapSearchDnString, $username), $password);
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$search = $this->ldap->query($this->ldapBaseDn, 'uid=' . $username);
$entries = $search->execute();
$count = count($entries);
if (!$count) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($count > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$ldapEntry = $entries[0];
$userRepository = $this->entityManager->getRepository('App\Entity\User');
if (!$user = $userRepository->findOneBy(['userName' => $username])) {
$user = new User();
$user->setUserName($username);
$user->setEmail($ldapEntry->getAttribute('mail')[0]);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
return $user;
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*
* #return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
return $user;
// Return a User object after making sure its data is "fresh".
// Or throw a UsernameNotFoundException if the user no longer exists.
throw new \Exception('TODO: fill in refreshUser() inside ' . __FILE__);
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass($class)
{
return User::class === $class || is_subclass_of($class, User::class);
}
}
Configure the firewall to use our custom user provider:
security.yaml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: ldap_user_provider
logout:
path: app_logout
guard:
authenticators:
- App\Security\LdapFormAuthenticator
Write an authentication guard:
App\SecurityLdapFormAuthenticator:
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LdapFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $urlGenerator;
private $csrfTokenManager;
public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager)
{
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
}
public function supports(Request $request)
{
return 'app_login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $userProvider->getUserEntityCheckedFromLdap($credentials['username'], $credentials['password']);
if (!$user) {
throw new CustomUserMessageAuthenticationException('Username could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
//in this scenario, this method is by-passed since user authentication need to be managed before in getUser method.
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$request->getSession()->getFlashBag()->add('info', 'connected!');
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('app_homepage'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_login');
}
}
My user entity looks like this:
`App\Entity\User`:
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password = 'password is not managed in entity but in ldap';
/**
* #ORM\Column(type="string", length=255)
*/
private $userName;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function setUserName(string $userName): self
{
$this->userName = $userName;
return $this;
}
}
For Symfony 6 I do like this.
No extra implementation
security:
role_hierarchy:
ROLE_USER: ROLE_USER
ROLE_ADMIN: [ROLE_USER, ROLE_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
pmh_db:
entity:
class: App\Entity\User
property: username
pmh_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: '%base_dn%'
search_dn: '%search_dn%'
search_password: '%search_password%'
default_roles: 'ROLE_USER'
uid_key: '%uid_key%'
extra_fields: ['email']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
pattern: ^/
provider: pmh_db
switch_user: { role: ROLE_ALLOWED_TO_SWITCH }
login_throttling:
max_attempts: 5
form_login_ldap:
login_path: app_login
check_path: app_login
service: Symfony\Component\Ldap\Ldap
dn_string: 'DOMAIN\{username}'
query_string: null
default_target_path: /
logout:
path: /logout
target: /
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
# by default, the feature is enabled by checking a
# checkbox in the login form (see below), uncomment the
# following line to always enable it.
always_remember_me: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: '^/login', roles: PUBLIC_ACCESS }
- { path: '^/admin', roles: [IS_AUTHENTICATED_FULLY, ROLE_ADMIN] }
- { path: '^/', roles: ROLE_USER }
I'm using Symfony 4 "Custom Authentication System with Guard (API Token Example)"Custom Authentication System with Guard (API Token Example)
I want to generate api token when user register from other app(i.e Advance Rest Client) and then want to use this token to access other api's like get articles list.
There is no option to generate token or may be I can't find out.
Here is my code:
config/packages/security.yaml
security:
encoders:
App\Entity\User:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
#property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api/
stateless: true
anonymous: false
guard:
authenticators:
- App\Security\TokenAuthenticator
main:
pattern: ^/
anonymous: true
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_logout
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
Controller/RegistrationController.php
class RegistrationController extends AbstractController{
/**
* #Route("/register/api", name="app_register_api")
*/
public function registerApi(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, TokenAuthenticator $authenticator){
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// do anything else you need here, like send an email
$data = $guardHandler->authenticateUserAndHandleSuccess(
$user,
$request,
$authenticator,
'api' // firewall name in security.yaml
);
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
}
}
Security/TokenAuthenticator.php
class TokenAuthenticator extends AbstractGuardAuthenticator
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning false will cause this authenticator
* to be skipped.
*/
public function supports(Request $request)
{
return $request->headers->has('X-AUTH-TOKEN');
}
/**
* Called on every request. Return whatever credentials you want to
* be passed to getUser() as $credentials.
*/
public function getCredentials(Request $request)
{
return [
'token' => $request->headers->get('X-AUTH-TOKEN'),
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$apiToken = $credentials['token'];
if (null === $apiToken) {
return;
}
// if a User object, checkCredentials() is called
return $this->em->getRepository(User::class)
->findOneBy(['apiToken' => $apiToken]);
}
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
// no credential check is needed in this case
// return true to cause authentication success
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = [
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = [
// you might translate this message
'message' => 'Authentication Required'
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
Next problem is how to get user detail from token in articles api?
<?php
namespace App\Controller\Rest;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use App\Entity\Article;
use App\Form\ArticleType;
use App\Service\ArticleService;
class ArticleController extends FOSRestController
{
/**
* #var ArticleService
*/
private $articleService;
/**
* ArticleController constructor.
* #param ArticleService $articleService
*/
public function __construct(ArticleService $articleService)
{
$this->articleService = $articleService;
}
/**
* Lists all Articles.
* http://sf4.local/api/articles
* #Rest\Get("/articles")
*
* #return Response
*/
public function getArticles()
{
$items = $this->articlesService->getAllArticles();
return $this->handleView($this->view($items));
}
}
I'm working in symfony with FosUser and FosAuthServer.
I have a front web site who access to an API by Oauth2. On the front web site, if the user is not logon, redirect him to the oauth serveur and after the user is login, redirect him to the font web site with an oauth2 token.
It's work well, but I need this :
if user is find in the database (my user provider), longin him
if the user is not find in the database, I search him in LDAP and I login him manually.
To do this, I use the AuthenticationFailureHandler to find the user in the LDAP. After find him, I create a User Entity and dispatch envent FOSUserEvents::SECURITY_IMPLICIT_LOGIN, but I don't know who can I create a token and redirect to the front web site.
security.yml
security:
encoders:
AppBundle\Entity\User: sha512
Symfony\Component\Security\Core\User\User: plaintext
providers:
api:
id: fos_user.user_provider.username_email
admin:
memory:
users:
admin: { password: password}
firewalls:
doc:
pattern: ^/api/doc
anonymous: true
oauth_token:
pattern: ^/oauth/v2/token
security: false
oauth_authorize:
pattern: ^/oauth/v2/auth
guard:
authenticators:
- app.security.login_form_authenticator
form_login:
provider: api
csrf_token_generator: security.csrf.token_manager
login_path: fos_user_security_login
check_path: fos_user_security_check
failure_handler: app.authentification_failure_handler
logout:
path: fos_user_security_logout
target: fos_user_security_login
anonymous: true
admin:
pattern: ^/admin
http_basic:
realm: 'Secured Area'
provider: admin
anonymous: false
api:
pattern: ^/api
fos_oauth: true
stateless: true
anonymous: false
access_control:
- { path: ^/oauth/v2/auth, roles: [ IS_AUTHENTICATED_ANONYMOUSLY ] }
- { path: ^/api/doc, roles: [ IS_AUTHENTICATED_ANONYMOUSLY ] }
- { path: ^/admin, roles: [ IS_AUTHENTICATED_FULLY ] }
- { path: ^/, roles: [ ROLE_USER ] }
Service.yml
app.authentification_failure_handler:
public: false
class: AppBundle\Handler\AuthenticationFailureHandler
arguments:
- '#utilities.active_directory.ageo'
- '#doctrine.orm.entity_manager'
- '#fos_user.user_manager'
- '#security.token_storage'
- '#debug.event_dispatcher'
- '#router'
- '#http_kernel'
- '#security.http_utils'
AuthentificationFaillureHandler.php
<?php
namespace AppBundle\Handler;
use AppBundle\Entity\Assure;
use AppBundle\Entity\BrokerStructur;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use FOS\OAuthServerBundle\Model\AuthCodeManager;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\Event\UserEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Model\UserManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\HttpUtils;
class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler
{
// private $cafeteriaEntityManager;
private $router;
/**
* #var \Ageo\UtilitiesBundle\ActiveDirectory\Ageo Connection au LDAP Ageo
*/
private $ldap;
/**
* #var $entityManagerSql Manager des entitées de la base mysql
*/
private $entityManagerSql;
/**
* #var UserManager Manager des utilisateurs (FosUserBundle)
*/
private $userManager;
/**
* #var EventDispatcher
*/
private $eventDispatcher;
/**
* #var TokenStorage
*/
private $tokenStorage;
public function __construct(\Ageo\UtilitiesBundle\ActiveDirectory\Ageo $ldap, EntityManager $entityManagerSql, UserManager $userManager,
TokenStorage $tokenStorage, \Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher $eventDispatcher,
RouterInterface $router, HttpKernelInterface $httpKernel, HttpUtils $httpUtils)
{
$this->ldap = $ldap;
$this->router = $router;
$this->entityManagerSql = $entityManagerSql;
$this->userManager = $userManager;
$this->tokenStorage = $tokenStorage;
$this->eventDispatcher = $eventDispatcher;
parent::__construct($httpKernel, $httpUtils, array(), null);
}
/**
* Si l'utilisateur n'est pas présent dans la base de données local, on le cherche dans l'Active directory pour le logguer
* On vérifie ensuite s'il existe dans la base de donné local. S'il existe, on le charge et on le met à jour, sinon, on le crée.
* #param Request $request
* #param AuthenticationException $exception
* #return RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$username = trim($request->request->get('_username'));
$password = trim($request->request->get('_password'));
/*
* Manupulation to find the user in LDAP => $userAd
*/
$loaclUser = $this->userManager->createUser();
$loaclUser
->setEnabled(true)
->setUsername($username)
->setPlainPassword($password)
->setEmail($userAd->hasAttribute('mail') ? $userAd->getAttribute('mail')[0] : null )
$this->userManager->updateUser($loaclUser);
$this->eventDispatcher->dispatch(FOSUserEvents::SECURITY_IMPLICIT_LOGIN, new UserEvent($loaclUser, $request));
//$this->eventDispatcher->dispatch(FOSUserEvents::REGISTRATION_COMPLETED, new FilterUserResponseEvent($loaclUser, $request, $response));
// I need to login my user on authServeur
// I have test this, but it's not working
if ($this->session->has('_security.oauth_authorize.target_path'))
{
parse_str(parse_url($this->session->get('_security.oauth_authorize.target_path'), PHP_URL_QUERY), $target_path);
$url = $this->session->get('_security.oauth_authorize.target_path');
}
$response = new RedirectResponse($url);
return $response;
}
}
Anyone say who can I do the same proccess of the login_check ?
I never done it but this seems to be the use case of the chain provider
security:
providers:
chain_provider:
chain:
providers: [api, admin, ldap_provider]
Its on symfony 2.8, i'm trying to use an API and even though i'm entering the onAuthenticationSuccess method after filling the rights parameters, the redirection always goes to the login page and so, doesnt do the job.
I think its a security issue or a route issue but my knowledge is limited.
here is the function i'm entering before being redirected:
/**
* {#inheritdoc}
*
* #throws InvalidParameterException
* #throws MissingMandatoryParametersException
* #throws RouteNotFoundException
* #throws ServiceCircularReferenceException
* #throws ServiceNotFoundException
* #throws InitializeDataStudioException
* #throws DBALException
* #throws ORMInvalidArgumentException
* #throws OptimisticLockException
* #throws \InvalidArgumentException
* #throws \Exception
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($request->getMethod() === 'POST') {
return null;
}
$route = $request->get('_route');
$request->query->remove('boid');
$request->query->remove('uid');
$request->query->remove('securityString');
$request->query->remove('sessid');
$parameters = array_merge($request->query->all(), $request->attributes->all());
$url = $this->router->generate($route, $parameters);
/** #var DatabaseManager $dbManager */
$dbManager = $this->container->get('app.database.manager');
if ($dbManager->getListUniqueKey() !== null) {
/** #var ExistingDataLoader $dataLoader */
$dataLoader = $this->container->get('app.database.loader.existing_data');
/** #var EntityManager $em */
$em = $this->container->get('doctrine')->getManager('default');
/** #var CustomTable $listTable */
$listTable = $em->getRepository('AppBundle:CustomTable')->findOneBy(['type' => Table::TYPE_LIST]);
$dataLoader->load($listTable);
}
return new RedirectResponse($url);
}
I'm being redirected to another route then to the login page.
security.yml :
To get started with security, check out the documentation:
http://symfony.com/doc/current/book/security.html
security:
# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers:
ds_provider:
id: app.security.dynamic_user_provider
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
logout:
path: /logout
target: /
guard:
authenticators:
- app.security.token_authenticator
# activate different ways to authenticate
# http_basic: ~
# http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate
# form_login: ~
# http://symfony.com/doc/current/cookbook/security/form_login_setup.html
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/data-studio, roles: ROLE_SUPER_ADMIN }
the routes i'm trying to access :
data-studio/api/calculated-fields/2/edit?dailyUpdate=1&dailyUpdateTime=01:00&boid=xxx&uid=xxx&securityString=xxxxxxxxx&sessid=xxxxxxxxx
redirecting to :
/data-studio/api/calculated-fields/2/edit?dailyUpdate=1&dailyUpdateTime=01%3A00&_route=api_calculated_field_edit&_route_params%5Bid%5D=2
then :
/login
When entering onAuthenticationSuccess i dump the token and i can see that i'm correctly authenticated with the role "ROLE_SUPER_ADMIN" :
["roles":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=>
array(1) {
[0]=>
object(Symfony\Component\Security\Core\Role\Role)#375 (1) {
["role":"Symfony\Component\Security\Core\Role\Role":private]=>
string(16) "ROLE_SUPER_ADMIN"
}
}
["authenticated":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=>
bool(true)
["attributes":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=>
array(0) {
}
}
Any help would be appreciated.
Cheers
Its the PUT method that cause this problem.
Once i switch the route to GET, it works fine.
Don't know why, help will still be appreciated.
[ EDIT ]
Had to return null in onAuthenticationSuccess function to make it work.
Not sure if it's the correct way to do but it solves my problem and doesnt seem to have any security impact.
I set up two authentication methods for my api:
Private token authenticator by following this tutorial
FOSOAuthServerBundle system for access_token auth: https://github.com/FriendsOfSymfony/FOSOAuthServerBundle
Both works like a charm separately.
I tried to combine those systems with this security config:
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
role_hierarchy:
ROLE_ALLOWED_TO_SWITCH: ~
ROLE_SUPPORT: ~
ROLE_ADMIN: [ROLE_SONATA_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_SUPPORT, ROLE_ALLOWED_TO_SWITCH]
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
api_key_user:
id: security.user.provider.api_key
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
oauth_token:
pattern: ^/oauth/v2/token
security: false
api:
pattern: ^/api
stateless: true
simple_preauth:
authenticator: security.authentication.authenticator.api_key
fos_oauth: true
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
login_path: /login
check_path: /login_check
anonymous: ~
logout:
path: /logout
switch_user: true
If I try to get an access token with this curl command:
curl "http://localhost:8000/oauth/v2/token?client_id=1_2rqa1al0trwgso8g8co4swsks48cwsckgc8cokswkcgos4csog&client_secret=25a78plm6c2ss044k4skckkwoo8kw4kcoccg8sg0skook4sgwg&grant_type=password&username=test&password=test
It works an I get an access_token, but when I try to use it:
curl -X GET http://localhost:8000/api/changelogs.json -H "Authorization: Bearer MmI2OWNkNjhjMGYwOTUyNDA2OTdlMDBjNjA1YmI3MjVhNTBiMTNhMjI0MGE1YmM3NzgwNjVmZWZmYWNhM2E4YQ" | json_pp
I get:
{
"error" : "invalid_grant",
"error_description" : "The provided access token is invalid."
}
By deactivating my single_preauth api key authenticator, it's works and I can access to my API.
It seems my api key authenticator block all another system.
Here, my ApiKeyAuthenticator class:
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
private $userProvider;
/**
* #param ApiKeyUserProvider $userProvider
*/
public function __construct(ApiKeyUserProvider $userProvider)
{
$this->userProvider = $userProvider;
}
/**
* {#inheritdoc}
*/
public function createToken(Request $request, $providerKey)
{
$apiKey = str_replace('Bearer ', '', $request->headers->get('Authorization', ''));
if (!$apiKey) {
throw new BadCredentialsException('No API key given.');
}
return new PreAuthenticatedToken('anon.', $apiKey, $providerKey);
}
/**
* {#inheritdoc}
*/
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$apiKey = $token->getCredentials();
$username = $this->userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
throw new AuthenticationException('The provided access token is invalid.');
}
$user = $this->userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
/**
* {#inheritdoc}
*/
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
/**
* {#inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new JsonResponse([
'error' => 'invalid_grant',
'error_description' => $exception->getMessage()
], 401);
}
}
But I can't find why.
How to combine this two authenticator methods?
Thanks for help.
Finaly found how to handle it but not sure it's the better and proper way.
Don't hesitate to suggest improvements on comments! ;)
First of all, remove fos_oauth key from security.yml file. It should looks like:
security:
firewalls:
# [...]
api:
pattern: ^/api
stateless: true
# This will handle both oauth access token and simple private token
simple_preauth:
authenticator: security.authentication.authenticator.api_key
# [...]
On ApiKeyUserProvider::getUsernameForApiKey method, you will search on both custom api key manager and OAuth access token manager.
The complete class should look like this.
class ApiKeyUserProvider implements UserProviderInterface
{
/**
* #var UserManagerInterface
*/
private $userManager;
/**
* #var ApiKeyManager
*/
private $apiKeyManager;
/**
* #var AccessTokenManagerInterface
*/
private $accessTokenManager;
/**
* #param UserManagerInterface $userManager
* #param ApiKeyManager $apiKeyManager
* #param AccessTokenManagerInterface $accessTokenManager
*/
public function __construct(UserManagerInterface $userManager, ApiKeyManager $apiKeyManager, AccessTokenManagerInterface $accessTokenManager)
{
$this->userManager = $userManager;
$this->apiKeyManager = $apiKeyManager;
$this->accessTokenManager = $accessTokenManager;
}
/**
* #param string $apiKey
*
* #return string|null
*/
public function getUsernameForApiKey($apiKey)
{
// FOSOAuth system
$token = $this->accessTokenManager->findTokenByToken($apiKey);
if ($token) {
return $token->getUser()->getUsername();
}
// Private key system
return $this->apiKeyManager->getUsernameForToken($apiKey);
}
/**
* {#inheritdoc}
*/
public function loadUserByUsername($username)
{
return $this->userManager->findUserByUsername($username);
}
/**
* {#inheritdoc}
*/
public function refreshUser(UserInterface $user)
{
throw new UnsupportedUserException();
}
/**
* {#inheritdoc}
*/
public function supportsClass($class)
{
return 'FOS\UserBundle\Model\User' === $class;
}
}
And voila! Both Private and OAuth token are correctly managed.