pull/7425/merge
Thomas B 4 years ago committed by GitHub
commit 55a4798532
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,12 +14,13 @@
"pear/auth_sasl": "~1.1.0",
"pear/net_idna2": "~0.2.0",
"pear/mail_mime": "~1.10.0",
"pear/net_smtp": "~1.8.1",
"pear/net_smtp": "~1.9.0",
"pear/crypt_gpg": "~1.6.3",
"pear/net_sieve": "~1.4.3",
"roundcube/plugin-installer": "~0.1.6",
"masterminds/html5": "~2.5.0",
"endroid/qr-code": "~1.6.5"
"endroid/qr-code": "~1.6.5",
"guzzlehttp/guzzle": "^5.3.3"
},
"require-dev": {
"phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6 || ^7"

@ -318,6 +318,84 @@ $config['smtp_timeout'] = 0;
$config['smtp_conn_options'] = null;
// ----------------------------------
// OAuth
// ----------------------------------
// Enable OAuth2 by defining a provider. Use 'generic' here
$config['oauth_provider'] = null;
// Provider name to be displayed on the login button
$config['oauth_provider_name'] = 'Google';
// Mandatory: OAuth client ID for your Roundcube installation
$config['oauth_client_id'] = null;
// Mandatory: OAuth client secret
$config['oauth_client_secret'] = null;
// Mandatory: URI for OAuth user authentication (redirect)
$config['oauth_auth_uri'] = null;
// Mandatory: Endpoint for OAuth authentication requests (server-to-server)
$config['oauth_token_uri'] = null;
// Optional: Endpoint to query user identity if not provided in auth response
$config['oauth_identity_uri'] = null;
// Optional: disable SSL certificate check on HTTP requests to OAuth server
// See http://docs.guzzlephp.org/en/stable/request-options.html#verify for possible values
$config['oauth_verify_peer'] = true;
// Mandatory: OAuth scopes to request (space-separated string)
$config['oauth_scope'] = null;
// Optional: additional query parameters to send with login request (hash array)
$config['oauth_auth_parameters'] = [];
// Optional: array of field names used to resolve the username within the identity information
$config['oauth_identity_fields'] = null;
// Boolean: automatically redirect to OAuth login when opening Roundcube without a valid session
$config['oauth_login_redirect'] = false;
///// Example config for Gmail
// Register your service at https://console.developers.google.com/
// - use https://<your-roundcube-url>/index.php/login/oauth as redirect URL
// $config['default_host'] = 'ssl://imap.gmail.com';
// $config['oauth_provider'] = 'google';
// $config['oauth_provider_name'] = 'Google';
// $config['oauth_client_id'] = "<your-credentials-client-id>";
// $config['oauth_client_secret'] = "<your-credentials-client-secret>";
// $config['oauth_auth_uri'] = "https://accounts.google.com/o/oauth2/auth";
// $config['oauth_token_uri'] = "https://oauth2.googleapis.com/token";
// $config['oauth_identity_uri'] = 'https://www.googleapis.com/oauth2/v1/userinfo';
// $config['oauth_scope'] = "email profile openid https://mail.google.com/";
// $config['oauth_auth_parameters'] = ['access_type' => 'offline', 'prompt' => 'consent'];
///// Example config for Outlook.com (Office 365)
// Register your OAuth client at https://portal.azure.com
// - use https://<your-roundcube-url>/index.php/login/oauth as redirect URL
// - grant permissions to Microsoft Graph API "IMAP.AccessAsUser.All", "SMTP.Send", "User.Read" and "offline_access"
// $config['default_host'] = 'ssl://outlook.office365.com';
// $config['smtp_server'] = 'ssl://smtp.office365.com';
// $config['login_password_maxlen'] = 2048; // access tokens can get very long
// $config['oauth_provider'] = 'outlook';
// $config['oauth_provider_name'] = 'Outlook.com';
// $config['oauth_client_id'] = "<your-credentials-client-id>";
// $config['oauth_client_secret'] = "<your-credentials-client-secret>";
// $config['oauth_auth_uri'] = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
// $config['oauth_token_uri'] = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
// $config['oauth_identity_uri'] = "https://graph.microsoft.com/v1.0/me";
// $config['oauth_identity_fields'] = ['email', 'userPrincipalName'];
// $config['oauth_scope'] = "https://outlook.office365.com/IMAP.AccessAsUser.All https://outlook.office365.com/SMTP.Send User.Read offline_access";
// $config['oauth_auth_parameters'] = ['nonce' => mt_rand()];
// ----------------------------------
// LDAP
// ----------------------------------

@ -188,6 +188,11 @@ if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
}
}
// handle oauth login requests
else if ($RCMAIL->task == 'login' && $RCMAIL->action == 'oauth' && $RCMAIL->oauth->is_enabled()) {
include INSTALL_PATH . 'program/steps/login/oauth.inc';
}
// end session
else if ($RCMAIL->task == 'logout' && isset($_SESSION['user_id'])) {
$RCMAIL->request_security_check(rcube_utils::INPUT_GET | rcube_utils::INPUT_POST);

@ -58,6 +58,16 @@ if (@file_exists(INSTALL_PATH . 'vendor/autoload.php')) {
require INSTALL_PATH . 'vendor/autoload.php';
}
// translate PATH_INFO to _task and _action GET parameters
if (!empty($_SERVER['PATH_INFO']) && preg_match('!^/([a-z]+)/([a-z]+)$!', $_SERVER['PATH_INFO'], $m)) {
if (!isset($_GET['_task'])) {
$_GET['_task'] = $m[1];
}
if (!isset($_GET['_action'])) {
$_GET['_action'] = $m[2];
}
}
// include Roundcube Framework
require_once 'Roundcube/bootstrap.php';

@ -33,7 +33,7 @@ class rcmail extends rcube
*
* @var array
*/
static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','oauth','dummy');
/**
* Current task.
@ -140,6 +140,9 @@ class rcmail extends rcube
$GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
}
// load oauth manager
$this->oauth = rcmail_oauth::get_instance();
// run init method on all the plugins
$this->plugins->init($this, $this->task);
}

@ -0,0 +1,479 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| CONTENTS: |
| Roundcube OAuth2 utilities |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
/**
* Roundcube OAuth2 utilities
*
* @package Webmail
* @subpackage Utils
*/
class rcmail_oauth
{
/** @var rcmail */
protected $rcmail;
/** @var array */
protected $options = array();
/** @var string */
protected $last_error = null;
/** @var boolean */
protected $no_redirect = false;
/** @var rcmail_oauth */
static protected $instance;
/**
* Singleton factory
*
* @return rcmail_oauth The one and only instance
*/
static function get_instance($options = array())
{
if (!self::$instance) {
self::$instance = new rcmail_oauth($options);
self::$instance->init();
}
return self::$instance;
}
/**
* Object constructor
*
* @param array $options Config options:
*/
public function __construct($options = array())
{
$this->rcmail = rcmail::get_instance();
$this->options = (array) $options + array(
'provider' => $this->rcmail->config->get('oauth_provider'),
'auth_uri' => $this->rcmail->config->get('oauth_auth_uri'),
'token_uri' => $this->rcmail->config->get('oauth_token_uri'),
'client_id' => $this->rcmail->config->get('oauth_client_id'),
'client_secret' => $this->rcmail->config->get('oauth_client_secret'),
'identity_uri' => $this->rcmail->config->get('oauth_identity_uri'),
'identity_fields' => $this->rcmail->config->get('oauth_identity_fields', ['email']),
'scope' => $this->rcmail->config->get('oauth_scope'),
'verify_peer' => $this->rcmail->config->get('oauth_verify_peer', true),
'auth_parameters' => $this->rcmail->config->get('oauth_auth_parameters', array()),
'login_redirect' => $this->rcmail->config->get('oauth_login_redirect', false),
);
}
/**
* Initialize this instance
*
* @return void
*/
protected function init()
{
// subscrbe to storage and smtp init events
if ($this->is_enabled()) {
$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('logout_after', [$this, 'logout_after']);
$this->rcmail->plugins->register_hook('unauthenticated', [$this, 'unauthenticated']);
}
}
/**
* Check if OAuth is generally enabled in config
*
* @return boolean
*/
public function is_enabled()
{
return !empty($this->options['provider']) &&
!empty($this->options['token_uri']) &&
!empty($this->options['client_id']);
}
/**
* Compose a fully qualified redirect URI for auth requests
*
* @return string
*/
public function get_redirect_uri()
{
// rewrite redirect URL to not contain query parameters because some providers do not support this
return preg_replace('/\?_task=[a-z]+/', 'index.php/login/oauth', $this->rcmail->url([], true, true));
}
/**
* Getter for the last error occured
*
* @return mixed
*/
public function get_last_error()
{
return $this->last_error;
}
/**
* Helper method to decode a JWT
*
* @param string $jwt
* @return array Hash array with decoded body
*/
public function jwt_decode($jwt)
{
list($headb64, $bodyb64, $cryptob64) = explode('.', $jwt);
$header = json_decode(base64_decode($headb64), true);
$body = json_decode(base64_decode($bodyb64), true);
if (isset($body['azp']) && $body['azp'] !== $this->options['client_id']) {
throw new RuntimeException('Failed to validate JWT: invalid azp value');
} else if (isset($body['aud']) && $body['aud'] !== $this->options['client_id']) {
throw new RuntimeException('Failed to validate JWT: invalid aud value');
} else if (!isset($body['azp']) && !isset($body['aud'])) {
throw new RuntimeException('Failed to validate JWT: missing aud/azp value');
}
return $body;
}
/**
* Login action: redirect to `oauth_auth_uri`
*
* @return void
*/
public function login_redirect()
{
if (!empty($this->options['auth_uri']) && !empty($this->options['client_id'])) {
// create a secret string
$_SESSION['oauth_state'] = rcube_utils::random_bytes(12);
// compose full oauth login uri
$delimiter = strpos($this->options['auth_uri'], '?') > 0 ? '&' : '?';
$query = http_build_query([
'response_type' => 'code',
'client_id' => $this->options['client_id'],
'scope' => $this->options['scope'],
'redirect_uri' => $this->get_redirect_uri(),
'state' => $_SESSION['oauth_state'],
] + (array)$this->options['auth_parameters']);
$this->rcmail->output->redirect($this->options['auth_uri'] . $delimiter . $query); // exit
} else {
// log error about missing config options
rcube::raise_error(array(
'message' => "Missing required OAuth config options 'oauth_auth_uri', 'oauth_client_id'",
'file' => __FILE__,
'line' => __LINE__,
), true, false);
}
}
/**
* Request access token with auth code returned from oauth login
*
* @param string $auth_code
* @param string $state
* @return array Authorization data as hash array with entries
* `username` as the authentication user name
* `authorization` as the oauth authorization string "<type> <access-token>"
* `token` as the complete oauth response to be stored in session
*/
public function request_access_token($auth_code, $state = null)
{
$oauth_token_uri = $this->options['token_uri'];
$oauth_client_id = $this->options['client_id'];
$oauth_client_secret = $this->options['client_secret'];
$oauth_identity_uri = $this->options['identity_uri'];
if (!empty($oauth_token_uri) && !empty($oauth_client_secret)) {
// validate state parameter against $_SESSION['oauth_state']
if (!empty($_SESSION['oauth_state']) && $_SESSION['oauth_state'] !== $state) {
throw new RuntimeException('Invalid state parameter');
}
// send token request to get a real access token for the given auth code
try {
$client = new Client([
'timeout' => 10.0,
'verify' => $this->options['verify_peer'],
]);
$response = $client->post($oauth_token_uri, array(
'body' => array(
'code' => $auth_code,
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client_secret,
'redirect_uri' => $this->get_redirect_uri(),
'grant_type' => 'authorization_code',
),
));
$data = $response->json();
// auth success
if (!empty($data['access_token'])) {
$username = null;
$authorization = sprintf('%s %s', $data['token_type'], $data['access_token']);
// decode JWT id_token if provided
if (!empty($data['id_token'])) {
try {
$identity = $this->jwt_decode($data['id_token']);
foreach ($this->options['identity_fields'] as $field) {
if (isset($identity[$field])) {
$username = $identity[$field];
unset($data['id_token']);
break;
}
}
} catch (\Exception $e) {
// log error
rcube::raise_error(array(
'message' => $e->getMessage(),
'file' => __FILE__,
'line' => __LINE__,
), true, false);
}
}
// request user identity (email)
if (empty($username) && !empty($oauth_identity_uri)) {
$identity = $client->get($oauth_identity_uri, array(
'headers' => array(
'Authorization' => $authorization,
'Accept' => 'application/json',
),
))->json();
foreach ($this->options['identity_fields'] as $field) {
if (isset($identity[$field])) {
$username = $identity[$field];
break;
}
}
}
$data['identity'] = $username;
$this->mask_auth_data($data);
$this->rcmail->session->remove('oauth_state');
// return auth data
return array(
'username' => $username,
'authorization' => $authorization,
'token' => $data,
);
} else {
throw new Exception('Unexpected response from OAuth service');
}
} catch (RequestException $e) {
$this->last_error = "OAuth token request failed: " . $e->getMessage();
rcube::raise_error(array(
'message' => $this->last_error . '; ' . $e->getResponse(),
'file' => __FILE__,
'line' => __LINE__,
), true, false);
return false;
} catch (Exception $e) {
$this->last_error = "OAuth token request failed: " . $e->getMessage();
rcube::raise_error(array(
'message' => $this->last_error,
'file' => __FILE__,
'line' => __LINE__,
), true, false);
return false;
}
} else {
$this->last_error = "Missing required OAuth config options 'oauth_token_uri', 'oauth_client_id', 'oauth_client_secret'";
rcube::raise_error(array(
'message' => $this->last_error,
'file' => __FILE__,
'line' => __LINE__,
), true, false);
return false;
}
}
/**
* Obtain a new access token using the refresh_token grant type
*
* If successful, this will update the `oauth_token` entry in
* session data.
*
* @param array $token
* @return array Updated authorization data
*/
public function refresh_access_token(array $token)
{
$oauth_token_uri = $this->options['token_uri'];
$oauth_client_id = $this->options['client_id'];
$oauth_client_secret = $this->options['client_secret'];
// send token request to get a real access token for the given auth code
try {
$client = new Client([
'timeout' => 10.0,
'verify' => $this->options['verify_peer'],
]);
$response = $client->post($oauth_token_uri, array(
'body' => array(
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client_secret,
'refresh_token' => $this->rcmail->decrypt($token['refresh_token']),
'grant_type' => 'refresh_token',
),
));
$data = $response->json();
// auth success
if (!empty($data['access_token'])) {
// update access token stored as password
$authorization = sprintf('%s %s', $data['token_type'], $data['access_token']);
$_SESSION['password'] = $this->rcmail->encrypt($authorization);
$this->mask_auth_data($data);
// update session data
$_SESSION['oauth_token'] = array_merge($token, $data);
return [
'token' => $data,
'authorization' => $authorization,
];
}
} catch (RequestException $e) {
$this->last_error = "OAuth refresh token request failed: " . $e->getMessage();
rcube::raise_error(array(
'message' => $this->last_error . '; ' . $e->getResponse(),
'file' => __FILE__,
'line' => __LINE__,
), true, false);
return false;
} catch (Exception $e) {
$this->last_error = "OAuth refresh token request failed: " . $e->getMessage();
rcube::raise_error(array(
'message' => $this->last_error,
'file' => __FILE__,
'line' => __LINE__,
), true, false);
return false;
}
}
/**
* Modify some properties of the received auth response
*
* @param array $token
* @return void
*/
protected function mask_auth_data(&$data)
{
// compute absolute token expiration date
$data['expires'] = time() + $data['expires_in'] - 600;
// encrypt refresh token if provided
if (isset($data['refresh_token'])) {
$data['refresh_token'] = $this->rcmail->encrypt($data['refresh_token']);
}
}
/**
* Check the given access token data if still valid
*
* ... and attempt to refresh if possible.
*
* @param array $token
* @return void
*/
protected function check_token_validity($token)
{
if ($token['expires'] < time() && isset($token['refresh_token'])) {
$this->refresh_access_token($token);
}
}
/**
* Callback for 'storage_init' hook
*
* @param array $options
* @return array
*/
public function storage_init($options)
{
if (isset($_SESSION['oauth_token']) && $options['driver'] === 'imap') {
// check token validity
$this->check_token_validity($_SESSION['oauth_token']);
// enforce XOAUTH2 authorization type
$options['auth_type'] = 'XOAUTH2';
}
return $options;
}
/**
* Callback for 'smtp_connect' hook
*
* @param array $options
* @return array
*/
public function smtp_connect($options)
{
if (isset($_SESSION['oauth_token'])) {
// check token validity
$this->check_token_validity($_SESSION['oauth_token']);
// enforce XOAUTH2 authorization type
$options['smtp_user'] = '%u';
$options['smtp_pass'] = '%p';
$options['smtp_auth_type'] = 'XOAUTH2';
}
return $options;
}
/**
* Callback for 'logout_after' hook
*
* @param array $options
* @return array
*/
public function logout_after($options)
{
$this->no_redirect = true;
}
/**
* Callback for 'unauthenticated' hook
*
* @param array $options
* @return array
*/
public function unauthenticated($options)
{
if ($this->options['login_redirect'] &&
!$this->rcmail->output->ajax_call &&
!$this->no_redirect &&
empty($options['error']) &&
$options['http_code'] === 200
) {
$this->login_redirect();
}
return $options;
}
}

@ -2235,6 +2235,12 @@ EOF;
$out .= html::p('formbuttons', html::tag('button', $button_attr, $this->app->gettext('login')));
}
// add oauth login button
if ($this->config->get('oauth_auth_uri') && $this->config->get('oauth_provider')) {
$link_attr = array('href' => $this->app->url(array('action' => 'oauth')), 'id' => 'rcmloginoauth', 'class' => 'button oauth ' . $this->config->get('oauth_provider'));
$out .= html::p('oauthlogin', html::a($link_attr, $this->app->gettext(array('name' => 'oauthlogin', 'vars' => array('provider' => $this->config->get('oauth_provider_name', 'OAuth'))))));
}
// surround html output with a form tag
if (empty($attrib['form'])) {
$out = $this->form_tag(array('name' => $form_name, 'method' => 'post'), $out);

@ -769,6 +769,20 @@ class rcube_imap_generic
$line = $this->readReply();
$result = $this->parseResult($line);
}
else if ($type == 'XOAUTH2') {
$auth = base64_encode("user=$user\1auth=$pass\1\1");
$this->putLine($this->nextTag() . " AUTHENTICATE XOAUTH2 $auth", true, true);
$line = trim($this->readReply());
if ($line[0] == '+') {
// send empty line
$this->putLine('', true, true);
$line = $this->readReply();
}
$result = $this->parseResult($line);
}
if ($result === self::ERROR_OK) {
// optional CAPABILITY response
@ -959,6 +973,7 @@ class rcube_imap_generic
case 'GSSAPI':
case 'PLAIN':
case 'LOGIN':
case 'XOAUTH2':
$result = $this->authenticate($user, $password, $auth_method);
break;

@ -22,6 +22,7 @@ $labels['username'] = 'Username';
$labels['password'] = 'Password';
$labels['server'] = 'Server';
$labels['login'] = 'Login';
$labels['oauthlogin'] = 'Login with $provider';
// taskbar
$labels['menu'] = 'Menu';

@ -224,3 +224,4 @@ $messages['listempty'] = 'The list is empty.';
$messages['listusebutton'] = 'Use the Create button to add a new record.';
$messages['keypaircreatesuccess'] = 'A new key pair has been successfully created for $identity.';
$messages['emptyattachment'] = 'This attachment appears to be empty.<br>Please, check with the person who sent this.';
$messages['oauthloginfailed'] = 'OAuth login failed. Please try again.';

@ -0,0 +1,88 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Perform OAuth2 user login |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$rcmail = rcmail::get_instance();
$auth_code = rcube_utils::get_input_value('code', rcube_utils::INPUT_GET);
$auth_error = rcube_utils::get_input_value('error', rcube_utils::INPUT_GET);
// auth code return from oauth login
if (!empty($auth_code)) {
$auth = $rcmail->oauth->request_access_token($auth_code, rcube_utils::get_input_value('state', rcube_utils::INPUT_GET));
// oauth success
if ($auth && isset($auth['username'], $auth['authorization'], $auth['token'])) {
// enforce XOAUTH2 auth type
$rcmail->config->set('imap_auth_type', 'XOAUTH2');
// 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'];
// log successful login
$rcmail->log_login();
// allow plugins to control the redirect url after login success
$redir = $rcmail->plugins->exec_hook('login_after', array('_task' => 'mail'));
unset($redir['abort'], $redir['_err']);
// send redirect
header('Location: ' . $rcmail->url($redir, true, false, $secure));
exit;
} else {
$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', array(
'code' => $error_code,
'host' => $storage_host,
'user' => $auth['username'],
));
$rcmail->kill_session();
// fall through -> login page
}
} else {
$OUTPUT->show_message('oauthloginfailed', 'warning');
}
}
// error return from oauth login
else if (!empty($auth_error)) {
$error_message = rcube_utils::get_input_value('error_description', rcube_utils::INPUT_GET) ?: $auth_error;
$OUTPUT->show_message($error_message, 'warning');
}
// login action: redirect to `oauth_auth_uri`
else if ($rcmail->task === 'login') {
// this will always exit() the process
$rcmail->oauth->login_redirect();
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#4285F4" d="M45.12 24.5c0-1.56-.14-3.06-.4-4.5H24v8.51h11.84c-.51 2.75-2.06 5.08-4.39 6.64v5.52h7.11c4.16-3.83 6.56-9.47 6.56-16.17z"></path><path fill="#34A853" d="M24 46c5.94 0 10.92-1.97 14.56-5.33l-7.11-5.52c-1.97 1.32-4.49 2.1-7.45 2.1-5.73 0-10.58-3.87-12.31-9.07H4.34v5.7C7.96 41.07 15.4 46 24 46z"></path><path fill="#FBBC05" d="M11.69 28.18C11.25 26.86 11 25.45 11 24s.25-2.86.69-4.18v-5.7H4.34C2.85 17.09 2 20.45 2 24c0 3.55.85 6.91 2.34 9.88l7.35-5.7z"></path><path fill="#EA4335" d="M24 10.75c3.23 0 6.13 1.11 8.41 3.29l6.31-6.31C34.91 4.18 29.93 2 24 2 15.4 2 7.96 6.93 4.34 14.12l7.35 5.7c1.73-5.2 6.58-9.07 12.31-9.07z"></path><path fill="none" d="M2 2h44v44H2z"></path></svg>

After

Width:  |  Height:  |  Size: 764 B

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 116 116" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M26.088,145.515L77.643,145.515L77.639,197.07L26.088,197.07L26.088,145.515Z" style="fill:rgb(246,83,20);"/></g>
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M82.907,145.515L134.462,145.515C134.462,162.7 134.465,179.885 134.459,197.07C117.277,197.067 100.092,197.07 82.91,197.07C82.904,179.885 82.907,162.7 82.907,145.515Z" style="fill:rgb(127,187,65);"/></g>
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M26.088,202.331C43.273,202.337 60.458,202.329 77.643,202.337C77.646,219.522 77.643,236.704 77.643,253.889L26.088,253.889L26.088,202.331Z" style="fill:rgb(0,161,241);"/></g>
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M82.91,202.337C100.092,202.331 117.277,202.334 134.462,202.334L134.462,253.889L82.907,253.889C82.91,236.704 82.904,219.519 82.91,202.337Z" style="fill:rgb(255,187,0);"/></g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -56,6 +56,12 @@
margin-right: 0;
margin-left: 0;
}
.oauthlogin {
margin-top: 1em;
padding-top: 1em;
border-top: 1px solid #ccc;
}
}
#rcmloginsubmit {

@ -149,6 +149,24 @@ button.btn {
vertical-align: middle;
margin-right: .4rem !important; // !important needed for a.btn
}
&.oauth.google:before,
&.oauth.outlook:before {
content: " ";
display: inline-block !important;
height: 1.5rem;
width: 1.5rem;
margin-right: .8rem !important;
background-size: 100% auto;
}
&.oauth.google:before {
background: url('../images/google-icon.svg') top left no-repeat;
}
&.oauth.outlook:before {
background: url('../images/microsoft-icon.svg') top left no-repeat;
}
}
a.button.icon {

@ -1064,6 +1064,7 @@ function rcube_elastic_ui()
// Make logon form prettier
if (rcmail.env.task == 'login' && context == document) {
$('#rcmloginsubmit').addClass('btn-lg text-uppercase w-100');
$('#rcmloginoauth').addClass('btn btn-secondary btn-lg w-100');
$('#login-form table tr').each(function() {
var input = $('input,select', this),
label = $('label', this),

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#4285F4" d="M45.12 24.5c0-1.56-.14-3.06-.4-4.5H24v8.51h11.84c-.51 2.75-2.06 5.08-4.39 6.64v5.52h7.11c4.16-3.83 6.56-9.47 6.56-16.17z"></path><path fill="#34A853" d="M24 46c5.94 0 10.92-1.97 14.56-5.33l-7.11-5.52c-1.97 1.32-4.49 2.1-7.45 2.1-5.73 0-10.58-3.87-12.31-9.07H4.34v5.7C7.96 41.07 15.4 46 24 46z"></path><path fill="#FBBC05" d="M11.69 28.18C11.25 26.86 11 25.45 11 24s.25-2.86.69-4.18v-5.7H4.34C2.85 17.09 2 20.45 2 24c0 3.55.85 6.91 2.34 9.88l7.35-5.7z"></path><path fill="#EA4335" d="M24 10.75c3.23 0 6.13 1.11 8.41 3.29l6.31-6.31C34.91 4.18 29.93 2 24 2 15.4 2 7.96 6.93 4.34 14.12l7.35 5.7c1.73-5.2 6.58-9.07 12.31-9.07z"></path><path fill="none" d="M2 2h44v44H2z"></path></svg>

After

Width:  |  Height:  |  Size: 764 B

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 116 116" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M26.088,145.515L77.643,145.515L77.639,197.07L26.088,197.07L26.088,145.515Z" style="fill:rgb(246,83,20);"/></g>
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M82.907,145.515L134.462,145.515C134.462,162.7 134.465,179.885 134.459,197.07C117.277,197.067 100.092,197.07 82.91,197.07C82.904,179.885 82.907,162.7 82.907,145.515Z" style="fill:rgb(127,187,65);"/></g>
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M26.088,202.331C43.273,202.337 60.458,202.329 77.643,202.337C77.646,219.522 77.643,236.704 77.643,253.889L26.088,253.889L26.088,202.331Z" style="fill:rgb(0,161,241);"/></g>
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M82.91,202.337C100.092,202.331 117.277,202.334 134.462,202.334L134.462,253.889L82.907,253.889C82.91,236.704 82.904,219.519 82.91,202.337Z" style="fill:rgb(255,187,0);"/></g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -126,7 +126,8 @@ input.button {
}
.formbuttons button,
.formbuttons input.button {
.formbuttons input.button,
.oauthlogin .button {
color: #ddd;
font-size: 110%;
padding: 4px 12px;
@ -1762,13 +1763,13 @@ ul.proplist.simplelist li {
border-color: #666;
}
#login-form button.button {
#login-form .button {
color: #444;
border-color: #f9f9f9;
background-color: #f9f9f9;
}
#login-form button.button:active {
#login-form .button:active {
color: #333;
background-color: #dcdcdc;
}
@ -1790,6 +1791,38 @@ ul.proplist.simplelist li {
text-align: center;
}
#login-form p.oauthlogin {
margin-top: 1.5em;
padding-top: 1.5em;
text-align: center;
border-top: 1px solid #666;
}
#login-form p.oauthlogin .button.oauth.google,
#login-form p.oauthlogin .button.oauth.outlook {
line-height: 1.5;
}
#login-form p.oauthlogin .button.oauth.google:before,
#login-form p.oauthlogin .button.oauth.outlook:before {
content: " ";
display: inline-block !important;
height: 1.6em;
width: 1.4em;
margin-right: .5em !important;
background-size: 100% auto;
vertical-align: middle;
}
#login-form p.oauthlogin .button.oauth.google:before {
background: url('./images/google-icon.svg') top left no-repeat;
}
#login-form p.oauthlogin .button.oauth.outlook:before {
background: url('./images/microsoft-icon.svg') top left no-repeat;
}
#login-form #logo {
margin-bottom: 20px;
border: none;

Loading…
Cancel
Save