I am not able to validate a user against more than one ldap library using the services and security config (Symfony 3.3).
I am using the Ldap symfony component and creating 2 ldap config services for two different hosts.
services.yml:
ldap1:
class: Symfony\Component\Ldap\Ldap
arguments: ['@ldap_adapter1']
ldap_adapter1:
class: Symfony\Component\Ldap\Adapter\ExtLdap\Adapter
arguments:
- host: serldap.abc.fr
port: 389
options:
protocol_version: 3
referrals: false
ldap2:
class: Symfony\Component\Ldap\Ldap
arguments: ['@ldap_adapter2']
ldap_adapter2:
class: Symfony\Component\Ldap\Adapter\ExtLdap\Adapter
arguments:
- host: ldap.xyz.fr
port: 389
options:
protocol_version: 3
referrals: false
security.yml:
security:
providers:
chain_provider:
chain:
providers: [ldap_1, ldap_2]
ldap_1:
ldap:
service: ldap1
base_dn: ou=abcaccount,dc=abc,dc=fr
search_dn: uid=a1,ou=abcaccount,dc=abc,dc=fr
search_password: pass1
default_roles: ROLE_USER
uid_key: uid
ldap_2:
ldap:
service: ldap2
base_dn: ou=xyzaccount,dc=xyz,dc=fr
search_dn: uid=a2,ou=xyzaccount,dc=xyz,dc=fr
search_password: pass2
default_roles: ROLE_USER
uid_key: uid
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: ~
provider: chain_provider
form_login_ldap:
login_path: login
check_path: login
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
If i add the dn_string under the form_login_ldap. i,e:
dn_string: 'uid={username},ou=xyzaccount,dc=xyz,dc=fr'
This works, the problem is this can only be configured for one Ldap. without this line I get the following error:
php.DEBUG: Warning: ldap_bind(): Unable to bind to server: Invalid DN syntax
2 questions:
Is there any way of verifying a user against 2 ldap libraries whilst keeping it simple?
It would be even better if they could choose the library they where validating against via the login form with this being passed through the as some kind of input?
e.g.
dn_string: 'uid={username},ou={chosenOUInForm},dc={chosenDC1InForm},dc={chosenDC2InForm}'
Thanks in advance.
I managed to get it working by adding the request_matcher param to the security:
security:
providers:
chain_provider:
chain:
providers: [ldap_1, ldap_2]
ldap_1:
ldap:
service: ldap1
base_dn: ou=abcaccount,dc=abc,dc=fr
search_dn: ~
search_password: ~
default_roles: ROLE_USER
uid_key: uid
ldap_2:
ldap:
service: ldap2
base_dn: ou=xyzaccount,dc=xyz,dc=fr
search_dn: ~
search_password: ~
default_roles: ROLE_USER
uid_key: uid
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
base:
pattern: ^/
request_matcher: app.base_firewall_matcher
anonymous: ~
form_login_ldap:
service: ldap1 # this doesn't matter for the base firewall as it is never passed with no check_path
login_path: login
one:
pattern: ^/
request_matcher: app.first_firewall_matcher
anonymous: ~
provider: ldap_1
form_login_ldap:
service: ldap1
login_path: login
check_path: login_1_check
dn_string: 'uid={username},ou=abcaccount,dc=abc,dc=fr'
two:
pattern: ^/
request_matcher: app.second_firewall_matcher
anonymous: ~
provider: ldap_2
form_login_ldap:
service: ldap2
login_path: login
check_path: login_2_check
dn_string: 'uid={username},ou=xyzaccount,dc=xyz,dc=fr'
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
Where the form page (twig page that the login path returns) will have one button to submit the form to login_1_check and one button to submit the form to login_2_check:
<form action="{{ path('login_1_check') }}" method="post">
<input type="text" id="username" name="_username"/>
<input type="password" id="password" name="_password" />
<button type="submit">login to LDAP 1</button>
<button type="submit" formaction="{{ path('login_2_check') }}">login to LDAP 2</button>
</form>
The firewalls are hit in order, therefore the base firewall is checked first and if true is returned (if the user is not already authenticated or the login form is not being submitted) it goes to the login path.
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
class BaseFirewallMatcher implements RequestMatcherInterface
{
// Should only be matched on login page when user first arrives to GET the form, if already validated with one
// of the other two security providers, they will be checked against their respective firewalls.
public function matches(Request $request){
$session = $request->getSession();
$oneValidated = $session->get("_security_one", null);
$twoValidated = $session->get("_security_two", null);
// If already validated with another security provider
if($oneValidated or $twoValidated){
return false;
}
else{
$url = $request->getPathInfo();
// If these two logins are not matched
if($url != "/login_1_check" && $url != "/login_2_check"){
return true;
}
else{
return false;
}
}
}
}
When a user submits by pressing one of the buttons the base firewall will not be triggered as the BaseFirewallMatcher will return false. The 1st LDAP check is then done going to the following request matcher.
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
class FirstFirewallMatcher implements RequestMatcherInterface
{
public function matches(Request $request){
$session = $request->getSession();
$oneValidated = $session->get("_security_one", null);
$twoValidated = $session->get("_security_two", null);
if($twoValidated){
return false;
}
else{
if($oneValidated){
return true;
}
$url = $request->getPathInfo();
if ($url == "/login_1_check"){
return true;
}
else{
return false;
}
}
}
}
This returns true, i.e. if user is already authenticated with ldap1 or the user has chosen to login to ldap1. The firewall is then triggered.
If this returns false e.g. when the user chooses to login to ldap2 or is already authenticated with ldap2 - it goes to the next request matcher:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
class SecondFirewallMatcher implements RequestMatcherInterface
{
public function matches(Request $request){
$session = $request->getSession();
$oneValidated = $session->get("_security_one", null);
$twoValidated = $session->get("_security_two", null);
if($oneValidated){
return false;
}
else{
if($twoValidated){
return true;
}
$url = $request->getPathInfo();
dump($url);
if ($url == "/login_2_check"){
return true;
}
else{
return false;
}
}
}
}
Remember to setup the services for each request_matcher:
app.base_firewall_matcher:
class: {pathtofirewallmatcher}\BaseFirewallMatcher
app.first_firewall_matcher:
class: {pathtofirewallmatcher}\FirstFirewallMatcher
app.second_firewall_matcher:
class: {pathtofirewallmatcher}\SecondFirewallMatcher
The code might not be perfect as in the end I moved onto a different type of authentication. But this seemed to work at the time and I assume you can add as many ldaps as required.