Пишем себе немного OpenID-авторизации

    image

    Взгляд в будущее


        В последнее время всякие социальные сети и вообще сервисы-лидеры интернета по посещаемости и количеству аккаунтов завели очень неплохую, на мой взгляд, привычку — предоставление уникальных OpenID-идентификаторов для пользователей, дабы с их использованием можно было зайти на сторонний сайт. Кроме того, параллельно развивается очень похожая, но все-таки не совсем производная технология OAuth, которая появилась на свет благодаря стараниям создателей небезызвестного Twitter и, цитируя википедию, «позволяет предоставить третьей стороне доступ к защищенным ресурсам пользователя, без необходимости передавать ей (третьей стороне) логин и пароль».
        Лично меня такая тенденция очень радует и, более того, я почти уверен, что за подобной технологией будущее. В частности, в будущем обязательно появятся новые мэшапы для агрегирования информации с кучи сайтов (в частности, хочется вспомнить очень хороший, но несправедливо забытый сервис Yahoo Pipes, который так и не смог покорить сердца и умы просто потому, что его время тогда еще не пришло. Возможно, все еще впереди), а именно такой «форм-фактор» требует логина на кучу сервисов сразу.
        Петь дифирамбы подобным технологиям можно очень долго, но лично меня, например, всегда напрягали сайты, на которых надо с нуля регистрироваться, чтобы что-нибудь скачать. Ведь все мы неизменно сталкивались с тем, что когда ищешь, где скачать тот или иной материал — он зачастую оказывается на каком-то совершенно левом и непонятном сайте с названием в духе allbooksmusicwarezzz.omg.su, который ко всему прочему еще и регистрацию требует. Да нет, дело не в пиратстве, дело в том, что сайтов со всяким барахлом, сделанных на коленке, уйма. А вот человеческая память на логины-пароли ограничена, и тут уж ничего не сделаешь. Но приятный момент здесь еще и в том, что многие OpenID-провайдеры кроме информации, непосредственно служащей для авторизации, могут по запросу предоставить еще и базовую информацию о пользователе — e-mail, полное имя, предпочтительный язык и т.п. Причем на многих подобных сервисах можно управлять тем, что отдавать, а что сохранить в секрете. Например, разве пользователю не будет приятно, когда он, зайдя на очередной сайт, увидит приветливую надпись «Добро пожаловать, Вася!» на чистом русском языке, да еще и профиль уже готов к употреблению, вместе с аватаром, привычками и кличкой нежно любимого кота?

    Делаем дело и работаем работу


        Довольно лирики, думаю, кому оно надо как разработчику — он и так все вышенаписанное уже знает, а простым пользователям дальнейший материал вряд ли будет интересен. Еще больше хвалебных речей и рассуждений легко найти в блоге Ивана Сагалаева, а мы давайте попробуем сделать свою систему авторизации через OpenID (например, для блога) на Python, с преферансом и пианистками.
        Для своего блога, который сейчас находится в разработке у меня в папочке Projects, я решил вообще отказаться от системы регистрации и авторизации через логин-пароль, а оставить только OpenID. В качестве фреймворка был выбран Pylons, а для прикручивания OpenID к Django-проектам существует и развивается проект с простым и понятным названием django-openid. Для Pylons, в общем-то, тоже существует решение под названием AuthKit, однако с ним у меня отношения как-то не очень сложились, а все, что я нашел в сети — это несколько сниппетов, в которых и пришлось разбираться.
        Для начала надо установить модуль python-openid, чтобы обеспечить поддержку технологии, а потом создаем контроллер (обработчик запроса по URL, ближайшая ассоциация — джанговский views.py) и начинаем колдовать.
    $ paster controller auth
        Сразу оговорюсь, что код рабочий ровно до той степени, которая обеспечивает непосредственно аутентификацию, что делать дальше и как это все оформлять — решать только вам, господа творцы. Начало довольно стандартное:
    Copy Source | Copy HTML
    1. from openid.consumer.consumer import Consumer, SUCCESS, FAILURE, DiscoveryFailure
    2. from openid.store import filestore
    3. from openid import sreg
    4. from datetime import datetime
    5. from hashlib import md5
    6.  
    7. class AuthController(BaseController):
    8.     def __before__(self):
    9.         self.openid_session = session.get("openid_session", {}) # проверяем, не существует ли openid-сессии
    10.  
    11.     def index(self):
    12.         return render('/accounts/enter.html')
    13.  
    14.     @rest.dispatch_on(POST="signin_POST") # разделяем GET- и POST-запросы по разным обработчикам для удобства
    15.     def signin(self):
    16.         if c.user: # проверяем, не попытался ли уже залогиненый юзер зайти еще раз
    17.             session['message'] = 'Already signed in.'
    18.             session.save()
    19.             redirect(url(action='index')) # и если да, то не пущаем
    20.         session.clear()
    21.         return render('/index.html')


        Теперь подходим к самому интересному:

    Copy Source | Copy HTML
    1. def signin_POST(self):
    2.         problem_msg = 'A problem ocurred comunicating to your OpenID server. Please try again.'
    3.  
    4.         g.openid_store = filestore.FileOpenIDStore('.') # создаем временное хранилище для хранения OpenID-данных, g здесь-массив глобальных переменных Pylons
    5.  
    6.         self.consumer = Consumer(self.openid_session, g.openid_store) # ага, вот и наш клиент
    7.         openid = request.params.get('openid', None) # достаем из запроса строку с OpenID - идентификатором
    8. ...


        Ага, а вот тут немного магии. SReg — это то самое расширение, которое позволяет нам запросить у сервера дополнительные данные о пользователе. Поля, значение которых хотелось бы узнать, перечисляем в списке optional, а дополнительные данные, если что, всегда можно запросить у пользователя потом. Если же какая-то дополнительная информация требуется прямо кровь из носу, то можно запросить ее в required, но если сервер ее не отдаст — будет ошибка.

    Copy Source | Copy HTML
    1. ...
    2.         sreg_request = sreg.SRegRequest(
    3.             #required=['email'],
    4.             optional=['fullname', 'timezone', 'language', 'email', 'nickname']
    5.         )
    6.  
    7.         if openid is None:
    8.             session['message'] = problem_msg
    9.             session.save()
    10.             return render('/index.html')
    11. ...


        Здесь я позволил себе схалявить и написал этот код только для того, чтобы объяснить разницу между простым OpenID и кросс-логином с гугловского аккаунта. Дело в том, что гугл не представляет пользователям OpenID-идентификатора вида vasya_pupkin.google.com, а все куда проще и веселее. URL идентификации у всех пользователей гугла выглядит абсолютно одинаково — www.google.com/accounts/o8/id. Любопытно то, что при запросе на этот URL гугл отдает готовый XRDS (XML-подобного вида документ, возвращаемый сервером по стандарту OpenID 2.0), который уже содержит все необходимое для авторизации, а вам как пользователю присваивается уникальный ID, который и является, по сути, идентификатором OpenID.
    Copy Source | Copy HTML
    1. if openid == 'google':
    2.     openid = 'https://www.google.com/accounts/o8/id'
    3.  
    4. try:
    5.     authrequest = self.consumer.begin(openid) # панеслася
    6. except DiscoveryFailure, e: # а вдруг ошибка в адресе или такой провайдер существует только в твоем воображении?
    7.     session['message'] = problem_msg
    8.     session.save()
    9.     return redirect(url(controller='auth', action='signin'))
    10.  
    11. authrequest.addExtension(sreg_request) # подключаем SReg, дабы извлечь требуемые поля для профиля
    12.  
    13. redirecturl = authrequest.redirectURL(h.url_for('/', qualified=True),
    14.     return_to=h.url_for(action='verified', qualified=True),
    15.     immediate=False
    16. ) # после всего, что у нас было с сервером, надо как-то жить дальше
    17. session['openid_session'] = self.openid_session
    18. session.save()
    19. return redirect(url(redirecturl))


    Ну, теперь можно поговорить с сервером, останемся ли мы друзьями.

    Copy Source | Copy HTML
    1. ...
    2.     def verified(self):
    3.         problem_msg = 'A problem ocurred comunicating to your OpenID server. Please try again.'
    4.         self.consumer = Consumer(self.openid_session, g.openid_store)
    5.         info = self.consumer.complete(request.params, (h.url_for(controller='auth',
    6.                                                  action='verified',
    7.                                                  qualified=True)))
    8.         if info.status == SUCCESS: # все пучком
    9.  
    10.             sreg_response = sreg.SRegResponse.fromSuccessResponse(info) # извлекаем затребованные в SReg поля
    11.  
    12.             user = User(by_openid=info.identity_url) # ищем юзера по идентификатору в базе
    13.  
    14.             if not user.exist: # а вот тут можно делать что угодно. Например, внести юзера в базу
    15.                 newuser = User()
    16.                 try:
    17.                     email = sreg_response.get('email', u''),
    18.                 except:
    19.                     email = u''
    20.                 newuser.create(
    21.                     openid = unicode(info.identity_url),
    22.                     email = email,
    23.                     password = unicode(md5(info.identity_url).hexdigest()),
    24.                     ip = request.environ['REMOTE_ADDR']
    25.                 )
    26.  
    27.             session.clear() # мутим сессию
    28.             session['openid'] = info.identity_url
    29.             session.save()
    30.  
    31.             if 'redirected_from' in session:
    32.                 red_url = session['redirected_from']
    33.                 del(session['redirected_from'])
    34.                 session.save()
    35.                 return redirect(url(red_url))
    36.             return redirect(url(controller='auth', action='index'))
    37.         else: # факир был пьян
    38.             session['message'] = problem_msg
    39.             session.save()
    40.             return redirect(url(action='signin'))


        Вот, собственно, и все. Что делать с полученными данными — засовывать в куки, продолжать регистрацию и просить у пользователя дополнительную информацию — решать только вам. Да, и еще, данный код не работает с OpenID от Yahoo. Если охота по завещанию Козьмы Пруткова позрить в корень — есть информация все в том же блоге Ивана Сагалаева. Буду рад услышать любую критику, уточнения, предложения. Постараюсь в дальнейшем разобраться с OAuth и организовать интересующимся немного кода по кросслогину из твиттера.

        За возможность почесать голову шилом, как Мастер Виноградинка, очень благодарю вот эту ссылку и всех товарищей, которые оставили там свои сниппеты.

    UPD: Хабраюзер mustangostang раскрывает секреты AX (как получить возвращаемую информацию с гугла), ибо гугл SReg не отдает.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 18
    • 0
      Спасибо за инфу, до этого я не слышал про существование OAuth, надо бы почитать про это.
      • +1
        … кличкой нежно любимого кота..


        Ни в SReg, ни в AX такого поля пока, слава Бёрнсу, нет :)

        Кстати, последний таки советую добавить. Потому как многие провайдеры отказываются от SReg в пользу AX (например Яндекс, myopenid и пр). И не плохо было бы уметь получать данные из обоих вариантов.

        А за статью — спасибо.
        • 0
          многие провайдеры отказываются от SReg в пользу AX (например Яндекс


          Откуда такая информация?
        • 0
          Google Profile это и есть OpenID-идентификатор (например: google.com/profiles/mrgallua)
          • 0
            ну и как, у вас получилось уговорить Google отдавать данные через SReg? буквально вчера он хотел передавать атрибуты только через AX, соответственно, с прелестными запросами идентификаторов вида: axschema.org/contact/email
            • 0
              Нет, не получилось :) Работает только аутентификация. Но еще разберусь, скорее всего, потому что это направление сейчас меня довольно сильно интересует.
              • +5
                ну так а что же вы вставляете в код, как будто так и надо? :-))

                email = sreg_response.get('email', u'')

                народ ведь будет честно копировать, а потом удивляться, почему при авторизации с gmail не подхватывается даже e-mail :-)

                собственно, нужно следующее для того, чтобы запросить AX-данные — на этапе формирования запроса:

                import openid.extensions.ax as ax
                ax_request = ax.FetchRequest()
                ax_request.add (ax.AttrInfo ('http://axschema.org/contact/email'))
                authrequest.addExtension(ax_request)


                на этапе проверки:

                ax_response = ax.FetchResponse.fromSuccessResponse(info)
                email = ax_response.get('http://axschema.org/contact/email', u'')


                код пишу без тестирования, но что-то в этом духе должно работать :-) какие еще атрибуты отдает гугл — написано вот тут: code.google.com/apis/accounts/docs/OpenID.html#Parameters

                удачи :-))
                • 0
                  Спасибо, добавил ссылку в пост для таких же интересующихся, как я :)
            • –2
              я ничего не понял из выше написанно, но это офигенно!
              • 0
                Гляньте в код python-openid. Лично мне очень захотелось его переписать. И это при то, что я лично его не сильно касался…
                ИМХО, очень плохо написано. Ждём другой, более приятной либы. Ну или глобального рефакторинга с тотальным документированием.
                • 0
                  Да, python-openid ужасен, у меня сложилось впечатление что писал либу ява кодер, который просто выучил синтаксис питона, но не удосужился почитать хотя бы pep8. Но как сказал Сагалаев — python-openid точно реализует спецификацию.
                • –1
                  дословный перевод на русский «Write you some OpenId...» звучит весьма коряво…
                  • 0
                    Тогда уж не you, а yourself, а название я не переводил, а придумал сам :)
                    • 0
                      Не спорю, но это американизм и по русски он не звучит.

                      Также не забываем о Learn you a Haskell… и Learn you some Erlang for geat good.

                      Поверьте, я прочитал много плохих переводов с английского на русский, и сразу вижу дословный перевод жаргонизмов и устойчивых выражений.

                      Я не говорю, что вы перевели, это просто оно так воспринимается.
                      • 0
                        у каждого свое восприятие. А мне просто захотелось немного похулиганить, вот и все
                  • 0
                    Есть ещё один малоизвестный метод для пользователей Гугла. Можно в любой html файл(обычно в корневой индекс) прописать 2строки которые дадут тот же операйди:
                    <link rel="openid2.provider" href="https://www.google.com/accounts/o8/ud?source=profiles" >
                    <link rel="openid2.local_id" href="http://www.google.com/profiles/[username или id]" >
                    • 0
                      Если же какая-то дополнительная информация требуется прямо кровь из носу, то можно запросить ее в required, но если сервер ее не отдаст — будет ошибка.


                      Насколько я помню никакой ошибки не будет и вообще никакой разницы между optional там нет. Может это на будущее оставили, а может баг такой был.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.