- Регистрация
- 21.07.20
- Сообщения
- 40.408
- Реакции
- 1
- Репутация
- 0
JSON Web Token — это открытый стандарт для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях.
You must be registered for see links
Когда речь идёт о хранении sensitive data в браузере, достаточно воспользоваться одним из двух доступных вариантов: cookies или localStorage. Тут каждый выбирает по вкусу. Однако я посвятил эту статью Secret Service – службе, которая работает через D-Bus и предназначена для хранения «секретов» в Linux.
У службы есть API, которым пользуется GNOME Keyring для хранения аутентификационных данных пользовательских приложений.
Почему Secret Service
Дело в том, что я получал токен не в браузере. Я писал клиентскую аутентификацию для консольного приложения, похожую на ту, что используется в git.
Вопрос о способе хранения реквизитов встал сразу, так как я не хотел вынуждать пользователей логиниться при очередном запуске моего приложения.
Сначала был вариант хранить токен в зашифрованном файле, но он отпал сразу потому, что я догадывался, что мои функции шифрования и дешифровки будут велосипедом.
Тогда я задумался о том, как хранит секреты Linux, и оказалось, что подобные механизмы реализованы и в других ОС.
В итоге ключом доступа к токену будет служить пароль учетной записи пользователя Linux.
Архитектура Secret Service вкратце
Основная структура данных Secret Service — это коллекция элементов с атрибутами и секретом.
Коллекция
Это набор всевозможных аутентификационных данных. В системе используется коллекция по-умолчанию под псевдоним «default». В нее записываются все пользовательские приложения. Seahorse нам её покажет.
Как видно, в моё хранилище сохранились Google Chrome и VSCode. Сюда же будет сохраняться и моё приложение.
Каждая такая запись называется элементом.
Элемент
Часть коллекции, хранящая атрибуты и секрет.
Атрибуты
Пара вида ключ, значение, которая содержит название приложения и служит для идентификации элемента.
Секрет
Название говорит само за себя, но здесь хранятся различные структуры данных в байтовой репрезентации, содержащие пользовательскую почту, пароль и так далее.
Алгоритм взаимодействия с Secret Service
Я долго думал над алгоритмом аутентификации, пока не набросал flow chart.
Определяем операторы
«Токен в хранилище?» — функция и условие.
«Извлечь токен из хранилища» — функция.
«Запросить регистрационные данные у пользователя» — функция.
«Запросить токен у API» — функция.
«Сохранить токен в хранилище» — функция.
«Использовать токен» — конец.
Я решил попробовать Click Framework для создания CLI приложения.
import click
Он предоставляет декораторы для определения команд, опций, аргументов и так далее. Все команды приложения принято оборачивать в группу. Я сделал это для того, чтобы задать точку входа в скрипт.
@click.group()
def cli():
pass
Для логина в приложение я создам одноименную команду.
В консоле это будет выглядеть так:
$ app login
Email:
Password:
Или так когда токен получен:
$ app login
Logged in!
Команда login
@cli.command(help="Login into your account.")
@click.option(
'--email',
prompt=True,
help='Registered email address.')
@click.option(
'--password',
prompt=True,
hide_input=True,
help='Password provided at registration.'
)
def login(email, password):
pass
if __name__ == '__main__':
cli()
Все декораторы применяются к функции login, которая пока не имплементирована, но принимает значения параметров email и password.
Декоратор @cli.command добавляет команду в группу, а @click.option делает параметры функции опциями команды.
В опции пароля, параметр hide_input скрывает символы при вводе в консоле.
Параметр prompt принимает булевые значения, согласно которым Сlick Framework решает, запрашивать ли значение параметра у пользователя.
С последним у меня проблемы
Я не могу просто присвоить ему True или False потому, что:
в случае True Click Framework запрашивает опцию при каждом запуске. Мне это не подходит. Достаточно запросить почту и пароль при первом запуске, получить токен от WEB API и сохранить его в Secret Service, а в дальнейшем запрашивать токен у Secret Service API;
В случае False Click Framework вообще не запрашивает опцию. Значит, я не смогу получить токен, если приложение запущено впервые и токен отсутствует в Secret Service.
Мне нужна функция, которая примет решение и вернет соответствующее значение в переменную prompt_desicion. А зависит это решение от наличия или отсутствия токена в Secret Service. Отсюда следует, что функция подключается к Secret Service API и запрашивает токен.
Последствия
На этом моменте я решил собрать всю аутентификационную логику в классе отдельного модуля. На мой взгляд, глобальные переменные, классы и не декорированные функции испортят читаемость в контексте паттерна Click Framework.
Напротив, если основной модуль будет содержать код, исключительно относящийся к Click Framework, он будет выглядеть понятно и лаконично.
В итоге модуль app содержит логику паттерна Click Framework. В него я буду импортировать модуль auth, в котором будет класс Auth с логикой аутентификации.
.
├── auth.py
└── app.py
Я буду хранить значение решения о запросе почты и пароля у пользователя в атрибуте prompt_desicion объекта auth класса Auth модуля auth.
@cli.command(help="Login into your account.")
@click.option(
'--email',
prompt=auth.prompt_desicion,
help='Registered email address.')
@click.option(
'--password',
prompt=auth.prompt_desicion,
hide_input=True,
help='Password provided at registration.'
)
def login(email, password):
pass
if __name__ == '__main__':
cli()
Пишем модуль аутентификации
Для Python доступен пакет SecreteStorage, который использует Secret Service API.
Он оперирует основными понятиями службы и включает в себя два модуля, которые реализуют классы и функции доступа к основным объектам Secret Service.
Здесь будет реализован класс доступа к Secret Service API и WEB API, определен атрибут prompt_desicion и соответствующий метод.
Импортируем необходимые пакеты
requests — для HTTP запросов к WEB API.
secretstorage — для запросов к Secret Service API.
json — для десериализации байтов в словарь.
import requests
import secretstorage
import json
Получаем секрет из Secrete Storage
class Auth:
def __init__(self, email=None, password=None):
# атрибуты, по которым осуществляется поиск элемента
# Secret Service
self._attributes = {'application': 'MyApp'}
# подключение к Dbus
self._connection = secretstorage.dbus_init()
# запрос коллекции по-умолчанию
self._collection = secretstorage.collection.get_default_collection(
self._connection
)
# запрос всех элементов коллекции с указанными атрибутами
self._items = self._collection.search_items(self._attributes)
# получение конечного атрибута
self._stored_secret = self.get_stored_secret()
На данном этапе я запросил нужный мне элемент коллекции.
Критерием поиска для Secret Service служит атрибут self._attributes.
О символе «_» в названиях атрибутов
Нижнее подчеркивание в названиях атрибутов означает, что к ним не предполагается обращаться извне. Впрочем, это не делает их недоступными для других объектов. Интерпретатор не изолирует их в отдельном пространстве имён, доступном только из области видимости объекта. Так обозначаются атрибуты, которые нигде, кроме самого объекта не используются. И это не более, чем солгашение.
Стоит отметить, что заданному критерию поиска в коллекции может соответствовать не один элемент. По этой причине автор(ы) SecretStorage решил(и) возвращать генератор в ответ на поисковый запрос. Не вдаваясь в подробности, это означает, что необходимо итерировать по self._items в поисках нужного элемента.
Я делаю это в методе get_stored_secret, который возвращает готовый секрет.
class Auth:
def get_stored_secret(self):
for item in self._items:
if item:
return json.loads(item.get_secret())
В цикле итератор становится экземпляром класса Item пакета secretstorage, поэтому на нём можно вызвать метод get_secret, который возвращает секрет.
В условии можно указать дополнительный критерий отбора элемента. Но мне достаточно факта самого существования такового.
Далее следует десериализация секрета и возвращение словаря.
True или False — вот, в чём вопрос
В отличие от источника, моя аллегория в заголовке носит менее риторический характер, и я смело могу ответить: «Для начала — False».
class Auth:
def __init__(self, email=None, password=None):
# все, что было написано до этого
self.prompt_desicion = False
Только постоянное изменяется; изменчивое подвергается не изменению, а только смене.
You must be registered for see links
И Буль своей логикой нисколько не противоречит Канту. Возможно, Гамлету стоило взять это на вооружение.
class Auth:
def __init__(self, email=None, password=None):
# все, что было написано до этого
# если секрет получен
if self._stored_secret:
# он будет доступен в атрибуте token
self.token = self._stored_secret['token']
# если пароль и почта запрошены у пользователя
elif email and password:
# получить токен у WEB API
self.token = self.get_token(email, password)
# сохранить токен как актуальный
self._valid_secret = {'token': self.token}
# сохранить токен в Secret Service
self.set_stored_secret()
else:
# если токена нет в Secret Storage, нужно запросить почту и пароль
# пользователя
self.prompt_desicion = True
Таким образом объекты моего класса инициализируются по-разному в зависимости от наличия параметров почты и пароля.
Инициализация без параметров
Запросить токен у Secret Storage API.
Если токен найден, не запрашивать почту и пароль у пользователя.
Использовать токен из Secret Storage.
Запросить токен у Secret Storage API.
Если токен найден, не запрашивать почту и пароль у пользователя и использовать токен из Secret Storage.
Если токен не найден, принять решение о запросе почты и пароля у пользователя.
Если пароль и почта предоставлены, запросить токен у WEB API.
Если токен получен, сохранить его в Secret Storage.
Осталось реализовать методы сохранения токена в Secret Storage и запроса к WEB API.
Получаем актуальный токен у WEB API
class Auth:
def get_token(self, email: str, password: str) -> str:
try:
response = requests.post(
API_URL,
data= {
'email': email,
'passwd': password
})
data = response.json()
except requests.exceptions.ConnectionError:
raise requests.exceptions.ConnectionError()
if response.status_code != 200:
raise requests.exceptions.HTTPError(data['msg'])
return data['data']['token']
В константе API_URL располагается адрес моего API. По понятным причинам, я не могу его опубликовать. Однако, он возвращает токен при POST запросе с параметрами «email» и «passwd».
Метод возвращает два исключения при ошибки подключения к API и во всех случаях, когда ответ API не содержит токена.
Во всех таких случаях API возвращает объект «msg» с соответствующим сообщением из тела ответа. После сериализации в блоке try я могу просто выводить это сообщение в консоль.
Сам токен хранится в объекте «data».
Сохраняем токен в Secret Storage
class Auth:
def set_stored_secret(self):
self._collection.create_item(
'MyApp',
self._attributes,
bytes((json.dumps(self._valid_secret)), 'utf-8')
)
Чтобы сохранить секрет методу create_item нужно название сохраняемого элемента, его атрибуты и сам секрет в байтовой репрезентации.
Используем модуль в Сlick Framework
Сперва импортируем модуль auth.
from auth import Auth
Затем инициализируем объект без параметров для проверки Secret Storage.
auth = Auth()
Передаем решение в декораторы опций.
@cli.command(help="Login into VPN Manager account.")
@click.option(
'--email',
prompt=auth.prompt_desicion,
help='Registered email address.')
@click.option(
'--password',
prompt=auth.prompt_desicion,
hide_input=True,
help='Password provided at registration.'
)
Дописываем команду login.
def login(email, password):
global auth
try:
# если было принято решение о запросе почты и пароля
if auth.prompt_desicion:
# получить новый токен и сохранить его в Secret Storage
auth = Auth(email, password)
except Exception:
return click.echo('No API connection')
# далее следует любая логика работы, связанная с токеном.
click.echo(auth.token)
Радуемся автоматически сгенерированной справке
Click Framework генерирует справку автоматически. Для этого ему нужны строки, которые я указывал в параметреhelpего декораторов.
$ python app.py
Usage: app.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
login Login into your account.
Справка команды login
$ python app.py login --help
Usage: app.py login [OPTIONS]
Login into your account
Options:
--email TEXT Registered email address
--password TEXT Password provided at registration
--help Show this message and exit.
Проверка
После запуска приложения командой приложения командой python app.py login оно запросит почту и пароль. Если это данные верны, то в Secrete Service появится соответствующий элемент.
В нём действительно хранится токен.
Ссылки
You must be registered for see links
You must be registered for see links
You must be registered for see links
You must be registered for see links