Skip to content

Commit

Permalink
Merge pull request #6 from monkeyiq/feature/passwordverify2
Browse files Browse the repository at this point in the history
A new PasswordVerify.php that does hashing in php rather than on the SQL side
  • Loading branch information
tvdijen committed Apr 29, 2024
2 parents 6dcf569 + 4c5dd73 commit c56c6fe
Show file tree
Hide file tree
Showing 7 changed files with 597 additions and 52 deletions.
65 changes: 64 additions & 1 deletion docs/sql.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
`sqlauth:SQL`
=============

This is a authentication module for authenticating a user against a SQL database.
These are authentication modules for authenticating a user against a
SQL database.

The SQL module performs password verification in the database itself
using database functions such as sha512 and storing a salt in the
database. The PasswordVerify module verifies passwords in php using
the password_verify() function. The PasswordVerify module was created
to ask the least of the database either because there is minimal
support in the database or to allow the same code to work against many
databases without modification. More information on PasswordVerify is
provided at the end of this document.

Options
-------
Expand All @@ -22,6 +32,9 @@ Options
`username_regex`
: (Optional) A regular expression that the username must match. Useful if the type of the username column in the database isn't a string (eg. an integer), or if the format is well known (eg. email address, single word with no spaces, etc) to avoid going to the database for a query that will never result in successful authentication.

`passwordhashcolumn`
: (Optional) Only When using the sqlauth:PasswordVerify module. This is the name of the column that contains the hashed password. The default is to look for a column 'passwordhash' in the database.

Writing a Query / Queries
-------------------------

Expand Down Expand Up @@ -178,3 +191,53 @@ be used (by just concatenating them to the input of the hash function), but shou
used instead as an additional security measure.

One way hashing algorithms like MD5 or SHA1 are considered insecure and should therefore be avoided.

The PasswordVerify module
-------------------------

Users and passwords have to be set in the database by other means than the PasswordVerify module.

For example:

```sql
CREATE TABLE users (
uid VARCHAR(30) NOT NULL PRIMARY KEY,
passwordhash TEXT NOT NULL,
givenName TEXT NOT NULL,
email TEXT NOT NULL,
eduPersonPrincipalName TEXT NOT NULL
);
```

A user can be added with a known password "FIXMEPASSWORD" as shown below.

```php
$dsn = "pgsql:host=...";
$username = "fixme";
$password = "";
$options = array();

$query = "insert into users values ('test@example.com',:passwordhash, 'test', 'test@example.com', 'test@example.com' )";

$db = new PDO($dsn, $username, $password, $options);
$db->exec("SET NAMES 'UTF8'");

$params = ["passwordhash" => password_hash("FIXMEPASSWORD", PASSWORD_ARGON2ID ) ];
$sth = $db->prepare($query);
$sth->execute($params);
```

Since the above is using the default passwordhash column name this can
then be used with the following addition to authsources.php.

```php
'smalldb-dbauth' => [
'sqlauth:PasswordVerify',
'dsn' => 'pgsql:host=...',
'username' => 'dbuser',
'password' => 'dbpassword',
'passwodhashcolumn' => 'passwordhash',
'query' => 'select uid, email, passwordhash, eduPersonPrincipalName from users where uid = :username ',
],

```
180 changes: 180 additions & 0 deletions src/Auth/Source/PasswordVerify.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\sqlauth\Auth\Source;

use SimpleSAML\Error;
use SimpleSAML\Logger;
use SimpleSAML\Module\sqlauth\Auth\Source\SQL;

use function array_key_exists;
use function array_keys;
use function count;
use function implode;
use function is_null;
use function password_verify;
use function sprintf;

/**
* Simple SQL authentication source
*
* This class is very much like the SQL class. The major difference is that
* instead of using SHA2 and other functions in the database we use the PHP
* password_verify() function to allow for example PASSWORD_ARGON2ID to be used
* for verification.
*
* While this class has a query parameter as the SQL class does the meaning
* is different. The query for this class should return at least a column
* called passwordhash containing the hashed password which was generated
* for example using
* password_hash('hello', PASSWORD_ARGON2ID );
*
* Auth only passes if the PHP code below returns true.
* password_verify($password, row['passwordhash'] );
*
* Unlike the SQL class the username is the only parameter passed to the SQL query,
* the query can not perform password checks, they are performed by the PHP code
* in this class using password_verify().
*
* If there are other columns in the returned data they are assumed to be attributes
* you would like to be returned through SAML.
*
* @package SimpleSAMLphp
*/

class PasswordVerify extends SQL
{
/**
* The column in the result set containing the passwordhash.
*/
protected string $passwordhashcolumn = 'passwordhash';

/**
* Constructor for this authentication source.
*
* @param array $info Information about this authentication source.
* @param array $config Configuration.
*/
public function __construct(array $info, array $config)
{
// Call the parent constructor first, as required by the interface
parent::__construct($info, $config);

if (array_key_exists('passwordhashcolumn', $config)) {
$this->passwordhashcolumn = $config['passwordhashcolumn'];
}
}



/**
* Attempt to log in using the given username and password.
*
* On a successful login, this function should return the users attributes. On failure,
* it should throw an exception. If the error was caused by the user entering the wrong
* username or password, a \SimpleSAML\Error\Error('WRONGUSERPASS') should be thrown.
*
* Note that both the username and the password are UTF-8 encoded.
*
* @param string $username The username the user wrote.
* @param string $password The password the user wrote.
* @return array Associative array with the users attributes.
*/
protected function login(string $username, string $password): array
{
$this->verifyUserNameWithRegex($username);

$db = $this->connect();
$params = ['username' => $username];
$attributes = [];

$numQueries = count($this->query);
for ($x = 0; $x < $numQueries; $x++) {
$data = $this->executeQuery($db, $this->query[$x], $params);

Logger::info('sqlauth:' . $this->authId . ': Got ' . count($data) .

Check failure on line 96 in src/Auth/Source/PasswordVerify.php

View workflow job for this annotation

GitHub Actions / Quality control

InvalidArgument

src/Auth/Source/PasswordVerify.php:96:72: InvalidArgument: Argument 1 of count expects Countable|array<array-key, mixed>, but SimpleSAML\Module\sqlauth\Auth\Source\tuples provided (see https://psalm.dev/004)
' rows from database');

/**
* Sanity check, passwordhash must be in each resulting tuple and must have
* the same value in every tuple.
*
* Note that $pwhash will contain the passwordhash value after this loop.
*/
$pwhash = null;
if ($x === 0) {
if (count($data) === 0) {

Check failure on line 107 in src/Auth/Source/PasswordVerify.php

View workflow job for this annotation

GitHub Actions / Quality control

InvalidArgument

src/Auth/Source/PasswordVerify.php:107:27: InvalidArgument: Argument 1 of count expects Countable|array<array-key, mixed>, but SimpleSAML\Module\sqlauth\Auth\Source\tuples provided (see https://psalm.dev/004)
// No rows returned - invalid username/password
Logger::error(sprintf(
'sqlauth:%s: No rows in result set. Probably wrong username/password.',
$this->authId,
));
throw new Error\Error('WRONGUSERPASS');
}

foreach ($data as $row) {

Check failure on line 116 in src/Auth/Source/PasswordVerify.php

View workflow job for this annotation

GitHub Actions / Quality control

UndefinedClass

src/Auth/Source/PasswordVerify.php:116:26: UndefinedClass: Class, interface or enum named SimpleSAML\Module\sqlauth\Auth\Source\tuples does not exist (see https://psalm.dev/019)

Check failure on line 116 in src/Auth/Source/PasswordVerify.php

View workflow job for this annotation

GitHub Actions / Quality control

RawObjectIteration

src/Auth/Source/PasswordVerify.php:116:26: RawObjectIteration: Possibly undesired iteration over regular object SimpleSAML\Module\sqlauth\Auth\Source\tuples (see https://psalm.dev/111)
if (
!array_key_exists($this->passwordhashcolumn, $row)
|| is_null($row[$this->passwordhashcolumn])
) {
Logger::error(sprintf(
'sqlauth:%s: column `%s` must be in every result tuple.',
$this->authId,
$this->passwordhashcolumn,
));
throw new Error\Error('WRONGUSERPASS');
}
if ($pwhash) {

Check failure on line 128 in src/Auth/Source/PasswordVerify.php

View workflow job for this annotation

GitHub Actions / Quality control

RiskyTruthyFalsyComparison

src/Auth/Source/PasswordVerify.php:128:25: RiskyTruthyFalsyComparison: Operand of type mixed|null contains type mixed, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)
if ($pwhash != $row[$this->passwordhashcolumn]) {
Logger::error(sprintf(
'sqlauth:%s: column %s must be THE SAME in every result tuple.',
$this->authId,
$this->passwordhashcolumn,
));
throw new Error\Error('WRONGUSERPASS');
}
}
$pwhash = $row[$this->passwordhashcolumn];
}

/**
* This should never happen as the count(data) test above would have already thrown.
* But checking twice doesn't hurt.
*/
if (is_null($pwhash)) {
if ($pwhash != $row[$this->passwordhashcolumn]) {

Check failure on line 146 in src/Auth/Source/PasswordVerify.php

View workflow job for this annotation

GitHub Actions / Quality control

PossiblyUndefinedVariable

src/Auth/Source/PasswordVerify.php:146:36: PossiblyUndefinedVariable: Possibly undefined variable $row, first seen on line 116 (see https://psalm.dev/018)
Logger::error(sprintf(
'sqlauth:%s: column `%s` does not contain a password hash.',
$this->authId,
$this->passwordhashcolumn,
));
throw new Error\Error('WRONGUSERPASS');
}
}

/**
* VERIFICATION!
* Now to check if the password the user supplied is actually valid
*/
if (!password_verify($password, $pwhash)) {
Logger::error(sprintf(
'sqlauth:%s: password is incorrect.',
$this->authId,
));
throw new Error\Error('WRONGUSERPASS');
}
}

$this->extractAttributes($attributes, $data, [$this->passwordhashcolumn]);
}

Logger::info(sprintf(
'sqlauth:%s: Attributes: %s',
$this->authId,
implode(',', array_keys($attributes)),
));

return $attributes;
}
}
Loading

0 comments on commit c56c6fe

Please sign in to comment.