diff options
author | Arnold Daniels <arnold@jasny.net> | 2015-09-23 17:42:24 +0200 |
---|---|---|
committer | Arnold Daniels <arnold@jasny.net> | 2015-09-27 16:54:20 +0200 |
commit | 2f5760bec45ad3b42cb9a4c0468c85a70c19867e (patch) | |
tree | 23ef2b8846c8fb9b15a4bec2cf4d1928f3ebac8a | |
parent | 6d7654315b70abc6f98b99635172f435d17b12d6 (diff) | |
download | sso-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.php | 42 | ||||
-rw-r--r-- | examples/ajax-broker/api.php | 27 | ||||
-rw-r--r-- | examples/ajax-broker/app.js | 105 | ||||
-rw-r--r-- | examples/ajax-broker/helpers.js | 57 | ||||
-rw-r--r-- | examples/ajax-broker/index.html | 71 | ||||
-rw-r--r-- | examples/broker/error.php | 25 | ||||
-rw-r--r-- | examples/broker/index.php | 46 | ||||
-rw-r--r-- | examples/broker/login.php | 61 | ||||
-rw-r--r-- | examples/server/MySSOServer.php | 86 | ||||
-rw-r--r-- | examples/server/SSOTestServer.php | 53 | ||||
-rw-r--r-- | examples/server/empty.png | bin | 125 -> 0 bytes | |||
-rw-r--r-- | examples/server/index.php | 19 | ||||
-rw-r--r-- | src/Broker.php | 195 | ||||
-rw-r--r-- | src/Server.php | 356 |
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 Binary files differdeleted file mode 100644 index 61dc432..0000000 --- a/examples/server/empty.png +++ /dev/null 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); } + |