LDAP role in Symfony 4 - ldap

I'm using the ldap component on my project in Symfony 4. It works great, authenticates successfully but the only role that I can set on users is the default_role provided in the option of the ldap provider in the security.yaml... All the doc I found wasn't very helpfull whether people use database to manage users or they don't talk about role when they're using the ldap component.
Here is my security.yaml if needed :
security.yaml :
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
#in_memory: { memory: null }
my_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: 'my_base_dn'
search_dn: 'my_search_dn'
search_password: '%env(resolve:LDAP_PASSWORD)%'
default_roles: ROLE_USER #rôle par défaut donné à l'utilisateur authentifié
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
form_login_ldap:
provider: my_ldap
service: Symfony\Component\Ldap\Ldap
dn_string: 'my_dn_string'
query_string: '(&(sAMAccountName={username})(memberOf=CN=***))'
login_path: login #route vers laquelle l'utilisateur sera redirigé si il tente d'accéder à une ressource protégé sans être authentifié
#/!\/!\/!\ NE PAS METTRE EN COMMENTAIRE SINON SUPPRESSION DES LOGS D'ERREURS !! /!\/!\/!\
check_path: login #route vers laquelle doit être envoyé la requête POST du formulaire
always_use_default_target_path: true
default_target_path: homePage #page vers laquelle l'utilisateur authentifié est redirigé
logout:
path: app_logout
target: login
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
If anyone have a solution or an idea !!

I am looking for the same feature, but it appears that this hasn't been implemented yet.
However, there is a feature request for this on GitHub.
IF your LDAP saves the group membership as an attribute of the user, one workaround would be to check the user object for this attribute in the controller and possibly deny access. You can create a separate method for this, and make sure to call it in every action method that needs security check, like so:
<?php
namespace App\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class SampleController extends AbstractController
{
public function index()
{
$ldapGroup = 'LDAP-User-Group';
$this->denyAccessUnlessLdapGroup($ldapGroup, 'ROLE_USER');
// continue if authenticated
}
/**
* Throws an exception unless the current user is member of the specified ldap group
*
* #throws AccessDeniedException
*
*/
private function denyAccessUnlessLdapGroup($ldapGroup, $attributes, string $message = 'Access Denied.') {
$ldapAttribute = 'memberof';
// usually you'll want to make sure the user is authenticated first
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
// returns your User object, or null if the user is not authenticated
// use inline documentation to tell your editor your exact User class
// in this case: \Symfony\Component\Ldap\Security\LdapUser
/** #var \App\Entity\User $user */
$user = $this->getUser();
/** #var \Synfony\Component\Ldap\Entry $entry */
$entry = $user->getEntry();
if(!$entry->getAttribute($ldapAttribute) ||
!in_array($ldapGroup, $entry->getAttribute($ldapAttribute))) {
$exception = $this->createAccessDeniedException($message);
$exception->setAttributes($attributes);
throw $exception;
}
}
}
Make sure to customize $ldapGroup and ldapAttribute. The value passed to $attributes is meaningless at this point; I have kept it for consistency for the other denyAccess* methods.

Related

Symfony 6.2 does not reach token handler on token authentication

Symfony 6.2 does not reach custom token handler while executing a request if token_extractors equals to header in the security.yml.
Here is the security.yml
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
access_token_provider:
entity:
class: App\Entity\AccessToken
property: secret
firewalls:
main:
lazy: true
provider: access_token_provider
stateless: true
pattern: ^/
access_token:
token_extractors: header
token_handler: App\Security\AccessTokenHandler
access_control:
- { path: ^/, roles: ROLE_ADMIN }
Here is the custom token handler, as you can see it shoud dump the token and die, and id does that if the token provided in query string.
namespace App\Security;
use App\Repository\AccessTokenRepository;
use Doctrine\ORM\NonUniqueResultException;
use SensitiveParameter;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
readonly class AccessTokenHandler implements AccessTokenHandlerInterface
{
public function __construct(private AccessTokenRepository $accessTokenRepository)
{
}
/**
* #throws NonUniqueResultException
*/
public function getUserBadgeFrom(#[SensitiveParameter] string $accessToken): UserBadge
{
var_dump($accessToken);
die;
$accessToken = $this->accessTokenRepository->getOneByToken($accessToken);
if (!$accessToken || !$accessToken->isValid()) {
throw new BadCredentialsException('Invalid credentials.');
}
return new UserBadge($accessToken->getId());
}
}
Authorization header generated by API client looks like this:
Authorization: Bearer 00000000-0000-0000-0000-000000000000
What's wrong in my implementation?
I tried to change token_extractors to query_string and provide token in GET parameters, and it does reach.
Query string looks like this:
localhost:8001/users?access_token=00000000-0000-0000-0000-000000000000
The problem was caused by Apache misconfiguration.
Apache didn't pass Authorization token to PHP due to default configuration and security reasons.
Adding CGIPassAuth On to a directory context solved this.
<Directory /var/www/html>
CGIPassAuth On
</Directory>
https://httpd.apache.org/docs/trunk/mod/core.html#cgipassauth

Symfony How to force Remember Me with Login Link?

I'm working on a project app where i've added the possibility for a team leader to add members in his team. Then i added a link which sends a login link for the members of the team.
I would like to force the "Remember Me" specifically for the members of the team. they would have to login just once using the link in the mail, and then they could close the browser and go back later and they would still be logged in.
Here is my security.yaml
login_link:
check_route: login_check
signature_properties: [ 'id' ]
lifetime: 900 #15minutes
And here is the function in the controller
/**
* #Route("/magic", name="app_magic")
*/
public function magic(UserRepository $userRepository, LoginLinkHandlerInterface $loginLinkHandler, MailerInterface $mailer): Response
{
$users = $userRepository->findBy(['chefEquipe' => $this->getUser()]);
foreach ($users as $user) {
$loginLinkDetails = $loginLinkHandler->createLoginLink($user);
$email = (new Email())
->from('email#test.com')
->to($user->getEmail())
->subject('Magic login link')
->text('You can use this link to login: ' . $loginLinkDetails->getUrl());
$mailer->send($email);
}
$this->addFlash('message', 'Un mail de connexion a été envoyé aux membres de votre équipe');
return $this->redirectToRoute('app_test_registration_team');
}
How could i do that without activating always remember me so that it does not affect the 'admin' user ?
Thanks a lot
You should enable always_remember_me in configs:
firewalls:
...
main:
...
remember_me:
...
always_remember_me: true

Symfony 4 login form with security and database users

I was a total noob on Symfony about a week ago and I thought I should just dive in Symfony 4. After a week of trying to solve the basic login problem, I believe the documentation is still missing some parts.
Now I've found a solution and I will share it along with some tips on what you might be doing wrong. First part of the answer is a list of suggestions, while the second part is the creation of a project with working login from scratch (supposing you already have composer installed and using a server like apache).
Part 1: Suggestions
403 Forbidden
Check the access_control: key in security.yaml. The order of the rules has impact, since no more than one rule will match each time. Keep most specific rules on top.
login_check
Make sure the form action sends you to the login_check path, or whatever you changed it to in security.yaml.
Also check that you have declared a route for the login_check path either in a controller or in routes.yaml.
input name
Symfony forms tend to encapsulate input names in an array, while it only expects them to be named _username and _password (you can change that in security.yaml) to count it as a login attempt. So inspect the inputs to make sure the name attributes are correct.
Part 2: Full Symfony 4 Login
Project Setup
Let's start by creating the project. Open cmd/terminal and go to the folder you want to contain the project folder.
cd .../MyProjects
composer create-project symfony/website-skeleton my-project
cd my-project
Now you have created a Symfony 4 website template in .../MyProjects/my-project and the cmd/terminal is in that path and will execute the rest of the commands properly.
Check in your .../MyProjects/my-project/public folder for a .htaccess file. If it exists you are fine, else run the following command.
composer require symfony/apache-pack
You can now find your site by visiting my-project.dev/public. If you want to remove this public path, you should do so using the .htaccess file, not moving the index.php.
Project Settings
1) Edit the DATABASE_URL key inside the .env file to correspond to your database settings.
2) Edit the config/packages/security.yaml file, so it looks like this:
security:
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
user:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
provider: user
form_login:
#login_path: login
#check_path: login_check
default_target_path: homepage
#username_parameter: _username
#password_parameter: _password
logout:
#path: /logout
#target: /
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
- { path: ^/admin, roles: ROLE_ADMIN }
Some explanation:
App\Entity\User is the User entity you 'll create in a while to handle the login.
The user provider is just a name that needs to have a match in providers and firewalls.
The logout key must be declared if you want to allow the user to... well, logout.
Values in #comment reveal the default value we'll be using later on and act as a reference of what you are more likely to change.
User Entity
A user must have a role, but could have more. So let's build a UserRole Entity first for a ManyToMany relationship.
php bin/console make:entity userRole
All entities start with an id property. Add a role too.
php bin/console make:entity user
User needs the username, password and roles properties, but you can add more.
Let's edit the src/Entity/User.php file:
Add the UserInterface interface to your User class.
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
Edit the generated getRoles(), to make it return string array.
public function getRoles(): array
{
$roles = $this->roles->toArray();
foreach($roles as $k => $v) {
$roles[$k] = $v->getRole();
}
return $roles;
}
getSalt() and eraseCredentials() are functions to implement the UserInterface interface.
public function getSalt()
{
return null;
}
public function eraseCredentials()
{
}
Using the bcrypt algorithm (as we set in security.yaml) we don't need a salt. It generates automatically one. No, you don't store this salt anywhere and yes, it will produce different hash for the same password every time. But yes, it will work somehow (magic...).
If you need a different algorithm, that uses salt, you need to add a salt property on the User entity.
Homepage
For testing purposes we will create a homepage
php bin/console make:controller homepage
Edit the generated src/Controller/HomepageController.php file to change the root to /
#Route("/", name="homepage")
Login Controller
php bin/console make:controller login
Edit the generated src/Controller/LoginController.php file to make it like this:
<?php
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use App\Form\LoginType;
class LoginController extends Controller
{
/**
* #Route("/login", name="login")
*/
public function index(AuthenticationUtils $authenticationUtils)
{
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
$form = $this->createForm(LoginType::class);
return $this->render('login/index.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
'form' => $form->createView(),
]);
}
/**
* #Route("/logout", name="logout")
*/
public function logout() {}
/**
* #Route("/login_check", name="login_check")
*/
public function login_check() {}
}
Login Form
php bin/console make:form login
You don't have to associate it to the User entity.
Edit the generated src/Form/LoginType.php file to add this:
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
replace this:
$builder
->add('_username')
->add('_password', PasswordType::class)
->add('login', SubmitType::class, ['label' => 'Login'])
;
and add this function, to prevent Symfony from changing the input names you requested above by enclosing them in login[...]
public function getBlockPrefix() {}
Login Template
Edit the templates/login/index.html.twig file to add this code in the {% block body %} ... {% endblock %}:
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
{{ form_start(form, {'action': path('login_check'), 'method': 'POST'}) }}
{{ form_widget(form) }}
{{ form_end(form) }}
Database Generation
php bin/console doctrine:migrations:generate
php bin/console doctrine:migrations:migrate
This should have generated your database, according to your User and UserRole entities.
Generate Password
The following command will provide you with a hashed password you can directly insert into the database. The password will be hashed with the algorithm specified in security.yaml.
php bin/console security:encode-password my-password
Hope this helps!
Thank you very much, the documentation lacks some important things, but this works very good. For others, check the order of the access-control entries, because one misplaced entry may block the whole process.
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
This was working, but this not
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }

Symfony 4: Authentication Token is lost from the Session after login_check redirect

Recently I have made some code changes to store sessions in Database using PdoSessionHandler. I am using Guard Authentication. checkCredentials is working fine is working fine, insert into "sessions" table is also working fine. But the Authentication token in the session is lost after /login_check redirect.
Authentication token is getting stored in the serialized format under "_security_secured_area" in the session and the session is also saved in the DB but after the redirect from /login_check to /login_redirect session is available with the same id but the auth token details are missing. Probably it is not able to populate auth details from the DB.
Here is my packages/security.yaml
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured_area:
pattern: ^/
anonymous: ~
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: _logout
target: _public_signin
logout_on_user_change: true
remember_me:
secret: '%kernel.secret%'
lifetime: 2592000 # 30 days in seconds
path: /
domain: ~
remember_me_parameter: _stay_signedin
# 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
token_provider: token_service
Here is my gurardAuthenticator:
/**
* Override to change what happens after successful authentication.
*
* #param Request $request
* #param TokenInterface $token
* #param string $providerKey
*
* #return RedirectResponse
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
/** #var User $user */
$user = $token->getUser();
if ($user->getNewUser() === true) {
$url = '_onboard';
} elseif ($user->getResetPass() === true) {
$url = '_change_temp_password';
} else {
$url = '_login_redirect';
}
//$request->getSession()->save();
// MAS: TODO Add Audit probably in listener
return new RedirectResponse($this->urlGenerator->generate($url));
}
After AuthenticationSuccess it automatically redirects to loginReditrectAction() in SecurityController.php but here PostAuthenticationGuardToken is lost, AuthenticationEvent is returning AnonymousToken.
Another observation I found when I printed session in loginRedirectAction() in SecurityContrller.php is "_security_secured_area" in session data is missing.
#session: Session {#149 ▼
#storage: NativeSessionStorage {#148 ▶}
-flashName: "flashes"
-attributeName: "attributes"
-data: &2 array:2 [▼
"_sf2_attributes" => &1 array:4 [▼
"_csrf/https-kinetxx" => "rvR8Rr2UcDM_-y16ehk_jgYvMREJ8mTNouYCT16RtfY"
"_security.last_username" => "ktx_provider"
"userTimeZone" => "America/Chicago"
"practiceTimeZone" => "America/New_York"
]
"_symfony_flashes" => &3 []
]
My SecurityController.php
/**
* #Route("/login_redirect", name="_login_redirect")
*
* #param Request $request
*
* #return RedirectResponse
*/
public function loginRedirectAction(Request $request)
{
dump($request);
dump($this->get('security.authorization_checker'));
die;
}
Can someone help me resolving this?
I had this problem and the solution was to change my provider entity from
implements UserInterface, \Serializable
to
implements AdvancedUserInterface, \Serializable, EquatableInterface
and adding the needed methods: isEqualTo(UserInterface $user), isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired(), isEnabled()

Symfony2 hold behavior when extending DefaultAuthenticationSuccessHandler

I want to be able to login in my app via the usual login form and using AJAX directly sending the user/password to the login_check path.
The idea is when user access via AJAX return Response with code 200 or 400 depending if the login success or fails.
Looking at other threads I finally decide to extend the default handlers DefaultAuthenticationSuccessHandler and DefaultAuthenticationFailureHandler to achieve this, export as services and specify in the success_handler property in my secure area.
File service.yml
services:
authentication.success_handler:
class: %mycustom.authentication.success_handler.class%
arguments: ['#security.http_utils', {} ]
public: false
tags:
- { name: 'monolog.logger', channel: 'security' }
authentication.failure_handler:
class: %mycustom.authentication.failure_handler.class%
arguments: ['#http_kernel', '#security.http_utils', {}, '#logger' ]
public: false
tags:
- { name: 'monolog.logger', channel: 'security' }
File security.yml
secured_meems_area:
pattern: ^/somrurl/
form_login:
login_path: /somrurl/login
check_path: /somrurl/api/login_check
success_handler: authentication.success_handler
failure_handler: authentication.failure_handler
require_previous_session: false
All this seems to work, except the behaviour of my extendend handler isn't like the original one. In the default implementation used by Symfony if you access a page/A and you are not logged on, Symfony redirects to the login page and after it you are redirected again to page/A. But this not occurs with my extended handler.
To solve it, I can specify a default_target_path when registering the handler as a service, but I want to understand why it doesn't follow the "normal" behavior.
Any ideas out there.
Thanks in advance.
SOLUTION !!!
After looking and test I found a solution here Symfony2 extending DefaultAuthenticationSuccessHandler
The idea is override the default symfony success/failure handlers instead of define my own and apply in the security.yml file.
File security.yml
secured_meems_area:
pattern: ^/somrurl/
form_login:
login_path: /somrurl/login
check_path: /somrurl/api/login_check
#
# DON'T USE !!!
#
# success_handler: authentication.success_handler
# failure_handler: authentication.failure_handler
#
require_previous_session: false
File service.yml. (NOTE the security. in the service name)
services:
security.authentication.success_handler:
class: %mycustom.authentication.success_handler.class%
arguments: ['#security.http_utils', {} ]
public: false
tags:
- { name: 'monolog.logger', channel: 'security' }
security.authentication.failure_handler:
class: %mycustom.authentication.failure_handler.class%
arguments: ['#http_kernel', '#security.http_utils', {}, '#logger' ]
public: false
tags:
- { name: 'monolog.logger', channel: 'security' }
This way we are overriden the default implementation with our own and don't need to specify the handlers in the security area.