- Symfony 2.8+
- Symfony 2.7
- HTTP Basic Authentication
- LDAP Authentication Username
- Mapping LDAP Groups to Roles
- Mapping LDAP Attributes
- Guard Specific Settings
- Guard Redirection
- Show Detailed Login Errors
- LDAP Login Event
- Load User Events
- User Refresh Settings
- Multiple Domain Login
Setting up LDAP form based authentication can be done fairly easily. Simply follow the example configs listed below
depending on your Symfony version. For Symfony 2.8+ the Guard component is used. In each example, don't forget to
register the needed routes (login
, login_check
) along with other boiler-plate code (the controller login action and
the login form).
The following example security configs secure your full site with a LDAP form login:
# app/config/security.yml
security:
encoders:
# This is the default user class returned from the LDAP provider below
LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser: plaintext
providers:
ldap:
id: ldap_tools.security.user.ldap_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: ~
# Use the default LDAP user provider defined above
provider: ldap
pattern: ^/
logout: ~
guard:
authenticators:
- ldap_tools.security.ldap_guard_authenticator
login:
pattern: ^/login$
anonymous: ~
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
# app/config/security.yml
security:
encoders:
# This is the default user class returned from the LDAP provider below
LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser: plaintext
providers:
ldap:
id: ldap_tools.security.user.ldap_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: ~
# Use the default LDAP user provider defined above
provider: ldap
pattern: ^/
logout: ~
ldap_tools_form:
check_path: login_check
login_path: login
login:
pattern: ^/login$
anonymous: ~
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
The LDAP provider is used for the purpose of these examples, but other user providers can be substituted. By default the
LDAP user provider provides an extended instance of \LdapTools\Object\LdapObject
.
HTTP Basic Authentication can be used by using a specific Guard setting. In your bundle config you would first set it:
# app/config/config.yml
ldap_tools:
# Setup your domain like usual...
domains:
example:
domain_name: example.local
servers: [ dc1.example.local ]
use_tls: true
# Tell it you want the guard to do HTTP Basic auth...
security:
guard:
http_basic: true
Then make sure to set your security. This would secure the whole site with LDAP:
# app/config/security.yml
security:
# https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
providers:
ldap:
id: ldap_tools.security.user.ldap_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt|error)|css|images|js)/
security: false
default:
guard:
authenticators:
- ldap_tools.security.ldap_guard_authenticator
The following logic is used to determine how to authenticate users against LDAP in the Guard/Authentication Provider of this bundle:
- Always prefer a DN from a default user from this bundle's LDAP user provider, or LdapObject instance from LdapTools
- If the
login_query_attribute
option is defined, query LDAP with the username against that value to get a DN for login. - Use the value of the
getUsername()
method from the user and attempt authentication against LDAP with that.
If you need to go with option 3 and you use OpenLDAP, you may need to define a bind_format
option in LdapTools to form
a full DN for the login. An example of this would be:
ldap_tools:
domains:
example:
# The simple name of the user will replace the %%username%%
# Double %% needed to escape the normal parameter resolution in Symfony
bind_format: "CN=%%username%%,CN=Users,DC=example,DC=local"
Now when you authenticate a user a full DN will be formed using the above string.
Mapping LDAP groups to specific roles can be done via the bundle configuration. The groups can be mapped using their common name, GUID, SID, or full DN:
# app/config/config.yml
# ...
ldap_tools:
security:
roles:
# Using the common group name
SUPER_ADMIN: [ 'Domain Admins' ]
# Using the distinguished name of the group
ROLE_APP_USER: 'CN=App Users,OU=Groups,DC=example,DC=local'
# Using the GUID or SID of a group
ROLE_APP_ADMINS: ['291d8444-9d5b-4b0a-a6d7-853408f704d5', 'S-1-5-21-917267712-1342860078-1792151419-500']
The above would grant any user that is a member of the Domain Admins
LDAP group the SUPER_ADMIN
role in Symfony. You
can specify any number of LDAP group names for a specific role. Group membership is checked recursively by default for
Active Directory. You can also mix DNs, SIDs, GUIDs, and names mapped to a specific role.
By default the ROLE_USER
is assigned to any LDAP user that successfully authenticates. To change this behavior you
can modify the bundle config:
# app/config/config.yml
# ...
ldap_tools:
security:
# Set to null for no role to be assigned by default. Or set it to another role altogether.
default_role: ROLE_APP_USER
By default this bundle uses the LdapTools user
schema type when loading LDAP users. It needs the username
and guid
attribute mappings of that user type for the user's username and GUID. By default the LdapTools user
schema type has
mappings for these for both Active Directory and OpenLDAP. If you have more specific requirements you can extend the
default user type and provide your own mappings.
For instance, lets assume you are using Active Directory but want to force users to use their UPN for their username. You can do this as follows:
-
Create a new file:
app/Resources/schema/ad.yml
. -
In the new schema file put in the following:
# Extends the default 'ad' schema for LdapTools
extends_default: ad
objects:
# Create a special 'LoginUser' type extending the default...
login_user:
extends_default: [ 'ad', 'user' ]
type: 'SymfonyUser'
attributes:
username: userPrincipalName
- Configure the bundle to use your custom schema and user type for the login.
# app/config/config.yml
ldap_tools:
general:
schema_folder: "%kernel.root_dir%/Resources/schema"
security:
ldap_object_type: 'SymfonyUser'
For a complete listing of possible directives you can use for a custom schema type see the LdapTools docs.
There are some guard specific settings you can configure on the app/config.yml
file:
# app/config/config.yml
# ...
ldap_tools:
security:
guard:
# This is the entry point/start path route name for the RedirectResponse of the Guard component
start_path: 'login'
default_target_path: '/'
always_use_target_path: false
target_path_parameter: '_target_path'
use_referrer: false
failure_path: null
failure_forward: false
failure_path_parameter: '_failure_path'
remember_me: false
http_basic: false
http_basic_domain: null
http_basic_realm: null
Due to the way the Guard is currently designed in Symfony, the redirection settings are handled through config options here. While this does not allow for firewall specific redirection settings, you can get more fine-grained by hooking into events.
On authentication success the Guard will trigger the event ldap_tools_bundle.login.handler.success
. You can inspect the
request, firewall provider name, token, etc and then use the setResponse()
method of the event to control the redirect.
You can do the same thing for failures in the ldap_tools_bundle.login.handler.failure
event.
By default errors as to why authentication may have failed are hidden and just shown as "Bad Credentials". To get it to
show more information you can set the hide_user_not_found
security configuration to false
. This is under the main
Symfony security configuration:
# app/config/security.yml
security:
hide_user_not_found: false
# ...
After a successful LDAP authentication attempt by the authentication provider a ldap_tools_bundle.login.success
event
is dispatched. You can call the getUser()
method of the event to get the LDAP user that was just authenticated. You
can then add/remove roles or do anything else you like before their token is created and their roles solidified. You
can also call the getToken()
method to get the credentials used in the attempt. A quick example:
namespace AppBundle\Event;
use LdapTools\Bundle\LdapToolsBundle\Event\LdapLoginEvent;
class LdapLoginListener
{
public function onLdapLoginSuccess(LdapLoginEvent $event)
{
// Get the LDAP user that logged in...
$user = $event->getUser();
// Get the credentials they used for the login...
$password = $event->getToken()->getCredentials();
// Do something with the user/password combo...
}
}
# app/config/services.yml
app.event.login_listener:
class: AppBundle\Event\LdapLoginListener
tags:
- { name: kernel.event_listener, event: ldap_tools_bundle.login.success, method: onLdapLoginSuccess }
Before and after a user is loaded from the user provider by their username a ldap_tools_bundle.user_load.before
event
and ldap_tools_bundle.user_load.after
is called (respectively). You can call the getUsername()
or getDomain()
method
of the before or after event. For the after event you can call getUser()
to get the user instance that was loaded, and
you can use the getLdapObject()
method to get the LDAP object that the user was loaded from. This is most useful when
using a separate bundle for the user provider, such as the FOSUserBundle, and gives you more fine-grained control over
the process.
namespace AppBundle\Event;
use LdapTools\Bundle\LdapToolsBundle\Event\LoadUserEvent;
class LoadUserListener
{
public function beforeLoadUser(LoadUserEvent $event)
{
// Get the username to be loaded...
$username = $event->getUsername();
// Get the domain for the username...
$domain = $event->getDomain();
// Do something with the username/domain before it hits the user provider...
}
public function afterLoadUser(LoadUserEvent $event)
{
// Get the username that was loaded...
$username = $event->getUsername();
// Get the domain for the username...
$domain = $event->getDomain();
// Get the actual user instance...
$user = $event->getUser();
// Get the LDAP object the user was loaded from...
$ldapObject = $event->getLdapObject();
// Do something with the user/username/domain/LDAP attributes before it is authenticated...
foreach($ldapObject->toArray() as $attribute => $value) {
# ...
}
}
}
# app/config/services.yml
app.event.login_listener:
class: AppBundle\Event\LoadUserListener
tags:
- { name: kernel.event_listener, event: ldap_tools_bundle.load_user.before, method: beforeLoadUser }
- { name: kernel.event_listener, event: ldap_tools_bundle.load_user.after, method: afterLoadUser }
When Symfony processes a request it will attempt to "refresh" the authenticated user using your user provider. If you are using this bundles LDAP user provider, it will not refresh the user by default to reduce the amount of LDAP queries against your LDAP server. This means if you want the app to re-query LDAP for your user and any roles associated with them on each request you have to change some settings:
Both of these settings are set to false
by default:
# app/config/config.yml
ldap_tools:
security:
# Set this to true if you want user attributes re-queried on each request.
refresh_user_attributes: true
# Set this to true if you want user roles re-queried on each request.
refresh_user_roles: true
# ...
Having both of these set to false by default means no LDAP queries will be performed for the user while they are still logged in. Any changes in LDAP (such as their account being renamed, disabled, locked, expired, etc) will not take effect until they logout and log back into the web application. Any group membership changes that affect roles will not be noticed after the initial login. To change this behavior modify the above settings as needed. However, this may effect application performance and the load against your LDAP server depending on the size of your app.
When you have multiple domains defined in your configuration, you may want to have users choose the domain they will
authenticate against on login. This can be done by defining a <select>
element in your login form that contains a list
of options whose values are the FQDN of the domains in your config. This element must have an ID of _ldap_domain
:
<form action="{{ path('login') }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
<label for="_ldap_domain">Domain:</label>
<select id="_ldap_domain" name="_ldap_domain">
<option value="example.local" selected="selected">Example</option>
<option value="domain2.local">Domain 2</option>
</select>
<button type="submit">login</button>
</form>