diff --git a/docs/index.txt b/docs/index.txt index b2eba5e993311ea9fa0d7989b9d38d851252b983..8ecea739c635fb614e30b1bb603756a7e4971278 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -30,6 +30,8 @@ SimpleSAMLphp Documentation * [simpleSAMLphp Modules](simplesamlphp-modules) - how to create own customized modules * [Installing third party modules with the pack.php tool](pack) * [Key rollover](./saml:keyrollover) + * [Creating authentication sources](./simplesamlphp-authsource) + * [Implementing custom username/password authentication](./simplesamlphp-customauth) Documentation on specific simpleSAMLphp modules: diff --git a/docs/simplesamlphp-authsource.txt b/docs/simplesamlphp-authsource.txt index 311099a2426aa3b3acd89d70b2ae9033ebebb4f6..ad63b4967bd9a962e899927127289fb447c3398a 100644 --- a/docs/simplesamlphp-authsource.txt +++ b/docs/simplesamlphp-authsource.txt @@ -44,6 +44,7 @@ If the username or password is incorrect, it should throw an error saying so: throw new SimpleSAML_Error_Error('WRONGUSERPASS'); +"[Implementing custom username/password authentication](./simplesamlphp-customauth)" describes how to implement username/password authentication using that base class. Generic rules & requirements diff --git a/docs/simplesamlphp-customauth.txt b/docs/simplesamlphp-customauth.txt new file mode 100644 index 0000000000000000000000000000000000000000..d32e8a4e081a1d092035bb2d14e27c6a2535cd20 --- /dev/null +++ b/docs/simplesamlphp-customauth.txt @@ -0,0 +1,350 @@ +Implementing custom username/password authentication +==================================================== + +This is a step-by-step guide for creating a custom username/password [authentication source](./simplesamlphp-authsource) for simpleSAMLphp. +An authentication source is responsible for authenticating the user, typically by getting a username and password, and looking it up in some sort of database. + +<!-- {{TOC}} --> + +Create a custom module +---------------------- + +All custom code for simpleSAMLphp should be contained in a [module](./simplesamlphp-modules). +This ensures that you can upgrade your simpleSAMLphp installation without overwriting your own code. +In this example, we will call the module `mymodule`. +It will be located under `modules/mymodule`. + +First we need to create the module directory: + + cd modules + mkdir mymodule + +Since this is a custom module, it should always be enabled. +Therefore we create a `default-enable` file in the module. +We do that by copying the `default-enable` file from the `core` module. + + cd mymodule + cp ../core/default-enable . + +Now that we have our own module, we can move on to creating an authentication source. + + +Creating a basic authentication source +-------------------------------------- + +Authentication sources are implemented using PHP classes. +We are going to create an authentication source named `mymodule:MyAuth`. +It will be implemented in the file `modules/mymodule/lib/Auth/Source/MyAuth.php`. + +To begin with, we will create a very simple authentication source, where the username and password is hardcoded into the source code. +Create the file `modules/mymodule/lib/Auth/Source/MyAuth.php` with the following contents: + + <?php + class sspmod_mymodule_Auth_Source_MyAuth extends sspmod_core_Auth_UserPassBase { + protected function login($username, $password) { + if ($username !== 'theusername' || $password !== 'thepassword') { + throw new SimpleSAML_Error_Error('WRONGUSERPASS'); + } + return array( + 'uid' => array('theusername'), + 'displayName' => array('Some Random User'), + 'eduPersonAffiliation' => array('member', 'employee'), + ); + } + } + +Some things to note: + + - The classname is `sspmod_mymodule_Auth_Source_MyAuth`. + This tells simpleSAMLphp to look for the class in `modules/mymodule/lib/Auth/Source/MyAuth.php`. + + - Our authentication source subclassese `sspmod_core_Auth_UserPassBase`. + This is a helper-class that implements much of the common code needed for username/password authentication. + + - The `login` function receives the username and password the user enters. + It is expected to authenticate the user. + If the username or password is correct, it must return a set of attributes for the user. + Otherwise, it must throw the `SimpleSAML_Error_Error('WRONGUSERPASS');` exception. + + - Attributes are returned as an associative array of `name => values` pairs. + All attributes can have multiple values, so the values are always stored in an array. + + +Configuring our authentication source +------------------------------------- + +Before we can test our authentication source, we must add an entry for it in `config/authsources.php`. +`config/authsources.php` contains an list of enabled authentication sources. + +The entry looks like this: + + 'myauthinstance' => array( + 'mymodule:MyAuth', + ), + +You can add it to the beginning of the list, so that the file looks something like this: + + <?php + $config = array( + 'myauthinstance' => array( + 'mymodule:MyAuth', + ), + /* Other authentication sources follow. */ + ); + +`myauthinstance` is the name of this instance of the authentication source. +(You are allowed to have multiple instances of an authentication source with different configuration.) +The instance name is used to refer to this authentication source in other configuration files. + +The first element of the configuration of the authentication source must be `'mymodule:MyAuth'`. +This tells simpleSAMLphp to look for the `sspmod_mymodule_Auth_Source_MyAuth` class. + + +Testing our authentication source +--------------------------------- + +Now that we have configured the authentication source, we can test it by accessing "authentication"-page of the simpleSAMLphp web interface. +By default, the web interface can be found on `http://yourhostname.com/simplesaml/`. +(Obviously, "yourhostname.com" should be replaced with your real hostname.) + +Then select the "Authentication"-tab, and choose "Test configured authentication sources". +You should then receive a list of authentication sources from `config/authsources.php`. +Select `myauthinstance`, and log in using "theusername" as the username, and "thepassword" as the password. +You should then arrive on a page listing the attributes we return from the `login` function. + +Next, you should log out by following the log out link. + + +Using our authentication source in an IdP +----------------------------------------- + +To use our new authentication source in an IdP we just need to update the IdP configuration to use it. +Open `metadata/saml20-idp-hosted.php`. +In that file you should locate the `auth`-option for your IdP, and change it to `myauthinstance`: + + <?php + /* ... */ + $metadata['__DYNAMIC:1__'] = array( + /* ... */ + /* + * Authentication source to use. Must be one that is configured in + * 'config/authsources.php'. + */ + 'auth' => 'myauthinstance', + /* ... */ + ); + +You can then test logging in to the IdP. +If you have logged in previously, you may need to log out first. + + +Adding configuration to our authentication source +------------------------------------------------- + +Instead of hardcoding options in our authentication source, they should be configurable. +We are now going to extend our authentication source to allow us to configure the username and password in `config/authsources.php`. + +First, we need to define the properties in the class that should hold our configuration: + + private $username; + private $password; + +Next, we create a constructor for the class. +The constructor is responsible for parsing the configuration and storing it in the properties. + + public function __construct($info, $config) { + parent::__construct($info, $config); + if (!is_string($config['username'])) { + throw new Exception('Missing or invalid username option in config.'); + } + $this->username = $config['username']; + if (!is_string($config['password'])) { + throw new Exception('Missing or invalid password option in config.'); + } + $this->password = $config['password']; + } + +We can then use the properties in the `login` function. +The complete class file should look like this: + + <?php + class sspmod_mymodule_Auth_Source_MyAuth extends sspmod_core_Auth_UserPassBase { + + private $username; + private $password; + + public function __construct($info, $config) { + parent::__construct($info, $config); + if (!is_string($config['username'])) { + throw new Exception('Missing or invalid username option in config.'); + } + $this->username = $config['username']; + if (!is_string($config['password'])) { + throw new Exception('Missing or invalid password option in config.'); + } + $this->password = $config['password']; + } + + protected function login($username, $password) { + if ($username !== $this->username || $password !== $this->password) { + throw new SimpleSAML_Error_Error('WRONGUSERPASS'); + } + return array( + 'uid' => array($this->username), + 'displayName' => array('Some Random User'), + 'eduPersonAffiliation' => array('member', 'employee'), + ); + } + + } + +We can then update our entry in `config/authsources.php` with the configuration options: + + 'myauthinstance' => array( + 'mymodule:MyAuth', + 'username' => 'theconfigusername', + 'password' => 'theconfigpassword', + ), + +Next, you should go to the "Test configured authentication sources" page again, and test logging in. +Note that we have updated the username & password to "theconfigusername" and "theconfigpassword". +(You may need to log out first before you can log in again.) + + +A more complete example - custom database authentication +-------------------------------------------------------- + +The [sqlauth:SQL](./sqlauth:sql) authentication source can do simple authentication against SQL databases. +However, in some cases it cannot be used, for example because the database layout is too complex, or because the password validation routines cannot be implemented in SQL. +What follows is an example of an authentication source that fetches an user from a database, and validates the password using a custom function. + +This code assumes that the database contains a table that looks like this: + + CREATE TABLE userdb ( + username VARCHAR(32) PRIMARY KEY NOT NULL, + password_hash VARCHAR(64) NOT NULL, + full_name TEXT NOT NULL); + +An example user (with password "secret"): + + INSERT INTO userdb (username, password_hash, full_name) + VALUES('exampleuser', 'QwVYkvlrAMsXIgULyQ/pDDwDI3dF2aJD4XeVxg==', 'Example User'); + +In this example, the `password_hash` contains a base64 encoded SSHA password. +A SSHA password is created like this: + + $password = 'secret'; + $numSalt = 8; /* Number of bytes with salt. */ + $salt = ''; + for ($i = 0; $i < $numSalt; $i++) { + $salt .= chr(mt_rand(0, 255)); + } + $digest = sha1($password . $salt, TRUE); + $password_hash = base64_encode($digest . $salt); + +The class follows: + + <?php + class sspmod_mymodule_Auth_Source_MyAuth extends sspmod_core_Auth_UserPassBase { + + /* The database DSN. + * See the documentation for the various database drivers for information about the syntax: + * http://www.php.net/manual/en/pdo.drivers.php + */ + private $dsn; + + /* The database username & password. */ + private $username; + private $password; + + public function __construct($info, $config) { + parent::__construct($info, $config); + + if (!is_string($config['dsn'])) { + throw new Exception('Missing or invalid dsn option in config.'); + } + $this->dsn = $config['dsn']; + if (!is_string($config['username'])) { + throw new Exception('Missing or invalid username option in config.'); + } + $this->username = $config['username']; + if (!is_string($config['password'])) { + throw new Exception('Missing or invalid password option in config.'); + } + $this->password = $config['password']; + } + + /** + * A helper function for validating a password hash. + * + * In this example we check a SSHA-password, where the database + * contains a base64 encoded byte string, where the first 20 bytes + * from the byte string is the SHA1 sum, and the remaining bytes is + * the salt. + */ + private function checkPassword($passwordHash, $password) { + $passwordHash = base64_decode($passwordHash); + $digest = substr($passwordHash, 0, 20); + $salt = substr($passwordHash, 20); + + $checkDigest = sha1($password . $salt, TRUE); + return $digest === $checkDigest; + } + + protected function login($username, $password) { + + /* Connect to the database. */ + $db = new PDO($this->dsn, $this->username, $this->password); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + /* Ensure that we are operating with UTF-8 encoding. + * This command is for MySQL. Other databases may need different commands. + */ + $db->exec("SET NAMES 'utf8'"); + + /* With PDO we use prepared statements. This saves us from having to escape + * the username in the database query. + */ + $st = $db->prepare('SELECT username, password_hash, full_name FROM userdb WHERE username=:username'); + + if (!$st->execute(array('username' => $username))) { + throw new Exception('Failed to query database for user.'); + } + + /* Retrieve the row from the database. */ + $row = $st->fetch(PDO::FETCH_ASSOC); + if (!$row) { + /* User not found. */ + SimpleSAML_Logger::warning('MyAuth: Could not find user ' . var_export($username, TRUE) . '.'); + throw new SimpleSAML_Error_Error('WRONGUSERPASS'); + } + + /* Check the password. */ + if (!$this->checkPassword($row['password_hash'], $password)) { + /* Invalid password. */ + SimpleSAML_Logger::warning('MyAuth: Wrong password for user ' . var_export($username, TRUE) . '.'); + throw new SimpleSAML_Error_Error('WRONGUSERPASS'); + } + + /* Create the attribute array of the user. */ + $attributes = array( + 'uid' => array($username), + 'displayName' => array($row['full_name']), + 'eduPersonAffiliation' => array('member', 'employee'), + ); + + /* Return the attributes. */ + return $attributes; + } + + } + +And configured in `config/authsources.php`: + + 'myauthinstance' => array( + 'mymodule:MyAuth', + 'dsn' => 'mysql:host=sql.example.org;dbname=userdatabase', + 'username' => 'db_username', + 'password' => 'secret_db_password', + ), +