Всем привет! Мы обновили Клуб, поддержали все изменения Vas3k'а за последнее время. Теперь у нас тоже есть свой OpenID провайдер! :)
Данная функция позволит вашим ресурсам (сайтам), приложениям, Телерамм-бота авторизовать пользователей и проверять, что они пришли из PMI Клуба. Нам это поможет для интеграции с другими сообществами руководителей проектов и всего такого
Оригинальная статья Vas3k'а тут: https://vas3k.club/post/openid/
Знающие эксперты и нетерпеливые могут уже сейчас пойти в настройки своего профиля, зарегистрировать там ключик и получить все необходимые токены и OAuth-эндпоинты: https://pmi.moscow/apps/
Для нормальных же людей, которые не реализуют OAuth-авторизацию каждый день, далее унаследован гайд из оригинальный статьи с дополнением.
Шаг 0. Разобраться с OAuth и OpenID Connect
У всего этого великолепия довольно большой порог входа, даже если использовать уже готовые либы (как мы и будем). Если для вас это первый раз в жизни — придётся немного сесть и во всём разобраться.
Зато потом можете сразу добавлять скилл «IAM» в резюме. Рекомендую!
Для погружения с нуля рекомендую вот этот гайд: An Illustrated Guide to OAuth and OpenID Connect (перевод на русский).
Там в картинках рассказано как выглядит весь этот обмен токенами, кодами, и зачем всё это нужно.
Шаг 1. Зарегистрировать своё «приложение» в Клубе
Чтобы Клуб мог выдавать вам токены, он должен знать что у вас за приложение. Это может быть бот, сайт, приложение, скрипт, да вообще что угодно. Создать его можно здесь: https://pmi.moscow/apps/
В результате у вас будет client_id и client_secret. Первый можно рассказывать всем. Второй нужно хранить в секретном месте, иначе любой сможет украсть вашу авторизацию и притвориться вами. Однако, если вашему приложению не нужен доступ к аккаунтам юзеров, то можно вообще забить на OpenID и использовать service_token — это такой универсальный токен, с которым вы сразу можете делать обычные запросы к API, как будто вы залогинены.
В этом гайде рассматривается только аутентификация живых пользователей на внешних ресурсах. Приходится колдовать :)
Шаг 2. Пишем код
Для проектов на Python рекомендуется использовать библиотеку Authlib. Она современная, даёт биндинги под Django и Flask, ну и сам Клуб внутри использует именно её. Вроде как Authlib даже умеет в асинхронность.
- Вот их гайд по написанию OAuth-клиентов.
Там много примеров и деталей, так что здесь показан лишь самый минимум, а точнее как сделать логин через Клуб за 10 строчек кода.
Для начала создадим саму кнопку входа для своего сайта:
<a href="/login/club">Войти через PMI Клуб</a>
В это же время на бекенде мы инициализируем наш OAuth-клиент. Вам надо лишь подставить свой client_id и client_secret, всё остальное Authlib возьмет из .well-known конфига (который мы указали в server_metadata_url), включая все эндпоинты и JWT-ключи.
Удобно. Спасибо десятилетиям полировки стандарта OpenID.
from authlib.integrations.django_client import OAuth
oauth = OAuth()
oauth.register({
name="club",
client_id="YOUR_CLIENT_ID",
client_secret=os.getenv("GET_YOUR_CLIENT_SECRET_HERE"),
server_metadata_url="https://pmi.moscow/.well-known/openid-configuration",
client_kwargs={"scope": "openid"},
})
Затем напишем простейший обработчик для нашей кнопки. Он по сути просто редиректит юзера на сайт Клуба с правильными GET-параметрами.
# GET /login/club
def login_club(request):
redirect_uri = "https://your.website/login/club/callback"
return oauth.club.authorize_redirect(request, redirect_uri)
Теперь у нас есть кнопка, которую юзер может кликнуть, и ему откроется классическая форма авторизации.
По нажатию "Разрешить" Клуб редиректнет юзера обратно на ваш сайт, но уже с неким новым волшебным временным кодом (который живет 5 минут). Он будет использовать тот redirect_uri, который вы указали в запросе. В нашем случае это /login/club/callback.
Код — это еще не токен, но уже почти. Если Клуб выдал вам код, значит он признал вас. Теперь вам надо просто обменять ваш временный код на настоящий access_token.
Напишем обработчик для этого.
Мы делаем всё с бекенда, потому что так безопаснее и токен не перехватят злые люди посередине (и так рекомендуется стандартом). Но если у вас чисто клиентское приложение, то на свой страх и риск можете делать это и с фронта.
# GET /login/club/callback
def login_club_callback(request):
try:
token = oauth.club.authorize_access_token(request)
except OAuthError as ex:
# тут обрабатываем всякие ошибки авторизации
return render(request, "error.html", {...})
# парсим базовую инфу о пользователе из id_token
# в нём есть юзернейм и почта
user_slug = token["userinfo"]["sub"]
user_email = token["userinfo"]["email"]
# этих данных может быть достаточно для создания профиля
user, _ = User.objects.get_or_create(...)
# логиним юзера у себя
# стандартными средствами джанги
login(request, user)
# красава!
return reverse("profile")
Если вы используете другую либу или вообще хотите реализовать весь флоу руками, то гляньте на наш OpenID Configuration — есть вся необходимая инфа о том, какие эндпоинты выдают токены и ключи. Этот формат по идее тоже стандартен и должен автоматически жраться и другими фреймворками.
Шаг 3. Делаем запросы к API
Выданный access_token живёт уже 24 часа, но его можно рефрешить через refresh_token, если вам по каким-то причинам надо обращаться к API Клуба позднее. Так что не забудьте сохранить всё в базу.
Хотя для простой аутентификации можно сразу сделать все нужные запросы и просто выбросить токен. Так даже безопаснее.
Например, чтобы получить данные профиля, то есть простой эндпоинт: https://pmi.moscow/user/me.json
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" https://pmi.moscow/user/me.json
Ну или то же самое на Питоне.
import requests
# запрашиваем профиль с аватаркой итд
club_profile = requests.get(
url="https://pmi.moscow/user/me.json",
headers={
"Authorization": f"{token['token_type']} {token['access_token']}"
}
).json()
# заводим юзера у себя
user, _ = User.objects.update_or_create(
club_slug=user_slug,
defaults=dict(
full_name=club_profile["user"]["full_name"],
avatar=club_profile["user"]["avatar"],
email=user_email,
)
)
👍 Вы великолепны! ❤️ MEOW ❤️
Остальные API-эндпоинты можно подсмотреть на странице https://pmi.moscow/apps/
Кроме старых добрых JSON-версий постов и профилей через наше маленькое API теперь можно запрашивать фид в формате JSONFeed и комментарии к постам. Вот так можно получить фид: https://pmi.moscow/feed.json
Вот так отфильтровать посты по рейтингу за год: https://pmi.moscow/all/top_year/feed.json
Здесь получить данные конкретного поста: https://pmi.moscow/post/103.json
И вот так его комментарии: https://pmi.moscow/post/103/comments.json
ДОПОЛНЕНИЕ
Туториал для Flutter
- Нам понадобится библиотека oauth2, которую ставим как "flutter pub add oauth2"
- Заполняем константы:
static const _authorizationEndpoint = 'https://pmi.moscow/auth/openid/authorize';
static const _tokenEndpoint = 'https://[mi.moscow/auth/openid/token';
static const _identifier = SECRET_ID;
static const _secret = SECRET_SECRET;
static const redirectUrl = CALLBACK;
- Создаем инстанс авторизации
final _grant = oauth2.AuthorizationCodeGrant(
_identifier,
Uri.parse(_authorizationEndpoint),
Uri.parse(_tokenEndpoint),
secret: _secret,
);
- Теперь создаем ссылку авторизации
final authUrl = _grant.getAuthorizationUrl(Uri.parse(redirectUrl));
- По этой ссылке совершаем переход
Это может быть внутренний WebVIew или редирект в браузер, не важно - авторизуемся в клуб и разрешаем доступ к нашему приложению
6. Хендлим callback
- Если это внутренний WebVIew то просто смотрим за ссылкой, как только она начинается на наш CALLBACK значит авторизация закончена
- Если вы открывали во внешнем браузере, то нужно будет системно обработать редирект (https://docs.flutter.dev/development/ui/navigation/deep-linking)
Мы получили ссылку вида CALLBACK?code=КОД
7. Теперь нам нужно получить наши данные
final callBackUri = Uri.parse(CALLBACK?code=КОД);
final client = await _grant.handleAuthorizationResponse(callBackUri.queryParameters);
- Вы великолепны, в нашем client можем получить токен вот так:
final accessToken = client.credentials.accessToken;