Аутентификация
An authentication gate plugin makes viewers sign in before reaching the resources selected by the server operator. The plugin supplies the login method, such as OAuth, a magic link, SAML, or a shared password. Owncast enforces the selected access mode.
- Ваш плагин является поставщиком идентификации. Он отображает экран входа, связывается с внешним поставщиком и решает, кто может войти.
- The Owncast host is the gatekeeper and session authority. It owns the session cookie, enforces the selected access mode, and never puts your plugin in the per-request hot path.
Всё на этой странице требует разрешения auth.gate, плюс users.register для создания аутентифицированного пользователя и http.serve для отображения процесса входа.
Что ограничивается
When an auth.gate plugin is enabled, the viewer page, chat, embeds,
/api/config, and the rest of the public web surface require login. A short
list of routes stays public in every mode, including Owncast's admin pages
(they keep their own admin authentication, so an operator can always disable a
broken gate), the instance logo, and the ActivityPub federation endpoints. See
what bypasses the gate for the full list.
The operator selects one cumulative access mode on the plugin's Authentication tab:
| Access mode | Effect |
|---|---|
| Website only (default) | The web interface requires sign-in. /hls/*, /api/status, and Owncast Directory listing stay public. |
| Website, video players, and other resources | Also gates Owncast-hosted /hls/*. Players such as VLC cannot complete the browser login. /api/status and directory listing stay public. |
| Website, video players, and server status requests | Gates the web interface, Owncast-hosted /hls/*, and /api/status. Owncast Directory listing is disabled. |
The modes are cumulative. There is no status-only mode that hides
/api/status while leaving HLS public. The default protects the website
without breaking existing players or uptime monitors.
Selecting either stream-protection mode blocks native players. VLC, QuickTime,
mobile apps, and restreamers cannot complete a browser login or carry the
session cookie. An Authorization header or query token does not bypass the
gate.
A viewer with a valid session is always let through, regardless of the selected mode.
When distributing your video stream directly from your server, stream protection is airtight: every byte flows through Owncast. С объектным хранилищем или CDN, списки воспроизведения переписываются на абсолютные удаленные URL, и сегменты извлекаются непосредственно из хранилища, так что брама никогда не видит эти запросы. Ограничение тем не менее останавливает анонимного посетителя от обнаружения списка сегментов, но утечка или общий URL сегмента остается доступным. Stream protection + local distribution is airtight. Stream protection + Object Storage is good friction, not airtight.
Как это работает
Once the gate is armed, every non-exempt request is checked. Under stream protection that includes each HLS segment, which a live viewer pulls every few seconds. Вызов в встроенный движок вашего плагина на каждую из этих операций может перегреть сервер, поэтому плагин остается вне горячего пути:
| Когда | Цена | Что происходит |
|---|---|---|
| Every non-exempt request | Проверьте подпись cookie + срок действия | valid passes. Missing or invalid gets a redirect to login, or a 401 for anything that is not a GET or HEAD |
/ страница загружается только | Необязательный вызов движка: onAuthCheck | пере-проверить у вашего поставщика, возвращает ok / refresh / deny |
Ваш плагин запускает только процесс входа (редко, примерно один раз за сессию зрителя) и необязательный onAuthCheck на каждой загрузке страницы. Хост Owncast создает и проверяет подписанный файловый cookie сессии, поэтому проверка на каждый запрос только с подписью и сроком действия: без поиска в базе данных, без вызова плагина.
The cookie is a signed envelope carrying an Owncast access token plus a session expiry. The host mints a fresh access token for the user each time it grants a session. The Owncast host owns the cookie end to end: it reserves the cookie name (owncast_session), signs it with a host-held secret, and attaches it to the response. Ваш плагин никогда не видит и не устанавливает токен, поэтому он не может подделать или скомпрометировать его. (Это также то, как чат автоматически получает идентификацию зрителя. Смотрите Идентификация чата ниже.)
Создание плагина брамы
Плагин брамы — это HTTP-сервисный плагин с процессом входа. Цикл управления, по конвенции, находится в пространстве имен вашего плагина /plugins/\<ваш-слуг>/:
Три элемента делают работу:
- Зарегистрируйте пользователя. Превратите внешнюю идентичность в реального пользователя Owncast с помощью
owncast.users.register. Передайте стабильный, привязанный к провайдеруauthId(например,"github:583231"). Хост называет его по вашему слугу, чтобы плагины не могли конфликтовать или подделывать друг друга. - Предоставьте сессию. Вызовите
owncast.auth.grantSessionс темuserId. Хост Owncast создает подписанный cookie и прикрепляет его к ответу. Это работает только внутри обработчикаonHttpRequest. - Перенаправить домой. Хост Owncast добавляет параметр запроса
return_to, когда он перенаправляет аутентифицированного посетителя на ваш экран входа, и санирует его до путеводителя одного источника (чтобы он не мог быть превращен в открытое перенаправление). Отправьте зрителя туда после успешного входа.
Чтобы выйти из системы, вызовите owncast.auth.endSession() и перенаправьте. Ваш плагин всё равно управляет тем, куда (он может перенаправить на выход из системы провайдера).
Отмена с onAuthCheck
Сессии без состояния, поэтому нет списка на запрос "разрешено ли этому пользователю еще". Это могло бы вернуть плагин обратно в горячий путь. Вместо этого определите необязательный обработчик onAuthCheck. Он срабатывает на каждой загрузке страницы / с разрешенной идентификацией зрителя и возвращает ok, refresh (переиздать cookie, опционально с новым TTL для скользящего истечения) или deny (закончить сессию и вернуться к входу). Плагин, поддерживаемый провайдером, повторно проверяет членство здесь (организация всё еще действительна? учетная запись не удалена?).
Поскольку проверка выполняется только по /, зритель, которому вы отменяете, сохраняет любую открытую вкладку работающей, пока они не обновят или пока cookie не истечет. Сессия TTL является жестким контролем для отзыва, поэтому держите её короткой, если важно быстро отозвать.
Рабочий пример: брама с общим паролем
Пример плагина basic-auth является самым простым возможным вариантом: один общий пароль, одна общая идентичность "Гость", без внешнего поставщика. Он доступен как в examples/js/basic-auth, так и в examples/python/basic-auth.
Его манифест объявляет разрешения и единственное поле конфигурации для пароля:
{
"name": "Basic Auth",
"slug": "basic-auth",
"version": "0.1.0",
"permissions": ["auth.gate", "users.register", "http.serve", "storage.kv"],
"config": {
"password": {
"type": "string",
"default": "letmein",
"description": "Shared password viewers must enter to watch"
}
}
}
Обработчик отображает форму пароля на /, проверяет поданный пароль на совпадение с заданным значением, и при успехе регистрирует общую личность, предоставляет сессию и перенаправляет обратно. onAuthCheck считывает переключаемый администратором флаг revoked, чтобы выгнать всех при их следующей загрузке страницы. (Помощник page(), который строит HTML-форму, пропускается ниже для краткости. Смотрите исходный код примера.)
- JavaScript
- Python
const { definePlugin, owncast, authCheck } = require('@owncast/plugin-sdk');
module.exports = definePlugin({
onHttpRequest(req) {
const query = req.query || {};
const returnTo = query.return_to || '/';
if (req.method === 'GET' && req.path === '/') {
return {
status: 200,
headers: { 'content-type': 'text/html' },
body: page(returnTo),
};
}
if (req.path === '/login') {
const expected = owncast.config.get('password', 'letmein');
if ((query.password || '') !== expected) {
return {
status: 200,
headers: { 'content-type': 'text/html' },
body: page(returnTo, 'Incorrect password.'),
};
}
// Everyone who knows the password shares one authenticated identity.
const { userId } = owncast.users.register({
authId: 'shared',
displayName: 'Guest',
});
owncast.auth.grantSession({ userId });
return { status: 302, headers: { Location: returnTo } };
}
if (req.path === '/logout') {
owncast.auth.endSession();
return { status: 302, headers: { Location: '/' } };
}
// Admin-only revocation toggle. req.authenticated is true for admins only.
if (req.path === '/revoke' || req.path === '/unrevoke') {
if (!req.authenticated) return { status: 403, body: 'admin only' };
owncast.kv.set('revoked', req.path === '/revoke' ? '1' : '');
return {
status: 200,
body: req.path === '/revoke' ? 'revoked' : 'unrevoked',
};
}
return { status: 404, body: 'not found' };
},
// Re-validate on each page load. While revoked, end every session.
onAuthCheck() {
if (owncast.kv.get('revoked') === '1') return authCheck.deny('access has been revoked');
return authCheck.ok();
},
});
from owncast_plugin import plugin, owncast, auth_check
@plugin.get("/")
def login_form(req):
return_to = (req.raw.get("query") or {}).get("return_to") or "/"
return {"status": 200, "headers": {"content-type": "text/html"}, "body": page(return_to)}
@plugin.get("/login")
def login(req):
query = req.raw.get("query") or {}
return_to = query.get("return_to") or "/"
expected = owncast.config.get("password", "letmein")
if (query.get("password") or "") != expected:
return {"status": 200, "headers": {"content-type": "text/html"},
"body": page(return_to, "Incorrect password.")}
# Everyone who knows the password shares one authenticated identity.
result = owncast.users.register("shared", display_name="Guest")
owncast.auth.grant_session(result.user_id)
return {"status": 302, "headers": {"Location": return_to}}
@plugin.get("/logout")
def logout(req):
owncast.auth.end_session()
return {"status": 302, "headers": {"Location": "/"}}
@plugin.get("/revoke")
def revoke(req):
if not req.authenticated: # true for admin requests only
return {"status": 403, "body": "admin only"}
owncast.kv.set("revoked", "1")
return {"status": 200, "body": "revoked"}
@plugin.on_auth_check
def check(_req):
# Re-validate on each page load. While revoked, end every session.
if owncast.kv.get("revoked") == "1":
return auth_check.deny("access has been revoked")
return auth_check.ok()
Для реального потока OAuth (CSRF state в storage.kv, обмен кода через network.fetch, соблюдение членства в организации и URL обратного вызова, построенный из owncast.server.info()), смотрите пример github-auth в SDK.
Включение брамы
Объявление auth.gate само по себе ничего не делает. Брама активирована включением плагина через нормальную жизнедеятельность включения/выключения в администраторе. Отключите её, и брама сразу же упадет.
- Можно включить только один плагин
auth.gateодновременно. Owncast отказывается включить второй, пока один уже активен ("сначала отключите другой"). - Настройте перед включением. Плагин можно установить и настроить, пока он отключен, а затем включить, чтобы он заработал. Используйте автоматически сгенерированную форму конфигурации для учетных данных, таких как идентификатор клиента OAuth и секрет.
Закрытие при сбое
Положение брамы не связано со здоровьем вашего плагина. Если брама активирована, но плагин недоступен (сбой, ошибка загрузки или автоматически отключен после повторных сбоев), Owncast отказывает всем зрителям и обслуживает статическую страницу "аутентификация временно недоступна". Она никогда не открывается. Администратор всегда доступен (администраторские маршруты используют существующую базовую аутентификацию Owncast и обходят браму), так что вы можете исправить конфигурацию или отключить плагин. Уже действительные сессии переживут сбой, потому что проверка файла cookie не требует вызова плагина.
A gate that is enabled but not running is still a gate. No access-policy setting can turn a failing-closed gate into an open one.
Что обходит браму
The gate covers the otherwise-public surface. Routes that enforce their own credentials bypass it. The selected access mode also leaves some resources public.
Always exempt:
- The active gate plugin's own namespace
/plugins/\<your-slug>/*and its static assets, so the login screen remains reachable. /admin/*and/api/admin/*, which use admin authentication.- External API routes under
/api/integrations/, which validate their own Bearer tokens. - Static viewer assets needed to render the page. HTML entry points are still gated.
/api/yp, which the Owncast Directory fetches anonymously. The most restrictive mode disables directory listing and makes this endpoint return404./logoand/logo/external, the instance logo, which the viewer shell and federation metadata both reference./federation/*, the ActivityPub protocol surface. Those handlers enforce Owncast's own federation and privacy settings.
Mode-dependent:
/hls/*stays public only in Website only mode./api/statusstays public in Website only and Website, video players, and other resources modes.
Everything else is gated, including embeds and /api/config.
Детали сессии
- Stateless signed cookie named
owncast_session,HttpOnly,Secure(on HTTPS requests),SameSite=Lax,Path=/. Lax, а не Strict, потому что обратный вызов поставщика является кросс-сайтовым перенаправлением на верхнем уровне. The host owns the name: a plugin that tries to set it in its own response has that header stripped. - TTL is set by your plugin when it calls
grantSession({ ttl }), defaulting to 24 hours and capped at 30 days. A sliding refresh is available throughonAuthCheck'srefreshverdict. Поскольку TTL служит контролем отзыва, это реальная настройка безопасности. - Секрет для подписи является ответственностью хоста Owncast. Он автоматически генерируется при первом использовании и сохраняется в конфигурации. Повторная активация недействительствует каждую сессию (паническая кнопка). Авторы плагина никогда с ним не взаимодействуют, и он отделён от любого секретного клиента OAuth, что является заботой вашей конфигурации плагина.
Идентификация чата
Вход через браму автоматически создает аутентифицированную идентификацию чата. Because users.register creates or links a real Owncast user (marked authenticated, with the display name you passed, or a generated one if you passed none) and the session cookie carries an access token for that user, chat reads the identity straight from the cookie: when /ws (or a chat REST call) arrives with no ?accessToken= query parameter, it falls back to the access token in the gate cookie. Никакой токен никогда не передаётся в localStorage браузера. The viewer signs in once and shows up in chat under that name.
Связано
- Разрешения:
auth.gate,users.register - API Owncast:
users.register,auth.grantSession,auth.endSession - События: обработчик
onAuthCheck - HTTP-услуги: модель запроса, на которой построен процесс входа
Improve this page
See something missing or incorrect? Edit this page and improve the documentation for everyone.
