OAuth аутентификация в приложении Flask
- пятница, 19 января 2018 г. в 03:14:11
Эта статья является бонусом к новому циклу статей Flask Mega-Tutorial (2018).
Автор тот же Мигель Гринберг. Статья не новая, но не утратила своей актуальности.
Технологии OAuth уже больше 10 лет, и 99% процентов интернет-аудитории имеет аккаунт минимум на одном из ресурсов, поддерживающих OAuth. Кнопка «Войти через» есть почти на каждом ресурсе? Разберемся как это делается с применением микрофреймворка Flask.
Многие веб-сайты предоставляют пользователям возможность упрощенной регистрации в "один клик", используя стороннюю службу аутентификации, с использованием учетной записи пользователя, в каком либо из известных социальных сервисов. В моем старом курсе Flask Mega-Tutorial я показал вам, как использовать один из этих протоколов, называемый OpenID, ( который ныне почил с миром прим. переводчика).
Прим. переводчика: Еще пара ссылок о том как устроен OAuth здесь, здесь
Лучший способ представить OAuth — перечислить список событий, которые происходят во время входа:
Обмен между приложением и сторонней службой не является тривиальным, но для пользователя это чрезвычайно просто, поскольку все, что нужно пользователю, — это войти на сайт третьей стороны и дать разрешение на обмен информацией с помощью приложения.
В настоящее время используются две версии протокола OAuth, как в соответствии с описанным выше общим процессом, так и с некоторыми различиями в реализации. OAuth 1.0a, используемый Twitter, является самым сложным из двух. OAuth 2, используемый Facebook, представляет собой несовместимую пересмотренную версию протокола, которая устраняет большую часть сложности версии 1.0a, полагаясь на защищенный HTTP для шифрования.
Прежде чем приложение сможет использовать стороннего поставщика OAuth, его необходимо зарегистрировать. Для Facebook и Twitter это делается на своих соответствующих сайтах разработчиков с созданием "app", которое представляет приложение для пользователей этих сайтов.
Создать приложение для Facebook вы можете здесь https://developer.facebook.com.
Жмем "НАЧАТЬ" и NEXT. По ходу заполняем различную информацию о себе.
http://localhost:5000
.В следующих разделах я собираюсь описать относительно простое приложение Flask, которое реализует аутентификацию Facebook и Twitter.
Я покажу вам важные части приложения в статье, но полное приложение доступно в этом репозитории GitHub: https://github.com/miguelgrinberg/flask-oauth-example. В конце этой статьи я покажу вам инструкции по ее запуску.
Пользователи в примере приложения хранятся в базе данных SQLAlchemy. Приложение использует расширение Flask-SQLAlchemy для работы с базой данных и расширение Flask-Login для отслеживания зарегистрированных пользователей.
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.login import LoginManager, UserMixin
db = SQLAlchemy(app)
lm = LoginManager(app)
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
social_id = db.Column(db.String(64), nullable=False, unique=True)
nickname = db.Column(db.String(64), nullable=False)
email = db.Column(db.String(64), nullable=True)
@lm.user_loader
def load_user(id):
return User.query.get(int(id))
В базе данных есть отдельная таблица для пользователей users, которая в дополнение к id
, который является основным ключом, содержит три столбца:
social_id
: строка, которая определяет уникальный идентификатор из сторонней службы аутентификации, используемой для входа в систему.
nickname
: псевдоним для пользователя. Должен быть определен для всех пользователей и не обязательно должен быть уникальным.
email
: адрес электронной почты пользователя. Этот столбец не является обязательным.Класс User наследуется от UserMixin
из Flask-Login, который дает ему методы, требуемые этим расширением. Функция обратного вызова user_loader
, также требуемая Flask-Login, загружает пользователя по его первичному ключу.
Для Python существует несколько клиентских пакетов OAuth. В этом примере я решил использовать Rauth. Однако, даже при использовании пакета OAuth существует множество аспектов проверки подлинности провайдерами, что затрудняет задачу.
Прежде всего, существуют две версии протокола OAuth, которые широко используются. Но даже среди провайдеров, использующих одну и ту же версию OAuth, есть много деталей, которые не являются частью спецификации и должны выполняться в соответствии с собственной документацией.
По этой причине я решил реализовать слой абстракции поверх Rauth, так что приложение Flask может быть написано в общем виде. Ниже представлен простой базовый класс, в котором будут написаны конкретные реализации провайдера:
class OAuthSignIn(object):
providers = None
def __init__(self, provider_name):
self.provider_name = provider_name
credentials = current_app.config['OAUTH_CREDENTIALS'][provider_name]
self.consumer_id = credentials['id']
self.consumer_secret = credentials['secret']
def authorize(self):
pass
def callback(self):
pass
def get_callback_url(self):
return url_for('oauth_callback', provider=self.provider_name,
_external=True)
@classmethod
def get_provider(self, provider_name):
if self.providers is None:
self.providers = {}
for provider_class in self.__subclasses__():
provider = provider_class()
self.providers[provider.provider_name] = provider
return self.providers[provider_name]
class FacebookSignIn(OAuthSignIn):
pass
class TwitterSignIn(OAuthSignIn):
pass
Базовый класс OAuthSignIn
определяет структуру, которой должны следовать подклассы, реализующие услуги каждого провайдера. Конструктор инициализирует имя провайдера, а также идентификатор приложения и секретный код, назначенные им, и полученные из конфигурации. Ниже приведен пример конфигурации приложения (конечно, вам нужно будет заменить эти коды своим собственным):
app.config['OAUTH_CREDENTIALS'] = {
'facebook': {
'id': '470154729788964',
'secret': '010cc08bd4f51e34f3f3e684fbdea8a7'
},
'twitter': {
'id': '3RzWQclolxWZIMq5LJqzRZPTl',
'secret': 'm9TEd58DSEtRrZHpz2EjrV9AhsBRxKMo8m3kuIZj3zLwzwIimt'
}
}
На верхнем уровне есть два важных события, поддерживаемых этим классом, которые являются общими для всех провайдеров OAuth:
authorize()
.callback()
. Поскольку у провайдера нет прямого доступа к внутренним методам приложения, он будет перенаправляться на URL-адрес, который его вызовет. URL-адрес, который поставщик должен перенаправить, возвращается методом get_callback_url()
и создается с использованием имени провайдера, так что каждый провайдер`` получает свой выделенный маршрут.Метод get_provider()
используется для поиска правильного экземпляра OAuthSignIn
с именем поставщика. Этот метод использует интроспекцию для поиска всех подклассов OAuthSignIn
, а затем сохраняет экземпляр каждого в словаре.
Rauth представляет провайдеры OAuth с объектом класса OAuth1Service
или OAuth2Service
, в зависимости от версии используемого протокола. Я создаю объект этого класса в подклассе OAuthSignIn
каждого провайдера. Реализации для Facebook и Twitter показаны ниже:
class FacebookSignIn(OAuthSignIn):
def __init__(self):
super(FacebookSignIn, self).__init__('facebook')
self.service = OAuth2Service(
name='facebook',
client_id=self.consumer_id,
client_secret=self.consumer_secret,
authorize_url='https://graph.facebook.com/oauth/authorize',
access_token_url='https://graph.facebook.com/oauth/access_token',
base_url='https://graph.facebook.com/'
)
class TwitterSignIn(OAuthSignIn):
def __init__(self):
super(TwitterSignIn, self).__init__('twitter')
self.service = OAuth1Service(
name='twitter',
consumer_key=self.consumer_id,
consumer_secret=self.consumer_secret,
request_token_url='https://api.twitter.com/oauth/request_token',
authorize_url='https://api.twitter.com/oauth/authorize',
access_token_url='https://api.twitter.com/oauth/access_token',
base_url='https://api.twitter.com/1.1/'
)
Для Facebook, который реализует OAuth 2, используется класс OAuth2Service
. Объект службы инициализируется именем службы и несколькими аргументами, специфичными для OAuth. Аргументами client_id
и client_secret
являются те, которые назначены приложению на сайте разработчика Facebook. Authorize_url
и access_token_url
— это URL-адреса, определенные Facebook для приложений, к которым необходимо подключиться во время процесса аутентификации. Наконец, base_url
задает URL-адрес префикса для любых вызовов API Facebook после завершения проверки подлинности.
Twitter реализует OAuth 1.0a, поэтому используется класс OAuth1Service
. В OAuth 1.0a идентификационные и секретные коды называются consumer_key
и consumer_secret
, но в остальном идентичны по функциональности аналогам OAuth 2. Протокол OAuth 1 требует, чтобы провайдеры отображали три URL вместо двух, есть дополнительный запрос request_token_url
. Аргументы name
и base_url
идентичны аргументам, используемым в службах OAuth 2. Следует отметить, что Twitter предлагает два параметра для параметра authorize_url
. URL, показанный выше, https://api.twitter.com/oauth/authorize
, является самым безопасным, так как он представит пользователю окно, в котором нужно будет разрешить приложению получать доступ к Twitter каждый раз. Изменение этого URL-адреса на https://api.twitter.com/oauth/authenticate
заставит Twitter запрашивать разрешение только в первый раз, а затем тихо разрешит доступ до тех пор, пока пользователь не выйдет из Twitter.
Обратите внимание, что нет стандартизации для точки входа URL-адресов, провайдеры OAuth определяют их по своему усмотрению. Чтобы добавить нового поставщика OAuth, вам нужно будет получить эти URL-адреса из его документации.
Когда пользователь нажимает ссылку «Вход в систему с ...» для инициирования аутентификации OAuth, вызов следует по такому маршруту:
@app.route('/authorize/<provider>')
def oauth_authorize(provider):
if not current_user.is_anonymous():
return redirect(url_for('index'))
oauth = OAuthSignIn.get_provider(provider)
return oauth.authorize()
Этот маршрут сначала гарантирует, что пользователь не вошел в систему, а затем просто получает подкласс OAuthSignIn
, соответствующий данному поставщику, и вызывает его метод authorize()
, чтобы инициировать процесс. Ниже показана реализация authorize()
для Facebook и Twitter:
class FacebookSignIn(OAuthSignIn):
# ...
def authorize(self):
return redirect(self.service.get_authorize_url(
scope='email',
response_type='code',
redirect_uri=self.get_callback_url())
)
class TwitterSignIn(OAuthSignIn):
# ...
def authorize(self):
request_token = self.service.get_request_token(
params={'oauth_callback': self.get_callback_url()}
)
session['request_token'] = request_token
return redirect(self.service.get_authorize_url(request_token[0]))
Для провайдеров OAuth 2, таких как Facebook, реализация просто выдает перенаправление на URL-адрес, созданный объектом службы rauth
. Область действия зависит от Поставщика, в этом конкретном случае я прошу, чтобы Facebook предоставил электронную почту пользователя. В response_type='code' аргумент говорит oauth-провайдеру, что приложение представляет собой веб-приложение (есть и другие возможные значения для различных процессов аутентификации). Наконец, аргумент redirect_uri назначает маршрут приложения, который должен вызвать поставщик после завершения проверки подлинности.
Поставщики OAuth 1.0 a используют несколько более сложный процесс, который включает получение маркера запроса от поставщика, который представляет собой список из двух элементов, первый из которых затем используется в качестве аргумента в перенаправлении. Весь маркер запроса сохраняется в пользовательском сеансе, так как он будет снова необходим в обратном вызове.
Поставщик OAuth перенаправляет обратно в приложение после аутентификации пользователя и дает разрешение на обмен информацией. Маршрут, который обрабатывает этот обратный вызов, показан ниже:
@app.route('/callback/<provider>')
def oauth_callback(provider):
if not current_user.is_anonymous():
return redirect(url_for('index'))
oauth = OAuthSignIn.get_provider(provider)
social_id, username, email = oauth.callback()
if social_id is None:
flash('Authentication failed.')
return redirect(url_for('index'))
user = User.query.filter_by(social_id=social_id).first()
if not user:
user = User(social_id=social_id, nickname=username, email=email)
db.session.add(user)
db.session.commit()
login_user(user, True)
return redirect(url_for('index'))
Этот маршрут создает экземпляр класса поставщика OAuthSignIn
и вызывает его метод callback()
. Этот метод имеет функцию завершения аутентификации с провайдером и получения информации о пользователе. Возвращаемое значение представляет собой кортеж с тремя значениями, уникальный идентификатор (называемый social_id
, чтобы отличать его от первичного ключа id
), псевдоним пользователя и адрес электронной почты пользователя. Идентификатор и псевдоним являются обязательными, но в этом примере приложения я сделал электронное письмо необязательным, так как Twitter никогда не делится этой информацией с приложениями.
Пользователь просматривается в базе данных по полю social_id
, и если не найден, новый пользователь добавляется в базу данных с информацией, полученной от провайдера, эффективно регистрируя новых пользователей автоматически. Затем пользователь регистрируется с помощью функции login_user()
в Flask-Login и, наконец, перенаправляется на домашнюю страницу.
Реализация метода callback()
для провайдеров OAuth для Facebook и Twitter показана ниже:
class FacebookSignIn(OAuthSignIn):
# ...
def callback(self):
def decode_json(payload):
return json.loads(payload.decode('utf-8'))
if 'code' not in request.args:
return None, None, None
oauth_session = self.service.get_auth_session(
data={'code': request.args['code'],
'grant_type': 'authorization_code',
'redirect_uri': self.get_callback_url()},
decoder=decode_json
)
me = oauth_session.get('me').json()
return (
'facebook$' + me['id'],
me.get('email').split('@')[0], # Facebook does not provide
# username, so the email's user
# is used instead
me.get('email')
)
class TwitterSignIn(OAuthSignIn):
# ...
def callback(self):
request_token = session.pop('request_token')
if 'oauth_verifier' not in request.args:
return None, None, None
oauth_session = self.service.get_auth_session(
request_token[0],
request_token[1],
data={'oauth_verifier': request.args['oauth_verifier']}
)
me = oauth_session.get('account/verify_credentials.json').json()
social_id = 'twitter$' + str(me.get('id'))
username = me.get('screen_name')
return social_id, username, None # Twitter does not provide email
В методе callback()
передается токен проверки, который приложение может использовать для связи с API-интерфейсом провайдера. В случае OAuth 2 это происходит как аргумент code
, тогда как для OAuth 1.0a это oauth_verifier
, оба заданные в строке запроса.
Этот код используется для получения oauth_session
из объекта службы rauth.
Обратите внимание, что в последних версиях API Facebook токен сеанса возвращается в формате JSON. Формат по умолчанию, ожидаемый rauth для этого токена, должен указываться в строке запроса. По этой причине, необходимо добавить аргумент decoder
, который декодирует содержимое JSON. В Python 2 достаточно передать json.loads
, но в Python 3 нам нужен дополнительный шаг, потому что полезная нагрузка возвращается как байты, которые json-парсер не понимает. Преобразование из байтов в строку выполняется во внутренней функции decode_json.
Объект oauth_session
может использоваться для предоставления API-запросов поставщику. Здесь он используется для запроса информации о пользователе, которая должна предоставляться определенным провайдером. Facebook предоставляет идентификатор пользователя и адрес электронной почты, но не дает имен пользователей, поэтому имя пользователя для приложения создается из левой части адреса электронной почты. Twitter предоставляет идентификатор и имя пользователя, но не поддерживает электронную почту, поэтому электронное письмо возвращается как None
.
Данные, полученные от провайдера, окончательно возвращаются в виде трехэлементного кортежа для функции просмотра. Обратите внимание, что в обоих случаях значение id
от провайдера добавляется с «facebook $»
или «twitter $»
до его возврата, чтобы сделать его уникальным для всех поставщиков. Поскольку это то, что приложение будет хранить как social_id
в базе данных, необходимо сделать это, чтобы два поставщика, которые назначили один и тот же id
двум различным пользователям, не конфликтовали в базе данных приложения.
Как я уже упоминал выше, пример приложения позволяет любому пользователю регистрироваться и входить в систему с помощью учетной записи Facebook или Twitter. Приложение демонстрирует, как регистрировать пользователей без необходимости вводить какую-либо информацию, все, что им нужно сделать, это войти в систему с провайдером и разрешить совместное использование информации.
Если вы хотите попробовать этот пример, вам необходимо выполнить некоторые подготовительные шаги:
Клонировать или загрузить репозиторий проекта: https://github.com/miguelgrinberg/flask-oauth-example
Создать виртуальную среду и установите пакеты по списку в файле requirements.txt (вы можете использовать Python 2.7 или 3.4).
Зарегистрируйте «приложение» с помощью Facebook и Twitter,
как описано выше.
Отредактируйте app.py с идентификатором и секретными кодами ваших приложений Facebook и Twitter.
После выполнения этих инструкций вы можете запустить приложение с помощью python app.py, а затем запустить http://localhost: 5000 в своем браузере.
Надеюсь, что эта статья полезна в демистификации OAuth.
Если у вас есть вопросы, напишите их ниже.
Miguel