Skip to content

Commit 7ecb81d

Browse files
Merge pull request #1284 from lightbluetom/general-approach-for-pdo-basic-auth
A more secure and generalized approach for PDO Basic Auth Backend
2 parents 6703fb7 + 590e9dc commit 7ecb81d

File tree

3 files changed

+284
-0
lines changed

3 files changed

+284
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
namespace Sabre\DAV\Auth\Backend;
4+
5+
/**
6+
* This is an authentication backend that uses a database to manage passwords.
7+
*
8+
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
9+
* @license http://sabre.io/license/ Modified BSD License
10+
*/
11+
class PDOBasicAuth extends AbstractBasic
12+
{
13+
/**
14+
* Reference to PDO connection.
15+
*
16+
* @var PDO
17+
*/
18+
protected $pdo;
19+
20+
/**
21+
* PDO table name we'll be using.
22+
*
23+
* @var string
24+
*/
25+
protected $tableName;
26+
27+
/**
28+
* PDO digest column name we'll be using
29+
* (i.e. digest, password, password_hash).
30+
*
31+
* @var string
32+
*/
33+
protected $digestColumn;
34+
35+
/**
36+
* PDO uuid(unique user identifier) column name we'll be using
37+
* (i.e. username, email).
38+
*
39+
* @var string
40+
*/
41+
protected $uuidColumn;
42+
43+
/**
44+
* Digest prefix:
45+
* if the backend you are using for is prefixing
46+
* your password hashes set this option to your prefix to
47+
* cut it off before verfiying.
48+
*
49+
* @var string
50+
*/
51+
protected $digestPrefix;
52+
53+
/**
54+
* Creates the backend object.
55+
*
56+
* If the filename argument is passed in, it will parse out the specified file fist.
57+
*/
58+
public function __construct(\PDO $pdo, array $options = [])
59+
{
60+
$this->pdo = $pdo;
61+
if (isset($options['tableName'])) {
62+
$this->tableName = $options['tableName'];
63+
} else {
64+
$this->tableName = 'users';
65+
}
66+
if (isset($options['digestColumn'])) {
67+
$this->digestColumn = $options['digestColumn'];
68+
} else {
69+
$this->digestColumn = 'digest';
70+
}
71+
if (isset($options['uuidColumn'])) {
72+
$this->uuidColumn = $options['uuidColumn'];
73+
} else {
74+
$this->uuidColumn = 'username';
75+
}
76+
if (isset($options['digestPrefix'])) {
77+
$this->digestPrefix = $options['digestPrefix'];
78+
}
79+
}
80+
81+
/**
82+
* Validates a username and password.
83+
*
84+
* This method should return true or false depending on if login
85+
* succeeded.
86+
*
87+
* @param string $username
88+
* @param string $password
89+
*
90+
* @return bool
91+
*/
92+
public function validateUserPass($username, $password)
93+
{
94+
$stmt = $this->pdo->prepare('SELECT '.$this->digestColumn.' FROM '.$this->tableName.' WHERE '.$this->uuidColumn.' = ?');
95+
$stmt->execute([$username]);
96+
$result = $stmt->fetchAll();
97+
98+
if (!count($result)) {
99+
return false;
100+
} else {
101+
$digest = $result[0][$this->digestColumn];
102+
103+
if (isset($this->digestPrefix)) {
104+
$digest = substr($digest, strlen($this->digestPrefix));
105+
}
106+
107+
if (password_verify($password, $digest)) {
108+
return true;
109+
}
110+
111+
return false;
112+
}
113+
}
114+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sabre\DAV\Auth\Backend;
6+
7+
use Sabre\HTTP;
8+
9+
abstract class AbstractPDOBasicAuthTest extends \PHPUnit\Framework\TestCase
10+
{
11+
use \Sabre\DAV\DbTestHelperTrait;
12+
13+
public function setup(): void
14+
{
15+
$this->dropTables('users');
16+
$this->createSchema('users');
17+
18+
// The supplied hash is a salted bcrypt hash of the plaintext : 'password'
19+
$this->getPDO()->query(
20+
"INSERT INTO users (username,digesta1) VALUES ('user','\$2b\$12\$IwetRH4oj6.AWFGGVy8fpet7Pgp1TafspB6iq1/fiLDxfsGZfi2jS')"
21+
);
22+
$this->getPDO()->query(
23+
"INSERT INTO users (username,digesta1) VALUES ('prefix_user','bcrypt\$\$2b\$12\$IwetRH4oj6.AWFGGVy8fpet7Pgp1TafspB6iq1/fiLDxfsGZfi2jS')"
24+
);
25+
}
26+
27+
public function testConstruct()
28+
{
29+
$pdo = $this->getPDO();
30+
$backend = new PDOBasicAuth($pdo);
31+
$this->assertTrue($backend instanceof PDOBasicAuth);
32+
}
33+
34+
public function testCheckNoHeaders()
35+
{
36+
$request = new HTTP\Request('GET', '/');
37+
$response = new HTTP\Response();
38+
39+
$options = [
40+
'tableName' => 'users',
41+
'digestColumn' => 'digesta1',
42+
'uuidColumn' => 'username',
43+
];
44+
$pdo = $this->getPDO();
45+
$backend = new PDOBasicAuth($pdo, $options);
46+
47+
$this->assertFalse(
48+
$backend->check($request, $response)[0]
49+
);
50+
}
51+
52+
public function testCheckUnknownUser()
53+
{
54+
$request = HTTP\Sapi::createFromServerArray([
55+
'REQUEST_METHOD' => 'GET',
56+
'REQUEST_URI' => '/',
57+
'PHP_AUTH_USER' => 'unkown_user',
58+
'PHP_AUTH_PW' => 'wrongpassword',
59+
]);
60+
$response = new HTTP\Response();
61+
62+
$options = [
63+
'tableName' => 'users',
64+
'digestColumn' => 'digesta1',
65+
'uuidColumn' => 'username',
66+
];
67+
$pdo = $this->getPDO();
68+
$backend = new PDOBasicAuth($pdo, $options);
69+
70+
$this->assertFalse(
71+
$backend->check($request, $response)[0]
72+
);
73+
}
74+
75+
public function testCheckAuthenticationFailure()
76+
{
77+
$request = HTTP\Sapi::createFromServerArray([
78+
'REQUEST_METHOD' => 'GET',
79+
'REQUEST_URI' => '/',
80+
'PHP_AUTH_USER' => 'user',
81+
'PHP_AUTH_PW' => 'wrongpassword',
82+
]);
83+
$response = new HTTP\Response();
84+
85+
$options = [
86+
'tableName' => 'users',
87+
'digestColumn' => 'digesta1',
88+
'uuidColumn' => 'username',
89+
];
90+
$pdo = $this->getPDO();
91+
$backend = new PDOBasicAuth($pdo, $options);
92+
93+
$this->assertFalse(
94+
$backend->check($request, $response)[0]
95+
);
96+
}
97+
98+
public function testCheckSuccess()
99+
{
100+
$request = HTTP\Sapi::createFromServerArray([
101+
'REQUEST_METHOD' => 'GET',
102+
'REQUEST_URI' => '/',
103+
'PHP_AUTH_USER' => 'user',
104+
'PHP_AUTH_PW' => 'password',
105+
]);
106+
$response = new HTTP\Response();
107+
108+
$options = [
109+
'tableName' => 'users',
110+
'digestColumn' => 'digesta1',
111+
'uuidColumn' => 'username',
112+
];
113+
$pdo = $this->getPDO();
114+
$backend = new PDOBasicAuth($pdo, $options);
115+
$this->assertEquals(
116+
[true, 'principals/user'],
117+
$backend->check($request, $response)
118+
);
119+
}
120+
121+
public function testPrefixSuccess()
122+
{
123+
$request = HTTP\Sapi::createFromServerArray([
124+
'REQUEST_METHOD' => 'GET',
125+
'REQUEST_URI' => '/',
126+
'PHP_AUTH_USER' => 'prefix_user',
127+
'PHP_AUTH_PW' => 'password',
128+
]);
129+
$response = new HTTP\Response();
130+
131+
$options = [
132+
'tableName' => 'users',
133+
'digestColumn' => 'digesta1',
134+
'uuidColumn' => 'username',
135+
'digestPrefix' => 'bcrypt$',
136+
];
137+
$pdo = $this->getPDO();
138+
$backend = new PDOBasicAuth($pdo, $options);
139+
$this->assertEquals(
140+
[true, 'principals/prefix_user'],
141+
$backend->check($request, $response)
142+
);
143+
}
144+
145+
public function testRequireAuth()
146+
{
147+
$request = new HTTP\Request('GET', '/');
148+
$response = new HTTP\Response();
149+
150+
$pdo = $this->getPDO();
151+
$backend = new PDOBasicAuth($pdo);
152+
$backend->setRealm('writing unittests on a saturday night');
153+
$backend->challenge($request, $response);
154+
155+
$this->assertEquals(
156+
'Basic realm="writing unittests on a saturday night", charset="UTF-8"',
157+
$response->getHeader('WWW-Authenticate')
158+
);
159+
}
160+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sabre\DAV\Auth\Backend;
6+
7+
class PDOBasicAuthSqliteTest extends AbstractPDOBasicAuthTest
8+
{
9+
public $driver = 'sqlite';
10+
}

0 commit comments

Comments
 (0)