tobento / app-user
User support for the app with authentication and authorization.
Requires
- php: >=8.0
- tobento/app: ^1.0.7
- tobento/app-country: ^1.0
- tobento/app-database: ^1.0
- tobento/app-http: ^1.0
- tobento/app-migration: ^1.0
- tobento/app-validation: ^1.0
- tobento/service-acl: ^1.0
- tobento/service-clock: ^1.0
- tobento/service-config: ^1.0
- tobento/service-repository: ^1.0
- tobento/service-repository-storage: ^1.0
- tobento/service-user: ^1.0.1
Requires (Dev)
- mockery/mockery: ^1.6
- nyholm/psr7: ^1.4
- phpunit/phpunit: ^9.5
- tobento/app-console: ^1.0
- tobento/app-event: ^1.0.1
- tobento/service-console: ^1.0.1
- tobento/service-container: ^1.0
- vimeo/psalm: ^4.0
README
User support for the app with authentication and authorization.
Table of Contents
- Getting Started
- Documentation
- Credits
Getting Started
Add the latest version of the app user project running this command.
composer require tobento/app-user
Requirements
- PHP 8.0 or greater
Documentation
App
Check out the App Skeleton if you are using the skeleton.
You may also check out the App to learn more about the app in general.
User Boot
The user boot does the following:
- installs and loads user config file
- user and role repositories implementation based on config
- adds middleware for authentication and authorization based on config
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\User\Boot\User::class); // Run the app $app->run();
User Config
The configuration for the user is located in the app/config/user.php
file at the default App Skeleton config location.
User Repositories And Factories
The following repositories and factories are available as defined in the app/config/user.php
config file. The default repository implementation uses the Tobento\Service\Storage\StorageInterface::class
as the storage, defined in the app/config/database.php
file, which is the file storage by default:
use Tobento\App\AppFactory; use Tobento\App\User\UserRepositoryInterface; use Tobento\App\User\UserFactoryInterface; use Tobento\App\User\AddressRepositoryInterface; use Tobento\App\User\AddressFactoryInterface; use Tobento\App\User\RoleRepositoryInterface; use Tobento\App\User\RoleFactoryInterface; use Tobento\Service\Repository\RepositoryInterface; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // User: $userRepository = $app->get(UserRepositoryInterface::class); // var_dump($userRepository instanceof RepositoryInterface); // bool(true) $userFactory = $app->get(UserFactoryInterface::class); // Address: $addressRepository = $app->get(AddressRepositoryInterface::class); // var_dump($addressRepository instanceof RepositoryInterface); // bool(true) $addressFactory = $app->get(AddressFactoryInterface::class); // Role: $roleRepository = $app->get(RoleRepositoryInterface::class); // var_dump($roleRepository instanceof RepositoryInterface); // bool(true) $roleFactory = $app->get(RoleFactoryInterface::class); // Run the app: $app->run();
You may check out the Repository Interface to learn more about it.
User Authentication And Authenticator
The following authentication and authenticator interfaces are available as defined in the app/config/user.php
config file by default.
use Tobento\App\AppFactory; use Tobento\App\User\Authentication\AuthInterface; use Tobento\App\User\Authentication\Token\TokenStoragesInterface; use Tobento\App\User\Authentication\Token\TokenStorageInterface; use Tobento\App\User\Authentication\Token\TokenTransportsInterface; use Tobento\App\User\Authentication\Token\TokenTransportInterface; use Tobento\App\User\Authenticator\TokenAuthenticatorInterface; use Tobento\App\User\Authenticator\UserVerifierInterface; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // Auth: $auth = $app->get(AuthInterface::class); // Token storages: $tokenStorages = $app->get(TokenStoragesInterface::class); // Default token storage: $tokenStorage = $app->get(TokenStorageInterface::class); // Token transports: $tokenTransports = $app->get(TokenTransportsInterface::class); // Default token transport: $tokenTransport = $app->get(TokenTransportInterface::class); // Default token authenticator: $tokenAuthenticator = $app->get(TokenAuthenticatorInterface::class); // Default user verifier: $userVerifier = $app->get(UserVerifierInterface::class); // Run the app: $app->run();
Retrieve Current User
The current user will be available by the ServerRequestInterface::class
after the process of the Tobento\App\User\Middleware\User::class
defined in the app/config/user.php
config file.
use Tobento\App\AppFactory; use Tobento\App\User\UserInterface; use Tobento\Service\User\UserInterface as ServiceUserInterface; use Tobento\Service\Acl\Authorizable; use Psr\Http\Message\ServerRequestInterface; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // Routes: $app->route('GET', 'user', function(ServerRequestInterface $request) { // Get the current user (may be authenticated or not): $user = $request->getAttribute(UserInterface::class); // var_dump($user instanceof UserInterface); // bool(true) // var_dump($user instanceof ServiceUserInterface); // bool(true) // var_dump($user instanceof Authorizable); // bool(true) // Check if authenticated: $user->isAuthenticated(); // bool(false) return $user?->toArray(); }); // Run the app: $app->run();
Retrieve Authenticated User
The authenticated user will be available by the ServerRequestInterface::class
after the process of the Tobento\App\User\Middleware\Authentication::class
defined in the app/config/user.php
config file.
use Tobento\App\AppFactory; use Tobento\App\User\UserInterface; use Tobento\App\User\Authentication\AuthInterface; use Tobento\App\User\Authentication\AuthenticatedInterface; use Tobento\App\User\Authentication\Token\TokenInterface; use Tobento\Service\User\UserInterface as ServiceUserInterface; use Tobento\Service\Acl\Authorizable; use Psr\Http\Message\ServerRequestInterface; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // Routes: $app->route('GET', 'user', function(ServerRequestInterface $request) { // Get the auth: $auth = $request->getAttribute(AuthInterface::class); // var_dump($auth instanceof AuthInterface); // bool(true) // You may check if user is authenticated: if ($auth->hasAuthenticated()) { return null; } // Get authenticated: $authenticated = $auth->getAuthenticated(); // var_dump($auth->getAuthenticated() instanceof AuthenticatedInterface); // bool(true) // Get the authenticated user: $user = $authenticated->user(); // var_dump($user instanceof UserInterface); // bool(true) // var_dump($user instanceof ServiceUserInterface); // bool(true) // var_dump($user instanceof Authorizable); // bool(true) // Get the authenticated token: $token = $authenticated->token(); // var_dump($token instanceof TokenInterface); // bool(true) // Get the authenticated via: // var_dump($authenticated->via()); // string(9) "loginlink" // Get the authenticated by (authenticator class name): // var_dump($authenticated->by()); // string(52) "Tobento\App\User\Authenticator\IdentityAuthenticator" return $user; }); // Run the app: $app->run();
Authenticate User
There are many ways to authenticate a user depending on your app/config/user.php
config file configuration.
General authentication workflow:
use Tobento\App\User\UserRepositoryInterface; use Tobento\App\User\Authentication\AuthInterface; use Tobento\App\User\Authentication\Authenticated; use Tobento\App\User\Authentication\Token\TokenStorageInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; class AuthController { public function authenticate( ServerRequestInterface $request, UserRepositoryInterface $userRepository, AuthInterface $auth, TokenStorageInterface $tokenStorage, ): ResponseInterface { // You may check if user is authenticated: // Or use the \Tobento\App\User\Middleware\Unauthenticated::class to do so. if ($auth->hasAuthenticated()) { // create and return response if is already authenticated. } // authenticate user manually: $user = $userRepository->findById(1); if (is_null($user)) { // create and return response if user does not exist. } // create token and start auth: $token = $tokenStorage->createToken( // Set the payload: payload: ['userId' => $user->id(), 'passwordHash' => $user->password()], // Set the name of which the user was authenticated via: authenticatedVia: 'login.auth', // The name is up to you. // Set the name of which the user was authenticated by (authenticator name) or null if none: authenticatedBy: null, // Set the point in time the token has been issued or null (now): issuedAt: new \DateTimeImmutable('now'), // Set the point in time after which the token MUST be considered expired or null: // The time might depend on the token storage e.g. session expiration! expiresAt: new \DateTimeImmutable('now +10 minutes'), ); $auth->start(new Authenticated(token: $token, user: $user)); // create and return response: return $createdResponse; } }
You may use an authenticator to authenticate a user:
See Identity Authenticator - Login Controller Example for instance.
Unauthenticate User
To unauthenticate a user call the close
method from the AuthInterface::class
:
use Tobento\App\User\Authentication\AuthInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; class AuthController { public function unauthenticate( AuthInterface $auth, ): ResponseInterface { $auth->close(); // create and return response: return $createdResponse; } }
Acl Boot
The acl boot does the following:
- implements acl interface and set roles
The acl boot gets booted automatically by the User Boot!
use Tobento\App\AppFactory; use Tobento\Service\Acl\AclInterface; // Create the app $app = (new AppFactory())->createApp(); // Adding boots // The acl boot gets booted automatically by the user boot: // $app->boot(\Tobento\App\User\Boot\Acl::class); $app->boot(\Tobento\App\User\Boot\User::class); $acl = $app->get(AclInterface::class); // Run the app $app->run();
You may check out the Acl Service to learn more about it.
Adding Roles
The acl boot will add all roles found from the Tobento\App\User\RoleRepositoryInterface::class
. You can add roles by the following ways:
Using a migration
use Tobento\Service\Migration\MigrationInterface; use Tobento\Service\Migration\ActionsInterface; use Tobento\Service\Migration\Actions; use Tobento\Service\Repository\Storage\Migration\RepositoryAction; use Tobento\Service\Repository\Storage\Migration\RepositoryDeleteAction; use Tobento\App\User\RoleRepositoryInterface; class RolesStorageMigration implements MigrationInterface { public function __construct( protected RoleRepositoryInterface $roleRepository, ) {} /** * Return a description of the migration. * * @return string */ public function description(): string { return 'User roles.'; } /** * Return the actions to be processed on install. * * @return ActionsInterface */ public function install(): ActionsInterface { return new Actions( RepositoryAction::newOrNull( repository: $this->roleRepository, description: 'Default roles', items: [ ['key' => 'guest', 'areas' => ['frontend'], 'active' => true], ['key' => 'registered', 'areas' => ['frontend'], 'active' => true], ['key' => 'business', 'areas' => ['frontend'], 'active' => true], ['key' => 'developer', 'areas' => ['backend'], 'active' => true], ['key' => 'administrator', 'areas' => ['backend'], 'active' => true], ['key' => 'editor', 'areas' => ['backend'], 'active' => true], ], ), ); } /** * Return the actions to be processed on uninstall. * * @return ActionsInterface */ public function uninstall(): ActionsInterface { return new Actions(); } }
Then Install The Migration using the app migration.
Using In Memory Storage
You may use the In Memory Storage instead of the default defined in the app/config/user.php
config file:
//... User\RoleRepositoryInterface::class => static function(ContainerInterface $c) { $storage = new InMemoryStorage([ 'roles' => [ 1 => [ 'key' => 'guest', 'areas' => ['frontend'], 'active' => true, ], ], ]); return new User\RoleStorageRepository( storage: $storage, table: 'roles', entityFactory: $c->get(User\RoleFactoryInterface::class), ); }, //...
You may verify roles by the Verify Role Middleware.
Adding Rules
By default, no rules are added. You might add rules by the following ways:
Using the app
use Tobento\App\AppFactory; use Tobento\Service\Acl\AclInterface; // Create the app $app = (new AppFactory())->createApp(); // Adding boots // The acl boot gets booted automatically by // the user boot: // $app->boot(\Tobento\App\User\Boot\Acl::class); $app->boot(\Tobento\App\User\Boot\User::class); $acl = $app->get(AclInterface::class); $acl->rule('articles.read') ->title('Article Read') ->description('If a user can read articles'); // Run the app $app->run();
Using the acl boot
use Tobento\App\Boot; use Tobento\App\User\Boot\Acl; class AnyServiceBoot extends Boot { public const BOOT = [ // you may ensure the acl boot. Acl::class, ]; public function boot(Acl $acl) { $acl->rule('articles.read') ->title('Article Read') ->description('If a user can read articles'); } }
Permissions will be verified by the Verify Permission Middleware.
Check out the Rules to learn more about it.
Authorize User By Permission
There are several ways to authorize a user to access resources by checking its permission.
You may check out the Acl - Permissions section to learn more about it.
Using the acl
use Tobento\Service\Acl\AclInterface; class ArticleController { public function index(AclInterface $acl): string { if ($acl->cant('articles.read')) { return 'can not read'; } return 'can read'; } }
Using the user
use Tobento\App\User\UserInterface; use Psr\Http\Message\ServerRequestInterface; class ArticleController { public function index(ServerRequestInterface $request): string { $user = $request->getAttribute(UserInterface::class); if ($user->cant('articles.read')) { return 'can not read'; } return 'can read'; } }
Using middleware
Use the Verify Permission Middleware to verify permission on routes.
Authorize User By Role
Using the acl
use Tobento\Service\Acl\AclInterface; class ArticleController { public function index(AclInterface $acl): string { $user = $this->acl->getCurrentUser(); if ( $user && $user->hasRole() && $user->role()->key() !== 'editor' ) { return 'can not read'; } // or if ($user?->getRoleKey() !== 'editor') { return 'can not read'; } return 'can read'; } }
Using the user
use Tobento\App\User\UserInterface; use Psr\Http\Message\ServerRequestInterface; class ArticleController { public function index(ServerRequestInterface $request): string { $user = $request->getAttribute(UserInterface::class); if ($user->hasRole() && $user->role()->key() !== 'editor') { return 'can not read'; } // or if ($user->getRoleKey() !== 'editor') { return 'can not read'; } return 'can read'; } }
Using middleware
Use the Verify Role Middleware to verify role on routes.
Http User Error Handler Boot
The http user error handler boot handles any user specific exceptions such as:
Tobento\App\User\Exception\TokenExpiredException
Tobento\App\User\Exception\AuthenticationException
Tobento\App\User\Exception\AuthorizationException
Tobento\App\User\Exception\PermissionDeniedException
Tobento\App\User\Exception\RoleDeniedException
Adding the boot:
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\User\Boot\HttpUserErrorHandler::class); $app->boot(\Tobento\App\User\Boot\User::class); // Run the app $app->run();
You may create a custom Error Handler or add an Error Handler With A Higher Priority of 3000
as defined on the Tobento\App\User\Boot\HttpUserErrorHandler::class
.
Middleware
User Middleware
The Tobento\App\User\Middleware\User::class
middleware will ensure that there is always a user available from the request attributes. Furthermore, it sets the current acl user.
Code snippet from the middleware process method:
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { // Get the authenticated user if exists: $user = $request->getAttribute(AuthInterface::class)?->getAuthenticated()?->user(); // Otherwise, create guest user: if (is_null($user)) { $user = $this->userFactory->createGuestUser(); } $request = $request->withAttribute(UserInterface::class, $user); // Set user on acl: if ($this->acl) { $this->acl->setCurrentUser($user); } return $handler->handle($request); }
Authentication Middleware
The Tobento\App\User\Middleware\Authentication::class
middleware will try to authenticate the user on every request by
using the defined token transport(s) to fetch the token from the request which will be authenticated by the token authenticator.
Code snippet from the middleware process method:
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { // Add auth to the request: $request = $request->withAttribute(AuthInterface::class, $this->auth); // Handle token: $tokenId = $this->tokenTransport->fetchTokenId($request); if (!is_null($tokenId)) { try { $token = $this->tokenStorage->fetchToken($tokenId); $user = $this->tokenAuthenticator->authenticate($token); $this->auth->start( new Authenticated(token: $token, user: $user), $this->tokenTransport->name() ); } catch (TokenNotFoundException $e) { // ignore TokenNotFoundException as to // proceed with handling the response. // other exceptions will be handled by the error handler! } } // Handle the response: $response = $handler->handle($request); if (! $this->auth->hasAuthenticated()) { return $response; } if ($this->auth->isClosed()) { $this->tokenStorage->deleteToken($this->auth->getAuthenticated()->token()); return $this->tokenTransport->removeToken( token: $this->auth->getAuthenticated()->token(), request: $request, response: $response, ); } return $this->tokenTransport->commitToken( token: $this->auth->getAuthenticated()->token(), request: $request, response: $response, ); }
Authentication With Middleware
Check out the Different Authentication Per Routes section to learn more about this middleware.
Authenticated Middleware
The Authenticated::class
middleware protects routes from unauthenticated users.
use Tobento\App\AppFactory; use Tobento\App\User\Middleware\Authenticated; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // Routes: $app->route('GET', 'account', function() { // only for authenticated user! return 'response'; })->middleware(Authenticated::class)->name('account'); $app->route('GET', 'account', function() { // only for authenticated user! return 'response'; })->middleware([ Authenticated::class, // you may allow access only to user authenticated via: 'via' => 'loginform|loginlink', // or you may allow access only to user authenticated except via: 'exceptVia' => 'remembered|loginlink', // you may specify a custom message to show to the user: 'message' => 'You have insufficient rights to access the resource!', // you may specify a message level: 'messageLevel' => 'notice', // you may specify a route name for redirection: 'redirectRoute' => 'login', // or you may specify an uri for redirection 'redirectUri' => '/login', ]); // you may use the middleware alias defined in user config: $app->route('GET', 'account', function() { return 'response'; })->middleware(['auth', 'via' => 'loginform|loginlink']); // Run the app: $app->run();
Unauthenticated Middleware
The Unauthenticated::class
middleware protects routes from authenticated users.
use Tobento\App\AppFactory; use Tobento\App\User\Middleware\Unauthenticated; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // Routes: $app->route('GET', 'login', function() { // only for unauthenticated user! return 'response'; })->middleware(Unauthenticated::class)->name('login'); $app->route('GET', 'login', function() { // only for unauthenticated user! return 'response'; })->middleware([ Unauthenticated::class, // you may allow access only to user authenticated via: 'via' => 'remembered|loginlink', // or you may allow access only to user authenticated except via: 'exceptVia' => 'remembered|loginlink', // you may specify a custom message to show to the user: 'message' => 'Already authenticated!', // you may specify a message level: 'messageLevel' => 'notice', // you may specify a route name for redirection: 'redirectRoute' => 'home', // or you may specify an uri for redirection 'redirectUri' => '/home', ]); // you may use the middleware alias defined in user config: $app->route('GET', 'login', function() { return 'response'; })->middleware(['guest', 'message' => 'Already authenticated!']); // Run the app: $app->run();
Verified Middleware
The Verified::class
middleware protects routes from unverified users.
use Tobento\App\AppFactory; use Tobento\App\User\Middleware\Verified; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // Routes: $app->route('GET', 'account', function() { // only for users with at least one channel verified! return 'response'; })->middleware(Verified::class)->name('account'); $app->route('GET', 'account', function() { // only for users with specific channels verified! return 'response'; })->middleware([ Verified::class, // specify the channels the user must have at least verified one of: 'oneOf' => 'email|smartphone', // OR specify the channels the user must have verified all: 'allOf' => 'email|smartphone', // you may specify a custom message to show to the user: 'message' => 'You are not verified to access the resource!', // you may specify a message level: 'messageLevel' => 'notice', // you may specify a route name for redirection: 'redirectRoute' => 'login', // or you may specify an uri for redirection 'redirectUri' => '/login', ]); // you may use the middleware alias defined in user config: $app->route('GET', 'account', function() { return 'response'; })->middleware(['verified', 'oneOf' => 'email|smartphone']); // Run the app: $app->run();
Check out the User Channel Verification section to learn more about it.
Verify Permission Middleware
The VerifyPermission::class
middleware protects routes from users without the defined permission(s). If a user has insufficient permission a Tobento\App\User\Exception\PermissionDeniedException::class
will be thrown.
use Tobento\App\AppFactory; use Tobento\App\User\Middleware\VerifyPermission; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // Routes: $app->route('GET', 'login', function() { // only for user with permission login.show! return 'response'; })->middleware(VerifyPermission::class)->name('login.show'); // you may specify the following parameters: $app->route('GET', 'login', function() { return 'response'; })->middleware([ VerifyPermission::class, // set the permission (optional). // if not set it uses the route name: 'permission' => 'login.show|anotherPermission', // you may specify a custom message to show to the user: 'message' => 'You do not have permission to access the resource!', // you may specify a message level: 'messageLevel' => 'notice', // you may specify a route name for redirection: 'redirectRoute' => 'home', // or you may specify an uri for redirection 'redirectUri' => '/home', ])->name('login.show'); // you may use the middleware alias defined in user config: $app->route('GET', 'login', function() { return 'response'; })->middleware('can'); // Run the app: $app->run();
Check out the Http User Error Handler Boot how to handle the exception.
Verify Route Permission Middleware
The VerifyRoutePermission::class
middleware protects routes from users without the defined permissions. If a user has insufficient permission a Tobento\App\User\Exception\PermissionDeniedException::class
will be thrown.
use Tobento\App\AppFactory; use Tobento\App\User\Middleware\VerifyRoutePermission; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // Routes: $app->routeResource('roles', RolesController::class)) ->middleware([ VerifyRoutePermission::class, 'permissions' => [ // 'route.name' => 'permission' 'roles.index' => 'roles', 'roles.show' => 'roles', 'roles.create' => 'roles.create', 'roles.store' => 'roles.create', 'roles.edit' => 'roles.edit', 'roles.update' => 'roles.edit', 'roles.delete' => 'roles.delete', ], // you may specify a custom message to show to the user: 'message' => 'You do not have permission to access the resource!', // you may specify a message level: 'messageLevel' => 'notice', // you may specify a route name for redirection: 'redirectRoute' => 'home', // or you may specify an uri for redirection 'redirectUri' => '/home', ]); // Run the app: $app->run();
Check out the Http User Error Handler Boot how to handle the exception.
Verify Role Middleware
The VerifyRole::class
middleware protects routes from users without the defined role(s). If a user has insufficient role a Tobento\App\User\Exception\RoleDeniedException::class
will be thrown.
use Tobento\App\AppFactory; use Tobento\App\User\Middleware\VerifyRole; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\User\Boot\User::class); $app->booting(); // Routes: $app->route('GET', 'login', function() { // only for user with role editor and administrator! return 'response'; })->middleware([ VerifyRole::class, // set the role. 'role' => 'editor|administrator', // you may specify a custom message to show to the user: 'message' => 'You do not have a required role to access the resource!', // you may specify a message level: 'messageLevel' => 'notice', // you may specify a route name for redirection: 'redirectRoute' => 'home', // or you may specify an uri for redirection 'redirectUri' => '/home', ]); // you may use the middleware alias defined in user config: $app->route('GET', 'login', function() { return 'response'; })->middleware(['role', 'role' => 'editor|administrator']); // Run the app: $app->run();
Check out the Http User Error Handler Boot how to handle the exception.
Authenticator
Identity Authenticator
The Tobento\App\User\Authenticator\IdentityAuthenticator::class
identifies the user by the email
and/or username
and/or smartphone
from the request input user
. Furthermore, it verifies the user password from the request input password
. You can specify which attributes are allowed for identification and if you want to verify the password.
Login Controller Example
use Tobento\App\User\Authenticator\IdentityAuthenticator; use Tobento\App\User\Authenticator\UserVerifiers; use Tobento\App\User\Authenticator\UserRoleAreaVerifier; use Tobento\App\User\Authentication\AuthInterface; use Tobento\App\User\Authentication\Authenticated; use Tobento\App\User\Authentication\Token\TokenStorageInterface; use Tobento\App\User\Exception\AuthenticationException; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; class LoginController { public function login( ServerRequestInterface $request, IdentityAuthenticator $authenticator, AuthInterface $auth, TokenStorageInterface $tokenStorage, ): ResponseInterface { // You may specify the identity attributes to be checked. // At least one attribute is required. $authenticator->identifyBy(['email', 'username', 'smartphone', 'password']); // or only identify by email and verify password. $authenticator->identifyBy(['email', 'password']); // you may verify user attributes: $authenticator = $authenticator->withUserVerifier( new UserVerifiers( new UserRoleAreaVerifier('frontend'), ), ); // You may change the request input names: $authenticator->userInputName('user'); $authenticator->passwordInputName('password'); // You may change the request method, // only 'POST' (default), 'GET' and 'PUT': $authenticator->requestMethod('POST'); // try to authenticate user: try { $user = $authenticator->authenticate($request); } catch (AuthenticationException $e) { // handle exception: // create and return response for exception: return $createdResponse; } // on success create token and start auth: $token = $tokenStorage->createToken( payload: ['userId' => $user->id(), 'passwordHash' => $user->password()], authenticatedVia: 'loginform', authenticatedBy: $authenticator::class, // issuedAt: $issuedAt, // expiresAt: $expiresAt, ); $auth->start(new Authenticated(token: $token, user: $user)); // create and return response: return $createdResponse; } }
Attributes Authenticator
The Tobento\App\User\Authenticator\AttributesAuthenticator::class
identifies the user by the specified user attributes. Unlike the Identity Authenticator this authenticator identifies the user by all attributes.
Login Controller Example
use Tobento\App\User\Authenticator\AttributesAuthenticator; use Tobento\App\User\Authenticator\UserVerifiers; use Tobento\App\User\Authenticator\UserRoleAreaVerifier; use Tobento\App\User\Authentication\AuthInterface; use Tobento\App\User\Authentication\Authenticated; use Tobento\App\User\Authentication\Token\TokenStorageInterface; use Tobento\App\User\Exception\AuthenticationException; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; class LoginController { public function login( ServerRequestInterface $request, AttributesAuthenticator $authenticator, AuthInterface $auth, TokenStorageInterface $tokenStorage, ): ResponseInterface { // Specify the user attributes to be identified: $authenticator->addAttribute( // specify the user attribute name: name: 'email', // specify the reuest input name: // optional, if not set name will be used! inputName: 'email', // you may specify the validation rules: // default rule is required|string|minLen:3|maxLen:150 validate: 'required|email', ); //$authenticator->addAttribute(name: 'smartphone'); // will verify password. $authenticator->addAttribute(name: 'password'); // you may verify user attributes: $authenticator = $authenticator->withUserVerifier( new UserVerifiers( new UserRoleAreaVerifier('frontend'), ), ); // You may change the request method, // only 'POST' (default), 'GET' and 'PUT': $authenticator->requestMethod('POST'); // try to authenticate user: try { $user = $authenticator->authenticate($request); } catch (AuthenticationValidationException $e) { // you may handle specific exception: // create and return response for exception: return $createdResponse; } catch (AuthenticationException $e) { // handle exception: // create and return response for exception: return $createdResponse; } // on success create token and start auth: $token = $tokenStorage->createToken( payload: ['userId' => $user->id(), 'passwordHash' => $user->password()], authenticatedVia: 'loginform', authenticatedBy: $authenticator::class, // issuedAt: $issuedAt, // expiresAt: $expiresAt, ); $auth->start(new Authenticated(token: $token, user: $user)); // create and return response: return $createdResponse; // create and return response: return $createdResponse; } }
User Verifier
User verifiers may be used to verify certain user attributes while authenticating a user. See Identity Authenticator for instance.
User Permission Verifier
use Tobento\App\User\Authenticator\UserPermissionVerifier; // User must have one the specified permission. $verifier = new UserPermissionVerifier('permission');
User Role Verifier
use Tobento\App\User\Authenticator\UserRoleVerifier; // User must have one of the specified role. $verifier = new UserRoleVerifier('editor', 'author');
User Role Area Verifier
use Tobento\App\User\Authenticator\UserRoleAreaVerifier; // User must have one of the specified role area. $verifier = new UserRoleAreaVerifier('frontend', 'api');
Token Authenticator
The token authenticator is responsible to authenticate the user based on the token. See Authentication Middleware for more detail.
You may add token verifiers in the app/config/user.php
file to verify certain token payload attributes:
use Tobento\App\User\Authentication; use Psr\Container\ContainerInterface; return [ // ... 'interfaces' => [ // ... // Default token authenticator: // Authenticator\TokenAuthenticatorInterface::class => Authenticator\TokenAuthenticator::class, // Example with token verifiers: Authenticator\TokenAuthenticatorInterface::class => static function(ContainerInterface $c) { return new Authenticator\TokenAuthenticator( verifier: new Authenticator\TokenVerifiers( new Authenticator\TokenPasswordHashVerifier( // The token issuers (storage names) to verify password hash. // If empty it gets verified for all issuers. issuers: ['session'], // The attribute name of the payload: name: 'passwordHash', ), ), ); }, // ... ], ];
Token Verifier
Token verifiers may be used to verify certain token payload attributes while authenticating a user by token. See Token Authenticator for instance.
Token Password Hash Verifier
The TokenPasswordHashVerifier::class
may be used to invalidate tokens if user changes password.
use Tobento\App\User\Authenticator\TokenPasswordHashVerifier; $verifier = new TokenPasswordHashVerifier( // The token issuers (storage names) to verify password hash. // If empty it gets verified for all issuers. issuers: ['session'], // Will only be verified if authenticated // via remembered or loginlink if specified: authenticatedVia: 'remembered|loginlink', // The attribute name of the payload: name: 'passwordHash', );
Token Payload Verifier
The TokenPayloadVerifier::class
may be used to invalidate tokens if the specified payload attribute does not match the given value.
use Tobento\App\User\Authenticator\TokenPayloadVerifier; $verifier = new TokenPayloadVerifier( // Specify the payload attribute name: name: 'remoteAddress', // Specify the value to match: value: $_SERVER['REMOTE_ADDR'] ?? null, // The token issuers (storage names) to verify password hash. // If empty it gets verified for all issuers. issuers: ['session'], // Will only be verified if authenticated // via remembered or loginlink if specified: authenticatedVia: 'remembered|loginlink', );
Token Storage
Null Storage
The NullStorage
does not store any token at all. This means you will never be authenticated.
In app/config/user.php
file:
use Tobento\App\User\Authentication; use Psr\Container\ContainerInterface; return [ // ... 'interfaces' => [ // ... // Define the token storages you wish to support: Authentication\Token\TokenStoragesInterface::class => static function(ContainerInterface $c) { return new Authentication\Token\TokenStorages( // add null storage: new Authentication\Token\NullStorage(), ); }, // Define the default token storage used for auth: Authentication\Token\TokenStorageInterface::class => static function(ContainerInterface $c) { // you might change to null: return $c->get(Authentication\Token\TokenStoragesInterface::class)->get('null'); }, // ... ], ];
In Memory Storage
The InMemoryStorage
does store tokens in memory only.
In app/config/user.php
file:
use Tobento\App\User\Authentication; use Psr\Container\ContainerInterface; use Psr\Clock\ClockInterface; return [ // ... 'interfaces' => [ // ... // Define the token storages you wish to support: Authentication\Token\TokenStoragesInterface::class => static function(ContainerInterface $c) { return new Authentication\Token\TokenStorages( // add inmemory storage: new Authentication\Token\InMemoryStorage( clock: $c->get(ClockInterface::class), ), ); }, // Define the default token storage used for auth: Authentication\Token\TokenStorageInterface::class => static function(ContainerInterface $c) { // you might change to inmemory: return $c->get(Authentication\Token\TokenStoragesInterface::class)->get('inmemory'); }, // ... ], ];
Repository Storage
The RepositoryStorage
uses the Service Repository Storage to store tokens.
In app/config/user.php
file:
use Tobento\App\User\Authentication; use Tobento\Service\Storage\StorageInterface; use Psr\Container\ContainerInterface; use Psr\Clock\ClockInterface; return [ // ... 'interfaces' => [ // ... // Define the token storages you wish to support: Authentication\Token\TokenStoragesInterface::class => static function(ContainerInterface $c) { return new Authentication\Token\TokenStorages( new Authentication\Token\RepositoryStorage( clock: $c->get(ClockInterface::class), repository: new Authentication\Token\TokenRepository( storage: $c->get(StorageInterface::class)->new(), table: 'auth_tokens', ), name: 'repository', ), ); }, // Define the default token storage used for auth: Authentication\Token\TokenStorageInterface::class => static function(ContainerInterface $c) { return $c->get(Authentication\Token\TokenStoragesInterface::class)->get('repository'); }, // ... ], ];
Delete expired tokens
You may call the deleteExpiredTokens
method to delete all expired tokens:
$repositoryStorage->deleteExpiredTokens();
Session Storage
Stores the token in PHP session.
In app/config/user.php
file:
use Tobento\App\User\Authentication; use Tobento\Service\Session\SessionInterface; use Psr\Container\ContainerInterface; use Psr\Clock\ClockInterface; return [ // ... 'interfaces' => [ // ... // Define the token storages you wish to support: Authentication\Token\TokenStoragesInterface::class => static function(ContainerInterface $c) { return new Authentication\Token\TokenStorages( // add session storage: new Authentication\Token\SessionStorage( session: $c->get(SessionInterface::class), clock: $c->get(ClockInterface::class), ), ); }, // Define the default token storage used for auth: Authentication\Token\TokenStorageInterface::class => static function(ContainerInterface $c) { // you might change to session: return $c->get(Authentication\Token\TokenStoragesInterface::class)->get('session'); }, // ... ], ];
Make sure you boot the App Http - Session Boot in your app:
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\User\Boot\User::class); $app->boot(\Tobento\App\Http\Boot\Session::class); // Run the app $app->run();
Token Transport
Cookie Transport
Stores the authentication token in a cookie.
In app/config/user.php
file:
use Tobento\App\User\Authentication; use Psr\Container\ContainerInterface; use Psr\Clock\ClockInterface; return [ // ... 'interfaces' => [ // ... // Define the token transport you wish to support: Authentication\Token\TokenTransportsInterface::class => static function(ContainerInterface $c) { return new Authentication\Token\TokenTransports( new Authentication\Token\CookieTransport( clock: $c->get(ClockInterface::class), cookieName: 'token', ), ); }, // Define the default token transport(s) used for auth: Authentication\Token\TokenTransportInterface::class => static function(ContainerInterface $c) { return $c->get(Authentication\Token\TokenTransportsInterface::class)->get('cookie'); //return $c->get(Authentication\Token\TokenTransportsInterface::class); // all //return $c->get(Authentication\Token\TokenTransportsInterface::class)->only(['cookie']); }, // ... ], ];
Make sure you boot the App Http - Cookies Boot in your app:
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\User\Boot\User::class); $app->boot(\Tobento\App\Http\Boot\Cookies::class); // Run the app $app->run();
Header Transport
Stores the authentication token in a HTTP header.
In app/config/user.php
file:
use Tobento\App\User\Authentication; use Psr\Container\ContainerInterface; return [ // ... 'interfaces' => [ // ... // Define the token transport you wish to support: Authentication\Token\TokenTransportsInterface::class => static function(ContainerInterface $c) { return new Authentication\Token\TokenTransports( new Authentication\Token\HeaderTransport(name: 'header', headerName: 'X-Auth-Token'), ); }, // Define the default token transport(s) used for auth: Authentication\Token\TokenTransportInterface::class => static function(ContainerInterface $c) { return $c->get(Authentication\Token\TokenTransportsInterface::class)->get('header'); //return $c->get(Authentication\Token\TokenTransportsInterface::class); // all //return $c->get(Authentication\Token\TokenTransportsInterface::class)->only(['header']); }, // ... ], ];
Events
Available Events
use Tobento\App\User\Event;
Supporting Events
Simply, install the App Event bundle.
Console
Before using commands, you will need to install the App Console bundle.
Acl Roles Command
List all acl roles:
php ap acl:roles
Acl Rules Command
List all acl rules:
php ap acl:rules
Delete Expired Tokens Command
You may delete expired tokens from token storages supporting it.
php ap auth:purge-tokens
You may delete only from specific token storages:
php ap auth:purge-tokens --storage=name
Migration
Role Permissions Action
You may use the RolePermissionsAction::class
to add and remove permissions for roles.
use Tobento\App\User\Migration\RolePermissionsAction; use Tobento\App\User\RoleRepositoryInterface; use Tobento\Service\Migration\MigrationInterface; use Tobento\Service\Migration\ActionsInterface; use Tobento\Service\Migration\Actions; class RolesPermissionMigration implements MigrationInterface { public function __construct( protected RoleRepositoryInterface $roleRepository, ) {} /** * Return a description of the migration. * * @return string */ public function description(): string { return 'Role permissions.'; } /** * Return the actions to be processed on install. * * @return ActionsInterface */ public function install(): ActionsInterface { return new Actions( new RolePermissionsAction( roleRepository: $this->roleRepository, add: [ 'developer' => ['roles', 'roles.create', 'roles.edit', 'roles.delete', 'roles.permissions'], 'administrator' => ['roles', 'roles.create', 'roles.edit', 'roles.delete', 'roles.permissions'], ], description: 'Roles permissions added for developer and administrator', ), ); } /** * Return the actions to be processed on uninstall. * * @return ActionsInterface */ public function uninstall(): ActionsInterface { return new Actions( new RolePermissionsAction( roleRepository: $this->roleRepository, remove: [ 'developer' => ['roles', 'roles.create', 'roles.edit', 'roles.delete', 'roles.permissions'], 'administrator' => ['roles', 'roles.create', 'roles.edit', 'roles.delete', 'roles.permissions'], ], description: 'Roles permissions removed for developer and administrator', ), ); } }
Then Install The Migration using the app migration.
Learn More
Different Authentication Per Routes
First, configure the app/config/user.php
file and define token storages and transports you wish to use:
use Tobento\App\User\Authentication; use Psr\Container\ContainerInterface; use Psr\Clock\ClockInterface; return [ // ... 'middlewares' => [ // Uncomment it and set it on each route individually! // User\Middleware\Authentication::class, ], 'interfaces' => [ // ... // Define the token storages you wish to support: Authentication\Token\TokenStoragesInterface::class => static function(ContainerInterface $c) { return new Authentication\Token\TokenStorages( new Authentication\Token\SessionStorage( session: $c->get(SessionInterface::class), clock: $c->get(ClockInterface::class), ), ); }, // Define the token transport you wish to support: Authentication\Token\TokenTransportsInterface::class => static function(ContainerInterface $c) { return new Authentication\Token\TokenTransports( new Authentication\Token\CookieTransport( clock: $c->get(ClockInterface::class), cookieName: 'token', ), new Authentication\Token\HeaderTransport(name: 'header', headerName: 'X-Auth-Token'), ); }, // ... ], ];
Finally, add middleware to your routes:
use Tobento\App\User\Middleware\AuthenticationWith; use Tobento\Service\Routing\RouteGroupInterface; // ... // Routes: // Web example: $app->routeGroup('', function(RouteGroupInterface $group) { // define your web routes: })->middleware([ AuthenticationWith::class, // specify the token transport name: 'transportName' => 'cookie', // specify the token storage name: 'storageName' => 'session', ]); // Api example: $app->routeGroup('api', function(RouteGroupInterface $group) { // define your api routes: })->middleware([ AuthenticationWith::class, // specify the token transport name: 'transportName' => 'header', // specify the token storage name: 'storageName' => 'session', ]); // ...
Another way to is to use a Http - Area Boot for each area "web" and "api" running in its own application.
Password Hashing
Use the PasswordHasherInterface::class
defined in the app/config/user.php
file to hash and verify user passwords.
Basic Usage
use Tobento\App\User\PasswordHasherInterface; class SomeService { public function __construct( private PasswordHasherInterface $passwordHasher, ) { // hash password: $hashedPassword = $passwordHasher->hash(plainPassword: 'password'); // verify password: $isValid = $passwordHasher->verify(hashedPassword: $hashedPassword, plainPassword: 'password'); } }
The following authenticators use the password hasher to verify the password:
User Channel Verification
Adding verified channels
use Tobento\App\User\UserRepositoryInterface; use DateTime; class SomeService { public function __construct( private UserRepositoryInterface $userRepository, ) { $updatedUser = $userRepository->addVerified( id: 5, // user id channel: 'email', verifiedAt: new DateTime('2023-09-24 00:00:00'), ); } }
Removing verified channels
use Tobento\App\User\UserRepositoryInterface; use DateTime; class SomeService { public function __construct( private UserRepositoryInterface $userRepository, ) { $updatedUser = $userRepository->removeVerified( id: 5, // user id channel: 'email', ); } }
User verified methods
use Tobento\App\User\UserInterface; var_dump($user instanceof UserInterface); // bool(true) // Get the verified channels: $verified = $user->getVerified(); // ['email' => '2023-09-24 00:00:00'] // Get the date verified at for a specific channel: $emailVerifiedAt = $user->getVerifiedAt(channel: 'email'); // '2023-09-24 00:00:00' or NULL // Returns true if the specified channels are verified, otherwise false. $verified = $user->isVerified(channels: ['email', 'smartphone']); // Returns true if at least one channel is verified, otherwise false. $verified = $user->isOneVerified(); // or one of the specified channels: $verified = $user->isOneVerified(channels: ['email', 'smartphone']);
App User Bundles
You may use the following user bundles for your app.
- App User Web - Login, register and more. (Coming soon)
- App User Manager - CRUD for users, roles and permissions. (Coming soon)
- App User Jwt - Authentication via JSON web token support. (Coming soon)
- App User Login Link - Authentication via login link. (Coming soon)