Как использовать Twisted для проверки Gmail с помощью проверки подлинности OAuth2.0

У меня был рабочий IMAP-клиент для почты Google, однако он недавно прекратил работать. Я считаю, что проблема в том, что gmail больше не позволяет вводить имена пользователей и пользователей TTL, но теперь требуется OAuth2.0.

Я хотел бы знать, как лучше всего изменить мой пример ниже, чтобы мой скрученный клиент IMAP аутентифицировался с использованием OAuth2.0. (И делать это без пакетов API Google, если это возможно.)

Пример использования имени пользователя/пароля (больше не работает)

class AriSBDGmailImap4Client(imap4.IMAP4Client):
    '''
    client to fetch and process SBD emails from gmail. the messages
    contained in the emails are sent to the AriSBDStationProtocol for
    this sbd modem.
    '''

    def __init__(self, contextFactory=None):
        imap4.IMAP4Client.__init__(self, contextFactory)

    @defer.inlineCallbacks
    def serverGreeting(self, caps):
        # log in
        try:
            # the line below no longer works for gmail
            yield self.login(mailuser, mailpass)
            try:
                yield self.uponAuthentication()
            except Exception as e:
                uponFail(e, "uponAuthentication")
        except Exception as e:
            uponFail(e, "logging in")

        # done. log out
        try:
            yield self.logout()
        except Exception as e:
            uponFail(e, "logging out")

    @defer.inlineCallbacks
    def uponAuthentication(self):
        try:
            yield self.select('Inbox')
            try:
                # read messages, etc, etc
                pass
            except Exception as e:
                uponFail(e, "searching unread")
        except Exception as e:
            uponFail(e, "selecting inbox")

У меня есть тривиальный factory для этого клиента. Он запускается с помощью reactor.connectSSL с URL-адресом хоста и порталом электронной почты Google.

Я следил за указаниями https://developers.google.com/gmail/api/quickstart/quickstart-python для "установленного приложения" (но я не знаю, был ли это правильный выбор). Я могу успешно запустить их "quickstart.py".

Моя быстрая и грязная попытка (не работает)

    @defer.inlineCallbacks
    def serverGreeting(self, caps):
        # log in
        try:
            #yield self.login(mailuser, mailpass)
            flow = yield threads.deferToThread(
                oauth2client.client.flow_from_clientsecrets,
                filename=CLIENT_SECRET_FILE, 
                scope=OAUTH_SCOPE)
            http = httplib2.Http()
            credentials = yield threads.deferToThread( STORAGE.get )
            if credentials is None or credentials.invalid:
                parser = argparse.ArgumentParser(
                    parents=[oauth2client.tools.argparser])
                flags = yield threads.deferToThread( parser.parse_args )
                credentials = yield threads.deferToThread(
                    oauth2client.tools.run_flow,
                    flow=flow, 
                    storage=STORAGE,
                    flags=flags, http=http)
            http = yield threads.deferToThread(
                credentials.authorize, http)

            gmail_service = yield threads.deferToThread(
                apiclient.discovery.build,
                serviceName='gmail', 
                version='v1',
                http=http)

            self.state = 'auth'

            try:
                yield self.uponAuthentication()
            except Exception as e:
                uponFail(e, "uponAuthentication")
        except Exception as e:
            uponFail(e, "logging in")

        # done. log out
        try:
            yield self.logout()
        except Exception as e:
            uponFail(e, "logging out")

В основном я просто скопировал "quickstart.py" в serverGreeting, а затем попытался установить состояние клиента в "auth".

Это аутентифицируется просто отлично, но затем скрученный не может выбрать папку "Входящие":

[AriSBDGmailImap4Client (TLSMemoryBIOProtocol), клиент] FAIL: Неизвестная команда {random gibberish}

Случайная бред содержит буквы и цифры и различается при каждом сбое команды select inbox.

Спасибо за вашу помощь!

Ответ 1

После большого количества чтения и тестирования я наконец смог реализовать рабочий вход в gmail с помощью OAuth2.

Важно отметить, что двухэтапный процесс с использованием "учетной записи службы" выполнял для меня НЕ. Я все еще не понимаю, почему этот процесс не может быть использован, но учетная запись службы, похоже, не имеет доступа к gmail в той же учетной записи. Это справедливо даже в том случае, если учетная запись службы имеет права "редактировать" и включен API Gmail.

Полезные ссылки

Обзор использования OAuth2 https://developers.google.com/identity/protocols/OAuth2

Руководство по использованию OAuth2 с "Установленными приложениями" https://developers.google.com/identity/protocols/OAuth2InstalledApp

Руководство по настройке учетной записи для использования OAuth2 с "Установленные приложения" https://developers.google.com/api-client-library/python/auth/installed-app

Коллекция подпрограмм OAuth2 без полного API Google https://code.google.com/p/google-mail-oauth2-tools/wiki/OAuth2DotPyRunThrough

Шаг 1 - Получите идентификатор клиента Google

Войдите в систему с учетной записью gmail в https://console.developers.google.com/

Запустите проект, включите Gmail API и создайте новый идентификатор клиента для установленного приложения. Инструкции на https://developers.google.com/api-client-library/python/auth/installed-app#creatingcred

Нажмите кнопку "Загрузить JSON" и сохраните этот файл где-нибудь, что будет недоступно для общественности (возможно, это не в репозитории кода).

Шаг 2 - Получите инструменты Python Google OAuth2

Загрузите файл oauth2.py script из https://code.google.com/p/google-mail-oauth2-tools/wiki/OAuth2DotPyRunThrough

Шаг 3 - Получить URL авторизации

Используйте script со второго шага, чтобы получить URL-адрес, разрешающий авторизовать свой проект Google.

В терминале:

python oauth2.py --user={[email protected]} --client_id={your client_id from the json file} --client_secret={your client_secret from the json file} --generate_oauth2_token

Шаг 4 - Получить код авторизации

Вставьте URL-адрес с шага 3 в свой браузер и нажмите кнопку "принять".

Скопируйте код с веб-страницы.

Вставьте код в терминал и нажмите enter. Вы получите:

 To authorize token, visit this url and follow the directions:   https://accounts.google.com/o/oauth2/auth?client_id{...}
 Enter verification code: {...}
 Refresh Token: {...}
 Access Token: {...}
 Access Token Expiration Seconds: 3600

Шаг 5 - Сохранить токен обновления

Скопируйте токен обновления с терминала и сохраните его где-нибудь. В этом примере я сохраняю его в текстовом файле в формате json с ключом "Обновить токен". Но он также может быть сохранен в частной базе данных.

Убедитесь, что токен обновления не доступен публике!

Шаг 6 - Создайте скрученный аутентификатор

Вот пример работы аутентификатора OAuth2. Для этого требуется выполнить команду oauth2.py script с шага 2.

import json
import oauth2

from zope.interface import implementer
from twisted.internet import threads

MY_GMAIL = {your gmail address}
REFRESH_TOKEN_SECRET_FILE = {name of your refresh token file from Step 5}
CLIENT_SECRET_FILE = {name of your cliend json file from Step 1}

@implementer(imap4.IClientAuthentication)
class GmailOAuthAuthenticator():
    authName     = "XOAUTH2"
    tokenTimeout = 3300      # 5 mins short of the real timeout (1 hour)

    def __init__(self, reactr):
        self.token   = None
        self.reactor = reactr
        self.expire  = None

    @defer.inlineCallbacks
    def getToken(self):

        if ( (self.token==None) or (self.reactor.seconds() > self.expire) ):
            rt = None
            with open(REFRESH_TOKEN_SECRET_FILE) as f:
                rt = json.load(f)

            cl = None
            with open(CLIENT_SECRET_FILE) as f:
                cl = json.load(f)

            self.token = yield threads.deferToThread(
                oauth2.RefreshToken,
                client_id = cl['installed']['client_id'], 
                client_secret = cl['installed']['client_secret'],
                refresh_token = rt['Refresh Token'] )

            self.expire = self.reactor.seconds() + self.tokenTimeout


    def getName(self):
        return self.authName

    def challengeResponse(self, secret, chal):
        # we MUST already have the token
        # (allow an exception to be thrown if not)

        t = self.token['access_token']

        ret = oauth2.GenerateOAuth2String(MY_GMAIL, t, False)

        return ret

Шаг 7 - Зарегистрируйте аутентификатор для протокола

В IMAP4ClientFactory:

    def buildProtocol(self, addr):
        p = self.protocol(self.ctx)
        p.factory = self
        x = GmailOAuthAuthenticator(self.reactor)
        p.registerAuthenticator(x)
        return p

Шаг 8 - Используйте токен доступа для аутентификации

Вместо использования "login", получите токен доступа (если необходимо), а затем используйте аутентификацию.

Изменив код примера из вопроса:

    @defer.inlineCallbacks
    def serverGreeting(self, caps):
        # log in
        try:
            # the line below no longer works for gmail
            # yield self.login(mailuser, mailpass)
            if GmailOAuthAuthenticator.authName in self.authenticators:
                yield self.authenticators[AriGmailOAuthAuthenticator.authName].getToken()

            yield self.authenticate("")

            try:
                yield self.uponAuthentication()
            except Exception as e:
                uponFail(e, "uponAuthentication")
        except Exception as e:
            uponFail(e, "logging in")