Back to Home

Django Single Sign-On and Microsoft Active Directory

django · sso · python

Django Single Sign-On and Microsoft Active Directory

Start


Once I had to develop a web application for corporate use in Python + Django. And the very first issue that I had to solve was transparent authorization on the site or Single Sign-On (SSO) .

A directory service based on Microsoft Active Directory is widely used at the enterprise, and by now almost all corporate applications allow using windows authorization and not having to enter usernames / passwords constantly, so the new application just had to satisfy the existing state of things and realize the above possibility for “ transparent ”user authorization.

Although a lot of articles have been written about the issue of implementing SSO for Django, it took a relatively long time to implement what I needed. Therefore, in order to save some of you from possible long searches for information and its assembly into a working scheme, I offer you my manual on how to make transparent authorization in a Django application using Active Directory accounts.

So we have:

  • Microsoft Active Directory
  • Windows domain name: company.ru
  • Windows 2012 Server Domain Controller Name: DC-1
  • Windows 2012 Server Domain Controller IP Address: 192.168.7.110
  • Server for our application: CentOS7, Apache, Python 3.5.1, Django 1.9.1
  • Hostname Linux Server with CentOS7: srv-app
  • Linux Server IP Address with CentOS7: 192.168.7.105
  • Django Application URL: srv-app.company.ru

Need to do:

  • A user registered in Active Directory when opening any page on srv-app.company.ru should automatically be authorized by Django without a login / password. During authorization, some information about him from Active Directory should be transferred to the user profile in Django (first_name, last_name, mail; the flags is_active, is_staff, is_supersuser should be set based on the user's membership in the corresponding Active Directory groups).
  • A user who is not registered in Active Directory should be denied access to the site.

Having studied a number of published articles and descriptions, it became clear that you can achieve the desired result by completing two main steps:
  • Configure transparent authentication when accessing the application by the Apache server using Kerberos.
  • Authorization in Django using access to the domain controller via LDAP to obtain the necessary information about the authorized user.

Step 1: Configure Transparent Authentication Using Kerberos


Obviously, implementing the SSO principle on a Windows AD network is possible using the Kerberos protocol. Therefore, the main task of the first stage of configuration will be installing Kerberos in Linux + Apache and setting up communication with the Windows AD domain controller.

Install and configure Kerberos on a Linux server


Configure / etc / hosts, /etc/resolv.conf on srv-app , DNS on DC-1

On srv-app, add to / etc / hosts:

    192.168.7.105   srv-app.company.ru

On srv-app, change /etc/resolv.conf (In reality, this file is generated by NetworkManager in CentOS7, so you need to make changes to / etc / sysconfig / network-scripts / ifcfg-eth0):

    search company.ru
    nameserver 192.168.7.110

On the DC-1 Domain Controller :

  • using the “DNS Manager” snap-in, add the srv-app host with the address 192.168.7.105
  • using the “AD - users and computers” snap-in, add the user svc-apache , set the password P @ ssw0rd for it . We will need this user later to create a keytab file that will connect our Linux machine and Active Directory.

Install modules for working with Kerberos


    [root@srv-app ~]# yum install mod_auth_kerb     # Устанавливаем модуль для apache
    [root@srv-app ~]# yum install krb5-workstation    # Устанавливаем пакет для настройки и тестирования kerberos

We configure Kerberos by editing the /etc/krb5.conf file

    [logging]
     default = FILE:/var/log/krb5libs.log
     kdc = FILE:/var/log/krb5kdc.log
     admin_server = FILE:/var/log/kadmind.log
    [libdefaults]
     dns_lookup_realm = false
     ticket_lifetime = 24h
     renew_lifetime = 7d
     forwardable = true
     rdns = false
     default_realm = COMPANY.RU
     default_ccache_name = KEYRING:persistent:%{uid}
    [realms]
     COMPANY.RU = {
      kdc = 192.168.7.110
      admin_server = 192.168.7.110
     }
    [domain_realm]
     .company.ru = COMPANY.RU
     company.ru = COMPANY.RU

A little explanation about the contents of the configuration file:

  • COMPANY.RU - set the domain name kerberos (realm) in linux. Remember that kerberos realm is case-sensitive (case-sensetive)

We perform several checks on the operation of kerberos on the srv-app computer

Earlier, on our domain controller, we created the user srv-apache with the password P @ ssw0rd . Let's try to log in to the CD using the kinit utility:

    [root@dsrv-app ~]# kinit [email protected]
    Password for [email protected]: ****

If there are no errors, let's see what tickets we have:

    [root@srv-app ~]# klist
    Ticket cache: FILE:/tmp/krb5cc_0
    Default principal: [email protected]
    Ticket cache: KEYRING:persistent:0:0
    Default principal: [email protected]
    Valid starting       Expires              Service principal
    20.12.2015 16:12:59  21.12.2015 02:12:59  krbtgt/[email protected]
            renew until 27.12.2015 16:12:55

Thus, we logged in to the CD using kerberos, now we will disconnect by deleting the
received ticket:

    [root@srv-app ~]# kdestroy

If everything works, then for further configuration, we need to create the krb5.keytab file for the authentication service using
Apache and mod_auth_kerb.

Generating keytab on a Windows Domain Controller

You can generate keytab on a DC-1 domain controller using the ktpass.exe command:

    ktpass.exe /princ HTTP/[email protected] /mapuser [email protected]  /crypto ALL /ptype KRB5_NT_PRINCIPAL /mapop set /pass P@ssw0rd /out c:\share\keytab

  • The key / princ HTTP/[email protected] sets the unique name of the client (principal) on the Linux machine, which will be allowed to authenticate with kerberos.
  • The keys / mapuser [email protected] and / pass P @ ssw0rd associate principal with a specific user in Active Directory
  • The / crypto ALL switch specifies the encryption method. Instead of / crypto ALL, you can specify a specific encryption method, for example / crypto AES256-SHA1
  • The / mapop set switch sets up mapping between the Linux principal and the Active Directory user account ( / mapop add will add this mapping to keytab)
  • Key / ptype KRB5_NT_PRINCIPAL - set the type of principal in the request (the specified type is the main one and it is recommended to use it)
  • The / out switch c: \ share \ keytab sets the path for the keytab output file.
  • If desired, additional key values ​​can be viewed using ktpass.exe / help

As a result, we get the file c: \ share \ keytab, which must be copied to srv-app and named /etc/krb5.keytab. Next, you must provide access to this file to the user from whose account the httpd server is running. In our case, it is apache . In order for apache to read this file, we simply allow all users to read it:

   chmod a+r /etc/krb5.keytab

Check if our keytab works as follows:

1. Using ktutil :

    [root@srv-app ~]# ktutil
    ktutil:  rkt /etc/krb5.keytab
    ktutil:  list
    slot KVNO Principal
    ---- ---- ---------------------------------------------------------------------
       1    3           HTTP/[email protected]
       2    3           HTTP/[email protected]
       3    3           HTTP/[email protected]
       4    3           HTTP/[email protected]
       5    3           HTTP/[email protected]
    ktutil:  q


2. Using kvno :

    # логинимся на KDC
    [root@srv-app ~]# kinit svc-apache
    Password for [email protected]:
    # запрашивем тикет для сервиса HTTP/[email protected] и печатает номера версий для каждого принципала в keytab
    [root@srv-app ~]# kvno HTTP/[email protected]
    HTTP/[email protected]: kvno = 3
    # Удаляем тикет
    [root@srv-app ~]# kdestroy

Configure Apache

The following is the /etc/httpd/conf.d/company_main.conf file, which contains configuration instructions for configuring Kerberos authentication when accessing the "/" URI:


       # Kerberos authentication:
       AuthType Kerberos
       AuthName "SRV-APP auth"
       KrbMethodNegotiate on
       KrbMethodK5Passwd off
       KrbServiceName HTTP/[email protected]
       KrbAuthRealms COMPANY.RU
       Krb5Keytab /etc/krb5.keytab
       KrbLocalUserMapping On
       Require valid-user
    

I want to pay attention to setting KrbMethodK5Passwd off . This setting leads to the fact that when you enter the specified section of the site, kerberos authentication will be performed using Single Sign-On technology . If authentication fails, there will immediately be a “401 Unautorized” error. However, if you change the setting to KrbMethodK5Passwd on , then after unsuccessful Single Sign-On authorization, an attempt will be made to authorize by name and password.

And one more undocumented feature that we will use: Setting KrbLocalUserMapping On causes the registered user name to be placed in the REMOTE_USER variable (in the case of KrbLocalUserMapping OffREMOTE_USER will contain username @ COMPANY RU).

You can read more information on mod_auth_kerb module settings here .

Single Sign-On (SSO) from Windows Workstations

I repeat once again that all the work on configuring kerberos authentication in Linux was done in order to be able to access the portal pages of a machine published on Linux using corporate accounts stored in Active Directory, in addition, to simplify the life of users, this entry should be “transparent” ”, Without asking for a password, which is achieved using Single Sign-On (SSO) technology, which is supported in Windows 7 and higher and Internet Explorer (and Mozilla Firefox).

However, in order for everything to go smoothly, the following settings must be made at the workstation from where such an input is made:

  1. The site to which you are logging in (in our case, srv-app.company.ru must be entered into the Local Intranet sites using the
    Internet Explorer menu : Tools -> Internet Options -> Security -> Local intranet -> Hosts -> Advanced
  2. In IE, the following options should be enabled (as a rule, they are enabled "by default"):
    • Tools -> Internet Options -> Advanced -> Security -> Allow Built-in Windows Check = ON
    • Tools -> Internet Options -> Security -> Local Intranet -> Security Level for this Zone -> Other -> User Authentication -> Automatic Network Logon Only in the Intranet Zone

  3. In addition, SSO will not work if you try to access the site by IP address. Must use the domain name srv-app.company.ru!

Stage 2. User authorization in Django


So, as a result of the work carried out in the first stage, we got the following results:

  • When we enter any section of our Django application as a user who has been authorized by a domain controller, we first get access to our Django application, and secondly, the Apache server places the name of the authorized user in the REMOTE_USER variable, which matches the sAMAccountName attribute of this user in Active Directory.
  • If the user is not logged in to AD, then Apache will return the “401 Unautorized” error to us (Using the ErrorDocument option, we can then redirect an unauthorized user to some guest page)

However, despite the fact that the Apache server authorized our user, for the Django application it is still unknown and, accordingly, the entire mechanism for authenticating / authorizing users and using sessions in Django remains unused.

Using RemoteUserBackend


Especially for such purposes, in Django there is a simple solution that includes an authorization mechanism in the system of users already authenticated by external applications, such as IIS or Apache (using methods similar to those used in step 1: mod_authnz_ldap, CAS, Cosign, WebAuth, mod_auth_sspi, mod_auth_krb).

According to the description on the official djangoproject.org website, to use transparent authentication and authorization in Django in this way, it is enough to use the RemoteUserBackend mechanism by completing the following steps:

1. Add django.contrib.auth.middleware.RemoteUserMiddleware in the settings file for the Django project settings.py to the MIDDLEWARE_CLASSES list immediately afterdjango.contrib.auth.middleware.AuthenticationMiddleware :

MIDDLEWARE_CLASSES = [
    '...',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.RemoteUserMiddleware',
    '...',
]

2. In the same place, replace ModelBackend with RemoteUserBackend in the AUTHENTICATION_BACKENDS list (or add this list to settings.py):

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.RemoteUserBackend',
]

As a result of these changes, RemoteUserMiddleware will retrieve username via request.META ['REMOTE_USER'] and automatically authenticate and authorize this user using RemoteUserBackend . In addition, RemoteUserBackend will add a new user to the auth_user table with this authorization, thereby using the standard account management mechanism for Django.

But, unfortunately, this method will not allow us to get from Active Directory and use in our application the information we need (first_name, last_name, mail, participation in AD groups, etc.).

Using django-auth-ldap


So, we need to access Active Directory using LDAP. Fortunately, django-auth-ldap is a great Django application for these purposes . You can install it standardly with pip:

pip install django-auth-ldap

After that, you will have to remove from settings.py the MIDDLEWARE_CLASSES added in the previous section , namely 'django.contrib.auth.middleware.RemoteUserMiddleware' , and the list AUTHENTICATION_BACKENDS should look like this:

AUTHENTICATION_BACKENDS = (
    'django_auth_ldap.backend.LDAPBackend',
    'django.contrib.auth.backends.ModelBackend',
)

In addition, the following configuration parameters must be included in settings.py:

settings.py
# Baseline LDAP configuration.
AUTH_LDAP_SERVER_URI = "ldap://DC-1.COMPANY.ru"
AUTH_LDAP_AUTHORIZE_ALL_USERS = True
AUTH_LDAP_PERMIT_EMPTY_PASSWORD = True
# Логин пользователя от чьего имени будут выполнятся запросы к LDAP (кроме авторизации)
AUTH_LDAP_BIND_DN = "cn=svc-apache,cn=Users,dc=company,dc=ru"
AUTH_LDAP_BIND_PASSWORD = "P@ssw0rd"
# Настройка будет пытаться найти пользователя в созданной нами OU Django и стандартной папке Users, 
# сопоставляя введенный login пользователя с аттрибутами sAMAccountName
AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(
        LDAPSearch("ou=Django,dc=company,dc=ru", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"),
        LDAPSearch("cn=Users,dc=company,dc=ru", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"),
)
# Set up the basic group parameters.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=Groups,ou=Django,dc=company,dc=ru",
    ldap.SCOPE_SUBTREE, "(objectClass=group)"
)
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn")
# Simple group restrictions
# AUTH_LDAP_REQUIRE_GROUP - если определено DN для этой настройки, то требуется присутсвие пользователя в этой группе
# в противном случае пользовталю будет отказано в аутентификации
# таким образом указываем, что для того чтобы пользователь был аутентифицирован он обязан находится в группе "active"
AUTH_LDAP_REQUIRE_GROUP = "cn=active,ou=Groups,ou=Django,dc=company,dc=ru"
# AUTH_LDAP_DENY_GROUP - если определено DN для этой настройки, то в случае члентсва пользователя в этой группе
# ему будет отказано в аутентификации
AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=Groups,ou=Django,dc=company,dc=ru"
# Populate the Django user from the LDAP directory.
# Указываем как переносить данные из AD в стандартный профиль пользователя Django
AUTH_LDAP_USER_ATTR_MAP = {
    "first_name": "givenName",
    "last_name": "sn",
    "email": "mail"
}
# Указываем как переносить данные из AD в расширенный профиль пользователя Django
AUTH_LDAP_PROFILE_ATTR_MAP = {
    "employee_number": "employeeNumber"
}
# Указываем привязку стандартных флагов is_active, is_staff и is_superuser к членству в группах AD
# Флаг is_active при использовании django_remote_auth_ldap сам по себе не оказывает вляния на разрешение аутнтификации
# поэтому для создания обычного поведения Django также определяме настройку AUTH_LDAP_REQUIRE_GROUP (см.выше)
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
    "is_active": "cn=active,ou=Groups,ou=Django,dc=company,dc=ru",
    "is_staff": "cn=staff,ou=Groups,ou=Django,dc=company,dc=ru",
    "is_superuser": "cn=superuser,ou=Groups,ou=Django,dc=company,dc=ru"
}
# Указываем привязку флагов расширенного профиля к членству в группах AD
AUTH_LDAP_PROFILE_FLAGS_BY_GROUP = {
    "is_awesome": "cn=awesome,ou=Groups,ou=Django,dc=company,dc=ru",
}
# This is the default, but I like to be explicit.
AUTH_LDAP_ALWAYS_UPDATE_USER = True
# Use LDAP group membership to calculate group permissions.
AUTH_LDAP_FIND_GROUP_PERMS = True
# Cache group memberships for an hour to minimize LDAP traffic
AUTH_LDAP_CACHE_GROUPS = True
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600


Django-auth-ldap is a great app. With its help, you can load almost any information on it from Django’s user profile from AD, there is even the opportunity to “pull up” the groups in which this user participates in the corresponding Django model.

However, for authorization using django-auth-ldap, you still need to ask the user for his username and password, which categorically does not suit us.

Although the documentation states that, it seems, you can "cross" django-auth-ldap and RemoteUserBackend :

Non-LDAP Users
LDAPBackend has one more feature pertaining to permissions, which is the ability to handle authorization for users that it did not authenticate. For example, you might be using RemoteUserBackend to map externally authenticated users to Django users. By setting AUTH_LDAP_AUTHORIZE_ALL_USERS, LDAPBackend will map these users to LDAP users in the normal way in order to provide authorization information. Note that this does not work with AUTH_LDAP_MIRROR_GROUPS; group mirroring is a feature of authentication, not authorization.

But this does not work, as we need. The user can really log in, but this happens exactly as if we just used RemoteUserBackend (see the previous section). Information from AD is not automatically loaded into the Django user profile.

Of course, you can do this yourself by using the following recommendation:

Updating Users
By default, all mapped user fields will be updated each time the user logs in. To disable this, set AUTH_LDAP_ALWAYS_UPDATE_USER to False. If you need to populate a user outside of the authentication process—for example, to create associated model objects before the user logs in for the first time—you can call django_auth_ldap.backend.LDAPBackend.populate_user(). You’ll need an instance of LDAPBackend, which you should feel free to create yourself. populate_user() returns the User or None if the user could not be found in LDAP.

from django_auth_ldap.backend import LDAPBackend
user = LDAPBackend().populate_user('alice')
if user is None:
    raise Exception('No user named alice')


Использование django-remote-auth-ldap


But, as it turned out, everything is much simpler. The bicycle has already been invented, and we can only take advantage of it. The django-remote-auth-ldap application is a small add-on for django_auth_ldap and allows you to authorize a user and load his data from AD during authorization without any extra effort.

Install django-remote-auth-ldap as standard ( django-auth-ldap is also required for this add-in to work):

pip install django-remote-auth-ldap

Next, add the following setting to settings.py:

DRAL_CHECK_DOMAIN = False

The fact is that django-remote-auth-ldap is apparently designed to work with IIS, which sets the REMOTE_USER variable in the format "DOMAIN / username", but we configured mod_auth_kerb so that the domain name does not get into REMOTE_USER. The above setting forces django-remote-auth-ldap to consider that in REMOTE_USER there is only one username without specifying a domain, i.e. exactly as we need.
Well, again, recommendations for setting MIDDLEWARE_CLASSES and AUTHENTICATION_BACKENDS :

1. In the settings file for the Django project settings.py add django.contrib.auth.middleware.RemoteUserMiddleware to the MIDDLEWARE_CLASSES list immediately afterdjango.contrib.auth.middleware.AuthenticationMiddleware :

MIDDLEWARE_CLASSES = [
    '...',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.RemoteUserMiddleware',
    '...',
]

2. AUTHENTICATION_BACKENDS should look like this:

AUTHENTICATION_BACKENDS = [
    'django_remote_auth_ldap.backend.RemoteUserLDAPBackend',
]

That's all, transparent authentication in Django is configured.

If the user included in your Django application is already authorized in Active Directory, then:

1. Apache will authorize it using Kerberos, will allow access to the pages of your application and write the name of the authorized user in REMOTE_USER.
2. RemoteUserMiddleware “sees” the value of REMOTE_USER and initiates authentication and authorization of the specified user in Django using django-remote-auth-ldap
3. django-remote-auth-ldap authenticates and authorizes the user using methods inherited from the django-auth- application ldap , which will pull up the information you need from Active Directory in Django.

Read Next