diff --git a/account.php b/account.php index 657f0983153d4c47eca69838e712812c2186b996..295073f881172d251c46a29d5e10300b0a19204f 100644 --- a/account.php +++ b/account.php @@ -98,14 +98,13 @@ elseif ($_POST) { $acct->sendConfirmEmail(); break; case 'import': - foreach (UserAuthenticationBackend::allRegistered() as $bk) { - if ($bk::$id == $_POST['backend']) { - $cl = new ClientSession(new EndUser($user)); - $acct->confirm(); - if ($user = $bk->login($cl, $bk)) - Http::redirect('tickets.php'); - break; - } + if ($bk = UserAuthenticationBackend::getBackend($_POST['backend'])) { + $cl = new ClientSession(new EndUser($user)); + if (!$bk->supportsInteractiveAuthentication()) + $acct->set('backend', null); + $acct->confirm(); + if ($user = $bk->login($cl, $bk)) + Http::redirect('tickets.php'); } break; } diff --git a/api/api.inc.php b/api/api.inc.php index ecde89f9b7d8c6c7aa6a6f1082c39fd5a9484c6d..fac03bccd1d848308809e78343838d5401325771 100644 --- a/api/api.inc.php +++ b/api/api.inc.php @@ -17,8 +17,6 @@ file_exists('../main.inc.php') or die('System Error'); // Disable sessions for the API. API should be considered stateless and // shouldn't chew up database records to store sessions -if (!function_exists('noop')) { function noop() {} } -session_set_save_handler('noop','noop','noop','noop','noop','noop'); define('DISABLE_SESSION', true); require_once('../main.inc.php'); diff --git a/api/http.php b/api/http.php index 90926d1e5b531771c057ca82276711520c305b9e..2efd1a98c271d103987ffd2eda667e738e5812cd 100644 --- a/api/http.php +++ b/api/http.php @@ -25,6 +25,8 @@ $dispatcher = patterns('', )) ); +Signal::send('api', $dispatcher); + # Call the respective function print $dispatcher->resolve($ost->get_path_info()); ?> diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index 3c558720ba239b9e2ef28648102d5f5aaa6737b1..2166850b3a402f0e4109d62e81034af4e56b8b69 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -522,7 +522,7 @@ body { color: #555; } #ticketForm div.clear, -#clientLogin div { +#clientLogin div.clear { clear: both; padding: 3px 0; overflow: hidden; @@ -643,14 +643,20 @@ label.required { color: #d00; display: block; } -#clientLogin #email { - width: 250px; - margin-right: 0; -} +#clientLogin #email, #clientLogin #ticketno { - width: 120px; margin-right: 0; } +#clientLogin input[type=text], +#clientLogin input[type=password] { + padding: 5px; + border-radius: 4px; + margin-bottom: 15px; +} +#clientLogin input[type=submit] { + padding: 3px 10px; + border-radius: 4px; +} #reply { margin-top: 20px; padding: 10px 5px; @@ -876,3 +882,37 @@ table.padded tr > th { height: 20px; padding-bottom: 5px; } + +.external-auth + .external-auth { + margin-top: 4px; +} + +a.external-sign-in { + text-decoration: none; +} +.external-auth-box { + vertical-align: middle; + border-radius: 4px; + border: 1px solid #777; +} +.external-auth-icon { + display: inline-block; + color: #333; + width: 30px; + padding: 5px 10px; + border-right: 1px solid #ddd; +} +.external-auth-name { + color: #333; + width: 100px; + padding: 5px 10px; + line-height:30px; + font-size: 11pt; +} +img.sign-in-image { + border: none; + max-height: 40px; + max-width: 200px; + width: auto; + height: auto; +} diff --git a/include/class.auth.php b/include/class.auth.php index b28d0c9876c87cc18810700d3703ab9fcbc878bc..39498a474578f4c87bf4f77bac651408421de4c5 100644 --- a/include/class.auth.php +++ b/include/class.auth.php @@ -223,6 +223,9 @@ abstract class AuthenticationBackend { return $result; } + elseif ($result instanceof ClientCreateRequest + && $bk instanceof UserAuthenticationBackend) + return $result; elseif ($result instanceof AccessDenied) { break; } @@ -308,8 +311,32 @@ abstract class AuthenticationBackend { abstract static function signOut($user); } -class RemoteAuthenticationBackend { - var $create_unknown_user = false; +/** + * ExternalAuthenticationBackend + * + * External authentication backends are backends such as Google+ which + * require a redirect to a remote site and a redirect back to osTicket in + * order for a user to be authenticated. For such backends, neither the + * username and password fields nor single sign on alone can be used to + * authenticate the user. + */ +interface ExternalAuthentication { + + /** + * Requests the backend to render an external link box. When the user + * clicks this box, the backend will be prompted to redirect the user to + * the remote site for authentication there. + */ + function renderExternalLink(); + + /** + * Function: triggerAuth + * + * Called when a user clicks the button rendered in the + * ::renderExternalLink() function. This method should initiate the + * remote authentication mechanism. + */ + function triggerAuth(); } abstract class StaffAuthenticationBackend extends AuthenticationBackend { @@ -454,6 +481,50 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend { } } +abstract class ExternalStaffAuthenticationBackend + extends StaffAuthenticationBackend + implements ExternalAuthentication { + + static $fa_icon = "signin"; + static $sign_in_image_url = false; + static $service_name = "External"; + + function renderExternalLink() { ?> + <a class="external-sign-in" title="Sign in with <?php echo static::$service_name; ?>" + href="login.php?do=ext&bk=<?php echo urlencode(static::$id); ?>"> +<?php if (static::$sign_in_image_url) { ?> + <img class="sign-in-image" src="<?php echo static::$sign_in_image_url; + ?>" alt="Sign in with <?php echo static::$service_name; ?>"/> +<?php } else { ?> + <div class="external-auth-box"> + <span class="external-auth-icon"> + <i class="icon-<?php echo static::$fa_icon; ?> icon-large icon-fixed-with"></i> + </span> + <span class="external-auth-name"> + Sign in with <?php echo static::$service_name; ?> + </span> + </div> +<?php } ?> + </a><?php + } + + function triggerAuth() { + $_SESSION['ext:bk:class'] = get_class($this); + } +} +Signal::connect('api', function($dispatcher) { + $dispatcher->append( + url('^/auth/ext$', function() { + if ($class = $_SESSION['ext:bk:class']) { + $bk = StaffAuthenticationBackend::getBackend($class::$id) + ?: UserAuthenticationBackend::getBackend($class::$id); + if ($bk instanceof ExternalAuthentication) + $bk->triggerAuth(); + } + }) + ); +}); + abstract class UserAuthenticationBackend extends AuthenticationBackend { static private $_registry = array(); @@ -580,6 +651,38 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend { } } +abstract class ExternalUserAuthenticationBackend + extends UserAuthenticationBackend + implements ExternalAuthentication { + + static $fa_icon = "signin"; + static $sign_in_image_url = false; + static $service_name = "External"; + + function renderExternalLink() { ?> + <a class="external-sign-in" title="Sign in with <?php echo static::$service_name; ?>" + href="login.php?do=ext&bk=<?php echo urlencode(static::$id); ?>"> +<?php if (static::$sign_in_image_url) { ?> + <img class="sign-in-image" src="<?php echo static::$sign_in_image_url; + ?>" alt="Sign in with <?php echo static::$service_name; ?>"/> +<?php } else { ?> + <div class="external-auth-box"> + <span class="external-auth-icon"> + <i class="icon-<?php echo static::$fa_icon; ?> icon-large icon-fixed-with"></i> + </span> + <span class="external-auth-name"> + Sign in with <?php echo static::$service_name; ?> + </span> + </div> +<?php } ?> + </a><?php + } + + function triggerAuth() { + $_SESSION['ext:bk:class'] = get_class($this); + } +} + /** * This will be an exception in later versions of PHP */ diff --git a/include/class.forms.php b/include/class.forms.php index 257f4ab9a1958d9269b78ba0e11f1d76d3b0487e..db13f4a436f217b128f57a2c75c621ce42f79bb7 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -146,12 +146,17 @@ class FormField { ), ); static $more_types = array(); + static $uid = 100; function __construct($options=array()) { - static $uid = 100; $this->ht = array_merge($this->ht, $options); if (!isset($this->ht['id'])) - $this->ht['id'] = $uid++; + $this->ht['id'] = self::$uid++; + } + + function __clone() { + $this->_widget = null; + $this->ht['id'] = self::$uid++; } static function addFieldTypes($group, $callable) { diff --git a/include/class.ostsession.php b/include/class.ostsession.php index beb344f9f3734f5e286e9d2e4aff0060d6406cb2..dc53af14ef11f7eda69ae94fbb78db6e1cfafd93 100644 --- a/include/class.ostsession.php +++ b/include/class.ostsession.php @@ -35,9 +35,6 @@ class osTicketSession { if (OsticketConfig::getDBVersion()) return session_start(); - elseif (defined('DISABLE_SESSION')) - return; - # Cookies // Avoid setting a cookie domain without a dot, thanks // http://stackoverflow.com/a/1188145 @@ -80,6 +77,7 @@ class osTicketSession { } function read($id){ + $this->isnew = true; if (!$this->data || $this->id != $id) { $sql='SELECT session_data FROM '.SESSION_TABLE .' WHERE session_id='.db_input($id) @@ -91,6 +89,7 @@ class osTicketSession { $this->id = $id; } $this->data_hash = md5($id.$this->data); + $this->isnew = false; return $this->data; } @@ -100,6 +99,9 @@ class osTicketSession { if (md5($id.$data) == $this->data_hash) return; + elseif (defined('DISABLE_SESSION') && $this->isnew) + return; + $ttl = ($this && get_class($this) == 'osTicketSession') ? $this->getTTL() : SESSION_TTL; diff --git a/include/class.user.php b/include/class.user.php index 89a35ab46f54b35e91a4810ffc654510e20e8e68..041b7eac3eb1d78a488f979d28138815b26bea81 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -775,8 +775,7 @@ class UserAccountModel extends VerySimpleModel { } function isPasswdResetEnabled() { - return !$this->hasStatus(UserAccountStatus::FORBID_PASSWD_RESET) - && (!$this->backend || $this->backend == 'client'); + return !$this->hasStatus(UserAccountStatus::FORBID_PASSWD_RESET); } function getStatus() { @@ -917,8 +916,13 @@ class UserAccount extends UserAccountModel { return $this->save(true); } - static function createForUser($user) { - return static::create(array('user_id'=>$user->getId())); + static function createForUser($user, $defaults=false) { + $acct = static::create(array('user_id'=>$user->getId())); + if ($defaults && is_array($defaults)) { + foreach ($defaults as $k => $v) + $acct->set($k, $v); + } + return $acct; } static function lookupByUsername($username) { diff --git a/include/client/accesslink.inc.php b/include/client/accesslink.inc.php index 0f1ef8c319afb1b388bc0710dd6e4fc30809ceae..a87d08d58767eaa189cba82e5196d2c1698e0624 100644 --- a/include/client/accesslink.inc.php +++ b/include/client/accesslink.inc.php @@ -14,12 +14,14 @@ link will be emailed to you.</p> <strong><?php echo Format::htmlchars($errors['login']); ?></strong> <br> <div> - <label for="email">E-Mail Address:</label><br/> - <input id="email" type="text" name="lemail" size="30" value="<?php echo $email; ?>"> + <label for="email">E-Mail Address: + <input id="email" placeholder="e.g. john.doe@osticket.com" type="text" + name="lemail" size="30" value="<?php echo $email; ?>"></label> </div> <div> <label for="ticketno">Ticket Number:</label><br/> - <input id="ticketno" type="text" name="lticket" size="16" value="<?php echo $ticketid; ?>"></td> + <input id="ticketno" type="text" name="lticket" placeholder="e.g. 051243" + size="30" value="<?php echo $ticketid; ?>"></td> </div> <p> <input class="btn" type="submit" value="Email Access Link"> @@ -28,9 +30,9 @@ link will be emailed to you.</p> <div style="display:table-cell;padding-left: 2em;padding-right:90px;"> <?php if ($cfg && $cfg->getClientRegistrationMode() !== 'disabled') { ?> Have an account with us? - <a href="account.php?do=create">Sign In</a> <?php + <a href="login.php">Sign In</a> <?php if ($cfg->isClientRegistrationEnabled()) { ?> - or <a href="login.php?do=create">register for an account</a> <?php + or <a href="account.php?do=create">register for an account</a> <?php } ?> to access all your tickets. <?php } ?> diff --git a/include/client/login.inc.php b/include/client/login.inc.php index fd1d7bc80abac91077654ae8a58d2cfc285b0444..062e1db7cd7ef443d23b9d1dbd8f422f8a1b730e 100644 --- a/include/client/login.inc.php +++ b/include/client/login.inc.php @@ -21,16 +21,13 @@ if ($content) { <form action="login.php" method="post" id="clientLogin"> <?php csrf_token(); ?> <div style="display:table-row"> - <div style="width:40%;display:table-cell;box-shadow: 12px 0 15px -15px rgba(0,0,0,0.4);padding-left: 2em;"> + <div style="width:40%;display:table-cell;box-shadow: 12px 0 15px -15px rgba(0,0,0,0.4);padding:15px;"> <strong><?php echo Format::htmlchars($errors['login']); ?></strong> - <br> <div> - <label for="username">Username:</label> - <input id="username" type="text" name="luser" size="30" value="<?php echo $email; ?>"> + <input id="username" placeholder="Email or Username" type="text" name="luser" size="30" value="<?php echo $email; ?>"> </div> <div> - <label for="passwd">Password:</label> - <input id="passwd" type="password" name="lpasswd" size="30" value="<?php echo $passwd; ?>"></td> + <input id="passwd" placeholder="Password" type="password" name="lpasswd" size="30" value="<?php echo $passwd; ?>"></td> </div> <p> <input class="btn" type="submit" value="Sign In"> @@ -39,9 +36,22 @@ if ($content) { <?php } ?> </p> </div> - <div style="display:table-cell;padding-left: 2em;"> -<?php if ($cfg && $cfg->isClientRegistrationEnabled()) { ?> - Not yet registered? <a href="account.php?do=create">Create an account</a> + <div style="display:table-cell;padding: 15px;vertical-align:top"> +<?php + +$ext_bks = array(); +foreach (UserAuthenticationBackend::allRegistered() as $bk) + if ($bk instanceof ExternalAuthentication) + $ext_bks[] = $bk; + +if (count($ext_bks)) { + foreach ($ext_bks as $bk) { ?> +<div class="external-auth"><?php $bk->renderExternalLink(); ?></div><?php + } +} +if ($cfg && $cfg->isClientRegistrationEnabled()) { + if (count($ext_bks)) echo '<hr style="width:70%"/>'; ?> + Not yet registered? <a href="account.php?do=create">Create an account</a> <?php } ?> </div> </div> diff --git a/include/staff/login.header.php b/include/staff/login.header.php index f1494a96faa069ab08d6b3833c4ff8bada6eeb2c..7fecca1735d3196074a756d3cac68f2e8889ce01 100644 --- a/include/staff/login.header.php +++ b/include/staff/login.header.php @@ -8,6 +8,7 @@ defined('OSTSCPINC') or die('Invalid path'); <meta http-equiv="refresh" content="7200" /> <title>osTicket:: SCP Login</title> <link rel="stylesheet" href="css/login.css" type="text/css" /> + <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome.min.css"> <meta name="robots" content="noindex" /> <meta http-equiv="cache-control" content="no-cache" /> <meta http-equiv="pragma" content="no-cache" /> diff --git a/include/staff/login.tpl.php b/include/staff/login.tpl.php index d65aeea471a03ae27b7748e8fa82468ce5a73e35..9abf430f39b21bd340f6e39a1002c27ac1e3893f 100644 --- a/include/staff/login.tpl.php +++ b/include/staff/login.tpl.php @@ -5,19 +5,33 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array(); <div id="loginBox"> <h1 id="logo"><a href="index.php">osTicket Staff Control Panel</a></h1> <h3><?php echo Format::htmlchars($msg); ?></h3> - <div><small><?php echo ($content) ? Format::display($content->getBody()) : ''; ?></small></div> + <div class="banner"><small><?php echo ($content) ? Format::display($content->getBody()) : ''; ?></small></div> <form action="login.php" method="post"> <?php csrf_token(); ?> <input type="hidden" name="do" value="scplogin"> <fieldset> <input type="text" name="userid" id="name" value="<?php echo $info['userid']; ?>" placeholder="username" autocorrect="off" autocapitalize="off"> <input type="password" name="passwd" id="pass" placeholder="password" autocorrect="off" autocapitalize="off"> + <?php if ($show_reset && $cfg->allowPasswordReset()) { ?> + <h3 style="display:inline"><a href="pwreset.php">Forgot my password</a></h3> + <?php } ?> + <input class="submit" type="submit" name="submit" value="Log In"> </fieldset> - <?php if ($show_reset && $cfg->allowPasswordReset()) { ?> - <h3 style="display:inline"><a href="pwreset.php">Forgot my password</a></h3> - <?php } ?> - <input class="submit" type="submit" name="submit" value="Log In"> </form> +<?php +$ext_bks = array(); +foreach (StaffAuthenticationBackend::allRegistered() as $bk) + if ($bk instanceof ExternalAuthentication) + $ext_bks[] = $bk; + +if (count($ext_bks)) { ?> +<div class="or"> + <hr/> +</div><?php + foreach ($ext_bks as $bk) { ?> +<div class="external-auth"><?php $bk->renderExternalLink(); ?></div><?php + } +} ?> </div> <div id="copyRights">Copyright © <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div> </body> diff --git a/login.php b/login.php index 5d1faa1ecd24944e76a4b453731ffb164a425301..e0fe762645f6bc9e1c92ec24fcb03ef1221621cb 100644 --- a/login.php +++ b/login.php @@ -71,6 +71,55 @@ elseif ($_POST && isset($_POST['lticket'])) { $errors['err'] = 'Invalid email or ticket number - try again!'; } } +elseif (isset($_GET['do'])) { + switch($_GET['do']) { + case 'ext': + // Lookup external backend + if ($bk = UserAuthenticationBackend::getBackend($_GET['bk'])) + $bk->triggerAuth(); + } +} +elseif ($user = UserAuthenticationBackend::processSignOn($errors, false)) { + // Users from the ticket access link + if ($user && $user instanceof TicketUser && $user->getTicketId()) + Http::redirect('tickets.php?id='.$user->getTicketId()); + // Users imported from an external auth backend + elseif ($user instanceof ClientCreateRequest) { + if ($cfg && $cfg->isClientRegistrationEnabled()) { + // Attempt to automatically register + $user_form = UserForm::getUserForm()->getForm($user->getInfo()); + $bk = $user->getBackend(); + $defaults = array( + 'timezone_id' => $cfg->getDefaultTimezoneId(), + 'dst' => $cfg->observeDaylightSaving(), + 'username' => $user->getUsername(), + ); + if ($bk->supportsInteractiveAuthentication()) + $defaults['backend'] = $bk::$id; + if ($user_form->isValid(function($f) { return !$f->get('private'); }) + && ($U = User::fromVars($user_form->getClean())) + && ($acct = ClientAccount::createForUser($U, $defaults)) + // Confirm and save the account + && $acct->confirm() + // Login, since `tickets.php` will not attempt SSO + && ($cl = new ClientSession(new EndUser($U))) + && ($bk->login($cl, $bk))) + Http::redirect('tickets.php'); + + // Unable to auto-register. Fill in what we have and let the + // user complete the info + $inc = 'register.inc.php'; + } + else { + $errors['err'] = 'Access Denied. Contact your help desk + administrator to have an account registered for you'; + // fall through to show login page again + } + } + elseif ($user instanceof AuthenticatedUser) { + Http::redirect('tickets.php'); + } +} if (!$nav) { $nav = new UserNav(); diff --git a/pwreset.php b/pwreset.php index 3680cdf3e17c0cea40594a56a9c9ad1e7dbfc571..2b63aabf63477ab6818d965138b548d244aa9630 100644 --- a/pwreset.php +++ b/pwreset.php @@ -16,16 +16,15 @@ if($_POST) { switch ($_POST['do']) { case 'sendmail': if (($acct=ClientAccount::lookupByUsername($_POST['userid']))) { - if (!$acct->hasPassword()) { - $banner = 'Unable to reset password. Contact your administrator'; - } - elseif (!$acct->isPasswdResetEnabled()) { + if (!$acct->isPasswdResetEnabled()) { $banner = 'Password reset is not enabled for your account. ' .'Contact your administrator'; } - elseif (!$acct->sendResetEmail()) { + elseif ($acct->sendResetEmail()) { $inc = 'pwreset.sent.php'; } + else + $banner = 'Unable to send reset email. Internal error'; } else $banner = 'Unable to verify username ' diff --git a/scp/css/login.css b/scp/css/login.css index c2512a2bf8cb478236298561a7b88da18ccccd6c..99f8f1220c1f388f52a37d8759c5ba5049566b9c 100644 --- a/scp/css/login.css +++ b/scp/css/login.css @@ -23,7 +23,9 @@ html { body { -webkit-font-smoothing:antialiased; - background:url(../images/login-background.jpg) top left repeat-x #fff; + background:url(../images/login-background.jpg); + background-repeat: repeat-x; + background-attachment: fixed; font-size: 16px; font-smoothing:antialiased; height:100%; @@ -105,15 +107,30 @@ fieldset input { width: 96%; } +hr { + margin: 20px; + border: none; + height: 0; + border-bottom: 1px solid #eee; +} + +div.banner { + color: #666; + line-height: 1.2em; +} +div.banner:not(:empty) { + margin-bottom: 1em; +} + input.submit { + border-radius: 4px; display:inline-block; - float:right; margin:0.25em; height:24px; line-height:24px; font-weight:bold; border:1px solid #666666; - padding:0 10px; + padding:0 30px; background: url('../images/grey_btn_bg.png?1312910883') top left repeat-x; color: #333; } @@ -132,3 +149,46 @@ input.submit:hover, input.submit:active { #copyRights a { color:#888; } + +.external-auth { + display:inline-block; +} +.external-auth + .external-auth { + margin-top: 4px; +} + +a.external-sign-in { + text-decoration: none; +} +.external-auth-box { + vertical-align: middle; + border-radius: 4px; + border: 1px solid #777; +} +.external-auth-icon { + display: inline-block; + color: #333; + width: 30px; + padding: 5px 10px; + border-right: 1px solid #ddd; +} +.external-auth-name { + color: #333; + width: 100px; + padding: 5px 10px; + line-height:30px; + font-size: 11pt; +} +img.sign-in-image { + border: none; + max-height: 40px; + max-width: 200px; + width: auto; + height: auto; +} + +input[type=text], +input[type=password] { + border-radius: 4px; + padding: 5px; +} diff --git a/scp/login.php b/scp/login.php index 94dc67ac3e81f52e2dc8068d0e626b5cfacbe9ee..b110ac6359d8c39f5feba665dc83e89c0238dabb 100644 --- a/scp/login.php +++ b/scp/login.php @@ -40,8 +40,17 @@ if($_POST) { $msg = $errors['err']?$errors['err']:'Invalid login'; $show_reset = true; } +elseif ($_GET['do']) { + switch ($_GET['do']) { + case 'ext': + // Lookup external backend + if ($bk = StaffAuthenticationBackend::getBackend($_GET['bk'])) + $bk->triggerAuth(); + } + Http::redirect('login.php'); +} // Consider single sign-on authentication backends -else if (!$thisstaff || !($thisstaff->getId() || $thisstaff->isValid())) { +elseif (!$thisstaff || !($thisstaff->getId() || $thisstaff->isValid())) { if (($user = StaffAuthenticationBackend::processSignOn($errors, false)) && ($user instanceof StaffSession)) @header("Location: $dest");