### Description
Allows LibreTime to support Trusted Header SSO Authentication.
**This is a new feature**:
Yes
**I have updated the documentation to reflect these changes**:
Yes
### Testing Notes
**What I did:**
I spun up an Authelia/Traefik pair and configured them to protect
LibreTime according to Authelia's documentation, I then tested that you
could log in via the trusted headers, and tested that old methods of
authentication were not affected.
**How you can replicate my testing:**
Using the following `docker-compose.yml` file
```yml
services:
postgres:
image: postgres:15
networks:
- internal
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${POSTGRES_USER:-libretime}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-libretime} # Change me !
healthcheck:
test: pg_isready -U libretime
rabbitmq:
image: rabbitmq:3.13-alpine
networks:
- internal
environment:
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_DEFAULT_VHOST:-/libretime}
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER:-libretime}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS:-libretime} # Change me !
healthcheck:
test: nc -z 127.0.0.1 5672
playout:
image: ghcr.io/libretime/libretime-playout:${LIBRETIME_VERSION:-latest}
networks:
- internal
init: true
ulimits:
nofile: 1024
depends_on:
- rabbitmq
volumes:
- ${LIBRETIME_CONFIG_FILEPATH:-./config.yml}:/etc/libretime/config.yml:ro
- libretime_playout:/app
environment:
LIBRETIME_GENERAL_PUBLIC_URL: http://nginx:8080
liquidsoap:
image: ghcr.io/libretime/libretime-playout:${LIBRETIME_VERSION:-latest}
networks:
- internal
command: /usr/local/bin/libretime-liquidsoap
init: true
ulimits:
nofile: 1024
ports:
- 8001:8001
- 8002:8002
depends_on:
- rabbitmq
volumes:
- ${LIBRETIME_CONFIG_FILEPATH:-./config.yml}:/etc/libretime/config.yml:ro
- libretime_playout:/app
environment:
LIBRETIME_GENERAL_PUBLIC_URL: http://nginx:8080
analyzer:
image: ghcr.io/libretime/libretime-analyzer:${LIBRETIME_VERSION:-latest}
networks:
- internal
init: true
ulimits:
nofile: 1024
depends_on:
- rabbitmq
volumes:
- ${LIBRETIME_CONFIG_FILEPATH:-./config.yml}:/etc/libretime/config.yml:ro
- libretime_storage:/srv/libretime
environment:
LIBRETIME_GENERAL_PUBLIC_URL: http://nginx:8080
worker:
image: ghcr.io/libretime/libretime-worker:${LIBRETIME_VERSION:-latest}
networks:
- internal
init: true
ulimits:
nofile: 1024
depends_on:
- rabbitmq
volumes:
- ${LIBRETIME_CONFIG_FILEPATH:-./config.yml}:/etc/libretime/config.yml:ro
environment:
LIBRETIME_GENERAL_PUBLIC_URL: http://nginx:8080
api:
image: ghcr.io/libretime/libretime-api:${LIBRETIME_VERSION:-latest}
networks:
- internal
init: true
ulimits:
nofile: 1024
depends_on:
- postgres
- rabbitmq
volumes:
- ${LIBRETIME_CONFIG_FILEPATH:-./config.yml}:/etc/libretime/config.yml:ro
- libretime_storage:/srv/libretime
legacy:
image: ghcr.io/libretime/libretime-legacy:${LIBRETIME_VERSION:-latest}
networks:
- internal
init: true
ulimits:
nofile: 1024
depends_on:
- postgres
- rabbitmq
volumes:
- ${LIBRETIME_CONFIG_FILEPATH:-./config.yml}:/etc/libretime/config.yml:ro
- libretime_assets:/var/www/html
- libretime_storage:/srv/libretime
nginx:
image: nginx
networks:
- internal
- net
ports:
- 8080:8080
depends_on:
- legacy
volumes:
- libretime_assets:/var/www/html:ro
- libretime_storage:/srv/libretime:ro
- ${NGINX_CONFIG_FILEPATH:-./nginx.conf}:/etc/nginx/conf.d/default.conf:ro
labels:
- 'traefik.enable=true'
- 'traefik.docker.network=libretime_net'
- 'traefik.http.routers.libretime.rule=Host(`libretime.example.com`)'
- 'traefik.http.routers.libretime.entrypoints=https'
- 'traefik.http.routers.libretime.tls=true'
- 'traefik.http.routers.libretime.tls.options=default'
- 'traefik.http.routers.libretime.middlewares=authelia@docker'
- 'traefik.http.services.libretime.loadbalancer.server.port=8080'
icecast:
image: ghcr.io/libretime/icecast:2.4.4
networks:
- internal
ports:
- 8000:8000
environment:
ICECAST_SOURCE_PASSWORD: ${ICECAST_SOURCE_PASSWORD:-hackme} # Change me !
ICECAST_ADMIN_PASSWORD: ${ICECAST_ADMIN_PASSWORD:-hackme} # Change me !
ICECAST_RELAY_PASSWORD: ${ICECAST_RELAY_PASSWORD:-hackme} # Change me !
traefik:
image: traefik:v2.11.12
container_name: traefik
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- net
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.api.rule=Host(`traefik.example.com`)'
- 'traefik.http.routers.api.entrypoints=https'
- 'traefik.http.routers.api.service=api@internal'
- 'traefik.http.routers.api.tls=true'
- 'traefik.http.routers.api.tls.options=default'
- 'traefik.http.routers.api.middlewares=authelia@docker'
ports:
- '80:80'
- '443:443'
command:
- '--api'
- '--providers.docker=true'
- '--providers.docker.exposedByDefault=false'
- '--entrypoints.http=true'
- '--entrypoints.http.address=:80'
- '--entrypoints.http.http.redirections.entrypoint.to=https'
- '--entrypoints.http.http.redirections.entrypoint.scheme=https'
- '--entrypoints.https=true'
- '--entrypoints.https.address=:443'
- '--log=true'
- '--log.level=DEBUG'
authelia:
image: authelia/authelia
container_name: authelia
networks:
- net
volumes:
- ./authelia:/config
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.authelia.rule=Host(`auth.example.com`)'
- 'traefik.http.routers.authelia.entrypoints=https'
- 'traefik.http.routers.authelia.tls=true'
- 'traefik.http.routers.authelia.tls.options=default'
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth' # yamllint disable-line rule:line-length
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email' # yamllint disable-line rule:line-length
- 'traefik.http.services.authelia.loadbalancer.server.port=9091'
restart: unless-stopped
environment:
- TZ=America/Los_Angeles
volumes:
postgres_data: {}
libretime_storage: {}
libretime_assets: {}
libretime_playout: {}
networks:
internal:
net:
```
The following libretime dev config modification:
```yml
general:
public_url: https://libretime.example.com
auth: LibreTime_Auth_Adaptor_Header
header_auth:
group_map:
host: lt-host
program_manager: lt-pm
admin: lt-admin
superadmin: lt-superadmin
```
And the following authelia config file:
```yml
---
###############################################################
# Authelia configuration #
###############################################################
server:
address: 'tcp://:9091'
buffers:
read: 16384
write: 16384
log:
level: 'debug'
totp:
issuer: 'authelia.com'
identity_validation:
reset_password:
jwt_secret: 'a_very_important_secret'
authentication_backend:
file:
path: '/config/users_database.yml'
access_control:
default_policy: 'deny'
rules:
- domain: 'traefik.example.com'
policy: 'one_factor'
- domain: 'libretime.example.com'
policy: 'one_factor'
session:
secret: 'insecure_session_secret'
cookies:
- name: 'authelia_session'
domain: 'example.com' # Should match whatever your root protected domain is
authelia_url: 'https://auth.example.com'
expiration: '1 hour' # 1 hour
inactivity: '5 minutes' # 5 minutes
regulation:
max_retries: 3
find_time: '2 minutes'
ban_time: '5 minutes'
storage:
encryption_key: 'you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this'
local:
path: '/config/db.sqlite3'
notifier:
filesystem:
filename: '/config/notification.txt'
...
```
And the following authelia users database:
```yml
---
###############################################################
# Users Database #
###############################################################
# This file can be used if you do not have an LDAP set up.
# List of users
users:
test:
disabled: false
displayname: "First Last"
password: "$argon2id$v=19$m=16,t=2,p=1$SWVVVzcySlRLUEFkWWh2eA$qPs1ZmzmDXR/9WckDzIN9Q"
email: test@example.com
groups:
- admins
- dev
- lt-admin
...
```
add the following entries to your `hosts` file:
```
127.0.0.1 traefik.example.com
127.0.0.1 auth.example.com
127.0.0.1 libretime.example.com
```
Then visit `libretime.example.com` in your browser, and login as the
user `test` with password of `password`. You should then be taken to the
LibreTime homepage, and when you click on login, you should be
automatically logged in.
### **Links**
https://www.authelia.com/integration/trusted-header-sso/introduction/
https://doc.traefik.io/traefik/middlewares/http/forwardauth/
---------
Co-authored-by: Kyle Robbertze <paddatrapper@users.noreply.github.com>
231 lines
8.7 KiB
PHP
231 lines
8.7 KiB
PHP
<?php
|
|
|
|
class LoginController extends Zend_Controller_Action
|
|
{
|
|
public function init()
|
|
{
|
|
$baseUrl = Config::getBasePath();
|
|
|
|
$this->view->headLink(['rel' => 'icon', 'href' => $baseUrl . 'favicon.ico', 'type' => 'image/x-icon'], 'PREPEND')
|
|
->appendStylesheet(Assets::url('css/bootstrap.css'))
|
|
->appendStylesheet(Assets::url('css/redmond/jquery-ui-1.8.8.custom.css'))
|
|
->appendStylesheet(Assets::url('css/styles.css'));
|
|
}
|
|
|
|
public function indexAction()
|
|
{
|
|
$CC_CONFIG = Config::getConfig();
|
|
|
|
$request = $this->getRequest();
|
|
$response = $this->getResponse();
|
|
$stationLocale = Application_Model_Preference::GetDefaultLocale();
|
|
|
|
// Enable AJAX requests from www.airtime.pro for the sign-in process.
|
|
CORSHelper::enableCrossOriginRequests($request, $response);
|
|
|
|
Application_Model_Locale::configureLocalization($request->getcookie('airtime_locale', $stationLocale));
|
|
|
|
if (Zend_Session::isStarted()) {
|
|
// Open the session for writing, because we close it for writing by default in Bootstrap.php as an optimization.
|
|
SessionHelper::reopenSessionForWriting();
|
|
|
|
$auth = Zend_Auth::getInstance();
|
|
$auth->getStorage();
|
|
|
|
if ($auth->hasIdentity()) {
|
|
$this->_redirect('showbuilder');
|
|
}
|
|
}
|
|
|
|
// uses separate layout without a navigation.
|
|
$this->_helper->layout->setLayout('login');
|
|
|
|
$this->view->error = false;
|
|
|
|
$form = new Application_Form_Login();
|
|
|
|
$authAdapter = Application_Model_Auth::getAuthAdapter();
|
|
|
|
if ($authAdapter instanceof LibreTime_Auth_Adaptor_Header || ($request->isPost() && $form->isValid($request->getPost()))) {
|
|
// get the username and password from the form
|
|
$username = $form->getValue('username');
|
|
$password = $form->getValue('password');
|
|
$locale = $form->getValue('locale');
|
|
|
|
// pass to the adapter the submitted username and password
|
|
$authAdapter->setIdentity($username)
|
|
->setCredential($password);
|
|
|
|
$result = $auth->authenticate($authAdapter);
|
|
if ($result->isValid()) {
|
|
Zend_Session::regenerateId();
|
|
// all info about this user from the login table omit only the password
|
|
$userInfo = $authAdapter->getResultRowObject(null, 'password');
|
|
|
|
// the default storage is a session with namespace Zend_Auth
|
|
$authStorage = $auth->getStorage();
|
|
$authStorage->write($userInfo);
|
|
|
|
Application_Model_LoginAttempts::resetAttempts($_SERVER['REMOTE_ADDR']);
|
|
Application_Model_Subjects::resetLoginAttempts($username);
|
|
|
|
// set the user locale in case user changed it in when logging in
|
|
Application_Model_Preference::SetUserLocale($locale);
|
|
|
|
$this->_redirect('showbuilder');
|
|
} else {
|
|
$form = $this->loginError($username);
|
|
}
|
|
}
|
|
|
|
$this->view->form = $form;
|
|
$this->view->airtimeVersion = $CC_CONFIG['airtime_version'];
|
|
$this->view->airtimeCopyright = AIRTIME_COPYRIGHT_DATE;
|
|
if (isset($CC_CONFIG['demo'])) {
|
|
$this->view->demo = $CC_CONFIG['demo'];
|
|
}
|
|
}
|
|
|
|
public function logoutAction()
|
|
{
|
|
// Open the session for writing, because we close it for writing by default in Bootstrap.php as an optimization.
|
|
SessionHelper::reopenSessionForWriting();
|
|
|
|
$auth = Zend_Auth::getInstance();
|
|
$auth->clearIdentity();
|
|
// Unset all session variables relating to CSRF prevention on logout
|
|
$csrf_namespace = new Zend_Session_Namespace('csrf_namespace');
|
|
$csrf_namespace->unsetAll();
|
|
$this->_redirect('showbuilder/index');
|
|
}
|
|
|
|
public function passwordRestoreAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$stationLocale = Application_Model_Preference::GetDefaultLocale();
|
|
|
|
Application_Model_Locale::configureLocalization($request->getcookie('airtime_locale', $stationLocale));
|
|
|
|
// uses separate layout without a navigation.
|
|
$this->_helper->layout->setLayout('login');
|
|
|
|
$form = new Application_Form_PasswordRestore();
|
|
|
|
$request = $this->getRequest();
|
|
if ($request->isPost()) {
|
|
if ($form->isValid($request->getPost())) {
|
|
$query = CcSubjsQuery::create();
|
|
$username = $form->username->getValue();
|
|
$email = $form->email->getValue();
|
|
|
|
if (empty($username)) {
|
|
$query->filterByDbEmail($email);
|
|
} elseif (empty($email)) {
|
|
$query->filterByDbLogin($username);
|
|
} else {
|
|
$query->filterByDbEmail($email)
|
|
->filterByDbLogin($username);
|
|
}
|
|
$user = $query->findOne();
|
|
|
|
if (!empty($user)) {
|
|
$auth = new Application_Model_Auth();
|
|
|
|
$success = $auth->sendPasswordRestoreLink($user, $this->view);
|
|
if ($success) {
|
|
$this->_helper->redirector('password-restore-after', 'login');
|
|
} else {
|
|
$form->email->addError($this->view->translate(_('Email could not be sent. Check your mail server settings and ensure it has been configured properly.')));
|
|
}
|
|
} else {
|
|
$form->email->addError($this->view->translate(_('That username or email address could not be found.')));
|
|
}
|
|
} else { // Form is not valid
|
|
$form->email->addError($this->view->translate(_('There was a problem with the username or email address you entered.')));
|
|
}
|
|
}
|
|
|
|
$this->view->form = $form;
|
|
}
|
|
|
|
public function passwordRestoreAfterAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$stationLocale = Application_Model_Preference::GetDefaultLocale();
|
|
|
|
Application_Model_Locale::configureLocalization($request->getcookie('airtime_locale', $stationLocale));
|
|
|
|
// uses separate layout without a navigation.
|
|
$this->_helper->layout->setLayout('login');
|
|
}
|
|
|
|
public function passwordChangeAction()
|
|
{
|
|
// uses separate layout without a navigation.
|
|
$this->_helper->layout->setLayout('login');
|
|
|
|
$request = $this->getRequest();
|
|
$token = $request->getParam('token', false);
|
|
$user_id = $request->getParam('user_id', 0);
|
|
|
|
$form = new Application_Form_PasswordChange();
|
|
$auth = new Application_Model_Auth();
|
|
$user = CcSubjsQuery::create()->findPK($user_id);
|
|
|
|
$stationLocale = Application_Model_Preference::GetDefaultLocale();
|
|
|
|
Application_Model_Locale::configureLocalization($request->getcookie('airtime_locale', $stationLocale));
|
|
|
|
// check validity of token
|
|
if (!$auth->checkToken($user_id, $token, 'password.restore')) {
|
|
Logging::debug('token not valid');
|
|
$this->_helper->redirector('index', 'login');
|
|
}
|
|
|
|
if ($request->isPost() && $form->isValid($request->getPost())) {
|
|
$user->setDbPass(md5($form->password->getValue()));
|
|
$user->save();
|
|
|
|
$auth->invalidateTokens($user, 'password.restore');
|
|
|
|
$zend_auth = Zend_Auth::getInstance();
|
|
$zend_auth->clearIdentity();
|
|
|
|
$authAdapter = Application_Model_Auth::getAuthAdapter();
|
|
$authAdapter->setIdentity($user->getDbLogin())
|
|
->setCredential($form->password->getValue());
|
|
|
|
$zend_auth->authenticate($authAdapter);
|
|
|
|
// all info about this user from the login table omit only the password
|
|
$userInfo = $authAdapter->getResultRowObject(null, 'password');
|
|
|
|
// the default storage is a session with namespace Zend_Auth
|
|
$authStorage = $zend_auth->getStorage();
|
|
$authStorage->write($userInfo);
|
|
|
|
$this->_helper->redirector('index', 'showbuilder');
|
|
}
|
|
|
|
$this->view->form = $form;
|
|
}
|
|
|
|
/**
|
|
* populates view with results from a login error and adds a new form.
|
|
*
|
|
* @param string $username user that failed to login
|
|
*
|
|
* @return new form
|
|
*/
|
|
private function loginError($username)
|
|
{
|
|
$this->view->message = _('Wrong username or password provided. Please try again.');
|
|
Application_Model_Subjects::increaseLoginAttempts($username);
|
|
Application_Model_LoginAttempts::increaseAttempts($_SERVER['REMOTE_ADDR']);
|
|
$form = new Application_Form_Login();
|
|
$this->view->error = true;
|
|
|
|
return $form;
|
|
}
|
|
}
|