From 10b6d0d14e31905f85f3f24f42df72f53c790003 Mon Sep 17 00:00:00 2001 From: Downtown Allday Date: Thu, 3 Feb 2022 21:04:00 -0500 Subject: [PATCH 1/8] Initial commit for supporting oauth, functionality that was added to Roundcube 1.5. It fixes #58. A new configuration variable "ifpl_oauth_login_redirect" is added that controls whether persistent_login will hide roundcube's user/password login form so that the persistent_login checkbox is presented next to the button that redirects to an oauth authorization server. You would set this to true INSTEAD OF setting Roundcube's "oauth_login_redirect" to true - when the only desired login choice is via oauth. This seemed like an acceptable approach but if there are other approaches, please share. This code is working but there are a couple of unfinished items to complete and more testing is needed. I refactored some of the original code to better handle the two authentication paths (plain and oauth), so it's a bit difficult to look at a diff an see what changed. Sorry. Some remaining items to fix and test: fix any indentation issues in source files more code comments update documentation add javascript updates for skins other than elastic testing: - test sql changes in mysql and postgres (testing to date has been with sqlite3) - pt cookie is updated from all areas of ui when access_token is refreshed - ensure auto-login fails gracefully when refresh_token is expired - ensure no regressions (username/password functionality works as before) - more... Also, I am not a php expert. I tested on PHP 7.2 and don't know if the code will work on newer versions of php or not. --- persistent_login.js | 31 ++- persistent_login.php | 457 +++++++++++++++++++++++++++++++++---------- sql/mysql.sql | 8 +- sql/postgres.sql | 6 + sql/sqlite.sql | 6 + 5 files changed, 398 insertions(+), 110 deletions(-) diff --git a/persistent_login.js b/persistent_login.js index ec5bd36..790d4bb 100644 --- a/persistent_login.js +++ b/persistent_login.js @@ -18,6 +18,7 @@ $(document).ready(function () { var html = ''; var parentElementSelector = 'form'; var skin = window.rcmail.env.skin; + var hide_login_form = window.rcmail.env.hide_login_form; // Insert different HTML for different skins. if (skin == 'classic' || skin == 'larry') { @@ -35,6 +36,20 @@ $(document).ready(function () { } else if (skin == 'elastic') { parentElementSelector = '#login-form table tbody'; + if (hide_login_form) { + // remove login and password entry rows + $(parentElementSelector).empty(); + // remove login button + $('#login-form .formbuttons').remove(); + // move ifpl checkbox below oauth button + $('p.oauthlogin').after($('#login-form table').detach()); + // swap oauthlogin container margins with table's + $('p.oauthlogin').addClass('mt-0 mb-0'); + $('#login-form table').css('margin-bottom', '1em'); + // make oauth login button a primary color (was secondary) + $('#rcmloginoauth').addClass('btn-primary'); + } + html = ` @@ -63,6 +78,16 @@ $(document).ready(function () { `; } + // oauth links: add _ifpl cookie when clicking link + if (hide_login_form) { + $('a#rcmloginoauth').prop('onclick', function() { + return function(evt) { + set_ifpl_cookie($('#_ifpl').prop('checked')) + return true; + }; + }); + } + // apppend "html" with checkbox to document. var element = $(parentElementSelector); if (element && element.length !== 0) { @@ -86,4 +111,8 @@ $(document).ready(function () { }); } // if (window.rcmail) -}); \ No newline at end of file + + function set_ifpl_cookie(value) { + window.rcmail.set_cookie('_ifpl', value ? "1" : "0"); + } +}); diff --git a/persistent_login.php b/persistent_login.php index ada4683..2edd74d 100644 --- a/persistent_login.php +++ b/persistent_login.php @@ -1,7 +1,7 @@ cookie_name = $rcmail->config->get('ifpl_cookie_name', '_pt'); $this->use_auth_tokens = $rcmail->config->get('ifpl_use_auth_tokens', false); $this->db_table_auth_tokens = $rcmail->config->get('db_table_auth_tokens', 'auth_tokens'); + $this->oauth_login_redirect = $rcmail->config->get('ifpl_oauth_login_redirect', false); // login form modification hook. $this->add_hook('template_object_loginform', array($this,'persistent_login_loginform')); @@ -76,6 +80,8 @@ function init() $this->add_hook('authenticate', array($this, 'authenticate')); $this->add_hook('login_after', array($this, 'login_after')); $this->add_hook('logout_after', array($this, 'logout_after')); + $this->add_hook('login_failed', array($this, 'login_failed')); + $this->add_hook('oauth_refresh_token', array($this, 'oauth_refresh_token')); } function startup($args) @@ -98,6 +104,8 @@ function startup($args) return $args; } + + function authenticate($args) { $this->authenticate_args = $args; @@ -107,117 +115,136 @@ function authenticate($args) return $args; } - // --- identify user by cookie. ------------------------------------ // - $rcmail = rcmail::get_instance(); - // use token mechanic to identify user. - if ($this->use_auth_tokens) { - - // remove all expired tokens from database. - $rcmail->get_dbh()->query( - "DELETE FROM " . $rcmail->db->table_name($this->db_table_auth_tokens) - ." WHERE expires < ".$rcmail->db->now()); + $auth = $this->auth_from_cookie(); + if ($auth == null) { + self::unset_persistent_cookie(); + return $args; + } - // 0 - user-id - // 1 - auth-token - $token_parts = explode('|', self::get_persistent_cookie()); + $this->authenticate_args['host'] = $auth['host']; + $this->authenticate_args['user'] = $auth['user_name']; - // abort: invalid cookie format. - if (empty($token_parts) || !is_array($token_parts) - || count($token_parts) != 2 - ) { - self::unset_persistent_cookie(); - return $args; - } + $authenticate_success = false; + + if ($auth['auth_type'] == 'PLAIN') { + $authenticate_success = $this->authenticate_plain( + $args, + $auth + ); + } + else if ($auth['auth_type'] == 'OAUTH') { + $authenticate_success = $this->authenticate_oauth( + $args, + $auth + ); + } + + if (! $authenticate_success) { + self::unset_persistent_cookie(); + $args['valid'] = false; + } + return $args; + } - // get auth_token data from db. - $res = $rcmail->get_dbh()->query( - "SELECT * FROM " . $rcmail->db->table_name($this->db_table_auth_tokens) - ." WHERE token = ?" - ." AND user_id = ?", - $token_parts[1], - $token_parts[0]); + /** + * Helper function to authenticate via oauth. The process exist if + * login was successful. Returns false if authentication failed. + * @return bool + */ + function authenticate_oauth(&$args, $auth) { + $rcmail = rcmail::get_instance(); - if (($data = $rcmail->get_dbh()->fetch_assoc($res))) { - // has the token been expired? - /*if (false) { - self::unset_persistent_cookie(); - $rcmail->get_dbh()->query("delete from " . $rcmail->db->table_name('auth_tokens') . " where `token`=? and `user_id`=?", $token_parts[1], $token_parts[0]); - error_log('persistent-login expired, of user ' . $token_parts[0]); - return $args; - }*/ + // refresh the access token + // 'oauth_refresh_token' hook will get called if refreshed + $_SESSION['oauth_token'] = $auth['auth_data']; + $rcmail->oauth->refresh([]); + + $data = $_SESSION['oauth_token']; + $authorization = sprintf( + '%s %s', + $data['token_type'], + $data['access_token'] + ); + + // check XOAUTH2 authorization against the IMAP server + $rcmail->config->set('imap_auth_type', 'XOAUTH2'); + $rcmail->config->set('login_password_maxlen', strlen($authorization)); + + if ($rcmail->login($auth['user_name'], $authorization, $rcmail->autoselect_host(), true)) { + + // log successful login + $rcmail->log_login(); + + // update our cookie + self::set_persistent_cookie(); - // set login data. - $args['user'] = $data['user_name']; - $args['pass'] = $rcmail->decrypt($data['user_pass']); - $args['host'] = $data['host']; - $args['cookiecheck'] = false; - $args['valid'] = true; - $args['abort'] = false; - $this->authenticate_args = $args; - - // remove token from db. - $rcmail->get_dbh()->query( - "DELETE FROM " . $rcmail->db->table_name($this->db_table_auth_tokens) - ." WHERE token = ? " - ." AND user_id = ?", - $token_parts[1], - $token_parts[0]); - } - else { - // seems like the token is invalid. - // this case can only happen if the token is used a 2nd time -> got hacked?! - // for security reason we invalidate all persistent-auth cookies of the user - // and log the wrong users IP! - self::unset_persistent_cookie(); - $rcmail->get_dbh()->query( - "DELETE FROM " . $rcmail->db->table_name($this->db_table_auth_tokens) - . " WHERE user_id = ?", - $token_parts[0]); - //error_log('seems like a persistent login cookie has been stolen. invalidated all auth-tokens of user ' . $token_parts[0]); - } + // update roundcube's sesssion cookies + $rcmail->session->regenerate_id(false); + $rcmail->session->set_auth_cookie(); + // success - redirect to mail + header('Location: ' . $rcmail->url(['task' => 'mail'], true, false)); + exit; } - // use only-cookie mechanic to identify the user. - else { - - // extract user data from auth_token. - // 0 -> user-id - // 1 -> username - // 2 -> password (encrypted) - // 3 -> host - // 4 -> expire timestamp - $plain_token = $rcmail->decrypt($_COOKIE[$this->cookie_name]); - $token_parts = explode('|', $plain_token); + return false; + } - //error_log('plain token from cookie = '.$plain_token); + /** + * Helper function to authenticate via username/password + * @return bool + */ + function authenticate_plain(&$args, $auth) { + // set login data. + $args['user'] = $auth['user_name']; + $args['pass'] = $auth['user_pass']; + $args['host'] = $auth['host']; + $args['cookiecheck'] = false; + $args['valid'] = true; + $args['abort'] = false; + return true; + } - if (!empty($token_parts) && is_array($token_parts) - && count($token_parts) == 5 - ) { - // cookie/token expired. (should never occur, because the browser shall delete the cookie) - if (time() > $token_parts[4]) { - self::unset_persistent_cookie(); - } - // set login data. - else { - $args['user'] = $token_parts[1]; - $args['pass'] = $rcmail->decrypt($token_parts[2]); - $args['host'] = $token_parts[3]; - $args['cookiecheck'] = false; - $args['valid'] = true; - $args['abort'] = false; - $this->authenticate_args = $args; + /** + * roundcube handler for 'oauth_refresh_token' hook, which is + * called after an oauth aceess_token is refreshed. Registered + * roundcube tasks "mail|addressbook|settings" must be set or + * this function won't be called during refreshes when the user is + * on one of those pages. + */ + function oauth_refresh_token($args) { + if (self::is_persistent_cookie_available()) { + $rcmail = rcmail::get_instance(); + + // remove old token when using token mechanic to identify user. + if ($this->use_auth_tokens) { + // 0 - user-id + // 1 - auth-token + $token_parts = explode('|', self::get_persistent_cookie()); + + // abort: invalid cookie format. + if (empty($token_parts) || !is_array($token_parts) + || count($token_parts) != 2 + ) { + return $args; } - } - else { - // invalid token. - self::unset_persistent_cookie(); + + // remove old token + $this->db_remove_token( + $rcmail, + $token_parts[1], // token + $token_parts[0] // user_id + ); } + // update the existing cookie if the user is logged + // in. they won't be logged in if a refresh occurred due + // to our automatically re-authenticating them + if (! empty($rcmail->user->ID)) { + self::set_persistent_cookie(); + } } - return $args; } @@ -231,6 +258,12 @@ function login_after($args) else if (rcube_utils::get_input_value('_ifpl', rcube_utils::INPUT_POST)) { self::set_persistent_cookie(); } + else if (rcube_utils::get_input_value('_ifpl', rcube_utils::INPUT_COOKIE) == '1') { + // oauth login + self::set_persistent_cookie(); + } + self::remove_cookie('_ifpl'); + // restore the user requested action unless it's an action // that's not compatible with the 'mail' task, which is always // set by roundcube after login @@ -252,11 +285,11 @@ function logout_after($args) && count($token_parts) == 2 ) { // remove token from db. - $rcmail->get_dbh()->query( - "DELETE FROM " . $rcmail->db->table_name($this->db_table_auth_tokens) - . " WHERE token = ? AND user_id = ?", - $token_parts[1], - $token_parts[0]); + $this->db_remove_token( + $rcmail, + $token_parts[1], // token + $token_parts[0] // user_id + ); } } @@ -266,6 +299,15 @@ function logout_after($args) return $args; } + /** + * roundcube handler to clean up if login failed + */ + function login_failed($args) + { + self::unset_persistent_cookie(); + return $args; + } + /////////////////////////////////////////////////////////////////////////// // template callback functions /////////////////////////////////////////////////////////////////////////// @@ -282,6 +324,12 @@ function persistent_login_loginform($content) // the javascript code adds the to the login form. $this->include_script('persistent_login.js'); + // set variable + self::remove_cookie('_ifpl'); + rcmail::get_instance() + ->output + ->set_env('hide_login_form', $this->oauth_login_redirect && rcmail::get_instance()->oauth->is_enabled()); + return $content; } @@ -335,8 +383,10 @@ function set_persistent_cookie() $user_name = $rcmail->user->data['username']; // user password - $user_password = $_SESSION['password']; + $user_password = isset($_SESSION['password']) ? $_SESSION['password'] : ''; + // auth data + $oauth_data = isset($_SESSION['oauth_token']) ? json_encode($_SESSION['oauth_token']) : null; if ($this->use_auth_tokens) { // generate new token in database and set it to user as cookie... @@ -351,9 +401,9 @@ function set_persistent_cookie() // insert token to database. $rcmail->get_dbh()->query( "INSERT INTO ".$rcmail->db->table_name($this->db_table_auth_tokens) - ." (token, expires, user_id, user_name, user_pass, host)" - ." VALUES (?, ?, ?, ?, ?, ?)", - $auth_token, $sql_expires, $user_id, $user_name, $user_password, $host); + ." (auth_type, token, expires, user_id, user_name, user_pass, host, auth_data)" + ." VALUES ('OAUTH', ?, ?, ?, ?, ?, ?, ?)", + $auth_token, $sql_expires, $user_id, $user_name, $user_password, $host, $oauth_data ? $rcmail->encrypt($oauth_data) : null); // set token as cookie. if (!self::set_cookie($this->cookie_name, $crypt_token, $ts_expires)) { @@ -363,7 +413,13 @@ function set_persistent_cookie() else { // create encrypted auth_token to store in cookie. // e.g.: "|||" - $plain_token = $user_id . '|' . $user_name . '|' . $user_password . '|' . $host . '|' . (time() + $this->cookie_expire_time); + if ($oauth_data) { + $plain_token = 'OAUTH' . '|' . $user_id . '|' . $user_name . '|' . $host . '|' . (time() + $this->cookie_expire_time) . '|' . $oauth_data; + } + else { + $plain_token = 'PLAIN' . '|' . $user_id . '|' . $user_name . '|' . $user_password . '|' . $host . '|' . (time() + $this->cookie_expire_time); + } + $crypt_token = $rcmail->encrypt($plain_token); //error_log('set plain token to cookie = '.$plain_token); @@ -399,6 +455,191 @@ function is_persistent_cookie_available() } } + /** + * retrieve the persistent cookie data as an array. The fields of + * the returned associate array match those of the database table + * column names. + * + * @return array + */ + function auth_from_cookie() { + $rcmail = rcmail::get_instance(); + + // use token mechanic to identify user. + if ($this->use_auth_tokens) { + + // purge expired records from the database + $this->clean_db($rcmail); + + // 0 - user-id + // 1 - auth-token + $token_parts = explode('|', self::get_persistent_cookie()); + + // abort: invalid cookie format. + if (empty($token_parts) || !is_array($token_parts) + || count($token_parts) != 2 + ) { + return null; + } + + // get auth_token data from db. + $res = $rcmail->get_dbh()->query( + "SELECT * FROM " . $rcmail->db->table_name($this->db_table_auth_tokens) + ." WHERE token = ?" + ." AND user_id = ?", + $token_parts[1], + $token_parts[0]); + + if (($data = $rcmail->get_dbh()->fetch_assoc($res))) { + $err_msg = null; + if ($data['auth_type'] == 'PLAIN') { + $data['user_pass'] = $rcmail->decrypt($data['user_pass']); + } + else if ($data['auth_type'] == 'OAUTH') { + $data['auth_data'] = json_decode($rcmail->decrypt($data['auth_data']), true); + if ($data['auth_data'] == null) { + $err_msg = "Database table auth_tokens contains an auth_data field that is not parsable json. token=" . $data['token']; + $data =null; + } + } + else { + $err_msg = "Database table auth_tokens contains an invalid auth_type. auth_type=" . $data['auth_type'] . " token=" . $data['token']; + $data = null; + } + + $this->db_remove_token( + $rcmail, + $token_parts[1], // token + $token_parts[0] // user_id + ); + + return $data; + } + else { + // seems like the token is invalid. + // this case can only happen if the token is used a 2nd time -> got hacked?! + // for security reason we invalidate all persistent-auth cookies of the user + // and log the wrong users IP! + self::unset_persistent_cookie(); + $this->db_remove_all_user_tokens( + $rcmail, + $token_parts[0] // user_id + ); + //error_log('seems like a persistent login cookie has been stolen. invalidated all auth-tokens of user ' . $token_parts[0]); + return null; + } + + } // end: use_auth_tokens + + + // use only-cookie mechanic to identify the user. + else { + // extract user data from auth_token. + // 0 -> auth_type + // + // for auth_type "PLAIN": + // 1 -> user-id + // 2 -> username + // 3 -> password (encrypted) + // 4 -> host + // 5 -> expire timestamp + // + // for auth_type "OAUTH": + // 1 -> user-id + // 2 -> username + // 3 -> host + // 4 -> expire timestamp + // 5 -> json from oauth server containing authorization + + $plain_token = $rcmail->decrypt($_COOKIE[$this->cookie_name]); + $token_parts = explode('|', $plain_token); + + //error_log('plain token from cookie = '.$plain_token); + + if (empty($token_parts) || !is_array($token_parts) || count($token_parts) < 1) { + // invalid token. + return null; + } + + $auth_type = $token_parts[0]; + + if ($auth_type == 'PLAIN' && count($token_parts) == 6) { + // cookie/token expired. (should never occur, because the browser shall delete the cookie) + if (time() > $token_parts[5]) { + return null; + } + else { + $data = []; + $data['auth_type'] = $auth_type; + $data['user_id'] = $token_parts[1]; + $data['user_name'] = $token_parts[2]; + $data['user_pass'] = $rcmail->decrypt($token_parts[3]); + $data['host'] = $token_parts[4]; + $data['expires'] = $token_parts[5]; + $data['auth_data'] = null; + return $data; + } + } + else if ($auth_type == 'OAUTH' && count($token_parts) == 6) { + $data = []; + $data['auth_type'] = $auth_type; + $data['user_id'] = $token_parts[1]; + $data['user_name'] = $token_parts[2]; + $data['user_pass'] = ''; + $data['host'] = $token_parts[3]; + $data['expires'] = $token_parts[4]; + $data['auth_data'] = json_decode($token_parts[5], true); + if ($data['auth_data'] == null) { + // json is invalid + return null; + } + return $data; + } + else { + // invalid auth_type or token. + return null; + } + + + } // end: use only-cookie mechanic to identify the user. + } + + /** + * purge database records with expired tokens + */ + function clean_db($rcmail) { + // remove all expired tokens from database. + $rcmail->get_dbh()->query( + "DELETE FROM " . $rcmail->db->table_name($this->db_table_auth_tokens) + ." WHERE expires < ".$rcmail->db->now()); + } + + /** + * remove the database record corresponding to the supplied + * roundcube user-id and token + */ + function db_remove_token($rcmail, $token, $user_id) { + // remove token from db. + $rcmail->get_dbh()->query( + "DELETE FROM " . $rcmail->db->table_name($this->db_table_auth_tokens) + ." WHERE token = ? " + ." AND user_id = ?", + $token, + $user_id); + } + + /** + * remove all database records corresponding to the supplied + * roundcube user-id + */ + function db_remove_all_user_tokens($rcmail, $user_id) { + $rcmail->get_dbh()->query( + "DELETE FROM " . $rcmail->db->table_name($this->db_table_auth_tokens) + . " WHERE user_id = ?", + $user_id); + } + + /** * generates a random string of numbers and letters. * diff --git a/sql/mysql.sql b/sql/mysql.sql index 1a052aa..0ef55dc 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -23,4 +23,10 @@ ALTER TABLE `auth_tokens` -- ALTER TABLE `auth_tokens` - ADD PRIMARY KEY(`token`); \ No newline at end of file + ADD PRIMARY KEY(`token`); + +-- +-- Update version 5.?? +-- +ALTER TABLE `auth_tokens` ADD COLUMN `auth_type` varchar(15) NOT NULL DEFAULT 'PLAIN'; +ALTER TABLE `auth_tokens` ADD COLUMN `auth_data` TEXT NULL; diff --git a/sql/postgres.sql b/sql/postgres.sql index 98bf2b2..a79e352 100644 --- a/sql/postgres.sql +++ b/sql/postgres.sql @@ -15,3 +15,9 @@ CREATE TABLE auth_tokens ( user_pass varchar(128) NOT NULL, host varchar(255) NOT NULL ); + +-- +-- Update version 5.?? +-- +ALTER TABLE auth_tokens ADD COLUMN auth_type varchar(15) NOT NULL DEFAULT 'PLAIN'; +ALTER TABLE auth_tokens ADD COLUMN auth_data TEXT NULL; diff --git a/sql/sqlite.sql b/sql/sqlite.sql index 5bffc9a..33f5cf7 100644 --- a/sql/sqlite.sql +++ b/sql/sqlite.sql @@ -12,3 +12,9 @@ CREATE TABLE IF NOT EXISTS `auth_tokens` ( ); CREATE INDEX IF NOT EXISTS `user_id_fk_auth_tokens` ON `auth_tokens`(`user_id`); + +-- +-- Update version 5.?? +-- +ALTER TABLE `auth_tokens` ADD COLUMN `auth_type` TEXT NOT NULL DEFAULT 'PLAIN'; +ALTER TABLE `auth_tokens` ADD COLUMN `auth_data` TEXT NULL; From 52c1be185bbb9f9dc03cea66f6e4999c53474f84 Mon Sep 17 00:00:00 2001 From: Downtown Allday Date: Fri, 4 Feb 2022 08:56:14 -0500 Subject: [PATCH 2/8] Use the recalled host for oauth login --- persistent_login.php | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/persistent_login.php b/persistent_login.php index 2edd74d..504cf7a 100644 --- a/persistent_login.php +++ b/persistent_login.php @@ -82,6 +82,7 @@ function init() $this->add_hook('logout_after', array($this, 'logout_after')); $this->add_hook('login_failed', array($this, 'login_failed')); $this->add_hook('oauth_refresh_token', array($this, 'oauth_refresh_token')); + } function startup($args) @@ -108,10 +109,12 @@ function startup($args) function authenticate($args) { + $this->authenticate_args = $args; // check for auth_token cookie. if (!self::is_persistent_cookie_available()) { + return $args; } @@ -149,11 +152,12 @@ function authenticate($args) } /** - * Helper function to authenticate via oauth. The process exist if + * Helper function to authenticate via oauth. The process exits if * login was successful. Returns false if authentication failed. * @return bool */ function authenticate_oauth(&$args, $auth) { + $rcmail = rcmail::get_instance(); // refresh the access token @@ -162,6 +166,7 @@ function authenticate_oauth(&$args, $auth) { $rcmail->oauth->refresh([]); $data = $_SESSION['oauth_token']; + $authorization = sprintf( '%s %s', $data['token_type'], @@ -171,8 +176,8 @@ function authenticate_oauth(&$args, $auth) { // check XOAUTH2 authorization against the IMAP server $rcmail->config->set('imap_auth_type', 'XOAUTH2'); $rcmail->config->set('login_password_maxlen', strlen($authorization)); - - if ($rcmail->login($auth['user_name'], $authorization, $rcmail->autoselect_host(), true)) { + $host = empty($auth['host']) ? $rcmail->autoselect_host() : $auth['host']; + if ($rcmail->login($auth['user_name'], $authorization, $host, true)) { // log successful login $rcmail->log_login(); @@ -196,6 +201,7 @@ function authenticate_oauth(&$args, $auth) { * @return bool */ function authenticate_plain(&$args, $auth) { + // set login data. $args['user'] = $auth['user_name']; $args['pass'] = $auth['user_pass']; @@ -214,6 +220,7 @@ function authenticate_plain(&$args, $auth) { * on one of those pages. */ function oauth_refresh_token($args) { + if (self::is_persistent_cookie_available()) { $rcmail = rcmail::get_instance(); @@ -244,12 +251,14 @@ function oauth_refresh_token($args) { if (! empty($rcmail->user->ID)) { self::set_persistent_cookie(); } + } return $args; } function login_after($args) { + // update the already existing cookie (because of expiration time). if (self::is_persistent_cookie_available()) { self::set_persistent_cookie(); @@ -275,6 +284,7 @@ function login_after($args) function logout_after($args) { + $rcmail = rcmail::get_instance(); if ($this->use_auth_tokens) { // get user-id and token from cookie. @@ -304,6 +314,7 @@ function logout_after($args) */ function login_failed($args) { + self::unset_persistent_cookie(); return $args; } @@ -353,6 +364,7 @@ function get_persistent_cookie() */ function set_persistent_cookie() { + // prepare data for login via cookie $rcmail = rcmail::get_instance(); @@ -513,6 +525,7 @@ function auth_from_cookie() { $token_parts[0] // user_id ); + return $data; } else { @@ -554,6 +567,7 @@ function auth_from_cookie() { $plain_token = $rcmail->decrypt($_COOKIE[$this->cookie_name]); $token_parts = explode('|', $plain_token); + //error_log('plain token from cookie = '.$plain_token); if (empty($token_parts) || !is_array($token_parts) || count($token_parts) < 1) { @@ -688,6 +702,7 @@ function set_cookie($name, $value, $exp = 0) function remove_cookie($name) { if (headers_sent()) { + return false; } if (class_exists('rcube_utils')) { @@ -698,6 +713,7 @@ function remove_cookie($name) return true; } + /** * Check if a given ip is in a network * @param string $ip IP to check in IPV4 format eg. 127.0.0.1 From c4b106167eff3f73ed0dfbd438975d8f28e03dc2 Mon Sep 17 00:00:00 2001 From: Downtown Allday Date: Fri, 4 Feb 2022 09:12:57 -0500 Subject: [PATCH 3/8] Honor persistent login choice with oauth login when ifpl_oauth_login_redirect is false --- persistent_login.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/persistent_login.js b/persistent_login.js index 790d4bb..4cb4ba0 100644 --- a/persistent_login.js +++ b/persistent_login.js @@ -79,14 +79,12 @@ $(document).ready(function () { } // oauth links: add _ifpl cookie when clicking link - if (hide_login_form) { - $('a#rcmloginoauth').prop('onclick', function() { - return function(evt) { - set_ifpl_cookie($('#_ifpl').prop('checked')) - return true; - }; - }); - } + $('a#rcmloginoauth').prop('onclick', function() { + return function(evt) { + set_ifpl_cookie($('#_ifpl').prop('checked')) + return true; + }; + }); // apppend "html" with checkbox to document. var element = $(parentElementSelector); From 34a0c7be903d704d069b8cd97d4170956e0a7357 Mon Sep 17 00:00:00 2001 From: downtownallday Date: Fri, 4 Feb 2022 09:55:47 -0500 Subject: [PATCH 4/8] update for classic and larry skins --- persistent_login.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/persistent_login.js b/persistent_login.js index 4cb4ba0..51f9659 100644 --- a/persistent_login.js +++ b/persistent_login.js @@ -23,10 +23,20 @@ $(document).ready(function () { // Insert different HTML for different skins. if (skin == 'classic' || skin == 'larry') { parentElementSelector = '#login-form form table tbody'; + if (hide_login_form) { + // remove login and password entry rows + $(parentElementSelector).empty(); + // remove login button + $('#login-form .formbuttons').remove(); + // move ifpl checkbox below oauth button + $('p.oauthlogin').after($('#login-form table').detach()); + // left-align checkbox + $('#login-form form table').css({'margin':'0','width':'100%'}); + } html = ` ` + rcmail.gettext('ifpl_rememberme', 'persistent_login') + ` - + From 699d4fffada19d292528acc4c449f711d1530443 Mon Sep 17 00:00:00 2001 From: downtownallday Date: Fri, 4 Feb 2022 10:33:15 -0500 Subject: [PATCH 5/8] Update readme --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index a4f2b11..2f557ab 100644 --- a/README.md +++ b/README.md @@ -38,5 +38,20 @@ Enable it in config: `persistent_login/config.inc.php` $rcmail_config['ifpl_use_auth_tokens'] = true; ``` +### OAuth with login redirect +Persistent Login works with OAuth as-is. However, if Roundcube's automatic login redirect setting `oauth_login_redirect` is true, the user will not have the opportunity to select "Keep me logged in" because Roundcube sends the user directly to the OAuth server for authentication (the login screen is not shown at all). + +To enable Persistent Login in this case, change Roundcube's `oauth_login_redirect` setting to __false__. In config/config.inc.php: +```php +$config["oauth_login_redirect"] = false; +``` + +Then, enable Persistent Login's `ifpl_oauth_login_redirect` setting. In `plugins/persistent_login/config.inc.php` +```php +$rcmail_config['ifpl_oauth_login_redirect'] = true; +``` + +The login screen will be displayed showing only the "Keep me logged in" checkbox and a button to authenticate with the OAuth server. + [roundcube]: http://roundcube.net/ [github-release]: https://github.com/mfreiholz/persistent_login/releases \ No newline at end of file From a8b58a5ba705ad9bf1c9c03105ab8ba4bf160459 Mon Sep 17 00:00:00 2001 From: downtownallday Date: Fri, 4 Feb 2022 11:00:28 -0500 Subject: [PATCH 6/8] Remove extraneous newlines --- persistent_login.php | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/persistent_login.php b/persistent_login.php index 504cf7a..4a3bf78 100644 --- a/persistent_login.php +++ b/persistent_login.php @@ -82,7 +82,6 @@ function init() $this->add_hook('logout_after', array($this, 'logout_after')); $this->add_hook('login_failed', array($this, 'login_failed')); $this->add_hook('oauth_refresh_token', array($this, 'oauth_refresh_token')); - } function startup($args) @@ -109,12 +108,10 @@ function startup($args) function authenticate($args) { - $this->authenticate_args = $args; // check for auth_token cookie. if (!self::is_persistent_cookie_available()) { - return $args; } @@ -157,7 +154,6 @@ function authenticate($args) * @return bool */ function authenticate_oauth(&$args, $auth) { - $rcmail = rcmail::get_instance(); // refresh the access token @@ -166,7 +162,6 @@ function authenticate_oauth(&$args, $auth) { $rcmail->oauth->refresh([]); $data = $_SESSION['oauth_token']; - $authorization = sprintf( '%s %s', $data['token_type'], @@ -201,7 +196,6 @@ function authenticate_oauth(&$args, $auth) { * @return bool */ function authenticate_plain(&$args, $auth) { - // set login data. $args['user'] = $auth['user_name']; $args['pass'] = $auth['user_pass']; @@ -220,7 +214,6 @@ function authenticate_plain(&$args, $auth) { * on one of those pages. */ function oauth_refresh_token($args) { - if (self::is_persistent_cookie_available()) { $rcmail = rcmail::get_instance(); @@ -251,14 +244,12 @@ function oauth_refresh_token($args) { if (! empty($rcmail->user->ID)) { self::set_persistent_cookie(); } - } return $args; } function login_after($args) { - // update the already existing cookie (because of expiration time). if (self::is_persistent_cookie_available()) { self::set_persistent_cookie(); @@ -284,7 +275,6 @@ function login_after($args) function logout_after($args) { - $rcmail = rcmail::get_instance(); if ($this->use_auth_tokens) { // get user-id and token from cookie. @@ -314,7 +304,6 @@ function logout_after($args) */ function login_failed($args) { - self::unset_persistent_cookie(); return $args; } @@ -364,7 +353,6 @@ function get_persistent_cookie() */ function set_persistent_cookie() { - // prepare data for login via cookie $rcmail = rcmail::get_instance(); @@ -428,7 +416,7 @@ function set_persistent_cookie() if ($oauth_data) { $plain_token = 'OAUTH' . '|' . $user_id . '|' . $user_name . '|' . $host . '|' . (time() + $this->cookie_expire_time) . '|' . $oauth_data; } - else { + else { $plain_token = 'PLAIN' . '|' . $user_id . '|' . $user_name . '|' . $user_password . '|' . $host . '|' . (time() + $this->cookie_expire_time); } @@ -525,7 +513,6 @@ function auth_from_cookie() { $token_parts[0] // user_id ); - return $data; } else { @@ -567,7 +554,6 @@ function auth_from_cookie() { $plain_token = $rcmail->decrypt($_COOKIE[$this->cookie_name]); $token_parts = explode('|', $plain_token); - //error_log('plain token from cookie = '.$plain_token); if (empty($token_parts) || !is_array($token_parts) || count($token_parts) < 1) { @@ -702,7 +688,6 @@ function set_cookie($name, $value, $exp = 0) function remove_cookie($name) { if (headers_sent()) { - return false; } if (class_exists('rcube_utils')) { @@ -713,7 +698,6 @@ function remove_cookie($name) return true; } - /** * Check if a given ip is in a network * @param string $ip IP to check in IPV4 format eg. 127.0.0.1 From ca97b8bfc16481bfd22c094dcc295b276712aa22 Mon Sep 17 00:00:00 2001 From: downtownallday Date: Fri, 4 Feb 2022 11:50:24 -0500 Subject: [PATCH 7/8] Fix standard login when using auth_token table and remove encryption of oauth data Roundcube is encrypting the refresh_token already, and the access_token is transitory, so am removing encryption from the "auth_data" field. --- persistent_login.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/persistent_login.php b/persistent_login.php index 4a3bf78..e5574b7 100644 --- a/persistent_login.php +++ b/persistent_login.php @@ -388,6 +388,15 @@ function set_persistent_cookie() // auth data $oauth_data = isset($_SESSION['oauth_token']) ? json_encode($_SESSION['oauth_token']) : null; + if (! empty($oauth_data)) { + $auth_type = 'OAUTH'; + $user_password = ''; + } + else { + $auth_type = 'PLAIN'; + $oauth_data = null; + } + if ($this->use_auth_tokens) { // generate new token in database and set it to user as cookie... $auth_token = time() . "-" . self::generate_random_token(); @@ -402,8 +411,8 @@ function set_persistent_cookie() $rcmail->get_dbh()->query( "INSERT INTO ".$rcmail->db->table_name($this->db_table_auth_tokens) ." (auth_type, token, expires, user_id, user_name, user_pass, host, auth_data)" - ." VALUES ('OAUTH', ?, ?, ?, ?, ?, ?, ?)", - $auth_token, $sql_expires, $user_id, $user_name, $user_password, $host, $oauth_data ? $rcmail->encrypt($oauth_data) : null); + ." VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + $auth_type, $auth_token, $sql_expires, $user_id, $user_name, $user_password, $host, $oauth_data); // set token as cookie. if (!self::set_cookie($this->cookie_name, $crypt_token, $ts_expires)) { @@ -413,7 +422,7 @@ function set_persistent_cookie() else { // create encrypted auth_token to store in cookie. // e.g.: "|||" - if ($oauth_data) { + if ($auth_type == 'OAUTH') { $plain_token = 'OAUTH' . '|' . $user_id . '|' . $user_name . '|' . $host . '|' . (time() + $this->cookie_expire_time) . '|' . $oauth_data; } else { @@ -496,7 +505,7 @@ function auth_from_cookie() { $data['user_pass'] = $rcmail->decrypt($data['user_pass']); } else if ($data['auth_type'] == 'OAUTH') { - $data['auth_data'] = json_decode($rcmail->decrypt($data['auth_data']), true); + $data['auth_data'] = json_decode($data['auth_data'], true); if ($data['auth_data'] == null) { $err_msg = "Database table auth_tokens contains an auth_data field that is not parsable json. token=" . $data['token']; $data =null; From 7efdd139ecf97e79e5e6b66b6da0b26947a1344a Mon Sep 17 00:00:00 2001 From: downtownallday Date: Fri, 4 Feb 2022 13:13:14 -0500 Subject: [PATCH 8/8] code comments and expiry check for oauth/cookie-mechanic --- persistent_login.php | 45 +++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/persistent_login.php b/persistent_login.php index e5574b7..7fbf69d 100644 --- a/persistent_login.php +++ b/persistent_login.php @@ -258,8 +258,8 @@ function login_after($args) else if (rcube_utils::get_input_value('_ifpl', rcube_utils::INPUT_POST)) { self::set_persistent_cookie(); } + // user just logged in using oauth and wants a cookie now else if (rcube_utils::get_input_value('_ifpl', rcube_utils::INPUT_COOKIE) == '1') { - // oauth login self::set_persistent_cookie(); } self::remove_cookie('_ifpl'); @@ -465,11 +465,25 @@ function is_persistent_cookie_available() } /** - * retrieve the persistent cookie data as an array. The fields of - * the returned associate array match those of the database table - * column names. + * retrieve the persistent cookie data as an array or null if auth + * data is unavailable. The fields of the returned associative + * array match those of the database table column names. + * + * returned array: [ + * 'auth_type' => 'OAUTH' | 'PLAIN', + * 'user_id' => {number; may be formatted as integer or string}, + * 'user_name' => {string}, + * 'user_pass' => {string; '' if auth_type=='OAUTH'}, + * 'host' => {string}, + * 'expires' => {string; 'YYYY-MM-DD HH:MM:SS' format if use_auth_tokens is truthy, otherwise the number of seconds from the epoch }, + * 'auth_data' => {array} | null + * ] * + * when using auth tokens, the function has the side effect of + * removing the correspoding row in the database. + * * @return array + * @return null */ function auth_from_cookie() { $rcmail = rcmail::get_instance(); @@ -577,19 +591,20 @@ function auth_from_cookie() { if (time() > $token_parts[5]) { return null; } - else { - $data = []; - $data['auth_type'] = $auth_type; - $data['user_id'] = $token_parts[1]; - $data['user_name'] = $token_parts[2]; - $data['user_pass'] = $rcmail->decrypt($token_parts[3]); - $data['host'] = $token_parts[4]; - $data['expires'] = $token_parts[5]; - $data['auth_data'] = null; - return $data; - } + $data = []; + $data['auth_type'] = $auth_type; + $data['user_id'] = $token_parts[1]; + $data['user_name'] = $token_parts[2]; + $data['user_pass'] = $rcmail->decrypt($token_parts[3]); + $data['host'] = $token_parts[4]; + $data['expires'] = $token_parts[5]; + $data['auth_data'] = null; + return $data; } else if ($auth_type == 'OAUTH' && count($token_parts) == 6) { + if (time() > $token_parts[4]) { + return null; + } $data = []; $data['auth_type'] = $auth_type; $data['user_id'] = $token_parts[1];