Аутентификация
Laminara не хранит аккаунты. Логины и пароли остаются там, где они у вас уже есть — в базе сайта, в отдельном файле или за вашим API. Laminara обращается к этому источнику через адаптер, проверяет пару логин/пароль и владеет только сессиями.
Раздел auth состоит из трёх частей: провайдер (откуда аккаунты), хеш пароля и хранилище сессий.
Все куски ниже — части этого раздела в конфиге сервера (/etc/laminara/config.json),
а не в настройках сборки.
Провайдеры
Аккаунты в JSON-файле. Годится для небольшого списка или на старте.
{ "provider": "jsonfile", "config": { "path": "/var/lib/laminara/users.json", "hash": "argon2id", "fields": { "username": "username", "password": "password", "uuid": "uuid" } }, "sessions": { "backend": "memory" }}Заводить игроков руками не нужно — это делает команда:
laminara-server exec "auth add Steve" # пароль придумается сам и покажется один разlaminara-server exec "auth add Steve пароль" # или задайте свойlaminara-server exec "auth passwd Steve" # сменить парольlaminara-server exec "auth remove Steve" # убрать игрокаlaminara-server exec "auth list" # кто заведёнКоманда сама считает хеш той схемой, что стоит в hash, кладёт постоянный uuid и пишет файл
целиком — испортить его опечаткой нельзя. Новый игрок может входить сразу, перезапускать проект
не нужно.
Править файл руками тоже можно: он перечитывается, как только меняется. Формат — массив
объектов, минимально нужны ник и хеш пароля (посчитать: laminara-server hash <пароль>):
[ { "username": "Steve", "password": "$argon2id$v=19$m=65536,t=1,p=2$c29tZXNhbHQ$…" }, { "username": "Alex", "password": "$argon2id$v=19$m=65536,t=1,p=2$b3RoZXJzYWx0$…" }]С постоянным uuid — тогда смена ника не обнулит прогресс игрока:
[ { "username": "Steve", "password": "$2a$12$…", "uuid": "8667ba71-b85a-4004-af54-457a9734eed7" }]С двухфакторной аутентификацией у отдельного игрока:
[ { "username": "Steve", "password": "$argon2id$…", "totp": "JBSWY3DPEHPK3PXP" }, { "username": "Alex", "password": "$argon2id$…" }]Если файл уже существует и ключи в нём называются иначе, подстройте fields под него, а не
наоборот:
"config": { "path": "/var/lib/laminara/players.json", "hash": "bcrypt", "fields": { "username": "nick", "password": "pass_hash", "uuid": "player_uuid", "twoFactorSecret": "totp_secret" }}[ { "nick": "Steve", "pass_hash": "$2a$12$…", "player_uuid": "8667ba71-…" }]Хеш для файла считает команда laminara-server hash (--algo, а для bcrypt ещё и --cost).
Файл читается один раз при старте, поэтому после правок проект нужно перезапустить.
Аккаунты в вашей базе — Postgres или MySQL. Laminara только читает.
{ "provider": "sql", "config": { "driver": "postgres", "dsn": "postgres://ro:pass@db:5432/site?sslmode=require", "table": "users", "hash": "bcrypt", "fields": { "username": "nickname", "password": "password_hash", "uuid": "uuid" } }, "sessions": { "backend": "redis", "redis": { "addr": "redis:6379" } }}По этому соединению ходят хеши паролей, поэтому база за пределами машины требует TLS: sslmode=require у Postgres, tls=true у MySQL и MariaDB. Отключать шифрование можно, только когда база на том же хосте.
fields сопоставляет логические поля с колонками вашей таблицы — подстраивать чужую схему под Laminara не нужно. Имена таблицы и колонок проверяются, произвольный SQL в них не подставить.
Если одной таблицей не обойтись — пароли лежат отдельно, есть флаг бана, нужен вид — задайте запрос сами вместо table и fields:
{ "provider": "sql", "config": { "driver": "mariadb", "dsn": "laminara:pass@tcp(127.0.0.1:3306)/site?charset=utf8mb4&tls=true", "hash": "argon2id", "query": "SELECT p.hash, u.uuid FROM users u JOIN user_passwords p ON p.user_id = u.id WHERE u.username = ? AND u.is_banned = 0" }}Запрос принимает логин единственным подстановочным знаком (? для MySQL и MariaDB, $1 для Postgres) и выбирает сначала пароль, затем при желании UUID. Ничего кроме этих двух колонок возвращать не нужно.
Так же решается и бан: условие живёт в вашей схеме, и только ваш запрос может его учесть. Игрок, помеченный забаненным на сайте, просто не найдётся — и не войдёт.
Строка подключения зависит от драйвера:
driver | dsn |
|---|---|
mysql, mariadb | логин:пароль@tcp(хост:3306)/база?charset=utf8mb4&parseTime=true&tls=true |
postgres | postgres://логин:пароль@хост:5432/база?sslmode=require |
sqlite | /var/lib/laminara/accounts.db |
Подстановочный знак в query тоже свой: ? у MySQL, MariaDB и SQLite, $1 у Postgres.
Готовая связка с базой сайта, где пароли лежат в bcrypt, а колонка uuid отсутствует:
{ "provider": "sql", "config": { "driver": "mariadb", "dsn": "laminara:пароль@tcp(127.0.0.1:3306)/site?charset=utf8mb4", "table": "users", "hash": "bcrypt", "fields": { "username": "username", "password": "password" } }}Без колонки uuid он выводится из ника — это рабочий вариант, пока ники не меняются.
Отдельный файл SQLite, если сайта нет и аккаунты ведёт только Laminara:
"config": { "driver": "sqlite", "dsn": "/var/lib/laminara/accounts.db", "table": "players", "hash": "argon2id", "fields": { "username": "name", "password": "hash", "uuid": "uuid" }}С двухфакторной аутентификацией — добавьте колонку с секретом:
"fields": { "username": "username", "password": "password", "uuid": "uuid", "twoFactorSecret": "totp_secret"}У кого колонка пуста, тот входит по одному паролю. С query двухфакторка не работает —
там выбираются только пароль и uuid.
Проверку логина делает ваш сервис — Laminara шлёт ему запрос.
{ "provider": "http", "config": { "url": "https://site.example/api/auth", "usernameField": "login", "passwordField": "password", "uuidField": "uuid", "successField": "ok" }, "sessions": { "backend": "redis", "redis": { "addr": "redis:6379" } }}Laminara кладёт логин и пароль в указанные поля запроса, считает вход успешным по полю successField в ответе и берёт оттуда же UUID.
На ваш адрес уходит POST с телом application/json:
{ "login": "Steve", "password": "введённый пароль" }Имена полей — это usernameField и passwordField; при двухфакторной аутентификации
добавляется codeField (по умолчанию totp).
Ответ читается так: вход засчитывается при коде 2xx, а если задан successField — ещё и по
его значению. Истинным считается true, ненулевое число и непустая строка, кроме "false"
и "0".
Простейший ответ:
{ "ok": true, "uuid": "8667ba71-b85a-4004-af54-457a9734eed7" }"config": { "url": "https://site.example/api/auth", "successField": "ok", "uuidField": "uuid"}Если ответ вложенный, путь пишется через точку:
{ "status": "ok", "data": { "success": true, "player": { "uuid": "8667ba71-…" } } }"config": { "url": "https://site.example/api/auth", "successField": "data.success", "uuidField": "data.player.uuid"}Когда сервис отвечает одним лишь кодом — 200 на успех, 401 на отказ — successField
не нужен вовсе:
"config": { "url": "https://site.example/api/auth" }Тогда любой другой код (500, таймаут) считается не отказом, а поломкой: игроку скажут, что
сервер недоступен, а не что пароль неверный.
Двухфакторная аутентификация — ваш сервис сообщает, что нужен код:
{ "ok": false, "need_2fa": true }"config": { "url": "https://site.example/api/auth", "successField": "ok", "secondFactorField": "need_2fa", "codeField": "totp"}Лаунчер попросит код и повторит запрос, добавив в тело поле totp.
Без uuidField uuid выводится из ника. Поле в ответе может отсутствовать у части игроков —
тогда для них тоже берётся выведенный.
Если ваш API закрыт ключом, добавьте заголовки — они уйдут с каждым запросом:
"config": { "url": "https://site.example/api/auth", "headers": { "Authorization": "Bearer ваш-секрет" }, "timeout": "10s", "successField": "ok"}В выводе settings значение заголовка с секретным именем скрывается, как и пароли.
По этому запросу уходит пароль игрока, поэтому url должен быть https://. Обычный http:// на внешний адрес открывает пароль любому между сервером и вашим API — при таком url сервер пишет предупреждение в журнал при каждом старте. Без TLS оставляйте только сервис на localhost за общим прокси.
Хеш пароля
Поле hash (у jsonfile и sql) говорит, как пароль захеширован в вашем источнике. Laminara хеширует введённый пароль тем же способом и сравнивает. Доступны:
argon2id · bcrypt · sha256 · sha512 · md5 · plain
Без этого поля берётся argon2id. Схему указывают под свою базу: если она не совпадёт с настоящей, вход не пройдёт ни у кого и сервер скажет, что хранимый пароль не читается.
Двухфакторная аутентификация
Второй фактор — одноразовый код из приложения-аутентификатора (TOTP): подойдёт Google Authenticator, Яндекс.Ключ и любое совместимое приложение. Включается на игрока, а не на весь проект: у кого секрет есть, тот входит по паролю и коду, у кого нет — как раньше.
Секрет выпускает консоль:
auth totp <игрок>Команда печатает base32-секрет и строку otpauth:// — приложение-аутентификатор подхватит из неё коды. Секрет впишите игроку в источник аккаунтов (как именно — зависит от провайдера, см. ниже), и вход без кода закроется; sql подхватывает секрет сразу, jsonfile — после перезапуска проекта. Отключить двухфакторку так же просто: уберите секрет. Игрок потерял телефон — выпустите новый секрет и попросите добавить его заново.
Ключ записи, по умолчанию totp (меняется полем fields.twoFactorSecret):
{ "username": "neo", "password": "…", "totp": "JBSWY3DPEHPK3PXP" }Колонка таблицы, задайте её полем fields.twoFactorSecret:
"fields": { "username": "login", "password": "password_hash", "twoFactorSecret": "totp_secret" }Пустая колонка — у игрока двухфакторки нет. Со своим запросом (query) второй фактор не работает: колонку в ответе неоткуда взять.
Код уходит вашему сайту вместе с логином и паролем — проверяете его вы. Поле кода в запросе задаёт codeField (по умолчанию totp); сам код подставляется, только когда игрок его ввёл.
Когда ваш сайт отвечает отказом и поднимает поле secondFactorField, лаунчер показывает поле для кода, а не «неверный пароль»:
"secondFactorField": "needTotp"Код меняется каждые 30 секунд; принимается код текущего и соседних интервалов, чтобы часы не обязаны совпадать до секунды. Один и тот же код принимается дважды — лаунчер проверяет его в двух шагах входа; третий раз не пройдёт.
Сессии
Пароль проверяется один раз; дальше клиент живёт по токенам, которые выдаёт и хранит Laminara. Токен доступа короткий, токен обновления — длинный. При обновлении старый токен отзывается, а попытка воспользоваться отозванным распознаётся.
backend: "memory"— сессии в памяти процесса. Просто, но не переживают перезапуск и не годятся для нескольких экземпляров.backend: "redis"с блокомredis(addr, при необходимостиpassword,db,tls) — сессии в Redis: переживают перезапуск и общие для нескольких экземпляров сервера. Старое полеredisAddrрядом сbackendтоже понимается, чтобы конфиги прошлых версий продолжали работать.
Эта настройка распоряжается обеими сессиями игрока: и входом в лаунчер, и входом в игре
через yggdrasil. На memory перезапуск сервера выкидывает игроков из лаунчера,
и им приходится вводить пароль заново — даже если Redis у вас стоит и работает для чего-то другого.
Что именно выбрано, показывает doctor двумя строками: «сессии» и «сессии в игре».
Сроки жизни токенов задаются полями accessTTL и refreshTTL рядом с provider (например, "15m" и "720h").