Commit bd2bf6fd authored by ale's avatar ale

Add a 'sso' plugin for mod_sso authentication

parent 97e9a562
<?php
/**
* Support the A/I sso module, and interactions with the A/I user panel
*
* @version 0.0.1
* @author joe
* @website http://www.autistici.org
* @licence Do what the fuck you want with this crap
*
**/
// a/i crappy sso module
require_once 'auth-token.php';
class ai_auth extends rcube_plugin
{
public $task = 'login|logout';
public function init()
{
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('authenticate', array($this, 'authenticate'));
$this->add_hook('login_failed', array($this, 'redirect_to_login'));
$this->add_hook('logout_after', array($this, 'redirect_to_logout'));
}
public function startup()
{
// Do not even render the page if you don't have a valid token.
if (!auth_token_get($sso_user, $sso_pass, $sso_error)) {
$this->redirect_to_login(array());
}
}
public function authenticate($args)
{
// This first check is probably not necessary now that
// we have blocked rendering of the form page, but let's
// be careful anyway.
if ($args['user'] && $args['pass']) { //user is coming from the webmail login screen
error_log('ai_auth: authenticate() called from login page');
auth_token_clear(); //if we had a token, it's most likely bogus.
$args['abort'] = true;
$args['error'] = 'AUTHFAIL';
return $args;
}
// If the token is invalid or absent, abort the login
// process (this will result in a redirect to the pannello
// login later, it's just clearer this way).
if (!auth_token_get($sso_user, $sso_pass, $sso_error)) {
auth_token_clear(); // clear the cookie anyway if it doesn't validate.
$args['valid'] = false; // TODO: 'valid' seems only related to CSRF.
$args['abort'] = true;
$args['error'] = 'AUTHFAIL';
return $args;
}
// Successful login, IMAP login data retrieved.
$args['user'] = $sso_user;
$args['pass'] = $sso_pass;
$args['valid'] = true;
return $args;
}
public function redirect_to_logout($userdata) {
$redir_url = 'https://' . $_SERVER['HTTP_HOST'] . '/pannello/logout';
auth_token_clear();
$rcmail = rcmail::get_instance();
$rcmail->output->add_script('location.href="' . $redir_url . '";');
$rcmail->output->send('plugin');
exit;
}
public function redirect_to_login($args) {
$redir_url = 'https://' . $_SERVER['HTTP_HOST'] . '/pannello/';
auth_token_clear();
$rcmail = rcmail::get_instance();
$rcmail->output->redirect($redir_url);
exit;
}
}
<?php
/*
* pezzetto di php che implementa i metodi necessari ad autenticare
* un utente su un server remoto passando poi un token per il canale insicuro.
*/
/*
struttura sql:
create database ai_pannello;
create table tokens (
token char(8) not null primary key,
user varchar(64) not null,
pass varchar(64) not null,
stamp timestamp default 0 not null
);
grant all on ai_pannello.* to tokens@'172.16.1.%' identified by 'ntogulo';
e periodicamente:
delete from ai_pannello.tokens where (now() - stamp) > 1800;
*/
// Called 'unsafe' because it's not authenticated.
class UnsafeOpensslAES
{
const METHOD = 'aes-256-cbc';
public static function decrypt($message, $key) {
if (mb_strlen($key, '8bit') !== 32) {
throw new Exception("Needs a 256-bit key!");
}
$ivsize = openssl_cipher_iv_length(self::METHOD);
$iv = mb_substr($message, 0, $ivsize, '8bit');
$ciphertext = mb_substr($message, $ivsize, null, '8bit');
return openssl_decrypt(
$ciphertext,
self::METHOD,
$key,
OPENSSL_RAW_DATA,
$iv
);
}
}
define( "AI_TOKEN_NAME", 'AITOKEN' );
define( "AI_MYSQL_USER", 'tokens' );
define( "AI_MYSQL_DBNAME", "ai_pannello" );
define( "AI_MYSQL_PASS_FILE", "/etc/ai/ai_pannello.pw" );
define( "AI_AUTH_TOKEN_ENCRYPTION_KEY_FILE", "/etc/ai/ai_pannello.key" );
$ai_host_map = array();
$ai_vpn_ip_map = array();
function __init_ai_hosts() {
global $ai_host_map, $ai_vpn_ip_map;
$fp = @fopen("/etc/ai/hosts", "r");
if (!$fp) {
die("Can't open /etc/ai/hosts");
}
while (($line = fgets($fp, 128)) !== false) {
if ($line == '' || $line[0] == '#') {
continue;
}
$parts = explode(" ", rtrim($line));
$hostname = $parts[0];
$hostid = $parts[1];
$ai_vpn_ip_map[$hostname] = "172.16.1." . $hostid;
$ai_host_map[$hostid] = $hostname;
}
fclose($fp);
}
__init_ai_hosts();
function __server_to_mysql($host) {
global $ai_vpn_ip_map;
return $ai_vpn_ip_map[$host];
}
function __parse_raw_token ($raw_token, &$host, &$token) {
global $ai_host_map;
if (preg_match( '/^([0-9]+)\/([A-Za-z0-9]{8})$/', $raw_token, $matches )) {
$host = $ai_host_map[$matches[1]];
$token = $matches[2];
return TRUE;
}
return FALSE;
}
$auth_token_mysql_pw = '';
function get_auth_token_mysql_password() {
global $auth_token_mysql_pw;
if (!$auth_token_mysql_pw) {
$auth_token_mysql_pw = rtrim(file_get_contents(AI_MYSQL_PASS_FILE));
}
return $auth_token_mysql_pw;
}
function get_auth_token_encryption_key() {
return file_get_contents(AI_AUTH_TOKEN_ENCRYPTION_KEY_FILE);
}
function auth_token_present() {
return (!empty($_COOKIE[AI_TOKEN_NAME]));
}
function auth_token_clear() {
$host = false;
$token = false;
$rawtoken = $_COOIKE[AI_TOKEN_NAME];
if ($rawtoken && __parse_raw_token( $rawtoken, $host, $token )) {
$my = mysqli_connect( __server_to_mysql($host), AI_MYSQL_USER, get_auth_token_mysql_password(), AI_MYSQL_DBNAME );
$result = @mysqli_query($my, "DELETE FROM tokens WHERE token='" . mysqli_real_escape_string($my, $token) . "'");
mysqli_close( $my );
}
// Note: we need to match the values that were set by the pannello!
// auth_token_set above is NOT called.
setcookie(AI_TOKEN_NAME, NULL, 1, "/webmail/", "", true, true);
}
function auth_token_get( &$user, &$pass, &$errmsg ) {
$rawtoken = $_COOKIE[AI_TOKEN_NAME];
if (empty($rawtoken)) {
return 0;
}
$host = @php_uname('n');
$token = '';
if (!__parse_raw_token( $rawtoken, $host, $token )) {
$errmsg = "errore nella decodifica del token: $host/$token";
return 0;
}
$my = mysqli_connect( __server_to_mysql($host), AI_MYSQL_USER, get_auth_token_mysql_password(), AI_MYSQL_DBNAME );
if (mysqli_connect_errno()) {
$errmsg = "errore nella connessione al database: " . mysqli_connect_errno();
return 0;
}
mysqli_query($my, 'SET NAMES utf8');
$result = mysqli_query( $my, "SELECT stamp, user, pass FROM tokens WHERE token='"
. mysqli_real_escape_string($my, $token) . "'");
// tolto: AND (NOW() - stamp) < 300
if (!$result) {
$errmsg = "errore nella query: " . mysqli_error( $my );
return 0;
}
if (mysqli_num_rows($result) < 1) {
$errmsg = "token '$token' non trovato";
return 0;
}
$row = mysqli_fetch_assoc( $result );
$user = $row['user'];
$encoded_pass = $row['pass'];
$pass = UnsafeOpensslAES::decrypt(base64_decode($encoded_pass), get_auth_token_encryption_key());
$errmsg = mysqli_error( $my );
mysqli_free_result( $result );
mysqli_close( $my );
return 1;
}
?>
<?php
// Global SSO logout URL.
$config['sso_logout_url'] = '';
<?php
/**
* Single-sign on with ai/sso and mod_sso.
*
* @version 0.1
* @author ale
* @website https://git.autistici.org/ai3/docker-roundcube
* @licence GNU GPLv3+
*/
class sso extends rcube_plugin
{
private $redirect_query;
function init()
{
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('authenticate', array($this, 'authenticate'));
$this->add_hook('login_after', array($this, 'login'));
$this->add_hook('logout_after', array($this, 'logout'));
}
function startup($args)
{
/*
The purpose of this hook is to set $_SESSION['password'] to the
SSO ticket, so that the rest of the Roundcube functionality can
use it to login.
*/
if (!empty($_SERVER['SSO_USER']) && !empty($_SERVER['SSO_TICKET'])) {
$rcmail = rcmail::get_instance();
$rcmail->add_shutdown_function(array('sso', 'shutdown'));
if (empty($_SESSION['user_id'])) {
$args['action'] = 'login';
$this->redirect_query = $_SERVER['QUERY_STRING'];
} else if (!empty($_SESSION['user_id']) && empty($_SESSION['password']) && !empty($_SERVER['SSO_TICKET'])) {
$_SESSION['password'] = $rcmail->encrypt($_SERVER['SSO_TICKET']);
}
}
return $args;
}
function authenticate($args)
{
if (!empty($_SERVER['SSO_USER'])) {
$args['user'] = $_SERVER['SSO_USER'];
if (!empty($_SERVER['SSO_TICKET'])) {
$args['pass'] = $_SERVER['SSO_TICKET'];
}
}
$args['cookiecheck'] = false;
$args['valid'] = true;
return $args;
}
function logout($args) {
// Redirect to global SSO logout path.
$this->load_config();
$sso_logout_url = rcmail::get_instance()->config->get('sso_logout_url');
header("Location: " . $sso_logout_url, true, 307);
}
function shutdown()
{
// No need to store the SSO ticket in the Roundcube session.
rcmail::get_instance()->session->remove('password');
}
function login($args)
{
if ($this->redirect_query) {
header('Location: ./?' . $this->redirect_query);
exit;
}
return $args;
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment