Авторизация в CLI приложении с помощью OAuth

Как выглядит процесс авторизации через OAuth в Command-line interface приложении? В стандартном сценарии провайдер перенаправляет обратно на сайт или в мобильное приложение (в случае с OAuth 2), а как перенаправлять в программу в терминале?

В статье будет рассмотрен процесс OAuth авторизации в CLI приложении на примере HeadHunter.

Исходный код можно найти здесь.

Что такое OAuth?

OAuth 2 — это стандарт авторизации, который дает возможность приложениям, таким как Facebook, GitHub и DigitalOcean, запросить доступ к пользовательским аккаунтам через HTTP. Методика заключается в перенаправлении процесса проверки подлинности на сервис, где зарегистрирован аккаунт пользователя, и предоставлении разрешения приложениям третьих сторон на использование этого аккаунта. OAuth 2 предусматривает механизмы авторизации специально для веб и настольных приложений, а также для устройств мобильных платформ.

Абстрактный процесс авторизации

862893069029bf7d4dff1c11b064bf3f.png

  1. Приложение запрашивает у пользователя авторизацию на доступ к ресурсам сервиса.

  2. Если пользователь авторизовал запрос, приложение получает разрешение на авторизацию.

  3. Приложение запрашивает токен доступа у сервера авторизации (API), предоставляя аутентификацию своей собственной личности и разрешение на авторизацию.

  4. Если личность приложения аутентифицирована и разрешение на авторизацию действительно, сервер авторизации (API) выдает токен доступа к приложению. Авторизация завершена.

  5. Приложение запрашивает ресурс у сервера ресурсов (API) и предоставляет токен доступа для аутентификации.

  6. Если токен доступа действителен, сервер ресурсов (API) передает ресурс приложению.

Авторизация в CLI

8082b60ab9395442684d95704c1eca57.png

  1. Инициирование авторизационного процесса пользователем.

  2. CLI запускает локально временный сервер.

  3. Переадресация пользователя командной строкой на веб-страницу авторизации с указанием идентификатора приложения и запросом на редирект обратно на локальный адрес после успешной авторизации на заданный порт.

  4. Введение пользователем своих учетных данных на сайте провайдера.

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

  6. Локальный сервер регистрирует полученный запрос, передает код авторизации в CLI приложение и завершает свою работу.

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

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

Регистрация приложения в HeadHunter

На странице https://dev.hh.ru/admin регистрируем новое приложение, в поле Redirect URI вводим http://localhost:1505/oauth/hh и ждем одобрения HH. После одобрения заявки будут доступны Client Id, Client Secret.

Создание приложения

В качестве CLI обертки был выбран Click, Flask будет выступать в роли временного веб-сервера.

Пользователь запускает CLI приложение, хочет авторизоваться. Для этого запускается подпроцесс для веб-сервера. Создается объект Queue и передается в дочерний процесс, в который сервер положит код авторизации.

from multiprocessing import Process, Queue

HH_REDIRECT_HOST = "localhost"
HH_REDIRECT_PORT = 1505
HH_REDIRECT_URI = f"http://{HH_REDIRECT_HOST}:{HH_REDIRECT_PORT}/oauth/hh"

def run_server(queue):
    flask_app.config['queue'] = queue
    flask_app.run(HH_REDIRECT_HOST, HH_REDIRECT_PORT, threaded=False, use_reloader=False)


def authorize(client_id: str) -> str:
    queue = Queue()

    server = Process(target=run_server, args=(queue,))
    click.launch(get_oauth_link(client_id))
    server.start()
    server.join()
    authorization_code = queue.get()
    if authorization_code is None:
        raise RuntimeError("No authorization code")
    click.echo(click.style("Authorization code was gotten", fg="green"))
    return authorization_code

Пользователь перенаправляется на страницу с предложением ввести логин, пароль на сайте HH:

def get_oauth_link(client_id: str) -> str:
    url = "https://hh.ru/oauth/authorize?"
    params = {"client_id": client_id,
              "redirect_uri": HH_REDIRECT_URI,
              "response_type": "code"}
    click.echo(url + urllib.parse.urlencode(params))
    return url + urllib.parse.urlencode(params)

В случает правильного логина и пароля HH перенаправляет пользователя на сайт с похожей ссылкой http://localhost:1505/oauth/hh?code=NU0OIMNIBUYINMPOKM8865LOINJI66U36BNKBP3F55V2UPJBUBL43553V2S9DJ

Веб-сервер, запущенный ранее, видит запрос, читает параметр code, передает его в межпроцессный буфер Queue и отключается.

flask_app = Flask(__name__)

log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)


@flask_app.route('/oauth/', methods=['GET'])
def oauth(provider):
    match provider:
        case 'hh':
            code = request.args.get('code', default=None, type=str)
        case _:
            return jsonify({"status": "ERROR", "message": "Wrong provider"})
    if code is None:
        return jsonify({"status": "ERROR", "message": "No code in redirect URI params"})
    flask_app.config['queue'].put(code)

    return jsonify({"status": "SUCCESSFUL"}) # никогда не вернется

@flask_app.teardown_request
def shutdown_process(response):
    os.kill(os.getpid(), signal.SIGINT)

CLI приложение в это время ждет, когда веб-сервер отключится. После отключения получает код авторизации и делает запрос на получение кода доступа к данным (по документации это должен делать удаленный сервер, передача Client Secret на пользователькое устройство не предусмотрено, но для упрощения будет создан толстый клиент без бэкэнда).

def json_api_factory(response_model: type):
    def json_api(func):
        def json_api_wrapper(*args, **kwargs):
            res = func(*args, **kwargs)
            if res.status_code == 200:
                return response_model(**res.json())
            else:
                click.echo(click.style(f"Request failed {res.status_code}: {res.json()}", fg='red'))

        return json_api_wrapper

    return json_api

class OAuthTokenResponse(BaseModel):
    access_token: str
    token_type: str
    refresh_token: str
    expires_in: int

class OAuth:
    def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.redirect_uri = redirect_uri

    @json_api_factory(OAuthTokenResponse)
    def token(self, authorization_code: str) -> OAuthTokenResponse:
        url = "https://hh.ru/oauth/token"
        body = {
            "grant_type": "authorization_code",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "redirect_uri": self.redirect_uri,
            "code": authorization_code
        }
        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }
        res = requests.post(url, data=body, headers=headers)
        return res

Полученный access_token добавляется в заголовки запросов:

Authorization: Bearer ACCESS_TOKEN

Вывод

OAuth необходим как безопасный метод авторизации, дающий сторонним приложениям ограниченный доступ к пользовательским данным, не раскрывая при этом логин и пароль от основного аккаунта. Он удобен тем, что позволяет пользователям легко и безопасно использовать различные сервисы и приложения, избегая необходимости создавать и запоминать новые учетные данные для каждого из них.

© Habrahabr.ru