Аутентификация c использованием внешней cookie
Contents
Требования
Авторизация с использованием внешней сессионной информации, передаваемой посредством cookie, может являться предпочтительной при тесной интеграции экземпляра вики МойнМойн с некоторым веб-приложением. В этом случае можно использовать вики для поддержки документации или отслеживания задач, равно как и ссылкаться на страницы вики со страниц веб-приложения. Также, вики-страницы могут включать специфические макрокоманды, использующие данные из приложения. В случае наличия механизма аутентификации в интегрируемом веб-приложении, необходимость авторизации как в приложении, так и на вики может раздражать как пользователей, котрым приходиться аутентифицироваться дважды (в приложении и на вики), так и администраторов, которым приходится поддерживать две независимых базы пользователей.
Для аутентификации пользователя в МойнМойн посредством внешней cookie, веб-приложение, производящее аутентификацию, должно уметь (или быть можифицировано таким способом, чтобы) создавать cookie при каждой успешной аутентификации, равно как и удалять cookie при завершении сессии. Для предотвращения неавторизованной генерации сессионной cookie третьей стороной, необходима возможность аутентифицировать cookie, передаваемые МойнМойн. Одним из способов добиться этого является хранение хэшей cookie в базе данных, файле или ином защищённом хранилище (недоступном третьей стороне), доступном экземпляру МойнМойн.
Стратегия реализации
Код вики должен быть изменён в двух местах: в wikiconfig.py должен быть добавлен новый класс ExternalCookie, который будет использоваться для аутентификации пользователя. При аутентификации в веб-приложении будет фактически выполнена аутентификация на вики. При завершении сессии в приложении также перестаёт быть доступна аутентификация на вики. Ряд переменных добавляется в конфигурацию экземпляра вики для изменения страницы «Settings: Preferences» и для автоматического создания учётных записей при необходимости.
Темы экземпляра вики могут быть модифицированы для изменения метода username класса Theme. Этот метод генерирует ссылки «Login» и «Logout» в области навигации страницы вики. Эти ссылки должны быть модифицированы для ссылания на страницы аутентификации и завершения сессии в веб-приложении.
Также необходимо добавить в веб-приложение механизмы создания/удаления cookie и (опционально) создания/удаления записей о сессиях в хранилище, в случае, если они отсутствуют. Если пользователи имеют доступ к вики без аутентификации, хорошей практикой является добавление перенаправления на referrer (страницу, с которой был осуществлён переход) страницы аутентификации в случае успешной аутентификации (аналогично тому, как ведёт себя процесс аутентификации в МойнМойн).
Нереализованные варианты
Синхронизация возможных времён неактивности сессии между приложением и экземпляром не является предпочтительной. Если сессия пользователя инвалидировалась после часа отсутствия активности, аутентификационная cookie может позволять редактировать и сохранять страницы на вики, только если веб-приложение не удаляет записи о просроченных сессиях из общего хранилища. Если же пользователь с просроченной сессией аутентифицируется вновь, то будет создана новая запись в общем хранилище и сгенерирована новая cookie MoinAuth — последующие операции на вики будут использовать новую cookie. Приведённый в качестве примера вариант файла wikiconfig.py содержит пример того, как просроченные записи могут удаляться из таблицы !MySQL после истечения их срока жизни при выполнении операции на вики. В то же время, при развёртывании большинства экземпляров вики скорее всего будет принято решение удалять неактуальные записи в хранилище силами веб-приложения.
Альтернативой является использование общего хранилища для шифрования cookie в веб-приложении и расшифровке её в методе external_auth. Если реализуется данный вариант, добавление временной метки к cookie приведёт к инвалидации просроченных cookie при проверки их времени жизни. Без этой проверки, любая сгенерированная cookie будет являться валидной сколь угодно долгий период времени до тех пор, пока не изменится метод шифрования. Тесты с !exPyCrypto показали, что время, затрачиваемое на расшифровку больше, чем проверка с использованием базы !MySQL: в случае использования базы данных время валидации cookie занимало обычно 0 секунд и не превышало значения в 0,02 секунды.
Ещё одним способом фильтровать украденные cookie является добавление пользовательского IP-адреса к cookie и сравнение его с IP-адресом, с которого происходит запрос к МойнМойн. Но данный способ не является надёжным ввиду динамического назначения адресов некоторыми ISP и, как следствие необходимости постоянной повторной аутентификации, равно как использование reverse proxy/NAT приводит к тому, что множество пользователей приходят с одного адреса.
Класс ExternalCookie
Первым шагом является переопределение метода external_auth класса Config путём добавления примерно следующего кода в конфигурацию вики:
Код, представленный ниже, предназначен для МойнМойн версии 1.9.
Откройте для редактирования файл wikiconfig.py (в случае отдельного экземпляра вики) или farmconfig.py (в случае использования вики-фермы)
Найдите строку, содержащую class Config(DefaultConfig): или class FarmConfig(DefaultConfig):
- Замените данную строку кодом, представленным ниже
При использовании farmconfig.py найдите и закомментируйте/раскомментируйте соответствуеющие объявления «class»
1 # +++++++++++++++ начало примера external_cookie
2
3 # Данный пример кода может быть полезен при реализации аутентификации с
4 # использованием внешней cookie (созданной внешней программой, не МойнМойн)
5 # c МойнМойн. См. места, помеченные +++, для изменения согласно потребностям.
6 # Данный код необходимсо скопировать в файл farmconfig.py или wikiconfig.py,
7 # заменяя имеющуюся строку
8 #
9 # class Config(DefaultConfig):
10 # или
11 # class FarmConfig(DefaultConfig):
12
13
14 from MoinMoin.config.multiconfig import DefaultConfig
15 from MoinMoin.auth import BaseAuth
16
17 # Данная функция добавлена в случае, если понадобится журналирование действий
18 # во время тестирования
19 import time
20 def writeLog(*args):
21 '''Write an entry in a log file with a timestamp and all of the args.'''
22 s = time.strftime('%Y-%m-%d %H:%M:%S ',time.localtime())
23 for a in args:
24 s = u'%s %s;' % (s,a)
25 log = open('/somelogfile', 'a') # +++ расположение файла с журналом событий
26 log.write('\n' + s + '\n')
27 log.close()
28 return
29
30 # Представленные ниже два метода являются примером того, как можно
31 # аутентифицировать по cookie другого приложения.
32 import MySQLdb
33 def verifySession(sidHash): # +++ для использования данного метода необходимо
34 # раскомментировать его вызов ниже в
35 # ExternalCookie.
36 """Return True if sidHash value exists (meaning user is currently logged on), false otherwise.
37
38 If you are not a MySQL user, find another way to store this information. ActiveSession is a two-column table
39 containing a hashed cookie value (sidHash) plus a date-time stamp (tStamp).
40 Your other application must add an entry to this table each time a user logs on and delete the entry when the user logs off.
41 """
42 db = MySQLdb.connect(db='mydb',user='myid',passwd='mypw') #+++ пользователь должен иметь доступ на чтение
43 c = db.cursor()
44 q = 'select sidHash from ActiveSession where sidHash="%s"' % sidHash
45 result = c.execute(q)
46 c.close()
47 if result == 1:
48 return True
49 return False
50
51 def verifySessionPlus(sidHash,timeout=3600*4): # +++ для использования данного
52 # метода необходимо
53 # раскомментировать его
54 # вызов ниже в
55 # ExternalCookie.
56 """Return True if sidHash value exists (meaning user is currently logged on), false otherwise.
57
58 This version of verifySession deletes entries inactive for more than 4 hours and
59 updates the tStamp field with each moin transaction. If performance is
60 important, find another way to delete inactive sessions.
61 """
62 db = MySQLdb.connect(db='mydb',user='myid',passwd='mypw') # +++ пользователь должен иметь доступ на запись
63 c = db.cursor()
64 q = 'delete from ActiveSession where tStamp<"%s"' % int(time.time() - timeout) # удаление неактивных записей
65 result = c.execute(q)
66 q = 'update ActiveSession set tStamp=%s where sidHash="%s"' % (int(time.time()),sidHash)
67 result = c.execute(q)
68 c.close()
69 if result == 1:
70 return True
71 return False
72
73
74 class ExternalCookie(BaseAuth):
75 name = 'external_cookie'
76 # +++ Следующие две строки могут быть полезны в случае, если
77 # переопределяется метод username в используемых на вики темах.
78 # В случае, если они закомментированы, страницы вики не будут содержать
79 # ссылок на аутентификацию или завершение сессии.
80 login_inputs = ['username', 'password'] # +++ необходимо для показа ссылки
81 # на страницу аутентификации
82 # в области навигации страниц
83 # logout_possible = True # +++ необходимо для показа ссылки на завершение
84 # сессии в области навигации вики-страниц.
85
86 def __init__(self, autocreate=False):
87 self.autocreate = autocreate
88 BaseAuth.__init__(self)
89
90 def request(self, request, user_obj, **kw):
91 """Return (user-obj,False) if user is authenticated, else return (None,True). """
92 # login = kw.get('login') # +++ пример не использует данную переменную;
93 # предполагается, что аутентификация
94 # выполняется во внешнем приложении
95 # user_obj = kw.get('user_obj') # +++ пример не использует данную переменную
96 # username = kw.get('name') # +++ пример не использует данную переменную
97 # logout = kw.get('logout') # +++ пример не использует данную переменную;
98 # предполагается, что завершение сессии
99 # выполняется во внешнем приложении
100 import Cookie
101 user = None # пользователь не аутентифицирован
102 try_next = True # если значение равно True, МойнМойн попытается
103 # воспользоваться следующем способом аутентификации
104 # в списке доступных способов аутентификации.
105
106 otherAppCookie = "MoinAuth" # +++ имя пользователя, почтовый адрес, псевдоним
107 # пользователя и идентификатор сессии
108 # разделяются символом "#"
109 try:
110 cookie = Cookie.SimpleCookie(request.cookies) # появилось в МойнМойн 1.9
111 except Cookie.CookieError:
112 cookie = None # игнорировать невалидные cookie
113
114 if cookie and otherAppCookie in cookie: # наличие данного cookie означает,
115 # что аутентификация пользователя
116 # уже выполнена во внешнем приложении
117 import urllib
118 if sys.version_info[:2] > (2, 5):
119 cookievalue = cookie[otherAppCookie].value # МойнМойн 1.9 и Python 2.6
120 else:
121 cookievalue = cookie[otherAppCookie] # МойнМойн 1.9 и Python 2.5
122 # writeLog('cookievalue',cookievalue)
123 # +++ декодирование и обработка значения cookie - необходимо отредактировать
124 # сообразно потребностям.
125 #~ cookievalue = urllib.unquote(cookievalue) # значение cookie является urlencoded,
126 # необходимо раскодировать его (МойнМойн 1.9)
127 #~ cookievalue = cookievalue.decode('utf-8') # декодирование кодировки cookie в Unicode (МойнМойн 1.9)
128 cookievalue = cookievalue[1: -1] # удаления кавычек в начале и в конце (МойнМойн 1.9)
129 # writeLog(u'значение cookie',cookievalue)
130 cookievalues = cookievalue.split('#') # cookie имеет вид имя_пользователя#почтовый_адрес#псевдоним#идентификатор_сессии
131
132 email = aliasname = sessionid = ''
133 try: # извлечение полей из cookie внешнего приложения
134 auth_username = cookievalues[0] # имя пользователя на вики
135 email = cookievalues[1] # почтовый адрес необходим пользователю для
136 # изменения и сохранения пользовательских предпочтений
137 aliasname = cookievalues[2] # псевдноим актуален только в случае,
138 # если имя пользователя неудобно для
139 # использования на вики
140 sessionid = cookievalues[3] # опциональный идентификатор сессии
141 # внешнего приложения - уникальная
142 # временная метка или случайное число
143 except IndexError:
144 pass # псевдоним и идентификатор сессии не нужны, кроме случая,
145 # если раскомментированы соответствующие строки ниже
146 # writeLog(u'имя пользователя',auth_username)
147 # writeLog(u'почтовый адрес',email)
148 # writeLog(u'псевдоним',aliasname)
149 # writeLog(u'идентификатор сессии',sessionid)
150
151 # +++ так как кто угодно может сгенерировать cookie, далее представлена
152 # проверка, что cookie создана именно внешним приложением
153 if auth_username:
154 import hashlib # +++ данная библиотека появилась в Python 2.5+;
155 # см. http://code.krypto.org/python/hashlib
156 # для версий Python 2.3, 2.4
157 sidHash = hashlib.md5(cookievalue).hexdigest() # МойнМойн 1.9
158 # writeLog('sidHash',sidHash)
159 sidOK = verifySession(sidHash) # проверка, что пользователь аутентифицирован
160 # во внешнем приложении
161 # +++ или же использовать verifySessionPlus
162 if not sidOK:
163 auth_username = None
164 # writeLog(u'имя пользователя после verifySession',auth_username)
165
166 if auth_username:
167 # пользователь аутентифицирован, необходимо создать объект, описывающий пользователя МойнМойн
168 from MoinMoin.user import User
169 # передача auth_username в конструктор User означает, что аутентификация уже выполнена.
170 user = User(request, name=auth_username, auth_username=auth_username, auth_method=self.name)
171 changed = False
172 if email != user.email: # обновлялся ли почтовый адрес?
173 user.email = email ;
174 changed = True # если да, необходимо обновить профиль
175 # if aliasname != user.aliasname: # +++ обновлялся ли псевдоним?
176 # user.aliasname = aliasname ;
177 # changed = True # если да, необходимо обновить профиль
178
179 if user:
180 user.create_or_update(changed)
181 if user and user.valid:
182 try_next = False # есть валидный пользователь; список доступных
183 # методов аутентификации нет нужды более обрабатывать
184 # writeLog(str(user), try_next)
185 return user, try_next
186
187 from MoinMoin.config import multiconfig, url_prefix_static
188
189 class Config(multiconfig.DefaultConfig):
190 # class FarmConfig(multiconfig.DefaultConfig):
191
192 auth = [ExternalCookie(autocreate=True)] # аутентификация по внешней cookie является
193 # единственным способом аутентификации
194 # параметр autocreate появился в версии 1.8.0
195
196 # +++ Ниже представлены рекомендуемые изменения в форме пользовательских предпочтений
197 # в случае, если external_cookie является единственным способом аутентификации
198 user_form_disable = ['name', 'aliasname', 'email',] # показывать, но не давать изменять
199 user_form_remove = ['password', 'password2', 'css_url', 'logout', 'create', 'account_sendmail','jid'] # удалить полностью
200 #~ user_autocreate = True # +++ МойнМойн будет создавать учётный записи автоматически при их отсутствии
201 cookie_lifetime = (12, 12) # (анонимная сессия, аутентифицированная сессия) по умолчанию равно (0,12) в МойнМойн 1.9
202
203 # +++++++++++++++ конец примера реализации external_cookie, далее следует остальная конфигурация экземпляра вики
Начальное тестирование
В случае наличие Firefox с расширением Web Developer, начальное тестирование можно осуществить довольно быстро. Достаточно аутентифицироваться в веб-приложении и кликнуть в Tools — Web Developer — Cookies — View Cookie Information. Скорее всего, можно будет обнаружить как минимум одну cookie, которая выглядит как идентификатор сессии, созданный веб-приложением во время аутентификации, обычно она содержит большое случайное число и, вероятно, временную метку.
Далее, необходимо открыть Tools — Web Developer — Cookies — Add Cookie. Дать cookie имя «MoinAuth» (с сохранением регистра). Указать в качестве значения «имя_пользователя#почтовый_адрес» без пробелов и с использованием «#» в качестве разделителя. Если веб-приложение и экземпляр вики находятся на одном поддомене, задать FQDN тем же, что использует веб-приложение (если нет, не указывать поддомен в доменном имени для того, чтобы cookie была доступна со всех поддоменов домена). Установить путь cookie в «/». Установить переключатель «Session Cookie» и сохранить cookie.
Далее, необходимо открыть новую вкладку и перейти на страницу вики. Вики должна показывать, что пользователь аутентифицирован. Далее можно проверить, что можно редактировать и сохранять вики-страницы и редактировать пользовательские предпочтения.
Изменения в веб-приложении
Приведённый в качестве примера метод external_cookie ожидает, что cookie будет содержать следующую информацию, разделённую символом «#»:
- Идентификатор пользователя на вики — необходим всегда
- Почтовый адрес пользователя — необходим, если необходимо предоставить пользователю возмодность обновлять и сохранять свои предпочтения на вики.
Псевдоним пользователя — обычно в нём нет необходимости, используется для переопределения идентификаторов пользователей, которые не являются ВикиИменами.
- Уникальный идентификатор сессии — большое случайное число или временная метка и случайное число.
Веб-приложение должно сохранять cookie с именем «MoinAuth» и путём «/». Обычно устанавливать путь в «/» не рекомендуется, так как это несёт с собой определённые риски по безопасности, так как оно позволяет приложениям под этим путём читать содержимое cookie. В данном случае это именно то, что необходимо — MoinMoin должен иметь возможность прочитать cookie, установленные другим приложением.
Как было показано ранее, можно легко создать cookie с необходимой информацией. Для предотвращения подобных случаев МойнМойн должен проверять валидность cookie путём поиска соответствующей записи в защищённом хранилище, созданном веб-приложением.
По возможности необходимо избежать создания дополнительных проблем с боезопасностью посредством создания связи имён пользователей и идентификаторов сессий или даже идентификаторов сессий, которые могут быть получены и использованы. Лучшим решением является хэширование cookie «MoinAuth» и сохранение результата в защищённом хранилище. Добавление временной метки к каждой записи позволит удалять просровенные записи в дальнейшем.
В примере файла wikiconfig.py присутствует неиспользуемый код (метод verifySession), который выполняет хэширование содержимого cookie и проверку наличия результата в таблице MySQL. Кроме того, имеется альтернативный код (метод verifySessionPlus), который удаляет из таблицы записи старше 4 часов, проверяет хешированную cookie и обновляет временную метку. Лучшим способом в данном случае является модификация вбе-приложения для удаления устаревших записей из таблицы, возможно, при каждой аутентификации пользователя.
Как только модификация веб-приложения будет начата, практически всё тестирование можно выполнять посредством расширения Firefox Web Developer. Перед аутентификацией в приложении cookie MoinAuth должна отсутствовать, после успешной аутентификации она должна появляться, и исчезать при завершении сессии. Кроме того, защищённое хранилище должно пополняться хэшированными значениями cookie после аутентификации и очищаться от более неактуальных во время завершения сессии. Доступ к страницам вики при наличии аутентифицированной сессии должен приводить к создании cookie «MOIN_SESSION».
Модификация тем
Следующим шагом являетсямодификация ссылок на выполнение аутентификации и завершения сессии в навигационной области вики-страниц. Если пользователи вики могут использовать только одну тему, необходимо модифицировать код ниже для указания на страницы аутентификации/завершения сессии в приложении. Если же пользователи могут выбирать тему, возможно, проще всего будет модифицировать непосредственно файл MoinMoin/theme/__init__.py.
Если аутентификация происходит строго перед доступом к вики-страницам, эта модификация, равно как и следующая, могут не выполняться. Если закомментировать переменные login_inputs и logout_possible в начале класса ExternalCookie, вики-страницы не будут содержать ссылки на аутентификацию и завершение сессии.
Код, представленный ниже, предназначен для МойнМойн версии 1.9.
1 def username(self, d):
2 """ Assemble the username / userprefs link
3
4 @param d: parameter dictionary
5 @rtype: unicode
6 @return: username html
7 """
8 request = self.request
9 _ = request.getText
10
11 userlinks = []
12 # Добавить ссылку на домашнюю страницы пользователя для
13 # зарегистрированных пользователей. Нет нужды проверять её
14 # существование, пользователь может создать её.
15 if request.user.valid and request.user.name:
16 interwiki = wikiutil.getInterwikiHomePage(request)
17 name = request.user.name
18 aliasname = request.user.aliasname
19 if not aliasname:
20 aliasname = name
21 title = "%s @ %s" % (aliasname, interwiki[0])
22 # ссылка на (внешнюю) домашнюю страницу пользователя
23 homelink = (request.formatter.interwikilink(1, title=title, id="userhome", generated=True, *interwiki) +
24 request.formatter.text(name) +
25 request.formatter.interwikilink(0, title=title, id="userhome", *interwiki))
26 userlinks.append(homelink)
27 # ссылка на действие userprefs
28 if 'userprefs' not in self.request.cfg.actions_excluded:
29 userlinks.append(d['page'].link_to(request, text=_('Settings'),
30 querystr={'action': 'userprefs'}, id='userprefs', rel='nofollow'))
31
32 if request.user.valid:
33 if request.user.auth_method in request.cfg.auth_can_logout:
34 userlinks.append('<a href="/myapp/Logout">%s</a>' % _('Logout', formatted=False)) # +++ страница завершения сессии во внешнем приложении
35 else:
36 query = {'action': 'login'}
37 # Специальная ссылка напрямую на аутентификацию если методы аутентификации не требуют ввода.
38 if request.cfg.auth_login_inputs == ['special_no_input']:
39 query['login'] = '1'
40 if request.cfg.auth_have_login:
41 userlinks.append('<a href="/myapp/Login">%s</a>' % _("Login", formatted=False)) # +++ страница аутентификации во внешнем приложении
42
43 userlinks = [u'<li>%s</li>' % link for link in userlinks]
44 html = u'<ul id="username">%s</ul>' % ''.join(userlinks)
45 return html
Модификация страницы аутентификации веб-приложения
На последнем этапе можно также рассмотреть необходимость модификации страницы аутентификации для пользователей вики, решивших перейти на страницу аутентификации со страницы вики. Большинство веб-серверов передают приложению referrer запроса, содержащий страницы, с которой был осуществлён запрос данной.
Можно рповерить его значение для определения, яляется ли referrer страницей вики и, если да, сохранить URL и перенаправить на него в случае успешной аутентификации.