How to query both samaaccountname and userprincipalname symfonyLdap - symfony

I have a Symfony login form authenticating against an Ldap server. I can successfully query and authenticate a user using either samaccountname or userprincipalname and the uid key in my config settings. I want to be able to allow the user to enter either their username or their username#domain.com
I have tried a preg_replace on the username in the loadUserbyUsername() method in the LdapUserProviderClass (I know is not ideal). That takes a username such as username#domain.com and passes on username. I was able to verify that the correct user was returned from the Ldap server but I'm still returned to the login form with 'Invalid Credentials'. I believe the reason why this happens in the AuthenticationUtils class request is processed and the username in the request is still username#domain.com and that does not match the username in the user object coming from the Ldap authentication which is username. If anyone has advice on how to accomplish allowing both username#domain.com and username being authenticated against Ldap I would greatly appreciate it.
SecurityController.php
public function login(Request $request, AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
$newLastUsername = trim(preg_replace('/#.*/', '',$lastUsername));
return $this->render('security/login.html.twig', ['last_username' => $newLastUsername, 'error' => $error]);
}
security.yml
providers:
dsg_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: '%env(BASE_DSN)%'
search_dn: '%env(SEARCH_DN)%'
search_password: '%env(SEARCH_PWD)%'
uid_key: '%env(UID_KEY)%'
#filter: '({uid_key}={_username})'
default_roles: ROLE_USER
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
form_login_ldap:
login_path: login
check_path: login
service: Symfony\Component\Ldap\Ldap
provider: dsg_ldap
dn_string: '%env(DN_STRING)%\{username}'
My LdapUserProvider.php
class LdapUserProvider extends SymfonyLdapUserProvider
{
/** #var array maps ldap groups to roles */
private $groupMapping = [
'**' => '**',
'**' => '**',
'**' => '**',
'**' => '**'
];
/** #var string extracts group name from dn string */
private $groupNameRegExp = '/CN=(.+?),/';
protected function loadUser($username, Entry $entry)
{
$roles = ['ROLE_USER'];
// Check if the entry has attribute with the group
if (!$entry->hasAttribute('memberOf')) {
return new User($username, '', $roles);
}
// Iterate through each group entry line
foreach ($entry->getAttribute('memberOf') as $groupLine) {
// Extract the group name from the line
$groupName = $this->getGroupName($groupLine);
// Check if the group is in the mapping
if (array_key_exists($groupName, $this->groupMapping)) {
// Map the group to the role(s) the user will have
$roles[] = $this->groupMapping[$groupName];
}
}
// Create and return the user object
return new User($username, null, $roles);
}
/**
* Get the group name from the DN
* #param string $dn
* #return string
*/
private function getGroupName($dn)
{
$matches = [];
return preg_match($this->groupNameRegExp, $dn, $matches) ? $matches[1] : '';
}
}
Symfony\Component\Security\Core\User\LdapUserProvider.php
public function loadUserByUsername($username)
{
try {
$this->ldap->bind($this->searchDn, $this->searchPassword);
// what i added
$username = trim(preg_replace('/#.*/', '',$username));
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$query = str_replace('{username}', $username, $this->defaultSearch);
$search = $this->ldap->query($this->baseDn, $query);
} catch (ConnectionException $e) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e);
}
$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');
}
$entry = $entries[0];
try {
if (null !== $this->uidKey) {
$username = $this->getAttributeValue($entry, $this->uidKey);
}
} catch (InvalidArgumentException $e) {
}
return $this->loadUser($username, $entry);
}

In the loadUserByUsername function, add another query if the first one fails. Your $this->defaultSearch is probably a constant which represents an LDAP filter.
If you create the same line in the first if (!$count) {...} like this:
$query = str_replace('{username}', $username, "(userPrincipalName={username})");
and then execute that query, you are performing a second search for the userPrincipalName which is of the form user#domain

Related

Symfony 5 Reset Password how do i get the right url?

I'll try to explain my problem.
I have recently started a new projet and wanted to implement a reset password functionnality.
Everything seems to work except the generation of the url which is send by email.
picture of my URL
My URL should look like this : http://localhost/projectName/public/reset-password/reset/xOdfPc0iGC7nmReqX02jcemgX4EIlt2tb5vNYgTZ
But the "/projectName/public/" is missing.
I don't understand what i did wrong.
Here is my twig template for the email :
<h1>Bonjour !</h1>
<p>Pour réinitialiser votre mot de passe, merci de vous rendre sur le lien suivant</p>
{{ url('app_reset_password', { token: resetToken.token }) }}
<p>Ce lien expirera dans {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p>
<p>A bientôt !</p>
Here is the function in the controller that generates the templated email :
private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer, TranslatorInterface $translator): RedirectResponse
{
$user = $this->entityManager->getRepository(User::class)->findOneBy([
'email' => $emailFormData,
]);
// Do not reveal whether a user account was found or not.
if (!$user) {
return $this->redirectToRoute('app_check_email');
}
try {
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
} catch (ResetPasswordExceptionInterface $e) {
// If you want to tell the user why a reset email was not sent, uncomment
// the lines below and change the redirect to 'app_forgot_password_request'.
// Caution: This may reveal if a user is registered or not.
//
// $this->addFlash('reset_password_error', sprintf(
// '%s - %s',
// $translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE, [], 'ResetPasswordBundle'),
// $translator->trans($e->getReason(), [], 'ResetPasswordBundle')
// ));
return $this->redirectToRoute('app_check_email');
}
$email = (new TemplatedEmail())
->from(new Address('assistance#asintel.com', 'AS Intel - Assistance'))
->to($user->getEmail())
->subject('Your password reset request')
->htmlTemplate('reset_password/email.html.twig')
->context([
'resetToken' => $resetToken,
])
;
$mailer->send($email);
// Store the token object in session for retrieval in check-email route.
$this->setTokenObjectInSession($resetToken);
return $this->redirectToRoute('app_check_email');
}
And this is the function with app_reset_password route :
/**
* Validates and process the reset URL that the user clicked in their email.
*
* #Route("/reset/{token}", name="app_reset_password")
*/
public function reset(Request $request, UserPasswordHasherInterface $userPasswordHasher, TranslatorInterface $translator, string $token = null): Response
{
if ($token) {
// We store the token in session and remove it from the URL, to avoid the URL being
// loaded in a browser and potentially leaking the token to 3rd party JavaScript.
$this->storeTokenInSession($token);
return $this->redirectToRoute('app_reset_password');
}
$token = $this->getTokenFromSession();
if (null === $token) {
throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
}
try {
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
} catch (ResetPasswordExceptionInterface $e) {
$this->addFlash('reset_password_error', sprintf(
'%s - %s',
$translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, [], 'ResetPasswordBundle'),
$translator->trans($e->getReason(), [], 'ResetPasswordBundle')
));
return $this->redirectToRoute('app_forgot_password_request');
}
// The token is valid; allow the user to change their password.
$form = $this->createForm(ChangePasswordFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// A password reset token should be used only once, remove it.
$this->resetPasswordHelper->removeResetRequest($token);
// Encode(hash) the plain password, and set it.
$encodedPassword = $userPasswordHasher->hashPassword(
$user,
$form->get('plainPassword')->getData()
);
$user->setPassword($encodedPassword);
$this->entityManager->flush();
// The session is cleaned up after the password has been changed.
$this->cleanSessionAfterReset();
return $this->redirectToRoute('main_index');
}
return $this->render('reset_password/reset.html.twig', [
'resetForm' => $form->createView(),
]);
}
Does someone have an idea what i should do to fix this problem ?
Thanks a lot

symfony validation doesn't work

I guess there is something I'm missing.
I have a User entity which is validated through a yml file but every time I send a post request to the route it seems it doesn't get my request. With this I mean that the route works fine but I keep getting the error messages that the password and username should not be blank (due to the constraints i set). So it seems it's not getting my request validated against the entity.
I made sure to have this settings triggered in my config:
validation: { enabled: true, enable_annotations: true }
Here is my routing.yml:
user_login_homepage:
path: /check
defaults: { _controller: UserLoginBundle:Login:checkCredentials }
methods: [POST]
Here is my validation.yml
User\UserBundle\Entity\User:
properties:
username:
- NotBlank: ~
password:
- NotBlank: ~
Here is my controller (LoginController.php)
public function checkCredentialsAction(Request $request)
{
$recursiveValidator = $this->get('validator');
$user = new User();
$errors = $recursiveValidator->validate($user);
if (count($errors) > 0) {
$errorsString = (string) $errors;
return new Response($errorsString);
}
return new Response('Yuppy');
}
I've just tried to follow the instructions but I'm not able to have it work :(
Am I missing something?
you are creating an empty User so It's correct the error, try this (I have imagine that username and password are passed into POST data right?):
$user = new User();
$postData = $request->request->all();
$user->setUsername($postData['username'];
$user->setPassword($postData['password'];
$errors = $recursiveValidator->validate($user);

Symfony2 Payum Bundle - Request SecuredCaptureRequest{model: Payment} is not supported

I have just installed and configured the payum bundle. I am having an exception:
Request SecuredCaptureRequest{model: Payment} is not supported.
It occurs after the redirect in the preparePaypalExpressCheckoutPaymentAction in the PaymentController.
Payum config:
payum:
contexts:
payment_with_paypal_express:
storages:
Service\Bundle\PaymentBundle\Entity\Payment:
doctrine:
driver: orm
paypal_express_checkout_nvp:
api:
options:
username: %paypal.express.username%
password: %paypal.express.password%
signature: %paypal.express.signature%
sandbox: %paypal.express.sandbox%
security:
token_storage:
Service\Bundle\PaymentBundle\Entity\PayumSecurityToken:
doctrine:
driver: orm
Payment controller:
class PaymentController extends HelperController
{
public function preparePaypalExpressCheckoutPaymentAction()
{
$paymentName = 'payment_with_paypal_express';
$storage = $this->get('payum')->getStorageForClass(
'Service\Bundle\PaymentBundle\Entity\Payment',
$paymentName
);
# ---- Set payment details below
$package = $this->getPackageRepository()->loadOneByAliasAndDuration(Package::TYPE_SUBSCRIPTION,1);
$accountPackages = new ArrayCollection();
$accountPackages->add((new AccountPackage())->setPackage($package)->setQuantity(1));
/**
* #var Payment $payment
*/
$payment = $storage->createModel();
# Account must be set first, packages must be set before paid attribute
$payment->setAccount($this->getAccount())
->setPackages($accountPackages)
->setPaid(false);
# ---- Set payment details above
$storage->updateModel($payment);
$captureToken = $this->get('payum.security.token_factory')->createCaptureToken(
$paymentName,
$payment,
'service_payment_done' // the route to redirect after capture;
);
$payment->setReturnUrl($captureToken->getTargetUrl())
->setCancelUrl($captureToken->getTargetUrl());
$storage->updateModel($payment);
return $this->redirect($captureToken->getTargetUrl());
}
public function paymentDoneAction(Request $request)
{
$token = $this->get('payum.security.http_request_verifier')->verify($request);
$payment = $this->get('payum')->getPayment($token->getPaymentName());
# $paymentDetails = $token->getDetails();
$status = new BinaryMaskStatusRequest($token);
$payment->execute($status);
if ($status->isSuccess()) {
$this->getUser()->addCredits(100);
$this->get('session')->getFlashBag()->set(
'notice',
'Payment success. Credits were added'
);
}
else if ($status->isPending()) {
$this->get('session')->getFlashBag()->set(
'notice',
'Payment is still pending. Credits were not added'
);
}
else {
$this->get('session')->getFlashBag()->set('error', 'Payment failed');
}
return $this->redirect('service_home');
}
}
Does someone have any hints what I am doing wrong? In the official documentation the payment details were presented as an object/array (a little confusing), but in my controller I made it an object, any thoughts there?
I worked it out. Forgot to extend my Payment details with ArrayObject from Payum:)

symfony2 Bad credentials at login

I've been trying to get a database login system working with Symfony2. I've read through the docs but I'm totally stumped now as to what to do...
Every time I try to log in I just get 'Bad Credentials'.
security.yml
security:
encoders:
D\UserBundle\Entity\User:
algorithm: sha512
encode_as_base64: false
iterations: 1
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]
providers:
user_db:
entity: { class: DUserBundle:User}
firewalls:
secured_area:
pattern: ^/
anonymous: ~
form_login:
login_path: /login
check_path: /login_check
logout:
path: /logout
target:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/user, roles: ROLE_USER }
UserRepository.php
<?php
namespace D\UserBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;
class UserRepository extends EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$q = $this
->createQueryBuilder('u')
->where('u.username = :username OR u.email = :email')
->setParameter('username', $username)
->setParameter('email', $username)
->getQuery()
;
try {
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $q->getSingleResult();
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf('Unable to find an active admin AcmeUserBundle:User object identified by "%s".', $username), null, 0, $e);
}
return $user;
}
public function refreshUser(UserInterface $user)
{
$class = get_class($user);
if (!$this->supportsClass($class)) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $class));
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $this->getEntityName() === $class || is_subclass_of($class, $this->getEntityName());
}
}
User.php
UserController.php
that's my database, table(users)
id:5
username:Pawel
salt:5633267d072cd3119af5270d64b4b45d
password:test
email:test#test.pl
is_active1
Your security.yml should look like this
providers:
user_db:
entity: { class: DUserBundle:User, property: username }
Yo do not need the UserRepository class at all.
Really late sorry but as the question is not answered yet...
I am not sure if you can use the alias of your entity as the "class" value for your provider.
Maybe try to set it up with the complete namespaced entity name:
volunteers:
entity:
class: D\UserBundle\Entity\User
But I think the main problem is that your password should - at least, and probably HAVE TO - be encrypted in your database.
If you want to use the security encoder that you define in your security.yml config file,
the right way - according to the documentation - is to manage it after your registration form validation.
Let's imagine you have a registration form for your users :
# Any routing file
d_userbundle_register:
pattern: /register
defaults:
_controller: DUserBundle:Users:register
requirements:
_method: get
d_userbundle_register_check:
pattern: /register_check
defaults:
_controller: DUserBundle:Users:registerCheck
requirements:
_method: post
And the registerCheckAction of your controller:
// UsersController.php
public function registerCheckAction()
{
$request = $this->getRequest();
$user = new D\UserBundle\Entity\User();
$registrationForm = $this->createForm(new D\UserBundle\Form\UserType(), $user);
$registrationForm->bind($request);
if ($registrationForm->isValid()) {
$em = $this->getDoctrine()->getManager(); // use getEntityManager() instead of getManager() for old symfony2 versions
// Get the factory encoder service
$encoderFactory = $this->get('security_encoder.factory');
// Retrieve the algorithms, iterations and everything defined in your security.yml file
$encoder = $encoderFactory->getEncoder($user);
// Encrypt the password with the user's plaintext password AND the user salt
$encryptedPassword = $encoder->encodePassword($user->getPassword(), $user->getSalt());
// Then set the user's encrypted password :)
$user->setPassword($encryptedPassword);
$em->persist($user);
$em->flush();
// Thanks the user
$request->getSession()->getFlashBag()->add('success', 'Heyoh, welcome on board !');
return $this->redirect($this->generateUrl('d_userbundle_login'));
}
return $this->render('DUserBundle:User:registration.html.twig', array(
'registration_form' => $registrationForm->createView()
));
}

symfony2 acl groups

Generally I have the following business model:
There're users and groups. Each user belongs to only one group and amount of groups is not determined beforehead (as well as amount of users for most sites).
Also there're several different busyness objects, which may belong to user.
Groups are not separate objects, which should be controlled by ACL themselves, but they should affect how other entities should be controlled much like unix groups.
There're 3 basic roles: SUPERADMIN, ADMIN and USER.
SUPERADMIN is able to do anything with any entity.
USER is generally able to read/write own entities (including him/her-self) and read
entitites from his/her group.
ADMIN should have full control of
entities within his group, but not from other groups. I don't
understand how to apply ACL inheritance here (and whether this could
be applied at all).
Also I'm interested in, how denying access could be applied in ACL. Like user have read/write access to all his fields except login. User should only read his login.
I.e. it is logical to provide read/write access to his own profile, but deny write to login, rather than defining read/write access to all his fields (except login) directly.
Ok, here it is. Code isn't perfect at all, but it's better, than nothing.
Voter service.
<?php
namespace Acme\AcmeBundle\Services\Security;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
class GroupedConcernVoter implements VoterInterface {
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$rc = $this->container->getParameter('grouped_concern_voter.config');
// some config normalization performed
$this->rightsConfig = $rc;
}
// even though supportsAttribute and supportsClass methods are required by interface,
// services that I saw, leaves them empty and do not use them
public function supportsAttribute($attribute)
{
return in_array($attribute, array('OWNER', 'MASTER', 'OPERATOR', 'VIEW', 'EDIT', 'CREATE', 'DELETE', 'UNDELETE', 'DEPLOY'))
// hacky way to support per-attribute edit and even view rights.
or preg_match("/^(EDIT|VIEW)(_[A-Z]+)+$/", $attribute);
}
public function supportsClass($object)
{
$object = $object instanceof ObjectIdentity ? $object->getType() : $object;
// all our business object, which should be manageable by that code have common basic class.
// Actually it is a decorator over Propel objects with some php magic... nevermind.
// If one wants similar solution, interface like IOwnableByUserAndGroup with
// getUserId and getGroupId methods may be defined and used
return is_subclass_of($object, "Acme\\AcmeBundle\\CommonBusinessObject");
}
function vote(TokenInterface $token, $object, array $attributes)
{
if (!$this->supportsClass($object)) {
return self::ACCESS_ABSTAIN;
}
if ($object instanceof ObjectIdentity) $object = $object->getType();
if (is_string($object)) {
$scope = 'own';
$entity = $object;
} else {
if ($object->getUserId() == $this->getUser()->getId()) {
$scope = 'own';
} else if ($object->getGroupId() == $this->getUser()->getGroupId()) {
$scope = 'group';
} else {
$scope = 'others';
}
$entity = get_class($object);
}
$user = $token->getUser();
$roles = $user->getRoles();
$role = empty($roles) ? 'ROLE_USER' : $roles[0];
$rights = $this->getRightsFor($role, $scope, $entity);
if ($rights === null) return self::ACCESS_ABSTAIN;
// some complicated logic for checking rights...
foreach ($attributes as $attr) {
$a = $attr;
$field = '';
if (preg_match("/^(EDIT|VIEW)((?:_[A-Z]+)+)$/", $attr, $m)) list(, $a, $field) = $m;
if (!array_key_exists($a, $rights)) return self::ACCESS_DENIED;
if ($rights[$a]) {
if ($rights[$a] === true
or $field === '')
return self::ACCESS_GRANTED;
}
if (is_array($rights[$a])) {
if ($field == '') return self::ACCESS_GRANTED;
$rfield = ltrim(strtolower($field), '_');
if (in_array($rfield, $rights[$a])) return self::ACCESS_GRANTED;
}
return self::ACCESS_DENIED;
}
}
private function getRightsFor($role, $scope, $entity)
{
if (array_key_exists($entity, $this->rightsConfig)) {
$rc = $this->rightsConfig[$entity];
} else {
$rc = $this->rightsConfig['global'];
}
$rc = $rc[$role][$scope];
$ret = array();
foreach($rc as $k => $v) {
if (is_numeric($k)) $ret[$v] = true;
else $ret[$k] = $v;
}
// hacky way to emulate cumulative rights like in ACL
if (isset($ret['OWNER'])) $ret['MASTER'] = true;
if (isset($ret['MASTER'])) $ret['OPERATOR'] = true;
if (isset($ret['OPERATOR']))
foreach(array('VIEW', 'EDIT', 'CREATE', 'DELETE', 'UNDELETE') as $r) $ret[$r] = true;
return $ret;
}
private function getUser() {
if (empty($this->user)) {
// Not sure, how this shortcut works. This is a service (?) returning current authorized user.
$this->user = $this->container->get('acme.user.shortcut');
}
return $this->user;
}
}
And config... actually, it is implementation-specific and its structure is completely arbitrary.
grouped_concern_voter.config:
global:
ROLE_SUPERADMIN:
own: [MASTER]
group: [MASTER]
others: [MASTER]
ROLE_ADMIN:
own: [MASTER]
group: [MASTER]
others: []
ROLE_USER:
own: [VIEW, EDIT, CREATE]
group: [VIEW]
others: []
"Acme\\AcmeBundle\\User":
# rights for ROLE_SUPERADMIN are derived from 'global'
ROLE_ADMIN:
own:
VIEW: [login, email, real_name, properties, group_id]
EDIT: [login, password, email, real_name, properties]
CREATE: true
group:
VIEW: [login, email, real_name, properties]
EDIT: [login, password, email, real_name, properties]
# rights for ROLE_ADMIN/others are derived from 'global'
ROLE_USER:
own:
VIEW: [login, password, email, real_name, properties]
EDIT: [password, email, real_name, properties]
group: []
# rights for ROLE_USER/others are derived from 'global'
"Acme\\AcmeBundle\\Cake":
# most rights are derived from global here.
ROLE_ADMIN:
others: [VIEW]
ROLE_USER:
own: [VIEW]
others: [VIEW]
And finally usage example. Somewhere in controller:
$cake = Acme\AcmeBundle\CakeFactory->produce('strawberry', '1.3kg');
$securityContext = $this->get('security.context');
if ($securityContext->isGranted('EAT', $cake)) {
die ("The cake is a lie");
}
when creating a group, create role ROLE_GROUP_(group id), promote group with this role, and grant permissions with rolesecurityidentity

Resources