Возможность авторизации через Клуб на внешних сайтах (+ API)

 Публичный пост
11 апреля 2023  472

Всем привет! Мы обновили Клуб, поддержали все изменения 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

  1. Нам понадобится библиотека oauth2, которую ставим как "flutter pub add oauth2"
  2. Заполняем константы:
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;
  1. Создаем инстанс авторизации
 final _grant = oauth2.AuthorizationCodeGrant(
    _identifier,
    Uri.parse(_authorizationEndpoint),
    Uri.parse(_tokenEndpoint),
    secret: _secret,
  );
  1. Теперь создаем ссылку авторизации
final authUrl = _grant.getAuthorizationUrl(Uri.parse(redirectUrl));
  1. По этой ссылке совершаем переход

Это может быть внутренний 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);
  1. Вы великолепны, в нашем client можем получить токен вот так:
final accessToken = client.credentials.accessToken;
Связанные посты
Откомментируйте первым 👇

😎

Автор поста открыл его для большого интернета, но комментирование и движухи доступны только участникам Клуба

Что вообще здесь происходит?


Войти