* @license BSD */ namespace Domframework; /** All the authentication protocol */ class Authentication { /** The email of the authenticated/nonauthenticated user */ //private $email = "anonymous"; /** The debug of the authentication methods */ public $debug = 0; /** The route object */ private $route; /** Number of authentication maximum by minute */ public $ratelimitAuth = 3; /** Directory to store the ratelimit files */ public $ratelimitDir = "/tmp/ratelimit/"; /** The rest authentication methods. Can be post, session, http, shibboleth, * jwt * Attention : session case = CSRF ! */ public $restMethods = array("http", "jwt"); /** The html authentication methods. Can be : post, session, http, shibboleth, * jwt * The "post" is already used when using verifAuthLoginPage method (usually * only in authentication page) */ public $htmlMethods = array("session"); /** The authentication methods. Can be ldap, sympa...*/ public $authMethods = array(); /** The authentication servers configuration * array ("authXXXX" => array ( * array ("ldapserver" => "ldaps://server.domain.fr", * "ldapport" => 636, * "ldaptimeout" => 5, * "ldapauth" => "uid=XXX,dc=domain,dc=fr", * "ldappwd" => "XXX", * "ldapbase" => "", * "ldapfilter" => "(mail=%s)", * "ldapfield" => "mail", * "ldapfiltersearch" => "(objectClass=inetOrgPerson)" * ), * ), * ); */ public $authServers = array(); /** The application Name displayed on authentication page */ public $appName = null; /** The class and method to use to log the errors */ public $loggingFunc; /** The constructor * @param object $route The route object */ public function __construct($route) { $this->route = $route; $this->loggingFunc = array($this, "logging"); } /* public function email () { return $this->email; }*/ /** Setter/Getter for debug * @param integer|null $debug The debug value to get/set * @return integer|self the actual value or this */ public function debug($debug = null) { if ($debug === null) { return $this->debug; } $this->debug = intval($debug); return $this; } /** Disconnect the user * @param string|null $url The url to be redirected after a valid * logout */ public function logout($url = "") { // TODO : Foreach authentication methods->logout (); if (session_id() === "") { session_start(); } if ($this->debug) { echo "
LOGOUT\n";
        }
        $authsession = new Authsession();
        $param = $authsession->getdetails();
        if ($this->debug) {
            echo "Logout for '" . $param["email"] . "'\n";
        }
        call_user_func(
            $this->loggingFunc,
            LOG_NOTICE,
            "Logout for '" . $param["email"] . "'"
        );
        $authsession->logout();
        unset($_SESSION["domframework"]["authentication"]);
        if (isset($this->authServers["authjwt"]["serverKey"])) {
            $tokenName = "DFKJWT";
            if (isset($this->authServers["authjwt"]["tokenName"])) {
                $tokenName = $this->authServers["authjwt"]["tokenName"];
            }
            // Unset the JSON Web Token as the authentication
            if ($this->route->debug) {
                echo "Unset the JSON Web Token '$tokenName'
\n"; } if ( ! key_exists("CONTENT_TYPE", $_SERVER) || $_SERVER["CONTENT_TYPE"] !== "application/json" ) { echo "\n"; } $algorithm = "HS256"; $cipherKey = null; $cacheDir = "data/jwtCache"; $authjwt = new Authjwt(); $authjwt->serverKey = $this->authServers["authjwt"]["serverKey"]; if (isset($this->authServers["authjwt"]["cipherKey"])) { $authjwt->cipherKey = $this->authServers["authjwt"]["cipherKey"]; } if (isset($this->authServers["authjwt"]["algorithm"])) { $authjwt->algorithm = $this->authServers["authjwt"]["algorithm"]; } if (isset($this->authServers["authjwt"]["cacheDir"])) { $authjwt->cacheDir = $this->authServers["authjwt"]["cacheDir"]; } $authjwt->logout(); } if ($this->debug) { echo "Redirect to authentication page"; } if ($this->debug) { $this->route->debug = $this->debug; } if ($url === "" || $url === null) { $_SESSION["domframework"]["authentication"]["message"] = dgettext("domframework", "You have been logged out"); $this->route->redirect("/authentication", ""); } else { $this->route->redirect($url); } } /** Display the login page * @param string|null $url The url to be redirected after a valid * authentication */ public function pageHTML($url = "") { // If the user is already connected, redirect to the main page of the site if (session_id() === "") { session_start(); } $auth = new Auth(); $authparams = new Authparams(array("session")); if (isset($_SESSION["domframework"]["authentication"]["message"])) { $message = $_SESSION["domframework"]["authentication"]["message"]; } else { $message = ""; } unset($_SESSION["domframework"]["authentication"]["message"]); $alreadyAuth = false; if ($authparams->email !== "anonymous") { $alreadyAuth = $authparams->email; } if ($this->appName !== null) { $auth->appName = $this->appName; } @header('X-Frame-Options: SAMEORIGIN'); echo $auth->pageHTML( $this->route->baseURL(), $message, $url, $alreadyAuth ); } /** Check the authentication page * @param string|null $url The url to be redirected after a valid * authentication */ public function verifAuthLoginPage($url = "") { if (session_id() === "") { session_start(); } if ($this->debug) { echo "Call verifAuthLoginPage ($url) : Start\n"; } // rate-limit the connections $ratelimiter = new Ratelimitfile(); // 3 connections by minutes $ratelimiter->maxEntries = $this->ratelimitAuth; $ratelimiter->storageDir = $this->ratelimitDir; if (isset($_SERVER["HTTP_X_FORWARDED_FOR"])) { $ipClient = $_SERVER["HTTP_X_FORWARDED_FOR"]; } else { $ipClient = $_SERVER["REMOTE_ADDR"]; } if ($ratelimiter->set("loggin-$ipClient") === false) { if ($this->debug) { echo "Call verifAuthLoginPage ($url) : Ratelimit\n"; } call_user_func( $this->loggingFunc, LOG_WARNING, "Ratelimiting for $ipClient" ); $_SESSION["domframework"]["authentication"]["message"] = dgettext("domframework", "Too much connections"); if ($url === "") { $this->route->redirect("/authentication", ""); } else { $this->route->redirect("/authentication/$url", ""); } } $authparams = new Authparams(array("post")); $res = $this->verifAuth($authparams->email, $authparams->password); if (! is_array($res)) { if ($this->debug) { echo "Call verifAuthLoginPage ($url) : ERROR\n"; } // Authentication error // Redirect to login page after logout call_user_func( $this->loggingFunc, LOG_WARNING, "Logging error for '$authparams->email' (HTML) : $res" ); $authsession = new Authsession(); $authsession->logout(); $baseURL = $this->route->baseURL(); $_SESSION["domframework"]["authentication"]["message"] = $res; // Check the provided URL before using it if ($url === "") { $this->route->redirect("/authentication", ""); } else { $this->route->redirect("/authentication/$url", ""); } } // Login OK : save in SESSION and go to main page if ($this->debug) { echo "Call verifAuthLoginPage ($url) : USER OK\n"; } call_user_func( $this->loggingFunc, LOG_NOTICE, "Logging in for '$authparams->email'" ); if (! array_key_exists("lastname", $res)) { $res["lastname"] = null; } if (! array_key_exists("firstname", $res)) { $res["firstname"] = null; } $session = new Authsession(); $session->savedata( $authparams->email, $authparams->password, $res["lastname"], $res["firstname"] ); if (isset($this->authServers["authjwt"]["serverKey"])) { // Set the JSON Web Token as the authentication is valid $tokenName = "DFKJWT"; if (isset($this->authServers["authjwt"]["tokenName"])) { $tokenName = $this->authServers["authjwt"]["tokenName"]; } $token = $this->createJwtToken($authparams->email); echo "\n"; } if ($url === "") { $this->route->redirect("/", ""); } else { $this->route->redirect("/$url", ""); } } /** Check all the REST API * @param boolean|null $savePassword return the user password if the * authentication is valid * @return array The details provided by the authentication mecanism */ public function verifAuthREST($savePassword = false) { if ($this->debug) { echo "=== entering verifAuthREST (restMethods=" . print_r($this->restMethods, true) . ")\n"; } $authparams = new Authparams($this->restMethods); $res = array("email" => "anonymous", "password" => "anonymous"); if ( $authparams->email !== "anonymous" && $authparams->password !== "anonymous" ) { $res = $this->verifAuth($authparams->email, $authparams->password); } if (! is_array($res)) { call_user_func( $this->loggingFunc, LOG_WARNING, "Logging error for '$authparams->email' (REST) : $res" ); // Authentication error throw new \Exception(dgettext( "domframework", "Authentication error" ), 403); } if ($savePassword === true && $authparams->email !== "anonymous") { $res["password"] = $authparams->password; } return $res; } /** Return the JSON Web Token * @param string|array $auth The user data to store in JSON Web Token cache. * The $this->authServers["authjwt"]["algorithm"], * $this->authServers["authjwt"]["cipherKey"] and * $this->authServers["authjwt"]["serverKey"] can be set */ public function createJwtToken($auth) { if (isset($this->authServers["authjwt"]["serverKey"])) { // Set the JSON Web Token as the authentication is valid $algorithm = "HS256"; $cipherKey = null; $cacheDir = "data/jwtCache"; if (isset($this->authServers["authjwt"]["algorithm"])) { $algorithm = $this->authServers["authjwt"]["algorithm"]; } if (isset($this->authServers["authjwt"]["cipherKey"])) { $cipherKey = $this->authServers["authjwt"]["cipherKey"]; } if (isset($this->authServers["authjwt"]["cacheDir"])) { $cacheDir = $this->authServers["authjwt"]["cacheDir"]; } $payloadArray = array(); $payloadArray["email"] = $auth; if (is_array($auth)) { $payloadArray = $auth; } if ( ! key_exists("email", $payloadArray) || $payloadArray["email"] === "anonymous" ) { throw new \Exception("JWT Must authenticate", 401); } $authjwt = new Authjwt(); $authjwt->serverKey = $this->authServers["authjwt"]["serverKey"]; $authjwt->cipherKey = $cipherKey; $authjwt->algorithm = $algorithm; $authjwt->cacheDir = $cacheDir; return $authjwt->createJwtToken($payloadArray); } } /** Check all the others pages of the site * @return array The details provided by the authentication mecanism */ public function verifAuthHTML() { // Do not force the session_start ! We don't want the cookie on all the // pages if ($this->debug) { echo "=== entering verifAuthHTML (htmlMethods=" . print_r($this->htmlMethods, true) . ")\n"; } $authparams = new Authparams($this->htmlMethods); // Don't ask to the provider if anonymous is known if ($authparams->email === "anonymous" || $authparams->email === null) { if ($this->debug) { echo "Anonymous\n"; } return array("email" => "anonymous", "lastname" => "Anonymous", "firstname" => "", "password" => ""); } if ($this->debug) { echo "verifAuth ($authparams->email, $authparams->password)\n"; } $res = $this->verifAuth($authparams->email, $authparams->password); if (! is_array($res)) { // Authentication error if ($this->debug) { echo "Previous session not found"; } $msg = dgettext("domframework", "Previous session not found"); $_SESSION["domframework"]["authentication"]["message"] = $msg; call_user_func( $this->loggingFunc, LOG_WARNING, "Previous session not found for '$authparams->email'" ); $url = $this->route->requestURL(); $this->route->redirect("/authentication/$url"); } return $res; } /** Do the real authentication process on all the providers defined in the * properties of the class. * @param string $email The email to check * @param string $password The password to check * @return array containing the user data if the authentication is * correct, * an exception if noting is found */ private function verifAuth($email, $password) { if ($this->debug) { echo "Entering in verifAuth ($email, xxxxxxxx)\n"; } if (! is_array($this->authMethods) || count($this->authMethods) === 0) { throw new \Exception("No authentication method defined", 500); } if ( $this->debug < 2 && isset($_SESSION["domframework"]["authentication"]["lastcheck"]) && $_SESSION["domframework"]["authentication"]["lastcheck"] + 180 < time() ) { // Test the authentication each 3 minutes if there is a session, else // return the previous values if ($this->debug) { echo "verifAuth : using auth cache (push in debug=2 to skip)\n"; } return $_SESSION["domframework"]["authentication"]["authcache"]; } $authServers = $this->authServers; foreach ($this->authMethods as $method) { if ($this->debug) { echo "Authentication method=$method\n"; } if (! is_string($method)) { throw new \Exception("The authentication method is not a string", 500); } $classname = "auth$method"; //require_once ("domframework/$classname.php"); if (! array_key_exists($classname, $authServers)) { throw new \Exception( "No authentication server '$classname' enabled", 500 ); } // If only one server is defined, the parameters can directely be pushed // to the classname if (! is_array(reset($authServers[$classname]))) { $authServers[$classname] = array($authServers[$classname]); } if ($this->debug >= 2) { echo "Authentication method=$method : authServers=" . var_export($authServers[$classname]) . "\n"; } if ( ! is_array($authServers[$classname]) || count($authServers[$classname]) === 0 ) { throw new \Exception("No authentication server defined for method " . "'$method'", 500); } foreach ($authServers[$classname] as $key => $serversParam) { if ($this->debug) { echo "Test auth server $method # $classname # $key\n"; } if (! is_array($serversParam)) { throw new \Exception("Auth Server $key configuration error : " . "not an array", 500); } $class = __NAMESPACE__ . "\\" . ucfirst($classname); $authmethod = new $class(); foreach ($serversParam as $param => $value) { if ($this->debug) { if (is_array($value)) { echo "Add param '$param' to auth$method : " . var_export($value, true) . "\n"; } else { echo "Add param '$param' with value '$value' to auth$method\n"; } } $authmethod->$param = $value; } $authmethod->connect(); try { $authmethod->authentication($email, $password); $_SESSION["domframework"]["authentication"]["authcache"] = $authmethod->getdetails(); $_SESSION["domframework"]["authentication"]["lastcheck"] = time(); return $authmethod->getdetails(); } catch (\Exception $e) { call_user_func( $this->loggingFunc, LOG_DEBUG, "Authentication error for '$email' : " . "$classname : " . $e->getMessage() ); } } } return dgettext("domframework", "Bad login/password"); } /** Add the authentication routes to the routing model for HTML * authentication. Not needed if using shibboleth, HTTP auth... */ public function routes() { $authObj = $this; $route = $this->route; $this->route ->get("authentication/logout({url})?", function ($url) use ($authObj) { if (session_id() === "") { session_start(); } $authObj->logout($url); }) ->get("authentication", function () use ($route) { $route->redirect("/authentication/"); }) ->get("authentication/({url})?", function ($url) use ($authObj) { if (session_id() === "") { session_start(); } $authObj->pageHTML($url); exit; }) ->post("authentication/({url})?", function ($url) use ($authObj) { if (session_id() === "") { session_start(); } $authObj->verifAuthLoginPage($url); exit; }) ; $this->route->authenticationURL = "/authentication/"; } /** The default method to display the error messages. * Do not display the debug messages, and write the errors on screen * @param integer $priority The priority of the message * @param string $message The message to log */ private function logging($priority, $message) { if ($this->debug === 0 && $priority > 4) { return; } file_put_contents("/tmp/auth.log", "$priority : $message\n", FILE_APPEND); } }