Skip to content

Commit

Permalink
OAuth: code refactorization
Browse files Browse the repository at this point in the history
 * OAuth: Refact.: migrate login flow into hooks (more evolutive code and less dependency to core code)
 * OAuth: Fix: logger prefix (include prefix during login phase)
  • Loading branch information
EdouardVanbelle committed Dec 23, 2023
1 parent 90555d2 commit a85be03
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 81 deletions.
73 changes: 16 additions & 57 deletions program/actions/login/oauth.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,70 +32,29 @@ public function run($args = [])
$auth_error = rcube_utils::get_input_string('error', rcube_utils::INPUT_GET);
$auth_state = rcube_utils::get_input_string('state', rcube_utils::INPUT_GET);

// on oauth error
if (!empty($auth_error)) {
$error_message = rcube_utils::get_input_string('error_description', rcube_utils::INPUT_GET) ?: $auth_error;
$rcmail->output->show_message($error_message, 'warning');
return;
}

// auth code return from oauth login
if (!empty($auth_code)) {
$auth = $rcmail->oauth->request_access_token($auth_code, $auth_state);

// oauth success
if ($auth && isset($auth['username'], $auth['authorization'], $auth['token'])) {
// enforce OAUTHBEARER/XOAUTH2 auth type
$rcmail->config->set('imap_auth_type', $rcmail->oauth->get_auth_type());
$rcmail->config->set('login_password_maxlen', strlen($auth['authorization']));

// use access_token and user info for IMAP login
$storage_host = $rcmail->autoselect_host();
if ($rcmail->login($auth['username'], $auth['authorization'], $storage_host, true)) {
// replicate post-login tasks from index.php
$rcmail->session->remove('temp');
$rcmail->session->regenerate_id(false);

// send auth cookie if necessary
$rcmail->session->set_auth_cookie();

// save OAuth token in session
$_SESSION['oauth_token'] = $auth['token'];

$rcmail->oauth->log_debug('login successful for OIDC sub=%s with username=%s which is rcube-id=%s', $auth['token']['identity']['sub'], $auth['username'], $rcmail->user->ID);

// log successful login
$rcmail->log_login();

// allow plugins to control the redirect url after login success
$redir = $rcmail->plugins->exec_hook('login_after', ['_task' => 'mail']);
unset($redir['abort'], $redir['_err']);

// send redirect
header('Location: ' . $rcmail->url($redir, true, false));
exit;
}
else {
$rcmail->output->show_message('loginfailed', 'warning');

// log failed login
$error_code = $rcmail->login_error();
$rcmail->log_login($auth['username'], true, $error_code);

$rcmail->plugins->exec_hook('login_failed', [
'code' => $error_code,
'host' => $storage_host,
'user' => $auth['username'],
]);

$rcmail->kill_session();
// fall through -> login page
}
}
else {
if (!$auth) {
$rcmail->output->show_message('oauthloginfailed', 'warning');
return;
}

// next action will be the login
$args['task'] = 'login';
$args['action'] = 'login';
return $args;
}
// error return from oauth login
elseif (!empty($auth_error)) {
$error_message = rcube_utils::get_input_string('error_description', rcube_utils::INPUT_GET) ?: $auth_error;
$rcmail->output->show_message($error_message, 'warning');
}

// login action: redirect to `oauth_auth_uri`
elseif ($rcmail->task === 'login') {
if ($rcmail->task === 'login') {
// this will always exit() the process
$rcmail->oauth->login_redirect();
}
Expand Down
106 changes: 82 additions & 24 deletions program/include/rcmail_oauth.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ class rcmail_oauth
/** @var string */
protected $logout_redirect_url;

/** @var array helper to map .well-known entry to config (discovery URI) */
// parameters used during the login phase
/** @var array */
protected $login_phase;

// helper to map .well-known entry to config (discovery URI)
/** @var array */
static protected $config_mapper = [
'issuer' => 'issuer',
'authorization_endpoint' => 'auth_uri',
Expand Down Expand Up @@ -102,8 +107,9 @@ static function get_instance($options = [])
*/
private function logger($level, $message)
{
$sub = $_SESSION['oauth_token']['identity']['sub'] ?? '-';
$ses = $_SESSION['oauth_token']['session_state'] ?? '-';
$token = $this->login_phase['token'] ?? $_SESSION['oauth_token'] ?? [];
$sub = $token['identity']['sub'] ?? '-';
$ses = $token['session_state'] ?? '-';
rcube::write_log('oauth', sprintf('%s: [ip=%s sub=%s ses=%s] %s', $level, rcube_utils::remote_ip(), $sub, $ses, $message));
}

Expand Down Expand Up @@ -287,15 +293,20 @@ public function init()
}

// subscribe to storage and smtp init events
$this->rcmail->plugins->register_hook('loginform_content', [$this, 'loginform_content']);
$this->rcmail->plugins->register_hook('startup', [$this, 'startup']);

$this->rcmail->plugins->register_hook('storage_init', [$this, 'storage_init']);
$this->rcmail->plugins->register_hook('smtp_connect', [$this, 'smtp_connect']);
$this->rcmail->plugins->register_hook('managesieve_connect', [$this, 'managesieve_connect']);
$this->rcmail->plugins->register_hook('logout_after', [$this, 'logout_after']);

$this->rcmail->plugins->register_hook('authenticate', [$this, 'authenticate']);
$this->rcmail->plugins->register_hook('login_after', [$this, 'login_after']);
$this->rcmail->plugins->register_hook('login_failed', [$this, 'login_failed']);
$this->rcmail->plugins->register_hook('logout_after', [$this, 'logout_after']);
$this->rcmail->plugins->register_hook('unauthenticated', [$this, 'unauthenticated']);

$this->rcmail->plugins->register_hook('refresh', [$this, 'refresh']);
$this->rcmail->plugins->register_hook('startup', [$this, 'startup']);
$this->rcmail->plugins->register_hook('loginform_content', [$this, 'loginform_content']);
}

/**
Expand Down Expand Up @@ -422,7 +433,7 @@ public function jwt_decode($jwt)
throw new RuntimeException('Failed to validate JWT: expired message');
}

$this->log_debug("'jwt: %s", json_encode($body));
$this->log_debug("jwt: %s", json_encode($body));

return $body;
}
Expand Down Expand Up @@ -609,11 +620,12 @@ public function request_access_token($auth_code, $state = null)
$this->last_error = null; // clean last error

// return auth data
return [
$this->login_phase = [
'username' => $username,
'authorization' => $authorization, // the payload to authentificate through IMAP, SMTP, SIEVE .. servers
'token' => $data,
];
return $this->login_phase;
}
catch (RequestException $e) {
$this->last_error = "OAuth token request failed: " . $e->getMessage();
Expand Down Expand Up @@ -932,17 +944,6 @@ public function refresh($options)
$this->check_token_validity($_SESSION['oauth_token']);
}


/**
* Returns the auth_type to use
*
* @return string The auth type: XOAUTH or OAUTHBEARER
*/
public function get_auth_type()
{
return $this->auth_type;
}

/**
* Callback for 'storage_init' hook
*
Expand All @@ -952,7 +953,15 @@ public function get_auth_type()
*/
public function storage_init($options)
{
if (isset($_SESSION['oauth_token']) && $options['driver'] === 'imap') {
if ($options['driver'] !== 'imap') {
return $options;
}

if ($this->login_phase) {
// login phase
$options['auth_type'] = $this->auth_type;
}
elseif (isset($_SESSION['oauth_token'])) {
if ($this->check_token_validity($_SESSION['oauth_token']) === self::TOKEN_REFRESHED) {
$options['password'] = $this->rcmail->decrypt($_SESSION['password']);
}
Expand Down Expand Up @@ -983,7 +992,6 @@ public function smtp_connect($options)
// check token validity
$this->check_token_validity($_SESSION['oauth_token']);

// enforce AUTHBEARER/XOAUTH2 authorization type
$options['smtp_user'] = '%u';
$options['smtp_pass'] = '%p';
$options['smtp_auth_type'] = $this->auth_type;
Expand All @@ -1004,14 +1012,59 @@ public function managesieve_connect($options)
if (isset($_SESSION['oauth_token'])) {
// check token validity
$this->check_token_validity($_SESSION['oauth_token']);

// enforce AUTHBEARER/XOAUTH2 authorization type
$options['auth_type'] = $this->auth_type;
}

return $options;
}

/**
* Callback for 'authenticate' hook
*
* @param array $options
*
* @return array the authenticate parameters
*/
public function authenticate($options)
{
if (!$this->login_phase) {
return;
}

$options['user'] = $this->login_phase['username'];
$options['pass'] = $this->login_phase['authorization'];
$this->rcmail->config->set('login_password_maxlen', strlen($this->login_phase['authorization']));

$this->log_debug("calling authenticate for user %s", $options['user']);

return $options;
}

/**
* Callback for 'login_after' hook
*
* @param array $options
*
* @return array
*/
public function login_after($options)
{
if (!$this->login_phase) {
return;
}

// save OAuth token in session
$_SESSION['oauth_token'] = $this->login_phase['token'];

$this->log_debug('login successful for OIDC sub=%s with username=%s which is rcube-id=%s',
$this->login_phase['token']['identity']['sub'], $this->login_phase['username'], $this->rcmail->user->ID);

// login phase is terminated
$this->login_phase = null;

return $options;
}

/**
* Callback for 'logout_after' hook
*
Expand Down Expand Up @@ -1045,7 +1098,11 @@ public function startup(array $args)
if ($args['task'] == 'login' && $args['action'] == 'oauth') {
// handle oauth login requests
$oauth_handler = new rcmail_action_login_oauth();
$oauth_handler->run();
$handler_answer = $oauth_handler->run();
if ($handler_answer && is_array($handler_answer)) {
// on success, handler will request next action = login
$args = $handler_answer + $args;
}
}
elseif ($args['task'] == 'login' && $args['action'] == 'backchannel') {
// handle oauth login requests
Expand Down Expand Up @@ -1114,6 +1171,7 @@ public function login_failed($options)
{
// no redirect on imap login failures
$this->no_redirect = true;
$this->login_phase = null;
return $options;
}

Expand Down

0 comments on commit a85be03

Please sign in to comment.