Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added auth and user scaffolding to skeleton app #34

Merged
merged 58 commits into from
Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
2a7da8e
Updated CI to only run on official branch pushes
davidbyoung Apr 19, 2023
b2dab7a
Fixed missing PHPDoc
davidbyoung Apr 19, 2023
f286f3d
Started the foundation of improved auth examples in demo
davidbyoung Apr 21, 2023
410a57d
Changed some names of auth handlers to be more specific, removed unus…
davidbyoung Apr 19, 2023
09e77a1
Added some authorization checks, removed copyrights
davidbyoung Apr 23, 2023
3583979
Fixed typo
davidbyoung Apr 24, 2023
60073a8
Started implementing some integration tests
davidbyoung Apr 29, 2023
35c00b1
Updated to sort elements alphabetically, updated to use latest Aphiri…
davidbyoung Apr 19, 2023
d3feb69
Updated wording about demo code
davidbyoung May 5, 2023
30c78e6
Switched things over to SQLite to be less hacky, fixed various bugs, …
davidbyoung May 6, 2023
13cf295
Merge branch 'auth-improvements' of https://github.com/aphiria/app in…
davidbyoung May 6, 2023
cb5722b
Renamed some stuff, cleaned up some tests
davidbyoung May 6, 2023
a44d378
Small refactorings
davidbyoung May 9, 2023
e659e1d
Updated to use principal builder, fixed logic with integration tests,…
davidbyoung May 13, 2023
05b59e2
Added some more tests
davidbyoung May 13, 2023
931c5f5
Updated more code to use PrincipalBuilder, discovered bug that is not…
davidbyoung May 14, 2023
8ee55b9
Removed deprecrated PHP-CS-Fixer rule
davidbyoung May 14, 2023
a51e980
Removed unnecessary comma
davidbyoung May 14, 2023
9beaa8e
Fixed tests
davidbyoung May 22, 2023
faa8428
Made cookie the default authentication scheme, removed unnecessary Au…
davidbyoung May 13, 2023
f9e44c3
Updated to latest Aphiria
davidbyoung Jun 22, 2023
41c18c9
Merge branch '1.x' into auth-improvements
davidbyoung Jun 22, 2023
6493f2e
Merged origin
davidbyoung Jun 22, 2023
4583fd3
Fixed namespaces in .env.dist file
davidbyoung Jun 22, 2023
1d95c0c
Started adding new classes and interfaces for mocked authenticator. …
davidbyoung Jun 23, 2023
d14660b
Ran linter
davidbyoung Jun 23, 2023
6932e96
Cleaned up some code and comments
davidbyoung Jun 23, 2023
ad701f8
Added some more comments
davidbyoung Jun 23, 2023
81738b9
Refactored authenticator in integration tests to implement an interface
davidbyoung Jun 23, 2023
9b2a80b
Fixed some bugs with mocked auth calls, added auth integration tests
davidbyoung Jun 23, 2023
e4a82dc
Made var names consistent
davidbyoung Jun 23, 2023
b149a5a
Updated integration test to use response parser
davidbyoung Jul 17, 2023
3a8e506
Updated to use latest assertions
davidbyoung Jul 17, 2023
5cea302
Fixed Psalm issue
davidbyoung Jul 17, 2023
90f031a
Fixed Psalm config
davidbyoung Jul 17, 2023
053c487
Fixed Psalm config
davidbyoung Jul 17, 2023
1b014ed
Fixed some Psalm errors
davidbyoung Jul 17, 2023
c5f0208
Fixed type for name identifier to be int
davidbyoung Jul 17, 2023
6bf8874
Updated tests to use in-memory SQLite DB to simplify testing
davidbyoung Jul 17, 2023
43a4f8d
Merge branch '1.x' into auth-improvements
davidbyoung Jul 17, 2023
5b97413
Moved some integration test methods into traits for better encapsulation
davidbyoung Jul 17, 2023
75dd204
Fixed Psalm error
davidbyoung Jul 17, 2023
0d17e35
Simplified logic
davidbyoung Jul 17, 2023
3948cf8
Updated to publish code coverage during CI
davidbyoung Jul 17, 2023
3207695
Added shepherd to Psalm during CI
davidbyoung Jul 17, 2023
e925324
Simplified mock authenticator because some properties were not being …
davidbyoung Jul 18, 2023
3d2fa85
Updated to use simpler way of setting auth schemes during mock authen…
davidbyoung Jul 18, 2023
0f640b8
Removed unused import
davidbyoung Aug 9, 2023
bbf0ef1
Made it easier to seed DBs from integration tests
davidbyoung Aug 9, 2023
2e556f7
Updated to use built-in mock auth in integration tests
davidbyoung Aug 9, 2023
dbc034a
Fixed in-memory SQLite usage, code style tweaks
davidbyoung Aug 10, 2023
1934082
Refactored default user credentials to be read from .env file, added …
davidbyoung Aug 10, 2023
e6a0a60
Moved demo code out of demo dir
davidbyoung Aug 10, 2023
13f286c
Fixed Psalm error
davidbyoung Aug 10, 2023
8d95183
Fixed Psalm error
davidbyoung Aug 10, 2023
d96efd2
Moved mock auth into a callback for more clarity on the scope of what…
davidbyoung Aug 14, 2023
41ea2a7
Updated to use simpler PrincipalBuilder
davidbyoung Aug 18, 2023
c188f07
Minor CS changes
davidbyoung Aug 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
[email protected]
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
Loading