'/tmp', 'files_cache_ttl' => 36000];
/**
* Cache that stores the special session data for the brokers.
*
* @var Cache
*/
protected $cache;
/**
* @var string
*/
protected $returnType;
/**
* @var mixed
*/
protected $brokerId;
/**
* Class constructor
*
* @param array $options
*/
public function __construct(array $options = [])
{
$this->options = $options + $this->options;
$this->cache = $this->createCacheAdapter();
}
/**
* Create a cache to store the broker session id.
*
* @return Cache
*/
protected function createCacheAdapter()
{
$adapter = new Adapter\File($this->options['files_cache_directory']);
$adapter->setOption('ttl', $this->options['files_cache_ttl']);
return new Cache($adapter);
}
/**
* Start the session for broker requests to the SSO server
*/
public function startBrokerSession()
{
if (isset($this->brokerId)) return;
$sid = $this->getBrokerSessionID();
if ($sid === false) {
return $this->fail("Broker didn't send a session key", 400);
}
$linkedId = $this->cache->get($sid);
if (!$linkedId) {
return $this->fail("The broker session id isn't attached to a user session", 403);
}
if (session_status() === PHP_SESSION_ACTIVE) {
if ($linkedId !== session_id()) throw new \Exception("Session has already started", 400);
return;
}
session_id($linkedId);
session_start();
$this->brokerId = $this->validateBrokerSessionId($sid);
}
/**
* Get session ID from header Authorization or from $_GET/$_POST
*/
protected function getBrokerSessionID()
{
$headers = getallheaders();
if (isset($headers['Authorization']) && strpos($headers['Authorization'], 'Bearer') === 0) {
$headers['Authorization'] = substr($headers['Authorization'], 7);
return $headers['Authorization'];
}
if (isset($_GET['access_token'])) {
return $_GET['access_token'];
}
if (isset($_POST['access_token'])) {
return $_POST['access_token'];
}
if (isset($_GET['sso_session'])) {
return $_GET['sso_session'];
}
return false;
}
/**
* Validate the broker session id
*
* @param string $sid session id
* @return string the broker id
*/
protected function validateBrokerSessionId($sid)
{
$matches = null;
if (!preg_match('/^SSO-(\w*+)-(\w*+)-([a-z0-9]*+)$/', $this->getBrokerSessionID(), $matches)) {
return $this->fail("Invalid session id");
}
$brokerId = $matches[1];
$token = $matches[2];
if ($this->generateSessionId($brokerId, $token) != $sid) {
return $this->fail("Checksum failed: Client IP address may have changed", 403);
}
return $brokerId;
}
/**
* Start the session when a user visits the SSO server
*/
protected function startUserSession()
{
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
}
/**
* Generate session id from session token
*
* @param string $brokerId
* @param string $token
* @return string
*/
protected function generateSessionId($brokerId, $token)
{
$broker = $this->getBrokerInfo($brokerId);
if (!isset($broker)) return null;
return "SSO-{$brokerId}-{$token}-" . hash('sha256', 'session' . $token . $broker['secret']);
}
/**
* Generate session id from session token
*
* @param string $brokerId
* @param string $token
* @return string
*/
protected function generateAttachChecksum($brokerId, $token)
{
$broker = $this->getBrokerInfo($brokerId);
if (!isset($broker)) return null;
return hash('sha256', 'attach' . $token . $broker['secret']);
}
/**
* Detect the type for the HTTP response.
* Should only be done for an `attach` request.
*/
protected function detectReturnType()
{
if (!empty($_GET['return_url'])) {
$this->returnType = 'redirect';
} elseif (!empty($_GET['callback'])) {
$this->returnType = 'jsonp';
} elseif (strpos($_SERVER['HTTP_ACCEPT'], 'image/') !== false) {
$this->returnType = 'image';
} elseif (strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
$this->returnType = 'json';
}
}
/**
* Attach a user session to a broker session
*/
public function attach()
{
$this->detectReturnType();
if (empty($_REQUEST['broker'])) return $this->fail("No broker specified", 400);
if (empty($_REQUEST['token'])) return $this->fail("No token specified", 400);
if (!$this->returnType) return $this->fail("No return url specified", 400);
$checksum = $this->generateAttachChecksum($_REQUEST['broker'], $_REQUEST['token']);
if (empty($_REQUEST['checksum']) || $checksum != $_REQUEST['checksum']) {
return $this->fail("Invalid checksum", 400);
}
$this->startUserSession();
$sid = $this->generateSessionId($_REQUEST['broker'], $_REQUEST['token']);
$this->cache->set($sid, $this->getSessionData('id'));
$this->outputAttachSuccess();
}
/**
* Output on a successful attach
*/
protected function outputAttachSuccess()
{
if ($this->returnType === 'image') {
$this->outputImage();
}
if ($this->returnType === 'json') {
header('Content-type: application/json; charset=UTF-8');
echo json_encode(['success' => 'attached']);
}
if ($this->returnType === 'jsonp') {
$data = json_encode(['success' => 'attached']);
echo $_REQUEST['callback'] . "($data, 200);";
}
if ($this->returnType === 'redirect') {
$url = $_REQUEST['return_url'];
header("Location: $url", true, 307);
echo "You're being redirected to $url";
}
}
/**
* Output a 1x1px transparent image
*/
protected function outputImage()
{
header('Content-Type: image/png');
echo base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQ'
. 'MAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZg'
. 'AAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=');
}
/**
* Authenticate
*/
public function login()
{
$this->startBrokerSession();
if (empty($_POST['username'])) $this->fail("No username specified", 400);
if (empty($_POST['password'])) $this->fail("No password specified", 400);
$validation = $this->authenticate($_POST['username'], $_POST['password']);
if ($validation->failed()) {
return $this->fail($validation->getError(), 400);
}
$this->setSessionData('sso_user', $_POST['username']);
$this->userInfo();
}
/**
* Log out
*/
public function logout()
{
$this->startBrokerSession();
$this->setSessionData('sso_user', null);
header('Content-type: application/json; charset=UTF-8');
http_response_code(204);
}
/**
* Ouput user information as json.
*/
public function userInfo()
{
$this->startBrokerSession();
$user = null;
$username = $this->getSessionData('sso_user');
if ($username) {
$user = $this->getUserInfo($username);
if (!$user) return $this->fail("User not found", 500); // Shouldn't happen
}
header('Content-type: application/json; charset=UTF-8');
echo json_encode($user);
}
/**
* Set session data
*
* @param string $key
* @param string $value
*/
protected function setSessionData($key, $value)
{
if (!isset($value)) {
unset($_SESSION[$key]);
return;
}
$_SESSION[$key] = $value;
}
/**
* Get session data
*
* @param type $key
*/
protected function getSessionData($key)
{
if ($key === 'id') return session_id();
return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
}
/**
* An error occured.
*
* @param string $message
* @param int $http_status
*/
protected function fail($message, $http_status = 500)
{
if (!empty($this->options['fail_exception'])) {
throw new Exception($message, $http_status);
}
if ($http_status === 500) trigger_error($message, E_USER_WARNING);
if ($this->returnType === 'jsonp') {
echo $_REQUEST['callback'] . "(" . json_encode(['error' => $message]) . ", $http_status);";
exit();
}
if ($this->returnType === 'redirect') {
$url = $_REQUEST['return_url'] . '?sso_error=' . $message;
header("Location: $url", true, 307);
echo "You're being redirected to $url";
exit();
}
http_response_code($http_status);
header('Content-type: application/json; charset=UTF-8');
echo json_encode(['error' => $message]);
exit();
}
/**
* Authenticate using user credentials
*
* @param string $username
* @param string $password
* @return \Jasny\ValidationResult
*/
abstract protected function authenticate($username, $password);
/**
* Get the secret key and other info of a broker
*
* @param string $brokerId
* @return array
*/
abstract protected function getBrokerInfo($brokerId);
/**
* Get the information about a user
*
* @param string $username
* @return array|object
*/
abstract protected function getUserInfo($username);
}