Skip to content

Commit

Permalink
Added auth and user scaffolding to skeleton app (#34)
Browse files Browse the repository at this point in the history
* Updated CI to only run on official branch pushes

* Fixed missing PHPDoc

* Started the foundation of improved auth examples in demo

* Changed some names of auth handlers to be more specific, removed unused code

* Added some authorization checks, removed copyrights

* Fixed typo

* Started implementing some integration tests

* Updated to sort elements alphabetically, updated to use latest Aphiria changes

* Updated wording about demo code

* Switched things over to SQLite to be less hacky, fixed various bugs, still a WIP

* Renamed some stuff, cleaned up some tests

* Small refactorings

* Updated to use principal builder, fixed logic with integration tests, moved DB out of Git

* Added some more tests

* Updated more code to use PrincipalBuilder, discovered bug that is not enforcing authentication during authorization

* Removed deprecrated PHP-CS-Fixer rule

* Removed unnecessary comma

* Fixed tests

* Made cookie the default authentication scheme, removed unnecessary Authenticate attribute, but tests are still broken

* Updated to latest Aphiria

* Merged origin

* Fixed namespaces in .env.dist file

* Started adding new classes and interfaces for mocked authenticator.  Still a WIP.

* Ran linter

* Cleaned up some code and comments

* Added some more comments

* Refactored authenticator in integration tests to implement an interface

* Fixed some bugs with mocked auth calls, added auth integration tests

* Made var names consistent

* Updated integration test to use response parser

* Updated to use latest assertions

* Fixed Psalm issue

* Fixed Psalm config

* Fixed Psalm config

* Fixed some Psalm errors

* Fixed type for name identifier to be int

* Updated tests to use in-memory SQLite DB to simplify testing

* Moved some integration test methods into traits for better encapsulation

* Fixed Psalm error

* Simplified logic

* Updated to publish code coverage during CI

* Added shepherd to Psalm during CI

* Simplified mock authenticator because some properties were not being used

* Updated to use simpler way of setting auth schemes during mock authentication

* Removed unused import

* Made it easier to seed DBs from integration tests

* Updated to use built-in mock auth in integration tests

* Fixed in-memory SQLite usage, code style tweaks

* Refactored default user credentials to be read from .env file, added command to generate them

* Moved demo code out of demo dir

* Fixed Psalm error

* Fixed Psalm error

* Moved mock auth into a callback for more clarity on the scope of what's being mocked

* Updated to use simpler PrincipalBuilder

* Minor CS changes
  • Loading branch information
davidbyoung authored Aug 20, 2023
1 parent c4f19ba commit 52b6555
Show file tree
Hide file tree
Showing 52 changed files with 1,811 additions and 549 deletions.
10 changes: 5 additions & 5 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
APP_BUILDER_API=\Aphiria\Framework\Api\SynchronousApiApplicationBuilder
APP_BUILDER_CONSOLE=\Aphiria\Framework\Console\ConsoleApplicationBuilder
APP_COOKIE_DOMAIN=
APP_COOKIE_SECURE=0
APP_ENV=development
APP_URL=http://localhost:8080
DB_DRIVER=postgres
DB_HOST=localhost
DB_USER=myuser
DB_PASSWORD=mypassword
DB_NAME=public
DB_PORT=5432
DB_PATH=/database/database.sqlite
LOG_LEVEL=debug
USER_DEFAULT_EMAIL=admin@example.com
USER_DEFAULT_PASSWORD=abc123
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: ci
on:
push:
branches:
- '*.x'
pull_request:
jobs:
ci:
Expand All @@ -19,7 +21,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl, dom, libxml, mbstring, pcntl, xdebug, zip
extensions: curl, dom, libxml, mbstring, pcntl, sqlite3, xdebug, zip
tools: composer:v2
coverage: xdebug
- name: Install Dependencies
Expand All @@ -29,4 +31,10 @@ jobs:
- name: Run Linter
run: composer phpcs-test
- name: Run Psalm Static Analysis
run: composer psalm
run: composer psalm -- --shepherd
- name: Upload Coverage Results To Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
composer global require php-coveralls/php-coveralls
php-coveralls --coverage_clover=./.coverage/clover.xml --json_path=./coveralls-upload.json -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/.phpunit.result.cache
/composer.lock
/composer.phar
/database/database.sqlite
/nbproject/
/phpunit.phar
/vendor/
3 changes: 2 additions & 1 deletion .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
'method_protected',
'method_private_static',
'method_private'
]
],
'sort_algorithm' => 'alpha'
],
'ordered_imports' => true,
'return_type_declaration' => ['space_before' => 'none'],
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [v1.0.0-alpha5](https://github.com/aphiria/app/compare/v1.0.0-alpha4...v1.0.0-alpha5) (?)

### Added

- Added auth and user scaffolding, backed by a SQLite database ([#34](https://github.com/aphiria/app/pull/34))

### Changed

- Updated PHPUnit and Psalm ([#33](https://github.com/aphiria/app/pull/33))
Expand Down
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<p align="center"><a href="https://www.aphiria.com" target="_blank" title="Aphiria"><img src="https://www.aphiria.com/images/aphiria-logo.svg" width="200" height="56"></a></p>

<p align="center">
<a href="https://github.com/aphiria/app/actions"><img src="https://github.com/aphiria/app/workflows/ci/badge.svg"></a>
<a href="https://github.com/aphiria/app/actions"><img src="https://github.com/aphiria/app/workflows/ci/badge.svg"></a><a href="https://coveralls.io/github/aphiria/app?branch=1.x"><img src="https://coveralls.io/repos/github/aphiria/app/badge.svg?branch=1.x" alt="Coverage Status"></a>
<a href="https://psalm.dev"><img src="https://shepherd.dev/github/aphiria/app/level.svg"></a>
<a href="https://packagist.org/packages/aphiria/app"><img src="https://poser.pugx.org/aphiria/app/v/stable.svg"></a>
<a href="https://packagist.org/packages/aphiria/app"><img src="https://poser.pugx.org/aphiria/app/v/unstable.svg"></a>
<a href="https://packagist.org/packages/aphiria/app"><img src="https://poser.pugx.org/aphiria/app/license.svg"></a>
</p>

> **Note:** This library is not stable yet.
> **Note:** This framework is not stable yet.
This application is a useful starting point for projects that use the Aphiria framework. Check out this repository, and get started building your own REST API.

Expand All @@ -29,11 +30,7 @@ php aphiria app:serve

## Demo

This app comes with an extremely simple demo that can store and retrieve users from local file storage. It should not be used in production - it is simply a demo of some Aphiria features. The demo routes can be found as PHP attributes in [_src/Demo/Api/Controllers/UserController.php_](src/Demo/Api/Controllers/UserController.php).

### Removing Demo Code

To remove the built-in demo code, simply delete the [_src/Demo_](src/Demo) and [_tests/Integration/Demo_](tests/Integration/Demo) directories, and remove the `DemoModule` from [_src/GlobalModule.php_](src/GlobalModule.php).
This app comes with a simple demo that can store, retrieve, and authenticate users from a local SQLite database.

## Learn More

Expand Down
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"require": {
"aphiria/aphiria": "1.x-dev",
"ext-mbstring": "*",
"ext-pdo": "*",
"ext-sqlite3": "*",
"php": ">=8.2",
"symfony/dotenv": "^6.1"
},
Expand All @@ -43,7 +45,9 @@
"php -r \"file_exists('.env') || copy('.env.dist', '.env');\""
],
"post-create-project-cmd": [
"php -r \"echo 'Important: make ' . __DIR__ . DIRECTORY_SEPARATOR . 'tmp writable' . PHP_EOL;\""
"php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"php -r \"echo 'Important: make ' . __DIR__ . DIRECTORY_SEPARATOR . 'tmp writable' . PHP_EOL;\"",
"php aphiria user:generate-default-credentials"
],
"post-install-cmd": [
"php -r \"shell_exec((file_exists(getcwd() . '/composer.phar') ? PHP_BINARY . ' composer.phar' : 'composer') . ' dump-autoload -o');\"",
Expand Down
Empty file added database/.gitkeep
Empty file.
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
</source>
<php>
<env name="APP_ENV" value="testing" force="true"/>
<env name="DB_PATH" value=":memory:"/>
</php>
</phpunit>
5 changes: 5 additions & 0 deletions psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
</ignoreFiles>
</projectFiles>
<issueHandlers>
<RedundantCastGivenDocblockType>
<errorLevel type="suppress">
<directory name="src" />
</errorLevel>
</RedundantCastGivenDocblockType>
<PropertyNotSetInConstructor>
<errorLevel type="suppress">
<directory name="tests" />
Expand Down
63 changes: 63 additions & 0 deletions src/Auth/Api/Controllers/AuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace App\Auth\Api\Controllers;

use Aphiria\Api\Controllers\Controller;
use Aphiria\Authentication\Attributes\Authenticate;
use Aphiria\Authentication\AuthenticationSchemeNotFoundException;
use Aphiria\Authentication\IAuthenticator;
use Aphiria\Authentication\NotAuthenticatedException;
use Aphiria\Authentication\UnsupportedAuthenticationHandlerException;
use Aphiria\Net\Http\IResponse;
use Aphiria\Net\Http\Response;
use Aphiria\Routing\Attributes\Post;
use Aphiria\Routing\Attributes\RouteGroup;

/**
* Defines the auth controller
*/
#[RouteGroup('/auth')]
final class AuthController extends Controller
{
/**
* @param IAuthenticator $authenticator The authenticator
*/
public function __construct(private readonly IAuthenticator $authenticator)
{
}

/**
* Attempts to log in a user with basic auth and sets an auth token cookie on success
*
* @return IResponse The login attempt response
* @throws NotAuthenticatedException|AuthenticationSchemeNotFoundException|UnsupportedAuthenticationHandlerException Thrown if there was an error with authentication
*/
#[Post('/login'), Authenticate('basic')]
public function logIn(): IResponse
{
// We authenticate via basic auth, and then log in using cookies for future requests
$response = new Response();
/** @psalm-suppress PossiblyNullArgument The user will be set by the basic auth handler */
$this->authenticator->logIn($this->getUser(), $this->request, $response, 'cookie');

return $response;
}

/**
* Logs out the user
*
* @return IResponse The logout response
* @throws AuthenticationSchemeNotFoundException|UnsupportedAuthenticationHandlerException Thrown if there was an issue with authentication
*/
#[Post('/logout')]
public function logOut(): IResponse
{
$response = new Response();
/** @psalm-suppress PossiblyNullArgument The request will be set */
$this->authenticator->logOut($this->request, $response, 'cookie');

return $response;
}
}
86 changes: 86 additions & 0 deletions src/Auth/AuthModule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace App\Auth;

use Aphiria\Application\IApplicationBuilder;
use Aphiria\Authentication\AuthenticationScheme;
use Aphiria\Authentication\Schemes\BasicAuthenticationOptions;
use Aphiria\Authentication\Schemes\CookieAuthenticationOptions;
use Aphiria\Authorization\AuthorizationPolicy;
use Aphiria\Authorization\RequirementHandlers\RolesRequirement;
use Aphiria\Authorization\RequirementHandlers\RolesRequirementHandler;
use Aphiria\Framework\Application\AphiriaModule;
use Aphiria\Net\Http\Headers\SameSiteMode;
use Aphiria\Net\Http\HttpStatusCode;
use App\Auth\Binders\AuthServiceBinder;
use App\Database\Components\DatabaseComponents;

/**
* Defines the auth module
*/
final class AuthModule extends AphiriaModule
{
use DatabaseComponents;

/**
* @inheritdoc
*/
public function configure(IApplicationBuilder $appBuilder): void
{
$this->withBinders($appBuilder, new AuthServiceBinder())
->withDatabaseSeeders($appBuilder, SqlTokenSeeder::class)
// Add our default authentication scheme
->withAuthenticationScheme(
$appBuilder,
new AuthenticationScheme(
'cookie',
CookieAuthenticationHandler::class,
new CookieAuthenticationOptions(
cookieName: 'authToken',
cookieMaxAge: 3600,
cookiePath: '/',
cookieDomain: (string)\getenv('APP_COOKIE_DOMAIN'),
cookieIsSecure: (bool)\getenv('APP_COOKIE_SECURE'),
cookieIsHttpOnly: true,
cookieSameSite: SameSiteMode::Strict,
loginPagePath: '/login',
forbiddenPagePath: '/access-denied',
claimsIssuer: (string)\getenv('APP_COOKIE_DOMAIN')
)
),
true
)
->withAuthenticationScheme(
$appBuilder,
new AuthenticationScheme(
'basic',
BasicAuthenticationHandler::class,
new BasicAuthenticationOptions((string)\getenv('APP_URL'))
)
)
->withAuthorizationRequirementHandler(
$appBuilder,
AuthorizedUserDeleterRequirement::class,
new AuthorizedUserDeleterRequirementHandler()
)
->withAuthorizationRequirementHandler(
$appBuilder,
RolesRequirement::class,
new RolesRequirementHandler()
)
->withAuthorizationPolicy(
$appBuilder,
new AuthorizationPolicy(
'authorized-user-deleter',
new AuthorizedUserDeleterRequirement('admin')
)
)
->withProblemDetails(
$appBuilder,
InvalidCredentialsException::class,
status: HttpStatusCode::BadRequest
);
}
}
26 changes: 26 additions & 0 deletions src/Auth/AuthorizedUserDeleterRequirement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace App\Auth;

/**
* Defines the requirement for users to delete other users' accounts
*/
final readonly class AuthorizedUserDeleterRequirement
{
/** @var list<string> The list of roles authorized to delete other users' accounts */
public array $authorizedRoles;

/**
* @param list<string>|string $authorizedRoles The role or list of roles that are authorized to delete other users' accounts
*/
public function __construct(array|string $authorizedRoles)
{
if (!\is_array($authorizedRoles)) {
$authorizedRoles = [$authorizedRoles];
}

$this->authorizedRoles = $authorizedRoles;
}
}
54 changes: 54 additions & 0 deletions src/Auth/AuthorizedUserDeleterRequirementHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace App\Auth;

use Aphiria\Authorization\AuthorizationContext;
use Aphiria\Authorization\IAuthorizationRequirementHandler;
use Aphiria\Security\ClaimType;
use Aphiria\Security\IPrincipal;
use App\Users\User;
use InvalidArgumentException;

/**
* Defines the requirement handler that checks if a user is authorized to delete another user's account
*
* @implements IAuthorizationRequirementHandler<AuthorizedUserDeleterRequirement, User>
*/
final class AuthorizedUserDeleterRequirementHandler implements IAuthorizationRequirementHandler
{
/**
* @inheritdoc
*/
public function handle(IPrincipal $user, object $requirement, AuthorizationContext $authorizationContext): void
{
if (!$requirement instanceof AuthorizedUserDeleterRequirement) {
throw new InvalidArgumentException('Requirement must be of type ' . AuthorizedUserDeleterRequirement::class);
}

$userToDelete = $authorizationContext->resource;

if (!$userToDelete instanceof User) {
throw new InvalidArgumentException('Resource must be of type ' . User::class);
}

if ($userToDelete->id === (int)$user->getPrimaryIdentity()?->getNameIdentifier()) {
// The user being deleted is the current user
$authorizationContext->requirementPassed($requirement);

return;
}

foreach ($requirement->authorizedRoles as $authorizedRole) {
if ($user->hasClaim(ClaimType::Role, $authorizedRole)) {
// The user is authorized to delete the user's account
$authorizationContext->requirementPassed($requirement);

return;
}
}

$authorizationContext->fail();
}
}
Loading

0 comments on commit 52b6555

Please sign in to comment.