LDAP
How to use an LDAP directory server with Kimai
Kimai supports authentication against your company directory server (LDAP or AD). LDAP users will be imported during the first login and their attributes and groups updated on each following login.
Installation
In order to use the LDAP authentication module of Kimai, you have to install the LDAP library:
composer require laminas/laminas-ldap --optimize-autoloader -n
If you see an error message like this:
laminas/laminas-ldap requires ext-ldap * -> it is missing from your system. Install or enable PHP's ldap extension.
you have to install the PHP LDAP extension first:
- e.g. on Ubuntu with
apt-get install php-ldap
- or with the PHP version prefixed
apt-get install php8.1-ldap
- FPM and CLI PHP use different configs, use
php -m
to verify that the module is really loaded
UPDATE WARNING
As this plugin uses composer to install further dependencies, your next update will not work as usual. Please read this troubleshooting guide how to handle the situation. After reverting all local changes and updating Kimai, you have to reinstall the composer package.
Activate LDAP authentication
You activate the LDAP authentication by adding the following code to the end of your local.yaml and reloading the cache afterwards.
kimai:
ldap:
activate: true
# more infos about the connection params can be found at:
# https://docs.laminas.dev/laminas-ldap/api/
connection:
# The default hostname of the LDAP server (mandatory setting).
# You can connect to multiple servers by setting their URLs like this:
# host: "ldap://ldap.example.local ldap://ldap2.example.local"
# host: "ldaps://ldap.example.local ldaps://ldap2.example.local"
host: 127.0.0.1
# Default port for your LDAP port server
# default: 389
#port: 389
# Whether or not the LDAP client should use SSL encrypted transport.
# The useSsl and useStartTls options are mutually exclusive.
# default: false
#useSsl: false
# Enable TLS negotiation (should be favoured over useSsl).
# The useSsl and useStartTls options are mutually exclusive.
# default: false
#useStartTls: false
# The default credentials username (your service account). Some servers
# require that this is given in DN form.
# It must be given in DN form if the LDAP server requires
# a DN to bind and binding should be possible with simple usernames.
# default: empty
#username:
# Password for the username (service-account) above
# default: empty
#password:
# LDAP search filter to find the user (%s will be replaced by the username).
# Should be set, to be compatible with your object structure.
# You might not need to set this filter, unless you have a special setup
# or use Microsofts Active directory.
#
# Defaults:
# - if bindRequiresDn is false: (&(objectClass=user)(sAMAccountName=%s))
# - if bindRequiresDn is true: (&%filter%(uid=%s))
# - %filter% = empty
# accountFilterFormat = (&(usernameAttribute=%s))
# - %filter% = (&(objectClass=posixAccount))
# accountFilterFormat = (&(objectClass=posixAccount))(&(usernameAttribute=%s))
#
# %filter% is the "filter" configuration defined below in the "user" section
#accountFilterFormat: (&(objectClass=inetOrgPerson)(uid=%s))
# If true, this instructs Kimai to retrieve the DN for the account,
# used to bind if the username is not already in DN form.
# default: true
#bindRequiresDn: true
# If set to true, this option indicates to the LDAP client that
# referrals should be followed, default: false
#optReferrals: false
# for the next options please refer to:
# https://docs.laminas.dev/laminas-ldap/api/
#allowEmptyPassword: false
#tryUsernameSplit:
#networkTimeout:
#accountCanonicalForm: 3
#accountDomainName: HOST
#accountDomainNameShort: HOST
user:
# baseDn to query for users (mandatory setting).
baseDn: ou=users, dc=kimai, dc=org
# Field used to match the login username in your LDAP.
# If "bindRequiresDn: false" is set, the username is used in "bind".
# Otherwise a search is executed to find the users "dn" by finding the user
# via this attribute with his "baseDn" and the "filter" below.
# default: uid
usernameAttribute: uid
# LDAP search base filter to find the user / the users DN.
# Do NOT include the rule (&(usernameAttribute=%s)), it will be appended
# automatically. The result of the search filter must return 1 result only.
# default: empty (results in (&(uid=%s)) with default usernameAttribute)
filter: (&(objectClass=inetOrgPerson))
# LDAP search base filter to find the user attributes.
# This is used for a slightly different query than the one above, which is
# used to query the users DN only.
# AD users might have too many results (Exchange activesync devices
# attributes) and therefor an incompatible result structure if not changed.
# See https://github.com/kimai/kimai/issues/875
# default: (objectClass=*)
#attributesFilter: (objectClass=Person)
# Configure the mapping between LDAP attributes and user entity
# The ldap_attr must be given in lowercase!
attributes:
# The following 2 rules are automatically prepended and can be overwritten.
# Username is set to the value of the configured "usernameAttribute" field
- { ldap_attr: "usernameAttribute", user_method: setUserIdentifier }
# Only applied if you don't configure a mapping for setEmail()
- { ldap_attr: "usernameAttribute", user_method: setEmail }
# An example which will set the display name in Kimai from the
# value of the "common name" field in your LDAP
- { ldap_attr: cn, user_method: setAlias }
# You can comment the following section, if you don't want to manage
# user roles in Kimai via LDAP groups. If you want to use the group
# sync, you have to set at least the "role.baseDn" config.
# default: deactivated as "role.baseDn" is empty by default
role:
# baseDn to query for groups, MUST be set to activate the "group import"
# default: empty (deactivated)
baseDn: ou=groups, dc=kimai, dc=org
# Filter to query user groups, all results will be matched against
# the configured "groups" mapping below.
# The full search filter will ALWAYS be generated like this:
# (&%filter(userDnAttribute=valueOfUsernameAttribute))
# The following example rule will be expanded to (for user "foo"):
# (&(&(objectClass=groupOfNames))(member=foo))
# default: empty
filter: (&(objectClass=groupOfNames))
# The following field is taken from the LDAP user entry and its
# value is used in the filter above as "valueOfUsernameAttribute".
# The attribute must be given in lowercase!
# The example below uses "posix group style memberUid".
# default: dn
#usernameAttribute: uid
# Field that holds the group name, which will be used to map the
# LDAP groups with Kimai roles (see groups mapping below).
# default: cn
#nameAttribute: cn
# Field that holds the users dn in your LDAP group definition.
# Value of this configuration is used in the filter (see above).
# default: member
#userDnAttribute: member
# Convert LDAP group name (nameAttribute) to Kimai role
# You will very likely have to define mappings, unless your groups
# are called "teamlead", "admin" or "super_admin"
#groups:
# - { ldap_value: group1, role: ROLE_TEAMLEAD }
# - { ldap_value: kimai_admin, role: ROLE_ADMIN }
This is the full available configuration, most of the values are optional and their default values were chosen for maximum compatibility with OpenLDAP setup.
Kimai uses the Laminas Framework LDAP module and uses the configured connection
parameters without modification.
Find out more about the settings in the detailed documentation.
You need to re-build the cache for changes to take effect.
User synchronization
User data is synchronized on each login, fetching the latest data from your LDAP.
How it works
- if
bindRequiredDn
is active, asearch
is executed to find the users DN by the given username - the authentication is checked with a
bind
- if the
bind
was successful:- another
bind
using the service account (connection.username/connection.password) is executed and under that scope:- a
search
is executed to find and map LDAP attributes to the Kimai profile - if configured, another
search
is executed to sync and map the users LDAP groups to Kimai roles
- a
- another
Password handling
Kimai does not store the user password when logged-in via LDAP. There is no fallback mechanism for login, if your LDAP is not available (only one LDAP server can be configured).
When using LDAP you should:
- Disable the “Password reset” function via System > Settings
- Disable the
password_own_profile
andpassword_other_profile
permissions for LDAP user (at least the normal user role)
There is no auto-cleanup available:
- If you delete users in LDAP, you have to delete or disable this account in Kimai manually
- If you deactivate users in LDAP, you can configure an attribute mapping to set the user deactivated flag via
setEnabled()
User attributes
Kimai does not rely on an objectClass
, but maps single LDAP attributes to the User entity by configuration.
An example could look like this:
kimai:
ldap:
user:
attributes:
- { ldap_attr: uid, user_method: setUserIdentifier }
- { ldap_attr: mail, user_method: setEmail }
- { ldap_attr: cn, user_method: setAlias }
You need to configure the attributes in lower-case, otherwise they won’t be processed.
In this example we tell Kimai to sync the following fields:
-
uid
will be the username in Kimai (will fail with a 500 if not unique) -
mail
will be the account email address (read “known limitations” below) -
cn
will be used for the display name in Kimai
Available methods on the User entity are: setUsername(string)
, setEmail(string)
, setAlias(string)
, setAvatar(url)
, setTitle(string)
, setLanguage(string)
, setTimezone(string)
, setAccountNumber(string)
.
Its unlikely that you will need those, but they also exist: setEnabled(bool)
, setSuperAdmin(bool)
, addRole(string)
.
Groups / Roles import
Kimai can use your LDAP groups and map them to user roles.
If configured, it will execute another search
against your LDAP after authentication and importing the user attributes.
Every user automatically owns the ROLE_USER role, you don’t have to create a mapping for it.
Assuming this role
configuration:
kimai:
ldap:
role:
baseDn: ou=groups, dc=kimai, dc=org
#filter: (&(objectClass=groupOfNames)) # additional group filter
#userDnAttribute: member # field to lookup the users
#nameAttribute: cn # group name to match
groups:
- { ldap_value: group1, role: ROLE_TEAMLEAD }
- { ldap_value: kimai_admin, role: ROLE_ADMIN }
- { ldap_value: administrator, role: ROLE_SUPER_ADMIN }
Kimai will search the baseDn
with userDnAttribute=user['dn']
(e.g. member=uid=user1,ou=users,dc=kimai,dc=org
) and extract the
group names from the result-sets attribute nameAttribute
.
After finding a list of group names, they will be converted to Kimai roles:
- first step is to lookup in
groups
mapping, if there is a match inldap_value
and uses therole
value without further processing - if no mapping was found, the group name will be UPPERCASED and prefixed with
ROLE_
=> e.g.admin
will becomeROLE_ADMIN
These converted names will be validated and only existing roles will pass to the user profile.
Remove the entire ‘role’ configuration block to deactivate role sync!
Known limitations
There are a couple of caveats that should be taken into account.
Missing email address
Kimai requires that every user account has an email address. If you do not configure an attribute for email, the username will be used as fallback for the email during the initial import of the user account.
This will lead to problems, when you try to update a user profile in Kimai - you will see an error saying that the email is not valid, even if you only tried to change the user roles.
- Bad solution: change the users email address manually, it will not be overwritten by the sync
- Good solution: sync the users email address to Kimai
Profile changes will be overwritten
As all configured user attributes will be synchronized on every login, manual profile changes in the internal user database won’t be permanent.
But: fields which are not synced, won’t be changed during the user login.
Role changes will be overwritten
If you configured the group sync, the assigned user roles in Kimai will be overwritten on login.
Roles are not merged, but replaced during authentication, so you cannot
demote or promote a User permanently to another role in Kimai.
The rule is: either manage all roles in Kimai or in LDAP, mixing is not possible.
Examples
Before you start to configure your LDAP, switch to ‘dev’ environment and tail ‘var/log/dev.log’.
Another simple solution to debug the generated queries is to start your OpenLDAP with sudo /usr/libexec/slapd -d256
.
Minimal OpenLDAP
A minimal setup with a local OpenLDAP with roles sync. This will only work for very basic LDAP setups, but it demonstrates the power of default values.
kimai:
ldap:
activate: true
connection:
host: 127.0.0.1
user:
baseDn: ou=users, dc=kimai, dc=org
role:
baseDn: ou=groups, dc=kimai, dc=org
The generated query to find the users DN looks like this (for the username “foo”):
SRCH base="ou=users,dc=kimai,dc=org" scope=2 deref=0 filter="(&(uid=foo))"
SRCH attr=dn
The query to find all user attributes looks like this:
SRCH base="uid=foo,ou=users,dc=kimai,dc=org" scope=2 deref=0 filter="(objectClass=*)"
SRCH attr=+ *
The generated query for the group-to-role mapping:
SRCH base="ou=groups,dc=kimai,dc=org" scope=2 deref=0 filter="(&(member=uid=foo,ou=users,dc=kimai,dc=org))"
SRCH attr=cn + *
The Kimai account will have the username and email set to the uid
, because we did not configure
another usernameAttribute (like cn
) or a mapping for the email.
If the role search would have returned groups with the cn
value admin
, super_admin
or teamlead
,
the new account would have been promoted into these roles.
OpenLDAP with group sync
A secured local OpenLDAP on port 543 with roles sync for the objectClass posixAccount
users:
kimai:
ldap:
activate: true
connection:
host: 127.0.0.1
useSsl: true
port: 543
username: kimai
password: serverToken
# auto generated fallback is the same, no need to set it explicit:
#accountFilterFormat: (&(objectClass=posixAccount)(uid=%s))
user:
baseDn: ou=users, dc=kimai, dc=org
filter: (&(objectClass=posixAccount))
usernameAttribute: uid
attributes:
- { ldap_attr: uid, user_method: setUserIdentifier }
- { ldap_attr: cn, user_method: setAlias }
- { ldap_attr: mail, user_method: setEmail }
role:
baseDn: ou=groups, dc=kimai, dc=org
filter: (&(objectClass=groupOfNames)(|(cn=teamlead)(cn=manager)(cn=devops)))
userDnAttribute: member
usernameAttribute: uid
groups:
- { ldap_value: teamlead, role: ROLE_TEAMLEAD }
- { ldap_value: manager, role: ROLE_ADMIN }
- { ldap_value: devops, role: ROLE_SUPER_ADMIN }
Connect to Active Directory
This is an example how you can connect your AD with Kimai.
kimai:
ldap:
activate: true
connection:
host: ad.example.com
username: user@ad.example.com
password: secret
accountDomainName: ad.example.com
accountDomainNameShort: AD
accountFilterFormat: (&(objectClass=Person)(sAMAccountName=%s))
user:
baseDn: dc=ad,dc=example,dc=com
filter: (&(objectClass=Person))
usernameAttribute: samaccountname
attributesFilter: (objectClass=Person)
attributes:
- { ldap_attr: mail, user_method: setEmail }
- { ldap_attr: displayname, user_method: setAlias }
- { ldap_attr: samaccountname, user_method: setUserIdentifier }
role:
baseDn: dc=ad,dc=example,dc=com
filter: (&(objectClass=group))
groups:
- { ldap_value: Leads, role: ROLE_TEAMLEAD }
- { ldap_value: Sysadmins, role: ROLE_SUPER_ADMIN }
- { ldap_value: Users, role: ROLE_USER }
You need to configure the attributes in lower-case, otherwise they won’t be processed.
The LDAP code is based on the work by @Maks3w , thanks for sharing!
Structure example
There is an article explaining an example Open LDAP structure for Kimai, that could be used.
Actually this is more of an example setup for a test environment instead of a best-practice example.
Troubleshooting / Logging
You can switch to APP_ENV=dev
mode and run composer install
(to get the missing dev packages) and then all LDAP queries will be logged.
Connecting with IP v6
When connecting to your LDAP server via IP v6, you have to use square brackets to enclose the IP, e.g.:
kimai:
ldap:
activate: true
connection:
host: "[fe80:20c:29ff:fefd:deea]"
LDAPS and self-signed TLS certificate
Kimai uses the Laminas LDAP implementation, you can read about it here and here.
When using Kimai in Docker (see this comment) you can get it to work by:
… installing the CA certificate to the system hosting the docker image and mapping the ca-certificates.crt file into the Kimai container in the volumes section of my docker-compose file:
- /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt