Skip to content

Commit

Permalink
Create cookie-based login and authentication system
Browse files Browse the repository at this point in the history
  • Loading branch information
acabal committed Jul 11, 2022
1 parent 4522136 commit 0bc3dc3
Show file tree
Hide file tree
Showing 46 changed files with 527 additions and 194 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ PHP 7+ is required.

```shell
# Install Apache, PHP, PHP-FPM, and various other dependencies.
sudo apt install -y git composer php-fpm php-cli php-gd php-xml php-apcu php-mbstring php-intl php-curl php-zip apache2 apache2-utils libfcgi0ldbl task-spooler ipv6calc mariadb-server libaprutil1-dbd-mysql attr
sudo apt install -y git composer php-fpm php-cli php-gd php-xml php-apcu php-mbstring php-intl php-curl php-zip apache2 apache2-utils libfcgi0ldbl task-spooler ipv6calc mariadb-server libaprutil1-dbd-mysql attr libapache2-mod-xsendfile

# Create the site root and logs root and clone this repo into it.
sudo mkdir /standardebooks.org/
Expand All @@ -26,7 +26,7 @@ echo -e "127.0.0.1\tstandardebooks.test" | sudo tee -a /etc/hosts
openssl req -x509 -nodes -days 99999 -newkey rsa:4096 -subj "/CN=standardebooks.test" -keyout /standardebooks.org/web/config/ssl/standardebooks.test.key -sha256 -out /standardebooks.org/web/config/ssl/standardebooks.test.crt

# Enable the necessary Apache modules.
sudo a2enmod headers expires ssl rewrite proxy proxy_fcgi authn_dbd
sudo a2enmod headers expires ssl rewrite proxy proxy_fcgi authn_dbd xsendfile

# Link and enable the SE Apache configuration file.
sudo ln -s /standardebooks.org/web/config/apache/standardebooks.test.conf /etc/apache2/sites-available/
Expand Down
42 changes: 11 additions & 31 deletions config/apache/standardebooks.org.conf
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ Define webroot /standardebooks.org/web
RewriteRule ^/images/covers/(.+?)\-[a-z0-9]{8}\-(cover|hero)(@2x)?\.(jpg|avif)$ /images/covers/$1-$2$3.$4

RewriteRule ^/ebooks/([^\./]+?)$ /ebooks/author.php?url-path=$1 [QSA]
RewriteRule ^/ebooks/([^\./]+?)/downloads$ /bulk-downloads/get.php?author=$1 [QSA]
RewriteRule ^/ebooks/([^\./]+?)/downloads$ /bulk-downloads/get.php?author=$1 [QSA]
RewriteRule ^/subjects/([^\./]+?)$ /ebooks/index.php?tags[]=$1 [QSA]
RewriteRule ^/collections/([^\./]+?)$ /ebooks/index.php?collection=$1 [QSA]
RewriteRule ^/collections/([^/]+?)/downloads$ /bulk-downloads/get.php?collection=$1
Expand Down Expand Up @@ -279,14 +279,14 @@ Define webroot /standardebooks.org/web
RewriteCond %{QUERY_STRING} \bquery=
RewriteRule ^/feeds/(opds|atom|rss)/all.xml$ /feeds/$1/search.php [QSA]

# Rewrite rules for bulk downloads
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1

# Enable mod_authn_dbd
DBDriver mysql
DBDParams "dbname=se user=www-data"
# HTTP Basic Auth configuration for:
# /bulk-downloads
# /feeds
# /polls/votes (we will allow access to view results at /polls/votes/index.php further down)
<DirectoryMatch "^${webroot}/www/(polls/votes|bulk-downloads|feeds/(opds|rss|atom))">
# HTTP Basic Auth configuration for /feeds
<DirectoryMatch "^${webroot}/www/feeds/(opds|rss|atom)">
AuthType Basic
AuthName "Enter your Patrons Circle email address and leave the password empty."
Require valid-user
Expand All @@ -300,34 +300,14 @@ Define webroot /standardebooks.org/web
# The hash is simply the hash of a blank password. We're only interested in the username/API key.
# We have to do this tortured query instead of a cleaner one, because the AuthDBDUserPWQuery
# function will only replace %s EXACTLY ONCE. We cannot have more than one %s in the query string.
AuthDBDUserPWQuery "\
select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from \
( \
select Email, Uuid from Patrons p inner join Users u using (UserId) where p.Ended is null \
union \
select Email, Uuid from ApiKeys fu inner join Users u using (UserId) where fu.Ended is null \
) x where %s in (Email, Uuid) limit 1 \
"
AuthDBDUserPWQuery "select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from Users u inner join Benefits b using (UserId) where %s in (u.Email, u.Uuid) and b.CanAccessFeeds = true limit 1"
</DirectoryMatch>

# Specific config for /bulk-downloads
<DirectoryMatch "^${webroot}/www/bulk-downloads">
<FilesMatch "\.php$">
# Disable HTTP Basic auth for the index and 401 pages
Require all granted
</FilesMatch>

<FilesMatch "\.zip$">
ErrorDocument 401 /bulk-downloads
</FilesMatch>
</DirectoryMatch>

# Specific config for /polls/votes
<DirectoryMatch "^${webroot}/www/polls/votes">
<FilesMatch "index.php$">
# Disable HTTP Basic auth for the index page
Require all granted
</FilesMatch>
<DirectoryMatch "${webroot}/www/bulk-downloads">
# Both directives are required
XSendFile on
XSendFilePath /standardebooks.org/web/www/bulk-downloads
</DirectoryMatch>

# Specific config for /feeds
Expand Down
42 changes: 11 additions & 31 deletions config/apache/standardebooks.test.conf
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ Define webroot /standardebooks.org/web
RewriteRule ^/images/covers/(.+?)\-[a-z0-9]{8}\-(cover|hero)(@2x)?\.(jpg|avif)$ /images/covers/$1-$2$3.$4

RewriteRule ^/ebooks/([^\./]+?)$ /ebooks/author.php?url-path=$1 [QSA]
RewriteRule ^/ebooks/([^\./]+?)/downloads$ /bulk-downloads/get.php?author=$1 [QSA]
RewriteRule ^/ebooks/([^\./]+?)/downloads$ /bulk-downloads/get.php?author=$1 [QSA]
RewriteRule ^/subjects/([^\./]+?)$ /ebooks/index.php?tags[]=$1 [QSA]
RewriteRule ^/collections/([^\./]+?)$ /ebooks/index.php?collection=$1 [QSA]
RewriteRule ^/collections/([^/]+?)/downloads$ /bulk-downloads/get.php?collection=$1
Expand Down Expand Up @@ -261,14 +261,14 @@ Define webroot /standardebooks.org/web
RewriteCond %{QUERY_STRING} \bquery=
RewriteRule ^/feeds/(opds|atom|rss)/all.xml$ /feeds/$1/search.php [QSA]

# Rewrite rules for bulk downloads
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1

# Enable mod_authn_dbd
DBDriver mysql
DBDParams "dbname=se user=www-data"
# HTTP Basic Auth configuration for:
# /bulk-downloads
# /feeds
# /polls/votes (we will allow access to view results at /polls/votes/index.php further down)
<DirectoryMatch "^${webroot}/www/(polls/votes|bulk-downloads|feeds/(opds|rss|atom))">
# HTTP Basic Auth configuration for /feeds
<DirectoryMatch "^${webroot}/www/feeds/(opds|rss|atom)">
AuthType Basic
AuthName "Enter your Patrons Circle email address and leave the password empty."
Require valid-user
Expand All @@ -282,34 +282,14 @@ Define webroot /standardebooks.org/web
# The hash is simply the hash of a blank password. We're only interested in the username/API key.
# We have to do this tortured query instead of a cleaner one, because the AuthDBDUserPWQuery
# function will only replace %s EXACTLY ONCE. We cannot have more than one %s in the query string.
AuthDBDUserPWQuery "\
select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from \
( \
select Email, Uuid from Patrons p inner join Users u using (UserId) where p.Ended is null \
union \
select Email, Uuid from ApiKeys fu inner join Users u using (UserId) where fu.Ended is null \
) x where %s in (Email, Uuid) limit 1 \
"
AuthDBDUserPWQuery "select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from Users u inner join Benefits b using (UserId) where %s in (u.Email, u.Uuid) and b.CanAccessFeeds = true limit 1"
</DirectoryMatch>

# Specific config for /bulk-downloads
<DirectoryMatch "^${webroot}/www/bulk-downloads">
<FilesMatch "\.php$">
# Disable HTTP Basic auth for the index and 401 pages
Require all granted
</FilesMatch>

<FilesMatch "\.zip$">
ErrorDocument 401 /bulk-downloads
</FilesMatch>
</DirectoryMatch>

# Specific config for /polls/votes
<DirectoryMatch "^${webroot}/www/polls/votes">
<FilesMatch "index.php$">
# Disable HTTP Basic auth for the index page
Require all granted
</FilesMatch>
<DirectoryMatch "${webroot}/www/bulk-downloads">
# Both directives are required
XSendFile on
XSendFilePath /standardebooks.org/web/www/bulk-downloads
</DirectoryMatch>

# Specific config for /feeds
Expand Down
7 changes: 0 additions & 7 deletions config/sql/se/ApiKeys.sql

This file was deleted.

8 changes: 8 additions & 0 deletions config/sql/se/Benefits.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE `Benefits` (
`UserId` int(10) unsigned NOT NULL,
`CanAccessFeeds` tinyint(1) unsigned NOT NULL,
`CanVote` tinyint(1) unsigned NOT NULL,
`CanBulkDownload` tinyint(1) unsigned NOT NULL,
PRIMARY KEY (`UserId`),
KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
7 changes: 7 additions & 0 deletions config/sql/se/Sessions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE `Sessions` (
`UserId` int(10) unsigned NOT NULL,
`Created` datetime NOT NULL,
`SessionId` char(36) NOT NULL,
KEY `idxUserId` (`UserId`),
KEY `idxSessionId` (`SessionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
6 changes: 6 additions & 0 deletions lib/Benefits.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?
class Benefits{
public $CanAccessFeeds = false;
public $CanVote = false;
public $CanBulkDownload = false;
}
6 changes: 4 additions & 2 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@

// No trailing slash on any of the below constants.
if(SITE_STATUS == SITE_STATUS_LIVE){
define('SITE_URL', 'https://standardebooks.org');
define('SITE_DOMAIN', 'standardebooks.org');
}
else{
define('SITE_URL', 'https://standardebooks.test');
define('SITE_DOMAIN', 'standardebooks.test');
}

const SITE_URL = 'https://' . SITE_DOMAIN;
const SITE_ROOT = '/standardebooks.org';
const WEB_ROOT = SITE_ROOT . '/web/www';
const REPOS_PATH = SITE_ROOT . '/ebooks';
Expand Down Expand Up @@ -49,6 +50,7 @@
const GET = 'GET';
const POST = 'POST';
const COOKIE = 'COOKIE';
const SESSION = 'SESSION';

const HTTP_VAR_INT = 0;
const HTTP_VAR_STR = 1;
Expand Down
2 changes: 2 additions & 0 deletions lib/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@
throw $ex; // Send the exception back to PHP for its usual logging routine.
});
}

$GLOBALS['User'] = Session::GetLoggedInUser();
5 changes: 5 additions & 0 deletions lib/Exceptions/InvalidFileException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?
namespace Exceptions;

class InvalidFileException extends SeException{
}
6 changes: 6 additions & 0 deletions lib/Exceptions/InvalidPermissionsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?
namespace Exceptions;

class InvalidPermissionsException extends SeException{
protected $message = 'You don’t have permission to perform that action.';
}
5 changes: 5 additions & 0 deletions lib/Exceptions/InvalidSessionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?
namespace Exceptions;

class InvalidSessionException extends SeException{
}
5 changes: 5 additions & 0 deletions lib/Exceptions/LoginRequiredException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?
namespace Exceptions;

class LoginRequiredException extends SeException{
}
5 changes: 5 additions & 0 deletions lib/Exceptions/PollVoteExistsException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@
namespace Exceptions;

class PollVoteExistsException extends SeException{
public $Vote = null;
protected $message = 'You’ve already voted in this poll.';

public function __construct(?\PollVote $vote = null){
$this->Vote = $vote;
}
}
3 changes: 3 additions & 0 deletions lib/HttpInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ private static function GetHttpVar(string $variable, int $type, string $set, $de
case COOKIE:
$vars = $_COOKIE;
break;
case SESSION:
$vars = $_SESSION;
break;
}

if(isset($vars[$variable])){
Expand Down
3 changes: 3 additions & 0 deletions lib/Patron.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public function Create(): void{
$this->Created = new DateTime();
Db::Query('INSERT into Patrons (Created, UserId, IsAnonymous, AlternateName, IsSubscribedToEmails) values(?, ?, ?, ?, ?);', [$this->Created, $this->UserId, $this->IsAnonymous, $this->AlternateName, $this->IsSubscribedToEmails]);


Db::Query('INSERT into Benefits (UserId, CanVote, CanAccessFeeds, CanBulkDownload) values (?, true, true, true) on duplicate key update CanVote = true, CanAccessFeeds = true, CanBulkDownload = true', [$this->UserId]);

// If this is a patron for the first time, send the first-time patron email.
// Otherwise, send the returning patron email.
$isReturning = Db::QueryInt('SELECT count(*) from Patrons where UserId = ?', [$this->UserId]) > 1;
Expand Down
31 changes: 15 additions & 16 deletions lib/PollVote.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ protected function GetUrl(): string{
protected function Validate(): void{
$error = new Exceptions\ValidationException();

if($this->UserId === null){
$error->Add(new Exceptions\InvalidPatronException());
if($this->UserId === null || $this->User === null){
$error->Add(new Exceptions\InvalidUserException());
}

if($this->PollItemId === null){
Expand All @@ -56,17 +56,17 @@ protected function Validate(): void{
// Basic sanity checks done, now check if we've already voted
// in this poll

if($this->User === null){
$error->Add(new Exceptions\InvalidPatronException());
// Do we already have a vote for this poll, from this user?
try{
$vote = PollVote::Get($this->PollItem->Poll->UrlName, $this->UserId);
$error->Add(new Exceptions\PollVoteExistsException($vote));
}
catch(Exceptions\InvalidPollVoteException $ex){
// User hasn't voted yet, carry on
}
else{
// Do we already have a vote for this poll, from this user?
if(Db::QueryInt('
SELECT count(*) from PollVotes pv inner join
(select PollItemId from PollItems pi inner join Polls p using (PollId)) x
using (PollItemId) where pv.UserId = ?', [$this->UserId]) > 0){
$error->Add(new Exceptions\PollVoteExistsException());
}

if(!$this->User->Benefits->CanVote){
$error->Add(new Exceptions\InvalidPatronException());
}
}

Expand All @@ -78,11 +78,10 @@ protected function Validate(): void{
public function Create(?string $email = null): void{
if($email !== null){
try{
$patron = Patron::GetByEmail($email);
$this->UserId = $patron->UserId;
$this->User = $patron->User;
$this->User = User::GetByEmail($email);
$this->UserId = $this->User->UserId;
}
catch(Exceptions\InvalidPatronException $ex){
catch(Exceptions\InvalidUserException $ex){
// Can't validate patron email - do nothing for now,
// this will be caught later when we validate the vote during creation.
// Save the email in the User object in case we want it later,
Expand Down
Loading

0 comments on commit 0bc3dc3

Please sign in to comment.