summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArnold Daniels <arnold@jasny.net>2015-09-23 17:42:24 +0200
committerArnold Daniels <arnold@jasny.net>2015-09-27 16:54:20 +0200
commit2f5760bec45ad3b42cb9a4c0468c85a70c19867e (patch)
tree23ef2b8846c8fb9b15a4bec2cf4d1928f3ebac8a
parent6d7654315b70abc6f98b99635172f435d17b12d6 (diff)
downloadsso-2f5760bec45ad3b42cb9a4c0468c85a70c19867e.zip
sso-2f5760bec45ad3b42cb9a4c0468c85a70c19867e.tar.gz
sso-2f5760bec45ad3b42cb9a4c0468c85a70c19867e.tar.bz2
Turning SSO into an lib
Fixes for AJAX
-rw-r--r--examples/ajax-broker/ajax.php42
-rw-r--r--examples/ajax-broker/api.php27
-rw-r--r--examples/ajax-broker/app.js105
-rw-r--r--examples/ajax-broker/helpers.js57
-rw-r--r--examples/ajax-broker/index.html71
-rw-r--r--examples/broker/error.php25
-rw-r--r--examples/broker/index.php46
-rw-r--r--examples/broker/login.php61
-rw-r--r--examples/server/MySSOServer.php86
-rw-r--r--examples/server/SSOTestServer.php53
-rw-r--r--examples/server/empty.pngbin125 -> 0 bytes
-rw-r--r--examples/server/index.php19
-rw-r--r--src/Broker.php195
-rw-r--r--src/Server.php356
14 files changed, 665 insertions, 478 deletions
diff --git a/examples/ajax-broker/ajax.php b/examples/ajax-broker/ajax.php
deleted file mode 100644
index 26953e3..0000000
--- a/examples/ajax-broker/ajax.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-session_save_path(__DIR__ .'/../../broker-sessions');
-require_once $_SERVER['DOCUMENT_ROOT'] . '/src/Broker.php';
-
-$command = $_REQUEST['command'];
-$broker = new Jasny\SSO\Broker('http://127.0.0.1:9000/examples/server/', 'BrokerApi', 'BrokerApi');
-
-if (!empty($_REQUEST['token'])) $broker->token = $_REQUEST['token'];
-
-if (empty($_REQUEST['command'])) {
- header("Content-Type: application/json");
- header("HTTP/1.1 406 Not Acceptable");
- echo json_encode(['error' => 'Command not specified']);
- exit();
-} elseif (realpath($_SERVER["SCRIPT_FILENAME"]) == realpath(__FILE__)) {
- error_log('executing: '. $_REQUEST['command']);
-
- try {
- $result = $broker->$_REQUEST['command']();
- header("Content-Type: application/json");
- error_log('result: ' . json_encode($result));
- echo json_encode($result);
- } catch (Exception $ex) {
- $errorCode = $ex->getCode();
- error_log('error code' . $errorCode);
-
- header("Content-Type: application/json");
- if ($errorCode == 401) header("HTTP/1.1 401 Unauthorized");
- if ($errorCode == 406) header("HTTP/1.1 406 Not Acceptable");
-
- echo json_encode(['error' => $ex->getMessage()]);
- exit();
- }
-} else {
- error_log('nothing to execute');
-
- header("Content-Type: application/json");
- header("HTTP/1.1 406 Not Acceptable");
- echo json_encode(['error' => 'Command not supported']);
- exit();
-}
diff --git a/examples/ajax-broker/api.php b/examples/ajax-broker/api.php
new file mode 100644
index 0000000..30d9607
--- /dev/null
+++ b/examples/ajax-broker/api.php
@@ -0,0 +1,27 @@
+<?php
+
+require_once __DIR__ . '/../../vendor/autoload.php';
+
+$broker = new Jasny\SSO\Broker(getenv('SSO_SERVER'), getenv('SSO_BROKER_ID'), getenv('SSO_BROKER_SECRET'));
+
+if (empty($_REQUEST['command']) || !method_exists($broker, $_REQUEST['command'])) {
+ header("Content-Type: application/json");
+ header("HTTP/1.1 400 Bad Request");
+ echo json_encode(['error' => 'Command not specified']);
+ exit();
+}
+
+try {
+ $result = $broker->{$_REQUEST['command']}();
+} catch (Exception $e) {
+ http_response_code($e->getCode() ?: 500);
+ $result = ['error' => $e->getMessage()];
+}
+
+if (!$result) {
+ http_response_code(204);
+ exit();
+}
+
+header("Content-Type: application/json");
+echo json_encode($result);
diff --git a/examples/ajax-broker/app.js b/examples/ajax-broker/app.js
new file mode 100644
index 0000000..0de88f0
--- /dev/null
+++ b/examples/ajax-broker/app.js
@@ -0,0 +1,105 @@
++function($) {
+ // Init
+ attach();
+
+ /**
+ * Attach session.
+ * Will redirect to SSO server.
+ */
+ function attach() {
+ var req = $.ajax({
+ url: 'api.php?command=attach',
+ dataType: 'jsonp'
+ });
+
+ req.done(function(data, code, error) {
+ if (code && code >= 400) { // jsonp failure
+ showError(error);
+ return;
+ }
+
+ loadUserInfo();
+ });
+
+ req.fail(function(jqxhr) {
+ showError(jqxhr.responseJSON || jqxhr.textResponse)
+ });
+ }
+
+ /**
+ * Do an AJAX request to the API
+ *
+ * @param command API command
+ * @param params POST data
+ * @param callback Callback function
+ */
+ function doApiRequest(command, params, callback) {
+ var req = $.ajax({
+ url: 'api.php?command=' + command,
+ data: params,
+ dataType: 'json'
+ });
+
+ req.done(callback);
+
+ req.fail(function(jqxhr) {
+ showError(jqxhr.responseJSON || jqxhr.textResponse);
+ });
+ }
+
+ /**
+ * Display the error message
+ *
+ * @param data
+ */
+ function showError(data) {
+ var message = typeof data === 'object' && data.error ? data.error : 'Unexpected error';
+ $('#error').text(message).show();
+ }
+
+ /**
+ * Load and display user info
+ */
+ function loadUserInfo() {
+ doApiRequest('getUserinfo', null, showUserInfo);
+ }
+
+ /**
+ * Display user info
+ *
+ * @param info
+ */
+ function showUserInfo(info) {
+ $('body').removeClass('anonymous, authenticated');
+ $('#user-info').html('');
+
+ if (info) {
+ for (var key in info) {
+ $('#user-info').append($('<dt>').text(key));
+ $('#user-info').append($('<dd>').text(info[key]));
+ }
+ }
+
+ $('body').addClass(info ? 'authenticated' : 'anonymous');
+ }
+
+ /**
+ * Submit login form through AJAX
+ */
+ $('#login-form').on('submit', function(e) {
+ e.preventDefault();
+
+ $('#error').text('').show();
+
+ var data = {
+ username: $(this).find('input[name="username"]').value,
+ password: $(this).find('input[name="password"]').value
+ };
+
+ doApiRequest('login', data, showUserInfo);
+ });
+
+ $('#logout').on('click', function() {
+ doApiRequest('logout', null, function() { showUserInfo(null); });
+ })
+}(jQuery);
diff --git a/examples/ajax-broker/helpers.js b/examples/ajax-broker/helpers.js
deleted file mode 100644
index ffb218d..0000000
--- a/examples/ajax-broker/helpers.js
+++ /dev/null
@@ -1,57 +0,0 @@
-function microAjax(B,A) {this.bindFunction=function (E,D) {return function () {return E.apply(D,[D]);};};this.stateChange=function (D) {if (this.request.readyState==4) {this.callbackFunction(this.request.responseText);}};this.getRequest=function () {if (window.ActiveXObject) {return new ActiveXObject("Microsoft.XMLHTTP");} else {if (window.XMLHttpRequest) {return new XMLHttpRequest();}}return false;};this.postBody=(arguments[2]||"");this.callbackFunction=A;this.url=B;this.request=this.getRequest();if (this.request) {var C=this.request;C.onreadystatechange=this.bindFunction(this.stateChange,this);if (this.postBody!=="") {C.open("POST",B,true);C.setRequestHeader("X-Requested-With","XMLHttpRequest");C.setRequestHeader("Content-type","application/x-www-form-urlencoded");C.setRequestHeader("Connection","close");} else {C.open("GET",B,true);}C.send(this.postBody);}};
-
-var token = '';
-
-function makeRequest(command, token, callback, postBody) {
- var url = '/examples/ajax-broker/ajax.php?command=' + encodeURIComponent(command);
-
- microAjax(url, callback, postBody);
-}
-
-function getToken() {
- makeRequest('getToken', '', function (data) {
- token = JSON.parse(data);
- console.log('token is ready:', token);
- });
-
- var buttons = document.querySelectorAll('button');
- console.log(buttons);
- for (var i = 0; i < buttons.length; i++) {
- buttons[i].disabled = false;
- }
-}
-
-function doRequest(command, callback, postbody) {
- makeRequest(command, token, function(data) {
- var outputDiv = document.querySelector('#output');
- outputDiv.innerHTML = data;
- callback(data);
- }, postbody || '');
-}
-
-function print() {
- console.log(arguments);
-}
-
-function login() {
- var username = document.querySelector('input[name="username"]').value;
- var password = document.querySelector('input[name="password"]').value;
- var query = [
- 'username='+ username,
- 'password='+ password
- ];
-
- doRequest('login', function(data){console.log(data);}, query.join('&'));
-}
-
-function attach() {
- doRequest('ajaxAttach', function(data){console.log(data);});
-}
-
-function detach() {
- doRequest('detach', function(data){console.log(data);});
-}
-
-function getUserInfo() {
- doRequest('getUserInfo', function(data){console.log(data);});
-}
diff --git a/examples/ajax-broker/index.html b/examples/ajax-broker/index.html
index 933f264..62497bf 100644
--- a/examples/ajax-broker/index.html
+++ b/examples/ajax-broker/index.html
@@ -1,19 +1,56 @@
+<!doctype html>
<html>
- <head>
- <title>Single Sign-On Ajax demo</title>
- <script src="helpers.js"></script>
- </head>
- <body>
- <h1>Single Sign-On Ajax demo</h1>
- <form id="login-form">
- username: <input type="text" name="username"><br>
- password: <input type="text" name="password"><br><br>
- <input type="button" onclick="login()" value="Login">
-</form>
- <button type="button" onclick="attach()" >Attach session</button>
- <button type="button" onclick="getUserInfo()" >Print User Info</button>
- <button type="button" onclick="detach()" >Detach</button>
- <div id="output">
- </div>
- </body>
+ <head>
+ <title>Single Sign-On Ajax demo</title>
+ <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
+
+ <style>
+ .state {
+ display: none;
+ }
+ body.anonymous .state.anonymous,
+ body.authenticated .state.authenticated {
+ display: initial;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <h1>Single Sign-On Ajax demo</h1>
+
+ <div id="error" class="alert alert-error" style="display: none;"></div>
+
+ <form id="login-form" class="form-horizontal state anonymous">
+ <div class="form-group">
+ <label for="inputUsername" class="col-sm-2 control-label">Username</label>
+ <div class="col-sm-10">
+ <input type="text" name="username" class="form-control" id="inputUsername">
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="inputPassword" class="col-sm-2 control-label">Password</label>
+ <div class="col-sm-10">
+ <input type="password" name="password" class="form-control" id="inputPassword">
+ </div>
+ </div>
+
+ <div class="form-group">
+ <div class="col-sm-offset-2 col-sm-10">
+ <button type="submit" class="btn btn-default">Login</button>
+ </div>
+ </div>
+ </form>
+
+ <div class="state authenticated">
+ <h3>Logged in</h3>
+ <dl id="user-info" class="dl-horizontal"></dl>
+
+ <a id="logout" class="btn btn-default">Logout</a>
+ </div>
+ </div>
+
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="app.js"></script>
+ </body>
</html>
+
diff --git a/examples/broker/error.php b/examples/broker/error.php
new file mode 100644
index 0000000..52c7a53
--- /dev/null
+++ b/examples/broker/error.php
@@ -0,0 +1,25 @@
+<?php
+require_once __DIR__ . '/../../vendor/autoload.php';
+
+$broker = new Jasny\SSO\Broker(getenv('SSO_SERVER'), getenv('SSO_BROKER_ID'), getenv('SSO_BROKER_SECRET'));
+$error = $_GET['sso_error'];
+
+?>
+<!doctype html>
+<html>
+ <head>
+ <title>Single Sign-On demo (<?= $broker->broker ?>)</title>
+ <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
+ </head>
+ <body>
+ <div class="container">
+ <h1>Single Sign-On demo <small>(<?= $broker->broker ?>)</small></h1>
+
+ <div class="alert alert-danger">
+ <?= $error ?>
+ </div>
+
+ <a href="/">Try again</a>
+ </div>
+ </body>
+</html>
diff --git a/examples/broker/index.php b/examples/broker/index.php
index de396a7..2a5f12d 100644
--- a/examples/broker/index.php
+++ b/examples/broker/index.php
@@ -1,9 +1,13 @@
<?php
-
require_once __DIR__ . '/../../vendor/autoload.php';
-$broker = new Jasny\SSO\Broker(getenv('SSO_SERVER_URL'), getenv('SSO_BROKER_ID'), getenv('SSO_BROKER_SECRET'));
-$broker->attach();
+if (isset($_GET['sso_error'])) {
+ header("Location: error.php?sso_error=" . $_GET['sso_error'], true, 307);
+ exit;
+}
+
+$broker = new Jasny\SSO\Broker(getenv('SSO_SERVER'), getenv('SSO_BROKER_ID'), getenv('SSO_BROKER_SECRET'));
+$broker->attach(true);
$user = $broker->getUserInfo();
@@ -11,27 +15,25 @@ if (!$user) {
header("Location: login.php", true, 307);
exit;
}
-
?>
<!doctype html>
<html>
- <head>
- <title>Single Sign-On demo (<?= $broker->broker ?>)</title>
- </head>
- <body>
- <h1>Single Sign-On demo</h1>
- <h2><?= $broker->broker ?></h2>
- <?php if ($user) : ?>
- <h3>Logged in</h3>
- <?php
-endif ?>
+ <head>
+ <title><?= $broker->broker ?> (Single Sign-On demo)</title>
+ <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
+ </head>
+ <body>
+ <div class="container">
+ <h1><?= $broker->broker ?> <small>(Single Sign-On demo)</small></h1>
+ <h3>Logged in</h3>
- <dl>
- <?php foreach ($user as $key => $value) : ?>
- <dt><?= $key ?></dt><dd><?= $value ?></dd>
- <?php
-endforeach; ?>
- </dl>
- <a id="logout" href="login.php?logout=1">Logout</a>
- </body>
+ <dl class="dl-horizontal">
+ <?php foreach ($user as $key => $value) : ?>
+ <dt><?= $key ?></dt><dd><?= $value ?></dd>
+ <?php endforeach; ?>
+ </dl>
+
+ <a id="logout" class="btn btn-default" href="login.php?logout=1">Logout</a>
+ </div>
+ </body>
</html>
diff --git a/examples/broker/login.php b/examples/broker/login.php
index 10fc0d4..e51fe39 100644
--- a/examples/broker/login.php
+++ b/examples/broker/login.php
@@ -1,37 +1,56 @@
<?php
-
require_once __DIR__ . '/../../vendor/autoload.php';
-$broker = new Jasny\SSO\Broker(getenv('SSO_SERVER_URL'), getenv('SSO_BROKER_ID'), getenv('SSO_BROKER_SECRET'));
+$broker = new Jasny\SSO\Broker(getenv('SSO_SERVER'), getenv('SSO_BROKER_ID'), getenv('SSO_BROKER_SECRET'));
$broker->attach();
if (!empty($_GET['logout'])) {
$broker->logout();
-} elseif ($broker->getUserInfo()
- || ($_SERVER['REQUEST_METHOD'] == 'POST' && $broker->login($_POST['username'], $_POST['password']))) {
+} elseif ($broker->getUserInfo() || ($_SERVER['REQUEST_METHOD'] == 'POST' && $broker->login($_POST['username'], $_POST['password']))) {
header("Location: index.php", true, 303);
exit;
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') $errmsg = "Login failed";
-
?>
<!doctype html>
<html>
- <head>
- <title>Single Sign-On demo (<?= $broker->broker ?>) - Login</title>
- </head>
- <body>
- <h1>Single Sign-On demo - Login</h1>
- <h2><?= $broker->broker ?></h2>
-
- <? if (isset($errmsg)): ?><div style="color:red"><?= $errmsg ?></div><? endif; ?>
- <form id="login" action="login.php" method="POST">
- <table>
- <tr><td>Username</td><td><input type="text" name="username" /></td></tr>
- <tr><td>Password</td><td><input type="password" name="password" /></td></tr>
- <tr><td></td><td><input type="submit" value="Login" /></td></tr>
- </table>
- </form>
- </body>
+ <head>
+ <title><?= $broker->broker ?> | Login (Single Sign-On demo)</title>
+ <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
+
+ <style>
+ h1 {
+ margin-bottom: 30px;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <h1><?= $broker->broker ?> <small>(Single Sign-On demo)</small></h1>
+
+ <?php if (isset($errmsg)): ?><div class="alert alert-danger"><?= $errmsg ?></div><?php endif; ?>
+
+ <form class="form-horizontal" action="login.php" method="post">
+ <div class="form-group">
+ <label for="inputUsername" class="col-sm-2 control-label">Username</label>
+ <div class="col-sm-10">
+ <input type="text" name="username" class="form-control" id="inputUsername">
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="inputPassword" class="col-sm-2 control-label">Password</label>
+ <div class="col-sm-10">
+ <input type="password" name="password" class="form-control" id="inputPassword">
+ </div>
+ </div>
+
+ <div class="form-group">
+ <div class="col-sm-offset-2 col-sm-10">
+ <button type="submit" class="btn btn-default">Login</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </body>
</html>
diff --git a/examples/server/MySSOServer.php b/examples/server/MySSOServer.php
new file mode 100644
index 0000000..b97eeb0
--- /dev/null
+++ b/examples/server/MySSOServer.php
@@ -0,0 +1,86 @@
+<?php
+
+use Jasny\ValidationResult;
+use Jasny\SSO;
+
+class MySSOServer extends SSO\Server
+{
+ /**
+ * Registered brokers
+ * @var array
+ */
+ private static $brokers = [
+ 'Alice' => ['secret'=>'8iwzik1bwd'],
+ 'Greg' => ['secret'=>'7pypoox2pc'],
+ 'Julias' => ['secret'=>'ceda63kmhp']
+ ];
+
+ /**
+ * System users
+ * @var array
+ */
+ private static $users = array (
+ 'jackie' => [
+ 'fullname' => 'Jackie Black',
+ 'email' => 'jackie.black@example.com',
+ 'password' => '$2y$10$lVUeiphXLAm4pz6l7lF9i.6IelAqRxV4gCBu8GBGhCpaRb6o0qzUO' // jackie123
+ ],
+ 'john' => [
+ 'fullname' => 'John Doe',
+ 'email' => 'john.doe@example.com',
+ 'password' => '$2y$10$RU85KDMhbh8pDhpvzL6C5.kD3qWpzXARZBzJ5oJ2mFoW7Ren.apC2' // john123
+ ],
+ );
+
+ /**
+ * Get the API secret of a broker and other info
+ *
+ * @param string $brokerId
+ * @return array
+ */
+ protected function getBrokerInfo($brokerId)
+ {
+ return isset(self::$brokers[$brokerId]) ? self::$brokers[$brokerId] : null;
+ }
+
+ /**
+ * Authenticate using user credentials
+ *
+ * @param string $username
+ * @param string $password
+ * @return ValidationResult
+ */
+ protected function authenticate($username, $password)
+ {
+ if (!isset($username)) {
+ return ValidationResult::error("username isn't set");
+ }
+
+ if (!isset($password)) {
+ return ValidationResult::error("password isn't set");
+ }
+
+ if (!isset(self::$users[$username]) || !password_verify($password, self::$users[$username]['password'])) {
+ return ValidationResult::error("Invalid credentials");
+ }
+
+ return ValidationResult::success();
+ }
+
+
+ /**
+ * Get the user information
+ *
+ * @return array
+ */
+ protected function getUserInfo($username)
+ {
+ if (!isset(self::$users[$username])) return null;
+
+ $user = compact('username') + self::$users[$username];
+ unset($user['password']);
+
+ return $user;
+ }
+}
+
diff --git a/examples/server/SSOTestServer.php b/examples/server/SSOTestServer.php
deleted file mode 100644
index 3bf697b..0000000
--- a/examples/server/SSOTestServer.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-use Jasny\ValidationResult;
-use Desarrolla2\Cache\Cache;
-use Desarrolla2\Cache\Adapter\Memory;
-use Jasny\SSO\Server;
-
-class SSOTestServer extends Server
-{
- private static $brokers = array (
- 'Alice' => array('secret'=>"Bob"),
- 'Greg' => array('secret'=>'Geraldo'),
- 'BrokerApi' => array('secret'=>'BrokerApi'),
- 'ServerApi' => array('secret' => 'ServerApi')
- );
-
- private static $users = array (
- 'admin' => array(
- 'fullname' => 'jackie',
- 'email' => 'jackie@admin.com'
- )
- );
-
- public function __construct()
- {
- parent::__construct();
- }
-
- protected function getBrokerInfo($broker)
- {
- return self::$brokers[$broker];
- }
-
- protected function authenticate($username, $password)
- {
- $result = new ValidationResult();
-
- if (!isset($username)) {
- return ValidationResult::error("username isn't set");
- } elseif (!isset($password)) {
- return ValidationResult::error("password isn't set");
- } elseif ($username != 'admin' || $password != 'admin') {
- return ValidationResult::error("Invalid credentials");
- }
-
- return $result;
- }
-
- protected function getUserInfo($user)
- {
- return self::$users[$user];
- }
-}
diff --git a/examples/server/empty.png b/examples/server/empty.png
deleted file mode 100644
index 61dc432..0000000
--- a/examples/server/empty.png
+++ /dev/null
Binary files differ
diff --git a/examples/server/index.php b/examples/server/index.php
index d68c55e..5416eb9 100644
--- a/examples/server/index.php
+++ b/examples/server/index.php
@@ -1,19 +1,18 @@
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
-require_once __DIR__ . '/SSOTestServer.php';
+require_once 'MySSOServer.php';
-$sso = new SSOTestServer();
-$request = isset($_REQUEST['command']) ? $_REQUEST['command'] : null;
+$ssoServer = new MySSOServer();
+$command = isset($_REQUEST['command']) ? $_REQUEST['command'] : null;
-if (!$request || !method_exists($sso, $request)) {
- error_log('Unkown command');
- header("HTTP/1.1 406 Not Acceptable");
+if (!$command || !method_exists($ssoServer, $command)) {
+ header("HTTP/1.1 404 Not Found");
header('Content-type: application/json; charset=UTF-8');
-
- echo "{error: 'Uknown command'}";
- die;
+
+ echo json_encode(['error' => 'Unknown command']);
+ exit();
}
-$sso->$request();
+$result = $ssoServer->$command();
diff --git a/src/Broker.php b/src/Broker.php
index cd2fa15..1306ab4 100644
--- a/src/Broker.php
+++ b/src/Broker.php
@@ -41,6 +41,8 @@ class Broker
*/
protected $userinfo;
+
+
/**
* Class constructor
*
@@ -57,14 +59,21 @@ class Broker
$this->url = $url;
$this->broker = $broker;
$this->secret = $secret;
-
- if (session_status() === PHP_SESSION_NONE) session_start();
- //error_log('session ' . json_encode($_SESSION));
-
- if (isset($_SESSION['SSO']['token'])) $this->token = $_SESSION['SSO']['token'];
- // if (isset($_SESSION['SSO']['userinfo'])) $this->userinfo = $_SESSION['SSO']['userinfo'];
-
- error_log('token ' . $this->token);
+
+ if (isset($_COOKIE[$this->getCookieName()])) $this->token = $_COOKIE[$this->getCookieName()];
+ }
+
+ /**
+ * Get the cookie name.
+ *
+ * Note: Using the broker name in the cookie name.
+ * This resolves issues when multiple brokers are on the same domain.
+ *
+ * @return string
+ */
+ protected function getCookieName()
+ {
+ return 'sso_token_' . strtolower($this->broker);
}
/**
@@ -74,26 +83,21 @@ class Broker
*/
protected function getSessionId()
{
- if (!isset($this->token)) return null;
-
- $checksum = md5('session' . $this->token . $_SERVER['REMOTE_ADDR'] . $this->secret);
+ if (!$this->token) return null;
- return "SSO-{$this->broker}-{$this->token}-" . $checksum;
+ $checksum = hash('sha256', 'session' . $this->token . $_SERVER['REMOTE_ADDR'] . $this->secret);
+ return "SSO-{$this->broker}-{$this->token}-$checksum";
}
/**
- * Get session token
- *
- * @return string
+ * Generate session token
*/
- public function getToken()
+ public function generateToken()
{
- if (!isset($this->token)) {
- $this->token = md5(uniqid(rand(), true));
- $_SESSION['SSO']['token'] = $this->token;
- }
-
- return $this->token;
+ if (isset($this->token)) return;
+
+ $this->token = base_convert(md5(uniqid(rand(), true)), 16, 36);
+ setcookie($this->getCookieName(), $this->token, time() + 3600);
}
/**
@@ -109,126 +113,97 @@ class Broker
/**
* Get URL to attach session at SSO server.
*
+ * @param array $params
* @return string
*/
- public function getAttachUrl()
+ public function getAttachUrl($params = [])
{
- $token = $this->getToken();
- $checksum = md5("attach{$token}{$_SERVER['REMOTE_ADDR']}{$this->secret}");
- return "{$this->url}?command=attach&broker={$this->broker}&token=$token&checksum=$checksum";
+ $this->generateToken();
+
+ $data = [
+ 'command' => 'attach',
+ 'broker' => $this->broker,
+ 'token' => $this->token,
+ 'checksum' => hash('sha256', 'attach' . $this->token . $_SERVER['REMOTE_ADDR'] . $this->secret)
+ ];
+
+ return $this->url . "?" . http_build_query($data + $params);
}
/**
* Attach our session to the user's session on the SSO server.
*
- * @param string $returnUrl The URL the client should be returned to after attaching
+ * @param string|true $returnUrl The URL the client should be returned to after attaching
*/
public function attach($returnUrl = null)
{
- error_log('trying to attach: ' . $this->token);
if ($this->isAttached()) return;
- error_log('trying to attach: ' . $this->token);
- $url = $this->getAttachUrl();
-
- if (isset($returnUrl)) {
- $url .= "&returnUrl=" . urlencode($returnUrl);
- } else {
- $url .= "&returnUrl=" . urlencode("http://{$_SERVER["SERVER_NAME"]}{$_SERVER["REQUEST_URI"]}");
+ if ($returnUrl === true) {
+ $protocol = !empty($_SERVER['HTTPS']) ? 'https://' : 'http://';
+ $returnUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
+
+ $params = ['return_url' => $returnUrl];
+ $url = $this->getAttachUrl($params);
header("Location: $url", true, 307);
- echo "You're redirected to <a href=\"$url\">$url</a>";
- exit();
- }
-
- public function ajaxAttach()
- {
- error_log('trying to attach using ajax: ' . $this->token . ' ' . session_id());
- error_log('with token: ' . $this->token);
- error_log('with sid: ' . session_id());
-
- $token = $this->getToken();
- $checksum = md5("attach{$token}{$_SERVER['REMOTE_ADDR']}{$this->secret}");
-
- $params = [
- 'token' => $this->token,
- 'broker' => $this->broker,
- 'token' => $token,
- 'checksum' => $checksum,
- 'clientSid' => session_id(),
- 'clientAddr' => $_SERVER['REMOTE_ADDR']
- ];
-
- return $this->request('attach', $params);
- }
-
- /**
- * Detach our session from the user's session on the SSO server.
- */
- public function detach()
- {
- $this->token = null;
- $this->userinfo = null;
-
- unset($_SESSION['SSO']);
- echo '{}';
+ echo "You're redirected to <a href='$url'>$url</a>";
exit();
}
-
/**
* Get the request url for a command
*
- * @param string $command
+ * @param string $command
+ * @param array $params Query parameters
* @return string
*/
- protected function getRequestUrl($command)
+ protected function getRequestUrl($command, $params = [])
{
- $getParams = array(
- 'command' => $command,
- 'broker' => $this->broker,
- 'token' => $this->token,
- 'checksum' => md5('session' . $this->token . $_SERVER['REMOTE_ADDR'] . $this->secret)
- );
-
- return $this->url . '?' . http_build_query($getParams);
+ $params['command'] = $command;
+ $params['sso_session'] = $this->getSessionId();
+
+ return $this->url . '?' . http_build_query($params);
}
/**
* Execute on SSO server.
*
- * @param string $command Command
- * @param array $params Post parameters
+ * @param string $method HTTP method: 'GET', 'POST', 'DELETE'
+ * @param string $command Command
+ * @param array|string $data Query or post parameters
* @return array
*/
- protected function request($command, $params = array(), $sid = null)
+ protected function request($method, $command, $data = null)
{
- $ch = curl_init($this->getRequestUrl($command));
+ $url = $this->getRequestUrl($command, !$data || $method === 'POST' ? [] : $data);
+
+ $ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
- if (!isset($sid)) $params[session_name()] = $this->getSessionId();
- else $params[session_name()] = $sid;
-
- curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
+ if ($method === 'POST') {
+ $post = is_string($data) ? $data : http_build_query($data);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
+ }
$response = curl_exec($ch);
if (curl_errno($ch) != 0) {
- throw new Exception("Server request failed: " . curl_error($ch));
+ throw new Exception("Server request failed: " . curl_error($ch), 500);
}
-
+
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
- $contentType = explode('; ', $contentType)[0];
+ list($contentType) = explode(';', curl_getinfo($ch, CURLINFO_CONTENT_TYPE));
if ($contentType != 'application/json') {
- throw new Exception("Response did not come from the SSO server. $response", $httpCode);
+ $message = "Expected application/json response, got $contentType";
+ error_log($message . "\n\n" . $response);
+ throw new Exception($message, $httpCode);
}
- error_log('response ' . $response);
$data = json_decode($response, true);
- if ($httpCode != 200) throw new Exception($data['error'], $httpCode);
+ if ($httpCode >= 400) throw new Exception($data['error'] ?: $response, $httpCode);
return $data;
}
@@ -249,14 +224,9 @@ class Broker
if (!isset($username)) $username = $_POST['username'];
if (!isset($password)) $password = $_POST['password'];
- $result = $this->request('login', compact('username', 'password'));
- if (!array_key_exists('error', $result)) {
- $this->userinfo = $result;
- // $_SESSION['SSO']['userinfo'] = $result;
- error_log('success');
- } else {
- error_log('failure');
- }
+ $result = $this->request('POST', 'login', compact('username', 'password'));
+ $this->userinfo = $result;
+
return $result;
}
@@ -273,18 +243,13 @@ class Broker
*/
public function getUserInfo()
{
- error_log('trying to get user info');
- try {
- // TODO: the data is not updated
- if (!isset($this->userinfo)) {
- $this->userinfo = $this->request('userInfo');
- }
-
- return $this->userinfo;
- } catch (Exception $ex) {
- return null;
+ if (!isset($this->userinfo)) {
+ $this->userinfo = $this->request('GET', 'userInfo');
}
+
+ return $this->userinfo;
}
+
/**
* Handle notifications send by the SSO server
diff --git a/src/Server.php b/src/Server.php
index 5c8d230..02d779d 100644
--- a/src/Server.php
+++ b/src/Server.php
@@ -5,120 +5,132 @@ require_once __DIR__ . '/../vendor/autoload.php';
use Desarrolla2\Cache\Cache;
use Desarrolla2\Cache\Adapter;
-use Jasny\ValidationResult;
/**
* Single sign-on server.
*
* The SSO server is responsible of managing users sessions which are available for brokers.
+ *
+ * To use the SSO server, extend this class and implement the abstract methods.
+ * This class may be used as controller in an MVC application.
*/
abstract class Server
{
- private $started = false;
-
/**
* Cache that stores the special session data for the brokers.
*
- * @var Desarrolla2\Cache\Cache
+ * @var Cache
+ */
+ protected $cache;
+
+ /**
+ * @var string
*/
- public $cache;
+ protected $returnType;
+
+ /**
+ * Class constructor
+ */
public function __construct()
{
$this->cache = $this->createCacheAdapter();
- $this->cache->set('hello world', 'bonjour');
- error_log('cache: ' . $this->cache->get('hello world'));
- error_log('request: ' . json_encode($_REQUEST));
}
/**
- * Start session and protect against session hijacking
+ * Create a cache to store the broker session id.
+ *
+ * @return Cache
*/
- protected function sessionStart()
+ protected function createCacheAdapter()
{
- if ($this->started) return;
-
- $this->started = true;
+ $adapter = new Adapter\File('/tmp');
+ $adapter->setOption('ttl', 10 * 3600);
+
+ return new Cache($adapter);
+ }
+
- // Broker session
+ /**
+ * Start session and protect against session hijacking
+ */
+ protected function startSession()
+ {
$matches = null;
if (
- isset($_REQUEST[session_name()])
- && preg_match('/^SSO-(\w*+)-(\w*+)-([a-z0-9]*+)$/', $_REQUEST[session_name()], $matches)
+ isset($_GET['sso_session'])
+ && preg_match('/^SSO-(\w*+)-(\w*+)-([a-z0-9]*+)$/', $_GET['sso_session'], $matches)
) {
- $sid = $_REQUEST[session_name()];
-
- /* for (cross domain) ajax attach calls */
- if (isset($_POST['clientSid'])
- && $this->generateSessionId($matches[1], $matches[2], $_POST['clientAddr']) == $sid) {
- error_log('setting sid');
- session_id($_POST['clientSid']);
- session_start();
-
- if (isset($_SESSION['client_addr']) && $_SESSION['client_addr'] != $_POST['clientAddr']) {
- unset($_SESSION['username']);
- }
-
- $_SESSION['client_addr'] = $_POST['clientAddr'];
- return;
- }
-
- $linkedId = $this->cache->get($sid);
- if ($linkedId) {
- session_id($linkedId);
- session_start();
- // TODO: the session cookie expires in 1 second.
- setcookie(session_name(), "", 1);
- } else {
- session_start();
- }
-
- error_log('session ' . json_encode($_SESSION));
-
- if (!isset($_SESSION['client_addr'])) {
- session_destroy();
- $this->fail("Not attached");
- }
-
- if ($this->generateSessionId($matches[1], $matches[2], $_SESSION['client_addr']) != $sid) {
- session_destroy();
- $this->fail("Invalid session id");
- }
-
- $this->broker = $matches[1];
- return;
+ $this->startBrokerSession($_GET['sso_session'], $matches[1], $matches[2]);
+ } else {
+ $this->startUserSession();
}
+ }
- // User session
+ /**
+ * Start the session for broker requests to the SSO server
+ */
+ protected function startBrokerSession($sid, $brokerId, $token)
+ {
+ $linkedId = $this->cache->get($sid);
+
+ if (!$linkedId) {
+ return $this->fail("The broker session id isn't attached to a user session", 403);
+ }
- error_log('starting user session');
+ if (session_status() === PHP_SESSION_ACTIVE) {
+ if ($linkedId !== session_id()) throw new \Exception("Session has already started.");
+ return;
+ }
+
+ session_id($linkedId);
session_start();
+
+ if (!isset($_SESSION['client_addr'])) {
+ session_destroy();
+ return $this->fail("Unknown client IP address for the attached session", 500);
+ }
- error_log('session ' . json_encode($_SESSION));
- error_log('session dd' . session_id());
- if (isset($_SESSION['client_addr']) && $_SESSION['client_addr'] != $_SERVER['REMOTE_ADDR']) {
- error_log('regenerate id');
+ if ($this->generateSessionId($brokerId, $token, $_SESSION['client_addr']) != $sid) {
+ session_destroy();
+ return $this->fail("Checksum failed: Client IP address may have changed", 403);
+ }
+
+ $this->broker = $brokerId;
+ return;
+ }
+
+ /**
+ * Start the session when a user visits the SSO server
+ */
+ protected function startUserSession()
+ {
+ if (session_status() !== PHP_SESSION_ACTIVE) session_start();
+
+ if (isset($_SESSION['client_addr']) && $_SESSION['client_addr'] !== $_SERVER['REMOTE_ADDR']) {
session_regenerate_id(true);
}
+
if (!isset($_SESSION['client_addr'])) {
$_SESSION['client_addr'] = $_SERVER['REMOTE_ADDR'];
}
}
-
+
+
/**
* Generate session id from session token
*
* @return string
*/
- protected function generateSessionId($broker, $token, $client_addr = null)
+ protected function generateSessionId($brokerId, $token, $client_addr = null)
{
- $brokerInfo = $this->getBrokerInfo($broker);
+ $broker = $this->getBrokerInfo($brokerId);
- if (!isset($brokerInfo)) return null;
+ if (!isset($broker)) return null;
if (!isset($client_addr)) $client_addr = $_SERVER['REMOTE_ADDR'];
- return "SSO-{$broker}-{$token}-" . md5('session' . $token . $client_addr . $brokerInfo['secret']);
+ return "SSO-{$brokerId}-{$token}-" . hash('sha256', 'session' . $token . $client_addr . $broker['secret']);
}
/**
@@ -126,76 +138,124 @@ abstract class Server
*
* @return string
*/
- protected function generateAttachChecksum($broker, $token)
+ protected function generateAttachChecksum($brokerId, $token)
{
- $brokerInfo = $this->getBrokerInfo($broker);
- if (!isset($brokerInfo)) return null;
+ $broker = $this->getBrokerInfo($brokerId);
+ if (!isset($broker)) return null;
- return md5('attach' . $token . $_SERVER['REMOTE_ADDR'] . $brokerInfo['secret']);
+ return hash('sha256', 'attach' . $token . $_SERVER['REMOTE_ADDR'] . $broker['secret']);
}
+
/**
- * Authenticate
+ * Detect the type for the HTTP response.
+ * Should only be done for an `attach` request.
*/
- public function login()
+ protected function detectReturnType()
{
- $this->sessionStart();
+ if (!empty($_REQUEST['return_url'])) {
+ $this->returnType = 'redirect';
+ } elseif (strpos($_SERVER['HTTP_ACCEPT'], 'image/') !== false) {
+ $this->returnType = 'image';
+ } elseif (strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
+ $this->returnType = 'json';
+ } elseif (!empty($_REQUEST['return_url'])) {
+ $this->returnType = 'jsonp';
+ }
+ }
- if (empty($_POST['username'])) $this->failLogin("No user specified");
- if (empty($_POST['password'])) $this->failLogin("No password specified");
+ /**
+ * 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);
- $validation = $this->authenticate($_POST['username'], $_POST['password']);
+ if (!$this->returnType) return $this->fail("No return url specified", 400);
- if ($validation->failed()) {
- $this->failLogin($validation->getErrors());
+ $checksum = $this->generateAttachChecksum($_REQUEST['broker'], $_REQUEST['token']);
+
+ if (empty($_REQUEST['checksum']) || $checksum != $_REQUEST['checksum']) {
+ return $this->fail("Invalid checksum", 400);
}
- $_SESSION['username'] = $_POST['username'];
- $this->userInfo();
+ $this->startUserSession();
+ $sid = $this->generateSessionId($_REQUEST['broker'], $_REQUEST['token']);
+
+ $this->cache->set($sid, session_id());
+ $this->outputAttachSuccess();
}
/**
- * Log out
+ * Output on a successful attach
*/
- public function logout()
+ protected function outputAttachSuccess()
{
- $this->sessionStart();
- unset($_SESSION['username']);
+ 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') {
+ echo $_REQUEST['callback'] . "(200);";
+ }
+
+ if ($this->returnType === 'redirect') {
+ $url = $_REQUEST['return_url'];
+ header("Location: $url", true, 307);
+ echo "You're being redirected to <a href='{$url}'>$url</a>";
+ }
+ }
- header('Content-type: application/json; charset=UTF-8');
- echo "{}";
+ /**
+ * Output a 1x1px transparent image
+ */
+ protected function outputImage()
+ {
+ header('Content-Type: image/png');
+ echo base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQ'
+ . 'MAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZg'
+ . 'AAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=');
}
+
/**
- * Attach a user session to a broker session
+ * Authenticate
*/
- public function attach()
+ public function login()
{
- $this->sessionStart();
+ $this->startSession();
- if (empty($_REQUEST['broker'])) $this->fail("No broker specified");
- if (empty($_REQUEST['token'])) $this->fail("No token specified");
+ if (empty($_POST['username'])) $this->fail("No user specified", 400);
+ if (empty($_POST['password'])) $this->fail("No password specified", 400);
- $checksum = $this->generateAttachChecksum($_REQUEST['broker'], $_REQUEST['token']);
- $sid = $this->generateSessionId($_REQUEST['broker'], $_REQUEST['token']);
- error_log('sid: ' . $sid);
- error_log('checksum: ' . $checksum);
- if (empty($_REQUEST['checksum'])
- || $checksum != $_REQUEST['checksum']) {
- $this->fail("Invalid checksum");
+ $validation = $this->authenticate($_POST['username'], $_POST['password']);
+
+ if ($validation->failed()) {
+ return $this->fail($validation->getError(), 400);
}
- // what if there already exists an entry ?
- $this->cache->set($sid, session_id());
+ $_SESSION['sso_user'] = $_POST['username'];
+ $this->userInfo();
+ }
- if (!empty($_REQUEST['returnUrl'])) {
- header('Location: ' . $_REQUEST['returnUrl'], true, 307);
- exit();
- }
+ /**
+ * Log out
+ */
+ public function logout()
+ {
+ $this->startSession();
+ unset($_SESSION['sso_user']);
- // Output an image specially for AJAX apps
header('Content-type: application/json; charset=UTF-8');
- echo json_encode(['token' => $_REQUEST['token']]);
+ http_response_code(204);
}
/**
@@ -203,58 +263,72 @@ abstract class Server
*/
public function userInfo()
{
- $this->sessionStart();
- if (!isset($_SESSION['username'])) $this->failLogin("Not logged in");
-
- $userData = $this->getUserInfo($_SESSION['username']);
- $userData['username'] = $_SESSION['username'];
+ $this->startSession();
+ $user = null;
+
+ if (isset($_SESSION['sso_user'])) {
+ $user = $this->getUserInfo($_SESSION['sso_user']);
+ if (!$user) return $this->fail("User not found", 500); // Shouldn't happen
+ }
header('Content-type: application/json; charset=UTF-8');
- echo json_encode($userData);
+ echo json_encode($user);
}
+
/**
* An error occured.
*
* @param string $message
+ * @param int $http_status
*/
- protected function fail($message)
+ protected function fail($message, $http_status = 500)
{
- error_log($message);
-
- header("HTTP/1.1 400 Bad Request");
+ if ($http_status === 500) trigger_error($message, E_USER_WARNING);
+
+ if ($this->returnType === 'jsonp') {
+ echo $_REQUEST['callback'] . "($http_status, '" . addslashes($message) . "');";
+ exit();
+ }
+
+ if ($this->returnType === 'redirect') {
+ $url = $_REQUEST['return_url'] . '?sso_error=' . $message;
+ header("Location: $url", true, 307);
+ echo "You're being redirected to <a href='{$url}'>$url</a>";
+ exit();
+ }
+
+ http_response_code($http_status);
header('Content-type: application/json; charset=UTF-8');
echo json_encode(['error' => $message]);
- exit;
+ exit();
}
+
/**
- * Login failure.
+ * Authenticate using user credentials
*
- * @param string $message
+ * @param string $username
+ * @param string $password
+ * @return \Jasny\ValidationResult
*/
- protected function failLogin($message)
- {
- header("HTTP/1.1 401 Unauthorized");
- header('Content-type: application/json; charset=UTF-8');
-
- echo json_encode(['error' => $message]);
- exit;
- }
-
+ abstract protected function authenticate($username, $password);
+
/**
- * Create a cache to store the broker session id.
+ * Get the secret key and other info of a broker
+ *
+ * @param string $brokerId
+ * @return array
*/
- protected function createCacheAdapter()
- {
- $adapter = new Adapter\File('/tmp');
- $adapter->setOption('ttl', 10 * 3600);
-
- return new Cache($adapter);
- }
-
- abstract protected function authenticate($username, $password);
abstract protected function getBrokerInfo($brokerId);
- abstract protected function getUserInfo($brokerId);
+
+ /**
+ * Get the information about a user
+ *
+ * @param string $username
+ * @return array|object
+ */
+ abstract protected function getUserInfo($username);
}
+