[Из песочницы] Django Single Sign-On и Microsoft Active Directory
Начало
Однажды мне пришлось заняться разработкой Web-приложения для корпоративного использования на Python+Django. И самым первым вопросом, который пришлось решать — это прозрачная авторизация на сайте или Single Sign-On (SSO).
На предприятии широко используется служба каталогов на базе Microsoft Active Directory, и к настоящему моменту практически все корпоративные приложения позволяют использовать windows-авторизацию и не вводить постоянно логины/пароли, поэтому новое приложение просто должно было удовлетворять существующему положению вещей и реализовывать указанную выше возможность для «прозрачной» авторизации пользователей.
Хотя о вопросе реализации SSO для Django написано немало статей, однако для того, чтобы реализовать то, что мне было необходимо, пришлось затратить относительно много времени. Поэтому, чтобы избавить некоторых из вас от возможных долгих поисков информации и ее сборки в работающую схему, предлагаю вам свой мануал, как сделать прозрачную авторизацию в приложении Django с использованием учетных записей Active Directory.
Итак мы имеем:
- Служба каталогов Microsoft Active Directory,
- Имя домена Windows: company.ru
- Имя контроллера домена Windows 2012 Server: DC-1
- IP Address контроллера домена Windows 2012 Server: 192.168.7.110
- Сервер для работы нашего приложения: CentOS7, Apache, Python 3.5.1, Django 1.9.1
- Hostname Linux Server с CentOS7: srv-app
- IP Address Linux Server с CentOS7: 192.168.7.105
- URL Приложения на Django: srv-app.company.ru
Нужно сделать:
- Пользователь, зарегистрированный в Active Directory при открытии любой страницы сайта на srv-app.company.ru должен автоматически, без запроса логина/пароля, быть авторизован Django. При авторизации, в профиль пользователя в Django должна быть перенесена некоторая информация о нем из Active Directory (first_name, last_name, mail; флаги is_active, is_staff, is_supersuser должны быть установлены на основании членства пользователя в соответствующих группах Active Directory).
- Пользователю, не зарегистрированному в Active Directory вход на сайт должен быть запрещен.
Изучив ряд опубликованных статей и описаний стало понятно, что добиться нужного результата можно, выполнив два основных шага:
- Настройка прозрачной аутентификации при доступе к приложению сервером Apache с использованием Kerberos.
- Авторизация в Django c использованием доступа к контроллеру домена по протоколу LDAP для получения необходимой информации об авторизующемся пользователе.
Этап 1. Настройка прозрачной аутентификации с использованием Kerberos
Совершенно очевидно, что реализация принципа SSO в сети Windows AD возможно используя протокол Kerberos. Поэтому основной задачей первого этапа настройки будет установка Kerberos в среде Linux+Apache и настройка связи с контроллером домена Windows AD.
Установка и настройка Kerberos на сервере Linux
Настройка /etc/hosts, /etc/resolv.conf на srv-app, DNS на DC-1
На srv-app добавляем в /etc/hosts:
192.168.7.105 srv-app.company.ru
На srv-app изменяем /etc/resolv.conf (В реальности этот файл в CentOS7 генерирует NetworkManager, поэтому изменения нужно вносить в /etc/sysconfig/network-scripts/ifcfg-eth0):
search company.ru
nameserver 192.168.7.110
На Контроллере домена DC-1:
- при помощи оснастки «Диспетчер DNS» добавим хост srv-app с адресом 192.168.7.105
- при помощи оснастки «AD — пользователи и компьютеры» добавим пользователя svc-apache, установим для него пароль P@ssw0rd. Этот пользователь понадобится нам позже для создания keytab-файла, который свяжет нашу Linux-машину и Active Directory.
Устанавливаем модули для работы с Kerberos
[root@srv-app ~]# yum install mod_auth_kerb # Устанавливаем модуль для apache
[root@srv-app ~]# yum install krb5-workstation # Устанавливаем пакет для настройки и тестирования kerberos
Конфигурируем Kerberos при помощи редактирования файла /etc/krb5.conf
[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
Немного объяснений по поводу содержимого конфигурационного файла:
- COMPANY.RU — задаем имя области kerberos (realm) в linux. Следует запомнить что kerberos realm чувствительный к регистру (case-sensetive)
Выполняем несколько проверок работы kerberos на компьютере srv-app
Ранее, на нашем контроллере домена мы создали пользователя srv-apache с паролем P@ssw0rd. Попробуем залогиниться на КД при помощи утилиты kinit:
[root@dsrv-app ~]# kinit svc-apache@COMPANY.RU
Password for admin@COMPANY.RU: ****
Если ошибок нет, посмотрим какие билеты (tickets) у нас имеются:
[root@srv-app ~]# klist
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: srv-apache@COMPANY.RU
Ticket cache: KEYRING:persistent:0:0
Default principal: svc-apache@COMPANY.RU
Valid starting Expires Service principal
20.12.2015 16:12:59 21.12.2015 02:12:59 krbtgt/COMPANY.RU@COMPANY.RU
renew until 27.12.2015 16:12:55
Таким образом мы залогинились на КД при помощи kerberos, теперь разорвем соединение, удалив
полученный билет:
[root@srv-app ~]# kdestroy
Если все работает, то для дальнейшей настройки нам необходимо создать файл krb5.keytab для сервиса аутентификации при помощи
Apache и mod_auth_kerb.Генерация keytab на Контроллере Домена Windows
Сгенерировать keytab можно на контроллере домена DC-1 при помощи команды ktpass.exe:
ktpass.exe /princ HTTP/srv-app.company.ru@COMPANY.RU /mapuser svc-apache@COMPANY.RU /crypto ALL /ptype KRB5_NT_PRINCIPAL /mapop set /pass P@ssw0rd /out c:\share\keytab
- Ключ /princ HTTP/srv-app.company.ru@COMPANY.RU задает уникальное имя клиента (principal) на Linux-машине, которому будет разрешено выполнять аутентификацию в kerberos.
- Ключи /mapuser svc-apache@COMPANY.RU и /pass P@ssw0rd, связывают principal с конкретным пользователем в Active Directory
- Ключ /crypto ALL задает способ шифрования. Вместо /crypto ALL можно указать конкретный способ шифрования например /crypto AES256-SHA1
- Ключ /mapop set устанавливает mapping между принципалом Linux и пользовательским аккаутном Active Directory (/mapop add добавит этот маппинг в keytab)
- Ключ /ptype KRB5_NT_PRINCIPAL — задать тип принципала в запросе (указанный тип является основным и рекомендуется использовать именно его)
- Ключ /out c:\share\keytab задает путь для выходного файла keytab.
- При желании дополнительно значения ключей можно посмотреть при помощи ktpass.exe /help
В итоге мы получаем файл c:\share\keytab, который необходимо скопировать на srv-app и назвать /etc/krb5.keytab. Далее необходимо предоставить доступ к этому файлу пользователю, из под учетной записи которого выполняется сервер httpd. В нашем случае это apache. Для того чтобы apache мог прочитать этот файл просто разрешаем его чтение всем пользователям:
chmod a+r /etc/krb5.keytab
Проверить работает ли наш keytab можно следующим образом:
1. При помощи ktutil:
[root@srv-app ~]# ktutil
ktutil: rkt /etc/krb5.keytab
ktutil: list
slot KVNO Principal
---- ---- ---------------------------------------------------------------------
1 3 HTTP/srv-app.company.ru@COMPANY.RU
2 3 HTTP/srv-app.company.ru@COMPANY.RU
3 3 HTTP/srv-app.company.ru@COMPANY.RU
4 3 HTTP/srv-app.company.ru@COMPANY.RU
5 3 HTTP/srv-app.company.ru@cCOMPANY.RU
ktutil: q
2. При помощи kvno:
# логинимся на KDC
[root@srv-app ~]# kinit svc-apache
Password for svc-apache@COMPANY.RU:
# запрашивем тикет для сервиса HTTP/srv-app.company.ru@COMPANY.RU и печатает номера версий для каждого принципала в keytab
[root@srv-app ~]# kvno HTTP/srv-app.company.ru@COMPANY.RU
HTTP/srv-app.company.ru@COMPANY.RU: kvno = 3
# Удаляем тикет
[root@srv-app ~]# kdestroy
Настройка Apache
Ниже приведен файл /etc/httpd/conf.d/company_main.conf, который содержит конфигурационные инструкции для настройки Kerberos-аутенификации при обращении к URI »/»:
# Kerberos authentication:
AuthType Kerberos
AuthName "SRV-APP auth"
KrbMethodNegotiate on
KrbMethodK5Passwd off
KrbServiceName HTTP/srv-app.company.ru@COMPANY.RU
KrbAuthRealms COMPANY.RU
Krb5Keytab /etc/krb5.keytab
KrbLocalUserMapping On
Require valid-user
Хочу обратить внимание на настройку KrbMethodK5Passwd off. Указанная настройка приводит к тому что при входе в указанный раздел сайта будет произведена kerberos-аутентификациия с использованием технологии Single Sign-On. При неуспешной аутентификации сразу будет ошибка »401 Unautorized». Однако, если изменить настройку на KrbMethodK5Passwd on, то после неуспешной авторизации Single Sign-On, будет предпринята попытка авторизации по имени и паролю.
И еще одна недокументированная возможность, которой мы воспользуемся: Настройка KrbLocalUserMapping On приводит к тому что в переменной REMOTE_USER будет помещено имя зарегистрированного пользователя (в случае KrbLocalUserMapping Off REMOTE_USER будет содержать username@COMPANY RU).
Дополнительную информацию по настройкам модуля mod_auth_kerb пожно прочитать здесь.
Single Sign-On (SSO) с рабочих станций Windows
Еще раз повторюсь, что вся работа по настройке kerberos аутентификации в Linux проделана для того, чтобы иметь возможность входить на страницы портала опубликованного на Linux машине с использованием корпоративных аккаунтов, хранящихся в Active Directory, кроме того для упрощения жизни пользователям этот вход должен быть «прозрачным», без запроса пароля, что достигается иcпользованием технологии Single Sign-On (SSO), которая поддерживается в Windows 7 и выше и браузером Internet Explorer (и Mozilla Firefox).
Однако для того чтобы все проходило гладко, на рабочей станции, откуда осуществляется такой вход, должны быть выполнены следующие настройки:
- Сайт на который осуществляется вход (в нашем случае srv-app.company.ru должен быть внесен в узлы «Местной интрасети» при помощи меню
Internet Explorer: Сервис → Свойства обозревателя → Безопасность → Местная интрасеть → Узлы → Дополнительно - В IE должны быть включены следующие опции (как правило они включены «по умолчанию»):
- Сервис → Свойства обозревателя → Дополнительно → Безопасность → Разрешить встроенную проверку Windows = ON
- Сервис → Свойства обозревателя → Безопасность → Местная интрасеть → Уровень безопасности для этой зоны → Другой → Проверка подлинности пользователя → Автоматический вход в сеть только в зоне интрасети
- Кроме того, SSO при попытке доступа на сайт по IP-адресу работать не будет. Необходимо обязательно использовать доменное имя srv-app.company.ru!
Этап 2. Авторизация пользователя в Django
Итак, в результате работы проведенной на первом этапе, мы получили следующие результаты:
- При входе в любой раздел нашего Django-приложения, пользователем, который был авторизован контроллером домена, мы во-первых получаем доступ к нашему Django-приложению, а во-вторых сервер Apache помещает в переменную REMOTE_USER имя авторизованного пользователя, которое совпадает с аттрибутом sAMAccountName этого пользователя в Active Directory.
- Если пользователь не авторизован в АД, то Apache вернет нам ошибку »401 Unautorized» (При помощи опции ErrorDocument мы можем в этом случае перенаправить неавторизованного пользователя на какую-либо страницу для гостей)
Однако, несмотря на то, что сервер Apache авторизовал нашего пользователя, для Django-приложения он все еще остается неизвестным и, соответственно, весь отлаженный в Django механизм аутентификации/авторизации пользователей и использования сессий остается пока незадействованным.Использование RemoteUserBackend
Специально для таких целей в Django существует простое решение, включающее механизм авторизации в системе пользователей, уже аутентифицированных внешними приложениями, такими как IIS или Apache (способами аналогичными, примененному нами на этапе 1: mod_authnz_ldap, CAS, Cosign, WebAuth, mod_auth_sspi, mod_auth_krb).
Согласно описанию на официальном сайте djangoproject.org, для использования прозрачной аутентификации и авторизации в Django таким способом, достаточно задействовать механизм RemoteUserBackend, выполнив следующие шаги:
1. В файле настроек Django-проекта settings.py добавить django.contrib.auth.middleware.RemoteUserMiddleware в список MIDDLEWARE_CLASSES сразу после django.contrib.auth.middleware.AuthenticationMiddleware:
MIDDLEWARE_CLASSES = [
'...',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.RemoteUserMiddleware',
'...',
]
2. Там же заменить ModelBackend на RemoteUserBackend в списке AUTHENTICATION_BACKENDS (либо добавить этот список в settings.py):
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.RemoteUserBackend',
]
В результате этих изменений RemoteUserMiddleware будет извлекать username посредством request.META['REMOTE_USER'] и автоматически выполнять аутентификацию и авторизацию (login) этого пользователя при помощи RemoteUserBackend. Кроме того RemoteUserBackend при такой авторизации добавит нового пользователя в таблицу auth_user, задействуя таким образом стандартный для Django механизм работы с учетными записями.
Но, к сожалению, этот способ не позволит нам получать из Active Directory и использовать в нашем приложении нужную нам информацию (first_name, last_name, mail, участие в группах AD и др).
Использование django-auth-ldap
Итак, нам нужно получить доступ к Active Directory при помощи протокола LDAP. К счастью отличным Django-приложением для этих целей является django-auth-ldap. Установить его можно стандартно при помощи pip:
pip install django-auth-ldap
После этого придется удалить из settings.py добавленные в предыдущем разделе MIDDLEWARE_CLASSES, а именно 'django.contrib.auth.middleware.RemoteUserMiddleware', а список AUTHENTICATION_BACKENDS должен выглядеть следующим образом:
AUTHENTICATION_BACKENDS = (
'django_auth_ldap.backend.LDAPBackend',
'django.contrib.auth.backends.ModelBackend',
)
Кроме того, в 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=groupOfNames)"
)
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 — это замечательное приложение. С его помощью в профиль пользователя Django можно загрузить практически любую информацию по нему из AD, даже есть возможность «подтянуть» группы в которых этот пользователь участвует в соответствующую модель Django.
Однако для авторизации при помощи django-auth-ldap все-таки необходимо запрашивать у пользователя его имя и пароль, что нам категорически не подходит.
Хотя в документации указано что, вроде бы, можно «скрестить» django-auth-ldap и RemoteUserBackend:
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.
Но это не работает, так как нам надо. Пользователь действительно сможет авторизоваться, но это происходит ровно так, как если бы мы просто использовали RemoteUserBackend (см.предыдущий раздел). Информация из AD в профиль пользователя Django автоматически не загружается.
Конечно, это можно сделать самостоятельно, воспользовашись следующей рекомендацией:
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
Но, как оказалось, все гораздо проще. Велосипед уже придуман, и нам остается только им воспользоватся. Приложение django-remote-auth-ldap является небольшой надстройкой над django_auth_ldap и позволяет без лишних усилий авторизовать пользователя и загрузить его данные из AD во время авторизации.
Устанавливаем django-remote-auth-ldap стандартно (django-auth-ldap также необходим для работы этой надстройки):
pip install django-remote-auth-ldap
Далее, необходимо добавить в settings.py следующую настройку:
DRAL_CHECK_DOMAIN = False
Дело в том, что django-remote-auth-ldap, видимо разработан для работы c IIS, который переменную REMOTE_USER устанавливает в формате «DOMAIN/username», мы же выполнили настройку mod_auth_kerb так, что имя домена в REMOTE_USER не попадает.
Ну и снова рекомендации для настройки MIDDLEWARE_CLASSES и AUTHENTICATION_BACKENDS:
1. В файле настроек Django-проекта settings.py добавить django.contrib.auth.middleware.RemoteUserMiddleware в список MIDDLEWARE_CLASSES сразу после django.contrib.auth.middleware.AuthenticationMiddleware:
MIDDLEWARE_CLASSES = [
'...',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.RemoteUserMiddleware',
'...',
]
2. AUTHENTICATION_BACKENDS должен выглядеть так:
AUTHENTICATION_BACKENDS = [
'django_remote_auth_ldap.backend.RemoteUserLDAPBackend',
]
На этом все, прозрачная аутентификация в Django настроена.
Если пользователь входящий в ваше Django-приложение, уже авторизован в Active Directory, то:
1. Apache авторизует его при помощи Kerberos, допустит к страницам вашего приложения и запишет в REMOTE_USER имя авторизованного пользователя.
2. RemoteUserMiddleware «увидит» значение REMOTE_USER и иницирует аутентификацию и авторизацию указанного пользователя в Django с помощью django-remote-auth-ldap
3. django-remote-auth-ldap аутентифицирует и авторизует пользователя при помощи методов, унаследованных от приложения django-auth-ldap, которое «подтянет» в Django необходимую вам информацию из Active Directory.