├── src ├── Controllers │ ├── Home.php │ └── Auth │ │ ├── EmailVerificationPromptController.php │ │ ├── EmailVerificationNotificationController.php │ │ ├── ConfirmablePasswordController.php │ │ ├── PasswordResetLinkController.php │ │ ├── VerifyEmailController.php │ │ ├── RegisteredUserController.php │ │ ├── NewPasswordController.php │ │ └── AuthenticatedSessionController.php ├── Contracts │ ├── ResetPasswordInterface.php │ ├── PasswordBrokerFactoryInterface.php │ ├── VerifyEmailInterface.php │ ├── AuthenticationBasicInterface.php │ ├── HasherInterface.php │ ├── AuthorizableInterface.php │ ├── PasswordResetRepositoryInterface.php │ ├── AuthenticatorInterface.php │ ├── UserProviderInterface.php │ ├── HasAccessTokensInterface.php │ ├── AuthFactoryInterface.php │ ├── PasswordBrokerInterface.php │ ├── AuthenticationInterface.php │ └── GateInterface.php ├── AuthServiceProvider.php ├── Traits │ ├── CanResetPasswordTrait.php │ ├── MustVerifyEmailTrait.php │ ├── AuthorizableTrait.php │ ├── InteractsWithTimeTrait.php │ ├── AuthenticatableTrait.php │ ├── GuardHelperTrait.php │ ├── AuthorizesRequestsTrait.php │ ├── UserProviderTrait.php │ ├── InteractsWithAuthentication.php │ └── HasAccessTokensTrait.php ├── Language │ └── en │ │ ├── Auth.php │ │ └── Passwords.php ├── Authorization │ ├── HandlesAuthorization.php │ ├── Events │ │ └── GateEvaluated.php │ └── Response.php ├── Views │ ├── Auth │ │ ├── messages.php │ │ ├── forgot_password.php │ │ ├── verify_email.php │ │ ├── confirm_password.php │ │ ├── layout.php │ │ ├── reset_password.php │ │ ├── login.php │ │ └── register.php │ └── Email │ │ └── layout.php ├── Filters │ ├── RedirectAuthenticatedFilter.php │ ├── AuthenticationBasicFilter.php │ ├── EmailVerifiedFilter.php │ ├── ConfirmPasswordFilter.php │ ├── AuthorizeFilter.php │ ├── ThrottleFilter.php │ └── AuthenticationFilter.php ├── Passwords │ ├── Hash │ │ ├── AbstractHasher.php │ │ ├── Argon2IdHasher.php │ │ ├── HashManager.php │ │ ├── BcryptHasher.php │ │ ├── AbstractManager.php │ │ └── ArgonHasher.php │ ├── PasswordBrokerManager.php │ ├── RateLimiter.php │ ├── PasswordResetRepository.php │ └── PasswordBroker.php ├── AbstractServiceProvider.php ├── Facades │ ├── RateLimiter.php │ ├── Hash.php │ ├── Passwords.php │ ├── Gate.php │ └── Auth.php ├── Exceptions │ ├── AuthenticationException.php │ └── AuthorizationException.php ├── Notifications │ ├── ResetPasswordNotification.php │ └── VerificationNotification.php ├── Helpers │ ├── auth_helper.php │ ├── Arr.php │ └── Str.php ├── Entities │ ├── User.php │ └── AccessToken.php ├── Commands │ └── AuthClearResetCommand.php ├── Models │ ├── UserModel.php │ └── AccessTokenModel.php ├── Config │ ├── Hashing.php │ ├── Services.php │ └── Auth.php ├── CookieRecaller.php ├── Collectors │ └── AuthCollector.php ├── Database │ └── Migrations │ │ └── 2020-12-28-223112_create_auth_tables.php └── UserDatabase.php ├── LICENSE.md ├── composer.json └── README.md /src/Controllers/Home.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected static $policies = []; 16 | 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | public static function register() 21 | { 22 | static::registerPolicies(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 19 | ? redirect()->to(session('intended') ?? config('Auth')->home) 20 | : view('Auth/verify_email'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Contracts/PasswordBrokerFactoryInterface.php: -------------------------------------------------------------------------------- 1 | email; 18 | } 19 | 20 | /** 21 | * Send the password reset notification. 22 | * 23 | * @return void 24 | */ 25 | public function sendPasswordResetNotification(string $token) 26 | { 27 | Events::trigger(ResetPasswordInterface::class, $this->getEmailForPasswordReset(), $token); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Language/en/Auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 16 | 'throttler' => 'You submitted over {0} requests within a minute. Please try again in {1} seconds.', 17 | 'throttle' => 'Too many login attempts. Please try again in {0} seconds.', 18 | ]; 19 | -------------------------------------------------------------------------------- /src/Authorization/HandlesAuthorization.php: -------------------------------------------------------------------------------- 1 | has('message')) : ?> 2 | 5 | 6 | 7 | has('error')) : ?> 8 | 13 | 14 | 15 | has('errors')) : ?> 16 | 23 | 24 | -------------------------------------------------------------------------------- /src/Filters/RedirectAuthenticatedFilter.php: -------------------------------------------------------------------------------- 1 | check()) { 23 | return redirect(config('Auth')->home); 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) 32 | { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Passwords/Hash/AbstractHasher.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected static $policies = []; 15 | 16 | /** 17 | * Register the service provider. 18 | * 19 | * @return void 20 | */ 21 | abstract static function register(); 22 | 23 | /** 24 | * Register the application's policies. 25 | * 26 | * @return void 27 | */ 28 | public static function registerPolicies() 29 | { 30 | foreach (static::policies() as $key => $value) { 31 | Gate::policy($key, $value); 32 | } 33 | } 34 | 35 | /** 36 | * Get the policies defined on the provider. 37 | * 38 | * @return array 39 | */ 40 | public static function policies() 41 | { 42 | return static::$policies; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user(); 22 | 23 | if ($user->hasVerifiedEmail()) { 24 | return redirect()->to(session('intended') ?? config('Auth')->home); 25 | } 26 | 27 | $status = Passwords::sendVerifyLink([ 28 | 'email' => $user->email, 29 | ]); 30 | 31 | return $status === PasswordBrokerInterface::VERIFY_LINK_SENT 32 | ? redirect()->back()->with('message', lang($status)) 33 | : redirect()->back()->with('error', lang($status)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Language/en/Passwords.php: -------------------------------------------------------------------------------- 1 | 'A new verification link has been sent to the email address you provided during registration.', 16 | 'confirm' => 'The provided password is incorrect.', 17 | 'reset' => 'Your password has been reset!', 18 | 'sent' => 'We have emailed your password reset link!', 19 | 'throttled' => 'Please wait before retrying.', 20 | 'token' => 'This password reset token is invalid.', 21 | 'expired' => 'This password reset token is expired.', 22 | 'user' => "We can't find a user with that email address.", 23 | ]; 24 | -------------------------------------------------------------------------------- /src/Facades/RateLimiter.php: -------------------------------------------------------------------------------- 1 | {$method}(...$arguments); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Contracts/HasherInterface.php: -------------------------------------------------------------------------------- 1 | auth = Services::auth(); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function before(RequestInterface $request, $arguments = null) 27 | { 28 | [$guard, $field, $method] = $arguments; 29 | 30 | $this->auth->guard($guard)->{$method}($field); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) 37 | { 38 | } 39 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Copyright (c) Agung Sugiarto 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | request->getPost(); 32 | 33 | if ( 34 | ! Auth::validate([ 35 | 'email' => auth()->user()->email, 36 | 'password' => $request->password, 37 | ]) 38 | ) { 39 | return redirect()->back()->with('error', lang('Passwords.confirm')); 40 | } 41 | 42 | session()->set('password_confirmed_at', time()); 43 | 44 | return redirect()->to(session('intended') ?? config('Auth')->home); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Authorization/Events/GateEvaluated.php: -------------------------------------------------------------------------------- 1 | user = $user; 47 | $this->ability = $ability; 48 | $this->result = $result; 49 | $this->arguments = $arguments; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Contracts/AuthorizableInterface.php: -------------------------------------------------------------------------------- 1 | verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'argon2id') { 27 | throw new RuntimeException('This password does not use the Argon2id algorithm.'); 28 | } 29 | 30 | if (strlen($hashedValue) === 0) { 31 | return false; 32 | } 33 | 34 | return password_verify($value, $hashedValue); 35 | } 36 | 37 | /** 38 | * Get the algorithm that should be used for hashing. 39 | * 40 | * @return int 41 | */ 42 | protected function algorithm() 43 | { 44 | return PASSWORD_ARGON2ID; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Contracts/PasswordResetRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | {$method}(...$arguments); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exceptions/AuthenticationException.php: -------------------------------------------------------------------------------- 1 | guards = $guards; 36 | $this->redirectTo = $redirectTo; 37 | } 38 | 39 | /** 40 | * Get the guards that were checked. 41 | * 42 | * @return array 43 | */ 44 | public function guards() 45 | { 46 | return $this->guards; 47 | } 48 | 49 | /** 50 | * Get the path the user should be redirected to. 51 | * 52 | * @return string 53 | */ 54 | public function redirectTo() 55 | { 56 | return $this->redirectTo; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Notifications/ResetPasswordNotification.php: -------------------------------------------------------------------------------- 1 | email = $email; 25 | $this->token = $token; 26 | $this->service = Services::email(); 27 | } 28 | 29 | /** 30 | * Sending email verification. 31 | * 32 | * @return bool 33 | */ 34 | public function send() 35 | { 36 | $this->service 37 | ->setTo($this->email) 38 | ->setSubject("Reset Password Notification") 39 | ->setMessage(view('Fluent\Auth\Views\Email\reset_email', [ 40 | 'token' => $this->token, 41 | 'email' => $this->email, 42 | 'expire' => config('Auth')->passwords[config('Auth')->defaults['password']]['expire'], 43 | ])) 44 | ->setMailType('html'); 45 | 46 | if (! $this->service->send()) { 47 | log_message('error', $this->service->printDebugger()); 48 | 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Notifications/VerificationNotification.php: -------------------------------------------------------------------------------- 1 | email = $email; 23 | $this->service = Services::email(); 24 | } 25 | 26 | /** 27 | * Sending email verification. 28 | * 29 | * @return bool 30 | */ 31 | public function send() 32 | { 33 | $this->service 34 | ->setTo($this->email) 35 | ->setSubject('Verify Email Notification') 36 | ->setMessage(view('Fluent\Auth\Views\Email\verify_email', [ 37 | 'hash' => sha1($this->email), 38 | 'expire' => Time::now()->addMinutes(config('Auth')->passwords[config('Auth')->defaults['password']]['expire'])->getTimestamp(), 39 | 'signature' => hash_hmac('sha256', $this->email, config('Encryption')->key), 40 | ])) 41 | ->setMailType('html'); 42 | 43 | if (! $this->service->send()) { 44 | log_message('error', $this->service->printDebugger()); 45 | 46 | return false; 47 | } 48 | 49 | return true; 50 | } 51 | } -------------------------------------------------------------------------------- /src/Contracts/AuthenticatorInterface.php: -------------------------------------------------------------------------------- 1 | email_verified_at); 22 | } 23 | 24 | /** 25 | * Mark the given user's email as verified. 26 | * 27 | * @return bool 28 | */ 29 | public function markEmailAsVerified() 30 | { 31 | return Services::auth() 32 | ->getProvider() 33 | ->instance() 34 | ->where('email', $this->getEmailForVerification()) 35 | ->set('email_verified_at', Time::now()) 36 | ->update(); 37 | } 38 | 39 | /** 40 | * Send the email verification notification. 41 | * 42 | * @return void 43 | */ 44 | public function sendEmailVerificationNotification() 45 | { 46 | Events::trigger(VerifyEmailInterface::class, $this->getEmailForVerification()); 47 | } 48 | 49 | /** 50 | * Get the email address that should be used for verification. 51 | * 52 | * @return string 53 | */ 54 | public function getEmailForVerification() 55 | { 56 | return $this->email; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agungsugiarto/codeigniter4-authentication", 3 | "description": "Provides an API for authentication and includes concrete authentication adapters for common use case scenarios", 4 | "keywords": [ 5 | "codeigniter4", 6 | "authentication", 7 | "auth" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Agung Sugiarto", 13 | "email": "me.agungsugiarto@gmail.com", 14 | "homepage": "https://agungsugiarto.github.io", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "codeigniter4/framework": "^4.1", 20 | "php": "^7.3 || ^8.0", 21 | "tightenco/collect": "^8.83" 22 | }, 23 | "require-dev": { 24 | "fakerphp/faker": "^1.13", 25 | "phpunit/phpunit": "^9.1" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Fluent\\Auth\\": "src/" 30 | }, 31 | "files": [ 32 | "src/Helpers/auth_helper.php" 33 | ] 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Fluent\\Auth\\Tests\\": "tests/" 38 | } 39 | }, 40 | "extra": { 41 | "branch-alias": { 42 | "dev-master": "2.x-dev" 43 | } 44 | }, 45 | "provide": { 46 | "codeigniter4/authentication-implementation": "1.0" 47 | }, 48 | "minimum-stability": "dev", 49 | "prefer-stable": true, 50 | "scripts": { 51 | "test": "phpunit" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Helpers/auth_helper.php: -------------------------------------------------------------------------------- 1 | guard($guard); 22 | } 23 | } 24 | 25 | if (! function_exists('user_id')) { 26 | /** 27 | * Provide codeigniter4/authentitication-implementation. 28 | * Get the unique identifier for a current user. 29 | * 30 | * @param string|null $guard 31 | * @return string|int|null 32 | */ 33 | function user_id($guard = null) 34 | { 35 | return auth($guard)->id(); 36 | } 37 | } 38 | 39 | if (! function_exists('with')) { 40 | /** 41 | * Return the given value, optionally passed through the given callback. 42 | * 43 | * @param mixed $value 44 | * @param callable|null $callback 45 | * @return mixed 46 | */ 47 | function with($value, callable $callback = null) 48 | { 49 | return is_null($callback) ? $value : $callback($value); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Entities/User.php: -------------------------------------------------------------------------------- 1 | 'datetime', 39 | ]; 40 | 41 | /** 42 | * Fill set password hash. 43 | * 44 | * @return $this 45 | */ 46 | public function setPassword(string $password) 47 | { 48 | $this->attributes['password'] = Hash::make($password); 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Filters/EmailVerifiedFilter.php: -------------------------------------------------------------------------------- 1 | response = Services::response(); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function before(RequestInterface $request, $arguments = null) 30 | { 31 | $user = auth()->user(); 32 | 33 | if ( 34 | ! $user || 35 | ($user instanceof VerifyEmailInterface && 36 | ! $user->hasVerifiedEmail()) 37 | ) { 38 | if ($request->isAjax()) { 39 | return $this->fail('Your email address is not verified', ResponseInterface::HTTP_FORBIDDEN); 40 | } 41 | 42 | session()->set('intended', current_url()); 43 | 44 | return redirect()->route('verification.notice')->with('error', 'Your email address is not verified'); 45 | } 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) 52 | { 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Commands/AuthClearResetCommand.php: -------------------------------------------------------------------------------- 1 | 'Password broker instance by name.', 48 | ]; 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function run(array $params) 54 | { 55 | try { 56 | Services::passwords()->broker(CLI::getOption('name'))->getRepository()->destroyExpired(); 57 | } catch (Exception $e) { 58 | return CLI::error($e->getMessage()); 59 | } 60 | 61 | return CLI::write(CLI::color('Expired reset tokens cleared!', 'green')); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Traits/AuthorizableTrait.php: -------------------------------------------------------------------------------- 1 | forUser($this)->check($abilities, $arguments); 19 | } 20 | 21 | /** 22 | * Determine if the entity has any of the given abilities. 23 | * 24 | * @param iterable|string $abilities 25 | * @param array|mixed $arguments 26 | * @return bool 27 | */ 28 | public function canAny($abilities, $arguments = []) 29 | { 30 | return Services::gate()->forUser($this)->any($abilities, $arguments); 31 | } 32 | 33 | /** 34 | * Determine if the entity does not have the given abilities. 35 | * 36 | * @param iterable|string $abilities 37 | * @param array|mixed $arguments 38 | * @return bool 39 | */ 40 | public function cant($abilities, $arguments = []) 41 | { 42 | return ! $this->can($abilities, $arguments); 43 | } 44 | 45 | /** 46 | * Determine if the entity does not have the given abilities. 47 | * 48 | * @param iterable|string $abilities 49 | * @param array|mixed $arguments 50 | * @return bool 51 | */ 52 | public function cannot($abilities, $arguments = []) 53 | { 54 | return $this->cant($abilities, $arguments); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Models/UserModel.php: -------------------------------------------------------------------------------- 1 | $faker->email, 61 | 'username' => $faker->userName, 62 | 'password' => 'secret', 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | request->getPost(); 31 | 32 | if (! $this->validate(['email' => 'required|valid_email'])) { 33 | return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); 34 | } 35 | 36 | // We will send the password reset link to this user. Once we have attempted 37 | // to send the link, we will examine the response then see the message we 38 | // need to show to the user. Finally, we'll send out a proper response. 39 | $status = Passwords::sendResetLink([ 40 | 'email' => $request->email, 41 | ]); 42 | 43 | return $status === PasswordBrokerInterface::RESET_LINK_SENT 44 | ? redirect()->back()->with('message', lang($status)) 45 | : redirect()->back()->withInput()->with('error', lang($status)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Contracts/UserProviderInterface.php: -------------------------------------------------------------------------------- 1 | response = Services::response(); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function before(RequestInterface $request, $arguments = null) 28 | { 29 | if ($this->shouldConfirmPassword()) { 30 | if ($request->isAjax()) { 31 | return $this->fail('Password confirmation required.', ResponseInterface::HTTP_LOCKED); 32 | } 33 | 34 | session()->set('intended', current_url()); 35 | 36 | return redirect()->route('password.confirm')->with('error', 'Password confirmation required.'); 37 | } 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) 44 | { 45 | } 46 | 47 | /** 48 | * Determine if the confirmation timeout has expired. 49 | * 50 | * @return bool 51 | */ 52 | protected function shouldConfirmPassword() 53 | { 54 | $confirmedAt = time() - session('password_confirmed_at'); 55 | 56 | return $confirmedAt > config('Auth')->passwordTimeout; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Exceptions/AuthorizationException.php: -------------------------------------------------------------------------------- 1 | code = $code ?: 403; 31 | } 32 | 33 | /** 34 | * Get the response from the gate. 35 | * 36 | * @return \Fluent\Auth\Authorization\Response 37 | */ 38 | public function response() 39 | { 40 | return $this->response; 41 | } 42 | 43 | /** 44 | * Set the response from the gate. 45 | * 46 | * @param \Fluent\Auth\Authorization\Response $response 47 | * @return $this 48 | */ 49 | public function setResponse($response) 50 | { 51 | $this->response = $response; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Create a deny response object from this exception. 58 | * 59 | * @return \Fluent\Auth\Authorization\Response 60 | */ 61 | public function toResponse() 62 | { 63 | return Response::deny($this->message, $this->code); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Models/AccessTokenModel.php: -------------------------------------------------------------------------------- 1 | fake(UserModel::class)->id, 62 | 'name' => $faker->streetName, 63 | 'token' => hash('sha256', $rawToken), 64 | 'raw_token' => $rawToken, // Only normally available on newly created record. 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Facades/Passwords.php: -------------------------------------------------------------------------------- 1 | {$method}(...$arguments); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Traits/InteractsWithTimeTrait.php: -------------------------------------------------------------------------------- 1 | parseDateInterval($delay); 22 | 23 | return $delay instanceof DateTimeInterface 24 | ? max(0, $delay->getTimestamp() - $this->currentTime()) 25 | : (int) $delay; 26 | } 27 | 28 | /** 29 | * Get the "available at" UNIX timestamp. 30 | * 31 | * @param DateTimeInterface|DateInterval|int $delay 32 | * @return int 33 | */ 34 | protected function availableAt($delay = 0) 35 | { 36 | $delay = $this->parseDateInterval($delay); 37 | 38 | return $delay instanceof DateTimeInterface 39 | ? $delay->getTimestamp() 40 | : Time::now()->addSeconds($delay)->getTimestamp(); 41 | } 42 | 43 | /** 44 | * If the given value is an interval, convert it to a DateTime instance. 45 | * 46 | * @param DateTimeInterface|DateInterval|int $delay 47 | * @return DateTimeInterface|int 48 | */ 49 | protected function parseDateInterval($delay) 50 | { 51 | if ($delay instanceof DateInterval) { 52 | $delay = Time::now()->add($delay); 53 | } 54 | 55 | return $delay; 56 | } 57 | 58 | /** 59 | * Get the current system time as a UNIX timestamp. 60 | * 61 | * @return int 62 | */ 63 | protected function currentTime() 64 | { 65 | return Time::now()->getTimestamp(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 26 | return redirect()->to(session('intended') ?? config('Auth')->home); 27 | } 28 | 29 | // Check if hash equal with current user email. 30 | if (! hash_equals($hash, sha1(auth()->user()->email))) { 31 | return redirect()->route('verification.notice')->with('error', lang('Passwords.token')); 32 | } 33 | 34 | $signature = hash_hmac('sha256', auth()->user()->email, config('Encryption')->key); 35 | 36 | // Check signature key 37 | if (! hash_equals($signature, $this->request->getVar('signature'))) { 38 | return redirect()->route('verification.notice')->with('error', lang('Passwords.token')); 39 | } 40 | 41 | // Check for token if expired 42 | if ($this->request->getVar('expires') < Time::now()->getTimestamp()) { 43 | return redirect()->route('verification.notice')->with('error', lang('Passwords.expired')); 44 | } 45 | 46 | auth()->user()->markEmailAsVerified(); 47 | 48 | Events::trigger('fireVerifiedUser', auth()->user()); 49 | 50 | return redirect()->to(session('intended') ?? config('Auth')->home); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Config/Hashing.php: -------------------------------------------------------------------------------- 1 | 10, 37 | ]; 38 | 39 | /** 40 | * -------------------------------------------------------------------------- 41 | * Argon Options 42 | * -------------------------------------------------------------------------- 43 | * 44 | * Here you may specify the configuration options that should be used when 45 | * passwords are hashed using the Argon algorithm. These will allow you 46 | * to control the amount of time it takes to hash the given password. 47 | * 48 | * @var array 49 | */ 50 | public $argon = [ 51 | 'memory' => 1024, 52 | 'threads' => 2, 53 | 'time' => 2, 54 | ]; 55 | } 56 | -------------------------------------------------------------------------------- /src/Entities/AccessToken.php: -------------------------------------------------------------------------------- 1 | 'datetime', 20 | 'scopes' => 'array', 21 | 'expires' => 'datetime', 22 | ]; 23 | 24 | /** 25 | * Returns the user associated with this token. 26 | * 27 | * @return mixed 28 | */ 29 | public function user() 30 | { 31 | if (empty($this->attributes['user'])) { 32 | $users = auth('token')->getProvider(); 33 | $this->attributes['user'] = $users->findById($this->user_id); 34 | } 35 | 36 | return $this->attributes['user']; 37 | } 38 | 39 | /** 40 | * Determines whether this token grants 41 | * permission to the $scope 42 | */ 43 | public function can(string $scope): bool 44 | { 45 | if (empty($this->scopes)) { 46 | return false; 47 | } 48 | 49 | // Wildcard present 50 | if (in_array('*', $this->scopes)) { 51 | return true; 52 | } 53 | 54 | // Check stored scopes 55 | return in_array($scope, $this->scopes); 56 | } 57 | 58 | /** 59 | * Determines whether this token does NOT 60 | * grant permission to $scope. 61 | */ 62 | public function cant(string $scope): bool 63 | { 64 | if (empty($this->scopes)) { 65 | return true; 66 | } 67 | 68 | // Wildcard present 69 | if (in_array('*', $this->scopes)) { 70 | return false; 71 | } 72 | 73 | // Check stored scopes 74 | return ! in_array($scope, $this->scopes); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Traits/AuthenticatableTrait.php: -------------------------------------------------------------------------------- 1 | attributes[$this->getAuthIdColumn()] ?? null; 25 | } 26 | 27 | /** 28 | * Returns the name of the column with the 29 | * email address of this user. 30 | */ 31 | public function getAuthEmailColumn(): string 32 | { 33 | return 'email'; 34 | } 35 | 36 | /** 37 | * Returns the email address for this user. 38 | * 39 | * @return string|null 40 | */ 41 | public function getAuthEmail() 42 | { 43 | return $this->attributes[$this->getAuthEmailColumn()] ?? null; 44 | } 45 | 46 | /** 47 | * Get the password for the user. 48 | * 49 | * @return string|null 50 | */ 51 | public function getAuthPassword() 52 | { 53 | return $this->password; 54 | } 55 | 56 | /** 57 | * Returns the "remember me" token for this user. 58 | * 59 | * @return string|null 60 | */ 61 | public function getRememberToken() 62 | { 63 | $column = $this->getRememberColumn(); 64 | 65 | // fix me sometime getter is not retrived. 66 | return $this->attributes[$column] ?? $this->{$column} ?? null; 67 | } 68 | 69 | /** 70 | * Sets the "remmeber me" token. 71 | * 72 | * @return self 73 | */ 74 | public function setRememberToken(string $value) 75 | { 76 | $this->attributes[$this->getRememberColumn()] = $value; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Returns the column name that stores the remember-me value. 83 | */ 84 | public function getRememberColumn(): string 85 | { 86 | return 'remember_token'; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/CookieRecaller.php: -------------------------------------------------------------------------------- 1 | recaller = @unserialize($recaller, ['allowed_classes' => false]) ?: $recaller; 31 | } 32 | 33 | /** 34 | * Get the user ID from the recaller. 35 | * 36 | * @return string 37 | */ 38 | public function id() 39 | { 40 | return explode('|', $this->recaller, 3)[0]; 41 | } 42 | 43 | /** 44 | * Get the "remember token" token from the recaller. 45 | * 46 | * @return string 47 | */ 48 | public function token() 49 | { 50 | return explode('|', $this->recaller, 3)[1]; 51 | } 52 | 53 | /** 54 | * Get the password from the recaller. 55 | * 56 | * @return string 57 | */ 58 | public function hash() 59 | { 60 | return explode('|', $this->recaller, 3)[2]; 61 | } 62 | 63 | /** 64 | * Determine if the recaller is valid. 65 | * 66 | * @return bool 67 | */ 68 | public function valid() 69 | { 70 | return $this->properString() && $this->hasAllSegments(); 71 | } 72 | 73 | /** 74 | * Determine if the recaller is an invalid string. 75 | * 76 | * @return bool 77 | */ 78 | protected function properString() 79 | { 80 | return is_string($this->recaller) && Str::contains($this->recaller, '|'); 81 | } 82 | 83 | /** 84 | * Determine if the recaller has all segments. 85 | * 86 | * @return bool 87 | */ 88 | protected function hasAllSegments() 89 | { 90 | $segments = explode('|', $this->recaller); 91 | 92 | return count($segments) === 3 && trim($segments[0]) !== '' && trim($segments[1]) !== ''; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Traits/GuardHelperTrait.php: -------------------------------------------------------------------------------- 1 | user())) { 30 | return $user; 31 | } 32 | 33 | throw new AuthenticationException(); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function hasUser(): bool 40 | { 41 | return ! is_null($this->user); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function check(): bool 48 | { 49 | return ! is_null($this->user()); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function id() 56 | { 57 | if ($this->user()) { 58 | return $this->user()->getAuthId(); 59 | } 60 | 61 | return null; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function setUser(AuthenticatorInterface $user) 68 | { 69 | $this->user = $user; 70 | 71 | $this->loggedOut = false; 72 | 73 | Events::trigger('fireAuthenticatedEvent', $user); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function getProvider() 82 | { 83 | return $this->provider; 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function setProvider(UserProviderInterface $provider) 90 | { 91 | $this->provider = $provider; 92 | 93 | return $this; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Helpers/Arr.php: -------------------------------------------------------------------------------- 1 | offsetExists($key); 26 | } 27 | 28 | return array_key_exists($key, $array); 29 | } 30 | 31 | /** 32 | * Remove one or many array items from a given array using "dot" notation. 33 | * 34 | * @param array $array 35 | * @param array|string $keys 36 | * @return void 37 | */ 38 | public static function forget(&$array, $keys) 39 | { 40 | $original = &$array; 41 | 42 | $keys = (array) $keys; 43 | 44 | if (count($keys) === 0) { 45 | return; 46 | } 47 | 48 | foreach ($keys as $key) { 49 | // if the exact key exists in the top-level, remove it 50 | if (static::exists($array, $key)) { 51 | unset($array[$key]); 52 | 53 | continue; 54 | } 55 | 56 | $parts = explode('.', $key); 57 | 58 | // clean up before each pass 59 | $array = &$original; 60 | 61 | while (count($parts) > 1) { 62 | $part = array_shift($parts); 63 | 64 | if (isset($array[$part]) && is_array($array[$part])) { 65 | $array = &$array[$part]; 66 | } else { 67 | continue 2; 68 | } 69 | } 70 | 71 | unset($array[array_shift($parts)]); 72 | } 73 | } 74 | 75 | /** 76 | * Get all of the given array except for a specified array of keys. 77 | * 78 | * @param array $array 79 | * @param array|string $keys 80 | * @return array 81 | */ 82 | public static function except($array, $keys) 83 | { 84 | static::forget($array, $keys); 85 | 86 | return $array; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Contracts/HasAccessTokensInterface.php: -------------------------------------------------------------------------------- 1 | activeToken, which is set during 57 | * authentication. If it hasn't been set, returns false. 58 | */ 59 | public function tokenCan(string $scope): bool; 60 | 61 | /** 62 | * Determines whether the user's token does NOT grant permissions to $scope. 63 | * First checks against $this->activeToken, which is set during 64 | * authentication. If it hasn't been set, returns true. 65 | */ 66 | public function tokenCant(string $scope): bool; 67 | 68 | /** 69 | * Returns the current access token for the user. 70 | * 71 | * @return AccessToken 72 | */ 73 | public function currentAccessToken(); 74 | 75 | /** 76 | * Sets the current active token for this user. 77 | * 78 | * @return $this 79 | */ 80 | public function withAccessToken(?AccessToken $accessToken); 81 | } 82 | -------------------------------------------------------------------------------- /src/Facades/Gate.php: -------------------------------------------------------------------------------- 1 | {$method}(...$arguments); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Views/Auth/forgot_password.php: -------------------------------------------------------------------------------- 1 | extend('Auth/layout') ?> 2 | 3 | section('content') ?> 4 |
5 |
6 |
7 |

8 | 9 | 10 | 11 | Back to log in 12 | 13 |

14 |
15 | 37 |
38 |
39 |
40 |
41 | endSection() ?> -------------------------------------------------------------------------------- /src/Views/Auth/verify_email.php: -------------------------------------------------------------------------------- 1 | extend('Auth/layout') ?> 2 | 3 | section('content') ?> 4 |
5 |
6 |
7 |

8 | 9 | 10 | 11 | Back to homepage 12 | 13 |

14 |
15 | 39 |
40 |
41 |
42 |
43 | endSection() ?> -------------------------------------------------------------------------------- /src/Filters/AuthorizeFilter.php: -------------------------------------------------------------------------------- 1 | gate = Services::gate(); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | * 33 | * @throws \Fluent\Auth\Exceptions\AuthenticationException 34 | * @throws \Fluent\Auth\Exceptions\AuthorizationException 35 | */ 36 | public function before(RequestInterface $request, $arguments = null) 37 | { 38 | [$ability] = $arguments; 39 | 40 | $this->gate->authorize($ability, $this->getGateArguments(isset($arguments[1]) ? $arguments[1] : null)); 41 | 42 | return $request; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) 49 | { 50 | } 51 | 52 | /** 53 | * Get the arguments parameter for the gate. 54 | * 55 | * @param array|null $models 56 | * @return \CodeIgniter\Model|array|string 57 | */ 58 | protected function getGateArguments($models) 59 | { 60 | if (is_null($models)) { 61 | return []; 62 | } 63 | 64 | return collect($models)->map(function ($model) { 65 | return $model instanceof Model ? $model : $this->getModel($model); 66 | })->all(); 67 | } 68 | 69 | /** 70 | * Get the model to authorize. 71 | * 72 | * @param string $model 73 | * @return \CodeIgniter\Model|string 74 | */ 75 | protected function getModel($model) 76 | { 77 | if ($this->isClassName($model)) { 78 | return trim($model); 79 | } else { 80 | return ((preg_match("/^['\"](.*)['\"]$/", trim($model), $matches)) ? $matches[1] : null); 81 | } 82 | } 83 | 84 | /** 85 | * Checks if the given string looks like a fully qualified class name. 86 | * 87 | * @param string $value 88 | * @return bool 89 | */ 90 | protected function isClassName($value) 91 | { 92 | return strpos($value, '\\') !== false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | request->getPost(); 35 | 36 | if (! $this->validate(static::rules())) { 37 | return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); 38 | } 39 | 40 | $user = static::createUser($request); 41 | 42 | Auth::login($user); 43 | 44 | // Automatic trigger send verify email if user 45 | // instanceof VerifyEmailInterface. 46 | if ($user instanceof VerifyEmailInterface) { 47 | Events::trigger(VerifyEmailInterface::class, $user->email); 48 | } 49 | 50 | return redirect(config('Auth')->home); 51 | } 52 | 53 | /** 54 | * Rules for validation. 55 | * 56 | * @return array 57 | */ 58 | protected static function rules() 59 | { 60 | return [ 61 | 'username' => 'required|alpha_numeric_space|min_length[3]|is_unique[users.username]', 62 | 'email' => 'required|valid_email|is_unique[users.email]', 63 | 'password' => 'required|min_length[8]', 64 | 'password_confirmation' => 'matches[password]', 65 | ]; 66 | } 67 | 68 | /** 69 | * Generate user registration. 70 | * 71 | * @param object $request 72 | * @return AuthenticatorInterface 73 | */ 74 | protected static function createUser($request) 75 | { 76 | $user = new UserModel(); 77 | $id = $user->insert(new User([ 78 | 'username' => $request->username, 79 | 'email' => $request->email, 80 | 'password' => $request->password, 81 | ])); 82 | 83 | return $user->find($id); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Filters/ThrottleFilter.php: -------------------------------------------------------------------------------- 1 | response = Services::response(); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function before(RequestInterface $request, $arguments = null) 31 | { 32 | if ($this->tooManyAttempts($request, $arguments)) { 33 | $seconds = RateLimiter::availableIn($this->throttleKey($request)); 34 | 35 | if ($request->isAJAX()) { 36 | return $this->fail(lang('Auth.throttle', [$seconds])); 37 | } 38 | 39 | return redirect()->back()->with('error', lang('Auth.throttler', [$this->maxAttempt($arguments), $seconds])); 40 | } 41 | 42 | RateLimiter::hit($this->throttleKey($request), $this->decaySecond($arguments)); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) 49 | { 50 | } 51 | 52 | /** 53 | * Determine if the given key has been "accessed" too many times. 54 | * 55 | * @return bool 56 | */ 57 | protected function tooManyAttempts(RequestInterface $request, array $arguments) 58 | { 59 | return RateLimiter::tooManyAttempts($this->throttleKey($request), $this->maxAttempt($arguments)); 60 | } 61 | 62 | /** 63 | * Get the number of attempts for the given key. 64 | * 65 | * @return int 66 | */ 67 | protected function maxAttempt(array $arguments) 68 | { 69 | return (int) array_values($arguments)[1]; 70 | } 71 | 72 | /** 73 | * Get the decay second for the given key. 74 | * 75 | * @return int 76 | */ 77 | protected function decaySecond(array $arguments) 78 | { 79 | return (int) array_values($arguments)[0]; 80 | } 81 | 82 | /** 83 | * Get the rate limiting throttle key for the request. 84 | * 85 | * @return string 86 | */ 87 | public function throttleKey(RequestInterface $request) 88 | { 89 | return 'throttle_' . md5(auth()->user()->email . "|{$request->getIPAddress()}"); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Contracts/AuthFactoryInterface.php: -------------------------------------------------------------------------------- 1 | userResolver()); 48 | }); 49 | } 50 | 51 | /** 52 | * Passwords broker services. 53 | * 54 | * @return PasswordBrokerFactoryInterface|PasswordBrokerInterface 55 | */ 56 | public static function passwords(bool $getShared = true) 57 | { 58 | if ($getShared) { 59 | return self::getSharedInstance('passwords'); 60 | } 61 | 62 | return new PasswordBrokerManager(new Factories(), $getShared); 63 | } 64 | 65 | /** 66 | * Create HashManager instance. 67 | * 68 | * @return AbstractManager|HashManager|HasherInterface 69 | */ 70 | public static function hash(bool $getShared = true) 71 | { 72 | if ($getShared) { 73 | return self::getSharedInstance('hash'); 74 | } 75 | 76 | return new HashManager(new Factories(), $getShared); 77 | } 78 | 79 | /** 80 | * Create a new rate limiter instance. 81 | * 82 | * @return RateLimiter 83 | */ 84 | public static function limiter(bool $getShared = true) 85 | { 86 | if ($getShared) { 87 | return self::getSharedInstance('limiter'); 88 | } 89 | 90 | return new RateLimiter(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Collectors/AuthCollector.php: -------------------------------------------------------------------------------- 1 | getProvider()); 34 | } 35 | 36 | /** 37 | * Returns the data of this collector to be formatted in the toolbar 38 | */ 39 | public function display(): string 40 | { 41 | if (auth('web')->check()) { 42 | $user = auth('web')->user(); 43 | $html = '

Current User

'; 44 | $html .= ''; 45 | $html .= ""; 46 | $html .= ""; 47 | $html .= ""; 48 | $html .= ""; 49 | $html .= ""; 50 | $html .= '
User ID#{$user->id}
Username{$user->username}
Email{$user->email}
User since{$user->created_at->humanize()}
Verified at{$user->email_verified_at}
'; 51 | } else { 52 | $html = '

Not logged in.

'; 53 | } 54 | 55 | return $html; 56 | } 57 | 58 | /** 59 | * Gets the "badge" value for the button. 60 | * 61 | * @return int|null ID of the current User, or null when not logged in 62 | */ 63 | public function getBadgeValue(): ?int 64 | { 65 | return auth('web')->check() ? auth('web')->id() : null; 66 | } 67 | 68 | /** 69 | * Display the icon. 70 | * 71 | * Icon from https://icons8.com - 1em package 72 | */ 73 | public function icon(): string 74 | { 75 | return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADLSURBVEhL5ZRLCsIwGAa7UkE9gd5HUfEoekxxJx7AhXoCca/fhESkJiQxBHwMDG3S/9EmJc0n0JMruZVXK/fMdWQRY7mXt4A7OZJvwZu74hRayIEc2nv3jGtXZrOWrnifiRY0OkhiWK5sWGeS52bkZymJ2ZhRJmwmySxLCL6CmIsZZUIixkiNezCRR+kSUyWH3Cgn6SuQIk2iuOBckvN+t8FMnq1TJloUN3jefN9mhvJeCAVWb8CyUDj0vxc3iPFHDaofFdUPu2+iae7nYJMCY/1bpAAAAABJRU5ErkJggg=='; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Facades/Auth.php: -------------------------------------------------------------------------------- 1 | {$method}(...$arguments); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Traits/AuthorizesRequestsTrait.php: -------------------------------------------------------------------------------- 1 | parseAbilityAndArguments($ability, $arguments); 21 | 22 | return Services::gate()->authorize($ability, $arguments); 23 | } 24 | 25 | /** 26 | * Authorize a given action for a user. 27 | * 28 | * @param \Fluent\Auth\Contracts\AuthenticatorInterface $user 29 | * @param mixed $ability 30 | * @param mixed|array $arguments 31 | * @return \Fluent\Auth\Authorization\Response 32 | * 33 | * @throws \Fluent\Auth\Exceptions\AuthorizationException 34 | */ 35 | public function authorizeForUser($user, $ability, $arguments = []) 36 | { 37 | [$ability, $arguments] = $this->parseAbilityAndArguments($ability, $arguments); 38 | 39 | return Services::gate()->forUser($user)->authorize($ability, $arguments); 40 | } 41 | 42 | /** 43 | * Guesses the ability's name if it wasn't provided. 44 | * 45 | * @param mixed $ability 46 | * @param mixed|array $arguments 47 | * @return array 48 | */ 49 | protected function parseAbilityAndArguments($ability, $arguments) 50 | { 51 | if (is_string($ability) && strpos($ability, '\\') === false) { 52 | return [$ability, $arguments]; 53 | } 54 | 55 | $method = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function']; 56 | 57 | return [$this->normalizeGuessedAbilityName($method), $ability]; 58 | } 59 | 60 | /** 61 | * Normalize the ability name that has been guessed from the method name. 62 | * 63 | * @param string $ability 64 | * @return string 65 | */ 66 | protected function normalizeGuessedAbilityName($ability) 67 | { 68 | $map = $this->resourceAbilityMap(); 69 | 70 | return $map[$ability] ?? $ability; 71 | } 72 | 73 | /** 74 | * Get the map of resource methods to ability names. 75 | * 76 | * @return array 77 | */ 78 | protected function resourceAbilityMap() 79 | { 80 | return [ 81 | 'index' => 'viewAny', 82 | 'show' => 'view', 83 | 'new' => 'create', 84 | 'create' => 'create', 85 | 'edit' => 'update', 86 | 'update' => 'update', 87 | 'delete' => 'delete', 88 | ]; 89 | } 90 | } -------------------------------------------------------------------------------- /src/Passwords/Hash/HashManager.php: -------------------------------------------------------------------------------- 1 | config->bcrypt ?? []); 20 | } 21 | 22 | /** 23 | * Create an instance of the Argon2i hash Driver. 24 | * 25 | * @return ArgonHasher 26 | */ 27 | public function createArgonDriver() 28 | { 29 | return new ArgonHasher($this->config->argon ?? []); 30 | } 31 | 32 | /** 33 | * Create an instance of the Argon2id hash Driver. 34 | * 35 | * @return Argon2IdHasher 36 | */ 37 | public function createArgon2idDriver() 38 | { 39 | return new Argon2IdHasher($this->config->argon ?? []); 40 | } 41 | 42 | /** 43 | * Get information about the given hashed value. 44 | * 45 | * @param string $hashedValue 46 | * @return array 47 | */ 48 | public function info($hashedValue) 49 | { 50 | return $this->driver()->info($hashedValue); 51 | } 52 | 53 | /** 54 | * Hash the given value. 55 | * 56 | * @param string $value 57 | * @param array $options 58 | * @return string 59 | */ 60 | public function make($value, array $options = []) 61 | { 62 | return $this->driver()->make($value, $options); 63 | } 64 | 65 | /** 66 | * Check the given plain value against a hash. 67 | * 68 | * @param string $value 69 | * @param string $hashedValue 70 | * @param array $options 71 | * @return bool 72 | */ 73 | public function check($value, $hashedValue, array $options = []) 74 | { 75 | return $this->driver()->check($value, $hashedValue, $options); 76 | } 77 | 78 | /** 79 | * Check if the given hash has been hashed using the given options. 80 | * 81 | * @param string $hashedValue 82 | * @param array $options 83 | * @return bool 84 | */ 85 | public function needsRehash($hashedValue, array $options = []) 86 | { 87 | return $this->driver()->needsRehash($hashedValue, $options); 88 | } 89 | 90 | /** 91 | * Get the default driver name. 92 | * 93 | * @return string 94 | */ 95 | public function getDefaultDriver() 96 | { 97 | return $this->config->driver; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Filters/AuthenticationFilter.php: -------------------------------------------------------------------------------- 1 | auth = Services::auth(); 27 | $this->response = Services::response(); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function before(RequestInterface $request, $arguments = null) 34 | { 35 | return $this->authenticate($request, $arguments); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) 42 | { 43 | } 44 | 45 | /** 46 | * Determine if the user is logged in to any of the given guards. 47 | * 48 | * @param RequestInterface $request 49 | * @param array $guards 50 | * @return void 51 | * @throws AuthenticationException 52 | */ 53 | protected function authenticate($request, $guards) 54 | { 55 | if (empty($guards)) { 56 | $guards = [null]; 57 | } 58 | 59 | foreach ($guards as $guard) { 60 | if ($this->auth->guard($guard)->check()) { 61 | return $this->auth->shouldUse($guard); 62 | } 63 | } 64 | 65 | return $this->unauthenticated($request, $guards); 66 | } 67 | 68 | /** 69 | * Handle an unauthenticated user. 70 | * 71 | * @param RequestInterface $request 72 | * @param array $guards 73 | * @return void 74 | * @throws AuthenticationException 75 | */ 76 | protected function unauthenticated($request, $guards) 77 | { 78 | if ($request->isAJAX()) { 79 | return $this->fail('Unauthenticated.', ResponseInterface::HTTP_UNAUTHORIZED); 80 | } 81 | 82 | throw new AuthenticationException( 83 | 'Unauthenticated.', 84 | $guards, 85 | ResponseInterface::HTTP_UNAUTHORIZED, 86 | $this->redirectTo($request) 87 | ); 88 | } 89 | 90 | /** 91 | * Get the path the user should be redirected to when they are not authenticated. 92 | * 93 | * @param RequestInterface $request 94 | * @return string|null 95 | */ 96 | protected function redirectTo($request) 97 | { 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Views/Auth/confirm_password.php: -------------------------------------------------------------------------------- 1 | extend('Auth/layout') ?> 2 | 3 | section('content') ?> 4 |
5 |
6 |
7 |

8 | 9 | 10 | 11 | Back to homepage 12 | 13 |

14 |
15 | 42 |
43 |
44 |
45 |
46 | endSection() ?> -------------------------------------------------------------------------------- /src/Helpers/Str.php: -------------------------------------------------------------------------------- 1 | 74 | */ 75 | public static function parseCallback($callback, $default = null) 76 | { 77 | return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default]; 78 | } 79 | 80 | /** 81 | * Determine if a given string starts with a given substring. 82 | * 83 | * @param string $haystack 84 | * @param string|string[] $needles 85 | * @return bool 86 | */ 87 | public static function startsWith($haystack, $needles) 88 | { 89 | foreach ((array) $needles as $needle) { 90 | if ((string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0) { 91 | return true; 92 | } 93 | } 94 | 95 | return false; 96 | } 97 | 98 | /** 99 | * Extract username from given email. 100 | * 101 | * @param string $email 102 | * @return string|empty 103 | */ 104 | public static function extractName(string $email) 105 | { 106 | return strstr($email, '@', true); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Contracts/PasswordBrokerInterface.php: -------------------------------------------------------------------------------- 1 | find($userId); 27 | } 28 | 29 | /** 30 | * Locate a user by the given credentials. 31 | * 32 | * @param array $credentials 33 | * @return AuthenticatorInterface|null 34 | */ 35 | public function findByCredentials(array $credentials) 36 | { 37 | if ( 38 | empty($credentials) || 39 | (count($credentials) === 1 && 40 | array_key_exists('password', $credentials)) 41 | ) { 42 | return; 43 | } 44 | 45 | /** @var Model $query */ 46 | $query = clone $this; 47 | 48 | foreach ($credentials as $key => $value) { 49 | if (Str::contains($key, 'password')) { 50 | continue; 51 | } 52 | 53 | if (is_array($value)) { 54 | $query->whereIn($key, $value); 55 | } else { 56 | $query->where($key, $value); 57 | } 58 | } 59 | 60 | return $query->first(); 61 | } 62 | 63 | /** 64 | * Find a user by their ID and "remember-me" token. 65 | * 66 | * @param int|string $userId 67 | * @param string $token 68 | * @return AuthenticatorInterface|null 69 | */ 70 | public function findByRememberToken($userId, $token) 71 | { 72 | $retrievedModel = $this->where('id', $userId)->first(); 73 | 74 | if (! $retrievedModel) { 75 | return; 76 | } 77 | 78 | $rememberToken = $retrievedModel->getRememberToken(); 79 | 80 | return $rememberToken && hash_equals($rememberToken, $token) 81 | ? $retrievedModel 82 | : null; 83 | } 84 | 85 | /** 86 | * Update the "remember me" token for the given user in storage. 87 | * 88 | * @param string $token 89 | * @return mixed 90 | */ 91 | public function updateRememberToken(AuthenticatorInterface $user, $token = null) 92 | { 93 | return $this->where($user->getAuthIdColumn(), $user->getAuthId()) 94 | ->set($user->getRememberColumn(), $token)->update(); 95 | } 96 | 97 | /** 98 | * Validate a user against the given credentials. 99 | * 100 | * @param array $credentials 101 | * @return bool 102 | */ 103 | public function validateCredentials(AuthenticatorInterface $user, array $credentials) 104 | { 105 | return Hash::check($credentials['password'], $user->getAuthPassword()); 106 | } 107 | 108 | /** 109 | * Get instance class user provider. 110 | * 111 | * @return BaseBuilder|Model 112 | */ 113 | public function instance() 114 | { 115 | return $this; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Database/Migrations/2020-12-28-223112_create_auth_tables.php: -------------------------------------------------------------------------------- 1 | forge->addField([ 18 | 'id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], 19 | 'email' => ['type' => 'varchar', 'constraint' => 255], 20 | 'username' => ['type' => 'varchar', 'constraint' => 30, 'null' => true], 21 | 'password' => ['type' => 'varchar', 'constraint' => 255], 22 | 'email_verified_at' => ['type' => 'datetime', 'null' => true], 23 | 'remember_token' => ['type' => 'varchar', 'constraint' => 255, 'null' => true], 24 | 'created_at' => ['type' => 'datetime', 'null' => true], 25 | 'updated_at' => ['type' => 'datetime', 'null' => true], 26 | 'deleted_at' => ['type' => 'datetime', 'null' => true], 27 | ]); 28 | 29 | $this->forge->addPrimaryKey('id'); 30 | $this->forge->addUniqueKey('email'); 31 | $this->forge->addUniqueKey('username'); 32 | 33 | $this->forge->createTable('users', true); 34 | 35 | /** 36 | * Password reset table. 37 | */ 38 | $this->forge->addField([ 39 | 'id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], 40 | 'email' => ['type' => 'varchar', 'constraint' => 255], 41 | 'token' => ['type' => 'varchar', 'constraint' => 255, 'null' => true], 42 | 'created_at' => ['type' => 'datetime', 'null' => false], 43 | 'updated_at' => ['type' => 'datetime', 'null' => true], 44 | ]); 45 | 46 | $this->forge->addPrimaryKey('id'); 47 | $this->forge->createTable('auth_password_resets', true); 48 | 49 | /** 50 | * Access tokens table. 51 | */ 52 | $this->forge->addField([ 53 | 'id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], 54 | 'user_id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true], 55 | 'name' => ['type' => 'varchar', 'constraint' => 255], 56 | 'token' => ['type' => 'varchar', 'constraint' => 64], 57 | 'last_used_at' => ['type' => 'datetime', 'null' => true], 58 | 'scopes' => ['type' => 'text', 'null' => true], 59 | 'created_at' => ['type' => 'datetime', 'null' => true], 60 | 'updated_at' => ['type' => 'datetime', 'null' => true], 61 | ]); 62 | 63 | $this->forge->addPrimaryKey('id'); 64 | $this->forge->addUniqueKey('token'); 65 | $this->forge->createTable('auth_access_tokens', true); 66 | } 67 | 68 | //-------------------------------------------------------------------- 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function down() 74 | { 75 | $this->forge->dropTable('users', true); 76 | $this->forge->dropTable('auth_password_resets', true); 77 | $this->forge->dropTable('auth_access_tokens', true); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Passwords/Hash/BcryptHasher.php: -------------------------------------------------------------------------------- 1 | rounds = $options['rounds'] ?? $this->rounds; 39 | $this->verifyAlgorithm = $options['verify'] ?? $this->verifyAlgorithm; 40 | } 41 | 42 | /** 43 | * Hash the given value. 44 | * 45 | * @param string $value 46 | * @param array $options 47 | * @return string 48 | * @throws RuntimeException 49 | */ 50 | public function make($value, array $options = []) 51 | { 52 | $hash = password_hash($value, PASSWORD_BCRYPT, [ 53 | 'cost' => $this->cost($options), 54 | ]); 55 | 56 | if ($hash === false) { 57 | throw new RuntimeException('Bcrypt hashing not supported.'); 58 | } 59 | 60 | return $hash; 61 | } 62 | 63 | /** 64 | * Check the given plain value against a hash. 65 | * 66 | * @param string $value 67 | * @param string $hashedValue 68 | * @param array $options 69 | * @return bool 70 | * @throws RuntimeException 71 | */ 72 | public function check($value, $hashedValue, array $options = []) 73 | { 74 | if ($this->verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'bcrypt') { 75 | throw new RuntimeException('This password does not use the Bcrypt algorithm.'); 76 | } 77 | 78 | return parent::check($value, $hashedValue, $options); 79 | } 80 | 81 | /** 82 | * Check if the given hash has been hashed using the given options. 83 | * 84 | * @param string $hashedValue 85 | * @param array $options 86 | * @return bool 87 | */ 88 | public function needsRehash($hashedValue, array $options = []) 89 | { 90 | return password_needs_rehash($hashedValue, PASSWORD_BCRYPT, [ 91 | 'cost' => $this->cost($options), 92 | ]); 93 | } 94 | 95 | /** 96 | * Set the default password work factor. 97 | * 98 | * @param int $rounds 99 | * @return $this 100 | */ 101 | public function setRounds($rounds) 102 | { 103 | $this->rounds = (int) $rounds; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Extract the cost value from the options array. 110 | * 111 | * @param array $options 112 | * @return int 113 | */ 114 | protected function cost(array $options = []) 115 | { 116 | return $options['rounds'] ?? $this->rounds; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Controllers/Auth/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | $this->request->getVar('email'), 28 | 'token' => $token, 29 | ]); 30 | } 31 | 32 | /** 33 | * Handle an incoming new password request. 34 | * 35 | * @return RedirectResponse 36 | */ 37 | public function create() 38 | { 39 | $request = (object) $this->request->getPost(); 40 | 41 | if (! $this->validate(static::rules())) { 42 | return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); 43 | } 44 | 45 | // Here we will attempt to reset the user's password. If it is successful we 46 | // will update the password on an actual user model and persist it to the 47 | // database. Otherwise we will parse the error and return the response. 48 | $status = Passwords::reset( 49 | static::credentials($request), 50 | function ($user) use ($request) { 51 | (new UserModel())->update($user->id, new User([ 52 | 'password' => $request->password, 53 | 'remember_token' => bin2hex(random_bytes(20)), 54 | ])); 55 | 56 | Events::trigger('firePasswordReset', $user); 57 | } 58 | ); 59 | 60 | // If the password was successfully reset, we will redirect the user back to 61 | // the application's home authenticated view. If there is an error we can 62 | // redirect them back to where they came from with their error message. 63 | return $status === PasswordBrokerInterface::PASSWORD_RESET 64 | ? redirect()->route('login')->with('message', lang($status)) 65 | : redirect()->back()->withInput()->with('error', lang($status)); 66 | } 67 | 68 | /** 69 | * Rules for validation. 70 | * 71 | * @return array 72 | */ 73 | protected static function rules() 74 | { 75 | return [ 76 | 'email' => 'required|valid_email', 77 | 'password' => 'required|min_length[8]', 78 | 'password_confirmation' => 'matches[password]', 79 | ]; 80 | } 81 | 82 | /** 83 | * Get the request credentials. 84 | * 85 | * @param object $request 86 | * @return array 87 | */ 88 | protected static function credentials($request) 89 | { 90 | return [ 91 | 'token' => $request->token, 92 | 'email' => $request->email, 93 | 'password' => $request->password, 94 | 'password_confirmation' => $request->password_confirmation, 95 | ]; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Views/Auth/layout.php: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | CodeIgniter4 Authentication 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | renderSection('content') ?> 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/Passwords/PasswordBrokerManager.php: -------------------------------------------------------------------------------- 1 | config = $factory::config('Auth', ['getShared' => $getShared]); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function broker($name = null) 37 | { 38 | $name = $name ?: $this->getDefaultDriver(); 39 | 40 | return $this->brokers[$name] ?? ($this->brokers[$name] = $this->resolve($name)); 41 | } 42 | 43 | /** 44 | * Resolve the given broker. 45 | * 46 | * @param string $name 47 | * @return PasswordBrokerInterface 48 | * @throws InvalidArgumentException 49 | */ 50 | protected function resolve($name) 51 | { 52 | $config = $this->getConfig($name); 53 | 54 | // The password broker uses a token repository to validate tokens and send user 55 | // password e-mails, as well as validating that password reset process as an 56 | // aggregate service of sorts providing a convenient interface for resets. 57 | return new PasswordBroker( 58 | $this->createTokenRepository($config), 59 | Services::auth()->createUserProvider($config['provider']) 60 | ); 61 | } 62 | 63 | /** 64 | * Create a token repository instance based on the given configuration. 65 | * 66 | * @param array $config 67 | * @return PasswordResetRepositoryInterface 68 | */ 69 | protected function createTokenRepository(array $config) 70 | { 71 | return new PasswordResetRepository($config['table'], $config['connection'], $config['expire'], $config['throttle']); 72 | } 73 | 74 | /** 75 | * Get the password broker configuration. 76 | * 77 | * @param string $name 78 | * @return array 79 | */ 80 | protected function getConfig($name) 81 | { 82 | if (isset($this->config->passwords[$name])) { 83 | return $this->config->passwords[$name]; 84 | } 85 | 86 | throw new InvalidArgumentException("Password resetter [{$name}] is not defined."); 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function getDefaultDriver() 93 | { 94 | return $this->config->defaults['password']; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function setDefaultDriver($name) 101 | { 102 | $this->config->defaults['password'] = $name; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Dynamically call the default driver instance. 109 | * 110 | * @param string $method 111 | * @param array $parameters 112 | * @return mixed 113 | */ 114 | public function __call($method, $parameters) 115 | { 116 | return $this->broker()->{$method}(...$parameters); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Authorization/Response.php: -------------------------------------------------------------------------------- 1 | code = $code; 42 | $this->allowed = $allowed; 43 | $this->message = $message; 44 | } 45 | 46 | /** 47 | * Create a new "allow" Response. 48 | * 49 | * @param string|null $message 50 | * @param mixed $code 51 | * @return \Fluent\Auth\Authorization\Response 52 | */ 53 | public static function allow($message = null, $code = null) 54 | { 55 | return new static(true, $message, $code); 56 | } 57 | 58 | /** 59 | * Create a new "deny" Response. 60 | * 61 | * @param string|null $message 62 | * @param mixed $code 63 | * @return \Fluent\Auth\Authorization\Response 64 | */ 65 | public static function deny($message = null, $code = null) 66 | { 67 | return new static(false, $message, $code); 68 | } 69 | 70 | /** 71 | * Determine if the response was allowed. 72 | * 73 | * @return bool 74 | */ 75 | public function allowed() 76 | { 77 | return $this->allowed; 78 | } 79 | 80 | /** 81 | * Determine if the response was denied. 82 | * 83 | * @return bool 84 | */ 85 | public function denied() 86 | { 87 | return ! $this->allowed(); 88 | } 89 | 90 | /** 91 | * Get the response message. 92 | * 93 | * @return string|null 94 | */ 95 | public function message() 96 | { 97 | return $this->message; 98 | } 99 | 100 | /** 101 | * Get the response code / reason. 102 | * 103 | * @return mixed 104 | */ 105 | public function code() 106 | { 107 | return $this->code; 108 | } 109 | 110 | /** 111 | * Throw authorization exception if response was denied. 112 | * 113 | * @return \Fluent\Auth\Authorization\Response 114 | * 115 | * @throws \Fluent\Auth\Authorization\AuthorizationException 116 | */ 117 | public function authorize() 118 | { 119 | if ($this->denied()) { 120 | throw (new AuthorizationException($this->message(), $this->code())) 121 | ->setResponse($this); 122 | } 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Convert the response to an array. 129 | * 130 | * @return array 131 | */ 132 | public function toArray() 133 | { 134 | return [ 135 | 'allowed' => $this->allowed(), 136 | 'message' => $this->message(), 137 | 'code' => $this->code(), 138 | ]; 139 | } 140 | 141 | /** 142 | * Get the string representation of the message. 143 | * 144 | * @return string 145 | */ 146 | public function __toString() 147 | { 148 | return (string) $this->message(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/UserDatabase.php: -------------------------------------------------------------------------------- 1 | connection = Config::connect($connection)->table($table); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function findById($userId) 42 | { 43 | return $this->connection->where('id', $userId)->get()->getFirstRow(User::class); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function findByRememberToken($userId, $token) 50 | { 51 | $retriveDatabase = $this->connection->where('id', $userId)->get()->getFirstRow(User::class); 52 | 53 | $rememberToken = $retriveDatabase->getRememberToken(); 54 | 55 | return $rememberToken && hash_equals($rememberToken, $token) 56 | ? $retriveDatabase 57 | : null; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function updateRememberToken(AuthenticatorInterface $user, $token) 64 | { 65 | return $this->connection 66 | ->where($user->getAuthIdColumn(), $user->getAuthId()) 67 | ->set($user->getRememberColumn(), $token) 68 | ->update(); 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function findByCredentials(array $credentials) 75 | { 76 | if ( 77 | empty($credentials) || 78 | (count($credentials) === 1 && 79 | array_key_exists('password', $credentials)) 80 | ) { 81 | return; 82 | } 83 | 84 | // First we will add each credential element to the query as a where clause. 85 | // Then we can execute the query and, if we found a user, return it in a 86 | // generic "user" object that will be utilized by the Guard instances. 87 | $query = $this->connection; 88 | 89 | foreach ($credentials as $key => $value) { 90 | if (Str::contains($key, 'password')) { 91 | continue; 92 | } 93 | 94 | if (is_array($value)) { 95 | $query->whereIn($key, $value); 96 | } else { 97 | $query->where($key, $value); 98 | } 99 | } 100 | 101 | // Now we are ready to execute the query to see if we have an user matching 102 | // the given credentials. If not, we will just return nulls and indicate 103 | // that there are no matching users for these given credential arrays. 104 | return $query->get()->getFirstRow(User::class); 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function validateCredentials(AuthenticatorInterface $user, array $credentials) 111 | { 112 | return Hash::check($credentials['password'], $user->getAuthPassword()); 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function instance() 119 | { 120 | return $this->connection; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeIgniter4 Authentication 2 | 3 | [![tests](https://github.com/agungsugiarto/codeigniter4-authentication/actions/workflows/php.yml/badge.svg)](https://github.com/agungsugiarto/codeigniter4-authentication/actions/workflows/php.yml) 4 | [![Latest Stable Version](https://poser.pugx.org/agungsugiarto/codeigniter4-authentication/v)](https://github.com/agungsugiarto/codeigniter4-authentication/releases) 5 | [![Total Downloads](https://poser.pugx.org/agungsugiarto/codeigniter4-authentication/downloads)](https://packagist.org/packages/agungsugiarto/codeigniter4-authentication/stats) 6 | [![Latest Unstable Version](https://poser.pugx.org/agungsugiarto/codeigniter4-authentication/v/unstable)](https://packagist.org/packages/agungsugiarto/codeigniter4-authentication) 7 | [![License](https://poser.pugx.org/agungsugiarto/codeigniter4-authentication/license)](https://github.com/agungsugiarto/codeigniter4-authentication/blob/master/LICENSE.md) 8 | 9 | ## About 10 | The `codeigniter4\authentication` component provides an API for authentication and 11 | includes concrete authentication adapters for common use case scenarios. 12 | 13 | - Inspired from https://github.com/lonnieezell/codigniter-shield 14 | - Most inspired from auth by laravel https://github.com/illuminate/auth 15 | 16 | ## Upgrade from v1.x to 2.x 17 | ### Composer Dependencies 18 | You should update the following dependencies in your application's composer.json file: 19 | 20 | `agungsugiarto/codeigniter4-authentication` to `^2.0` 21 | 22 | ### User Entity 23 | 24 | Open class `App\Entities\User` add interface and trait to implement. 25 | ```diff 26 | namespace Fluent\Auth\Entities; 27 | 28 | - use CodeIgniter\Entity; 29 | + use CodeIgniter\Entity\Entity; 30 | use Fluent\Auth\Contracts\AuthenticatorInterface; 31 | + use Fluent\Auth\Contracts\AuthorizableInterface; 32 | use Fluent\Auth\Contracts\HasAccessTokensInterface; 33 | use Fluent\Auth\Contracts\ResetPasswordInterface; 34 | use Fluent\Auth\Contracts\VerifyEmailInterface; 35 | use Fluent\Auth\Facades\Hash; 36 | use Fluent\Auth\Traits\AuthenticatableTrait; 37 | use Fluent\Auth\Traits\AuthorizableTrait; 38 | use Fluent\Auth\Traits\CanResetPasswordTrait; 39 | use Fluent\Auth\Traits\HasAccessTokensTrait; 40 | use Fluent\Auth\Traits\MustVerifyEmailTrait; 41 | 42 | class User extends Entity implements 43 | AuthenticatorInterface, 44 | + AuthorizableInterface, 45 | HasAccessTokensInterface, 46 | ResetPasswordInterface, 47 | VerifyEmailInterface 48 | { 49 | use AuthenticatableTrait; 50 | + use AuthorizableTrait; 51 | use CanResetPasswordTrait; 52 | use HasAccessTokensTrait; 53 | use MustVerifyEmailTrait; 54 | } 55 | ``` 56 | 57 | ### AuthServiceProvider 58 | 59 | Open `App\Providers\AuthServiceProvider` 60 | ```diff 61 | namespace Fluent\Auth; 62 | 63 | + use Fluent\Auth\Facades\Gate; 64 | use Fluent\Auth\AbstractServiceProvider; 65 | 66 | class AuthServiceProvider extends AbstractServiceProvider 67 | { 68 | + /** 69 | + * The policy mappings for the application. 70 | + * 71 | + * @var array 72 | + */ 73 | + protected static $policies = []; 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public static function register() 79 | { 80 | + static::registerPolicies(); 81 | } 82 | } 83 | ``` 84 | 85 | ## Documentation 86 | - [Authentication](docs/en/authentication.md) 87 | - [Authorization](docs/en/authorization.md) 88 | 89 | ## Community Authentication Guards 90 | - JWT (JSON Web Token) - [agungsugiarto/codeigniter4-authentication-jwt](https://github.com/agungsugiarto/codeigniter4-authentication-jwt) 91 | 92 | ## Authentication Demo 93 | - [codeigniter4-authentication-demo](https://github.com/agungsugiarto/codeigniter4-authentication-demo) 94 | 95 | Changelog 96 | -------- 97 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 98 | 99 | ## Contributing 100 | Contributions are very welcome. 101 | 102 | ## License 103 | 104 | Released under the MIT License, see [LICENSE](LICENSE.md). 105 | -------------------------------------------------------------------------------- /src/Passwords/RateLimiter.php: -------------------------------------------------------------------------------- 1 | cache = Services::cache(); 36 | } 37 | 38 | /** 39 | * Register a named limiter configuration. 40 | * 41 | * @return $this 42 | */ 43 | public function for(string $name, Closure $callback) 44 | { 45 | $this->limiters[$name] = $callback; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Get the given named rate limiter. 52 | * 53 | * @return Closure 54 | */ 55 | public function limiter(string $name) 56 | { 57 | return $this->limiters[$name] ?? null; 58 | } 59 | 60 | /** 61 | * Determine if the given key has been "accessed" too many times. 62 | * 63 | * @param string $key 64 | * @param int $maxAttempts 65 | * @return bool 66 | */ 67 | public function tooManyAttempts($key, $maxAttempts) 68 | { 69 | if ($this->attempts($key) >= $maxAttempts) { 70 | if ($this->cache->get($key . '_timer')) { 71 | return true; 72 | } 73 | 74 | $this->resetAttempts($key); 75 | } 76 | 77 | return false; 78 | } 79 | 80 | /** 81 | * Increment the counter for a given key for a given decay time. 82 | * 83 | * @param string $key 84 | * @param int $decaySeconds 85 | * @return int 86 | */ 87 | public function hit($key, $decaySeconds = 60) 88 | { 89 | $this->cache->save( 90 | $key . '_timer', 91 | $this->availableAt($decaySeconds), 92 | $decaySeconds 93 | ); 94 | 95 | $this->cache->remember($key, $decaySeconds, function () { 96 | return 0; 97 | }); 98 | 99 | return $this->cache->increment($key); 100 | } 101 | 102 | /** 103 | * Get the number of attempts for the given key. 104 | * 105 | * @param string $key 106 | * @return mixed 107 | */ 108 | public function attempts($key) 109 | { 110 | return $this->cache->get($key) ?? 0; 111 | } 112 | 113 | /** 114 | * Reset the number of attempts for the given key. 115 | * 116 | * @param string $key 117 | * @return mixed 118 | */ 119 | public function resetAttempts($key) 120 | { 121 | return $this->cache->delete($key); 122 | } 123 | 124 | /** 125 | * Get the number of retries left for the given key. 126 | * 127 | * @param string $key 128 | * @param int $maxAttempts 129 | * @return int 130 | */ 131 | public function retriesLeft($key, $maxAttempts) 132 | { 133 | $attempts = $this->attempts($key); 134 | 135 | return $maxAttempts - $attempts; 136 | } 137 | 138 | /** 139 | * Clear the hits and lockout timer for the given key. 140 | * 141 | * @param string $key 142 | * @return void 143 | */ 144 | public function clear($key) 145 | { 146 | $this->resetAttempts($key); 147 | 148 | $this->cache->delete($key . '_timer'); 149 | } 150 | 151 | /** 152 | * Get the number of seconds until the "key" is accessible again. 153 | * 154 | * @param string $key 155 | * @return int 156 | */ 157 | public function availableIn($key) 158 | { 159 | return $this->cache->get($key . '_timer') - $this->currentTime(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | request->getPost(); 51 | 52 | // Credentials for attempt login. 53 | $credentials = ['email' => $request->email, 'password' => $request->password]; 54 | 55 | // Credential if remember. 56 | $remember = $this->filled('remember'); 57 | 58 | // Rate limiter how many can be attempt. 59 | if (RateLimiter::tooManyAttempts($this->throttleKey(), static::MAX_ATTEMPT)) { 60 | $seconds = RateLimiter::availableIn($this->throttleKey()); 61 | 62 | return redirect()->back()->withInput()->with('error', lang('Auth.throttle', [$seconds])); 63 | } 64 | 65 | // Validate this credentials request. 66 | if (! $this->validate(['email' => 'required|valid_email', 'password' => 'required'])) { 67 | return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); 68 | } 69 | 70 | // Try to login this credentials. 71 | if (! Auth::attempt($credentials, $remember)) { 72 | // Save throttle state. 73 | RateLimiter::hit($this->throttleKey(), static::DECAY_SECOND); 74 | 75 | return redirect()->back()->withInput()->with('error', lang('Auth.failed')); 76 | } 77 | 78 | // Clear the throttle key 79 | RateLimiter::clear($this->throttleKey()); 80 | 81 | // Finnaly we're success login. 82 | return redirect(config('Auth')->home)->withCookies(); 83 | } 84 | 85 | /** 86 | * Destroy an authenticated session. 87 | * 88 | * @return RedirectResponse 89 | */ 90 | public function delete() 91 | { 92 | Auth::logout(); 93 | 94 | return redirect('/')->withCookies(); 95 | } 96 | 97 | /** 98 | * Determine if the request contains a non-empty value for an input item. 99 | * 100 | * @param string|array $key 101 | * @return bool 102 | */ 103 | protected function filled($key) 104 | { 105 | $keys = is_array($key) ? $key : func_get_args(); 106 | 107 | foreach ($keys as $value) { 108 | if ($this->isEmptyString($value)) { 109 | return false; 110 | } 111 | } 112 | 113 | return true; 114 | } 115 | 116 | /** 117 | * Determine if the given input key is an empty string for "has". 118 | * 119 | * @param string $key 120 | * @return bool 121 | */ 122 | protected function isEmptyString($key) 123 | { 124 | $value = $this->request->getVar($key); 125 | 126 | return ! is_bool($value) && ! is_array($value) && trim((string) $value) === ''; 127 | } 128 | 129 | /** 130 | * Get the rate limiting throttle key for the request. 131 | * 132 | * @param object $request 133 | * @return string 134 | */ 135 | public function throttleKey() 136 | { 137 | return md5("{$this->request->getPost('email')}|{$this->request->getIPAddress()}"); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Passwords/PasswordResetRepository.php: -------------------------------------------------------------------------------- 1 | connection = Config::connect($connection)->table($table); 48 | $this->expires = $expires * 60; 49 | $this->throttle = $throttle; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function create(ResetPasswordInterface $user) 56 | { 57 | $email = $user->getEmailForPasswordReset(); 58 | 59 | $this->destroy($user); 60 | 61 | // We will create a new, random token for the user so that we can e-mail them 62 | // a safe link to the password reset form. Then we will insert a record in 63 | // the database so that we can verify the token within the actual reset. 64 | $token = $this->createNewToken(); 65 | 66 | $this->connection->insert($this->getPayload($email, $token)); 67 | 68 | return $token; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function createNewToken() 75 | { 76 | return hash_hmac('sha256', bin2hex(random_bytes(20)), config('Encryption')->key); 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function exists(ResetPasswordInterface $user, $token) 83 | { 84 | $record = $this->connection->where('email', $user->getEmailForPasswordReset())->get()->getFirstRow(); 85 | 86 | $expiredAt = Time::now()->subSeconds($this->expires); 87 | 88 | return $record && ! $record->created_at < $expiredAt && Hash::check($token, $record->token); 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function recentlyCreatedToken(ResetPasswordInterface $user) 95 | { 96 | if ($this->throttle <= 0) { 97 | return false; 98 | } 99 | 100 | $record = $this->connection->where('email', $user->getEmailForPasswordReset())->get()->getFirstRow(); 101 | 102 | $expiredAt = Time::now()->subSeconds($this->throttle); 103 | 104 | return $record && $record->created_at > $expiredAt; 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function destroy(ResetPasswordInterface $user) 111 | { 112 | return $this->connection->where('email', $user->getEmailForPasswordReset())->delete(); 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function destroyExpired() 119 | { 120 | $expiredAt = Time::now()->subSeconds($this->expires); 121 | 122 | return $this->connection->where('created_at <', $expiredAt)->delete(); 123 | } 124 | 125 | /** 126 | * Build the record payload for the table. 127 | * 128 | * @param string $email 129 | * @param string $token 130 | * @return array 131 | */ 132 | protected function getPayload($email, $token) 133 | { 134 | return [ 135 | 'email' => $email, 136 | 'token' => Hash::make($token), 137 | 'created_at' => Time::now(), 138 | 'updated_at' => Time::now(), 139 | ]; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Contracts/GateInterface.php: -------------------------------------------------------------------------------- 1 | be($user, $guard); 19 | } 20 | 21 | /** 22 | * Set the currently logged in user for the application. 23 | * 24 | * @param \Fluent\Auth\Contracts\AuthenticatorInterface $user 25 | * @param string|null $guard 26 | * @return $this 27 | */ 28 | public function be(AuthenticatorInterface $user, $guard = null) 29 | { 30 | auth()->guard($guard)->setUser($user); 31 | 32 | auth()->shouldUse($guard); 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * Assert that the user is authenticated. 39 | * 40 | * @param string|null $guard 41 | * @return $this 42 | */ 43 | public function assertAuthenticated($guard = null) 44 | { 45 | $this->assertTrue($this->isAuthenticated($guard), 'The user is not authenticated'); 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Assert that the user is not authenticated. 52 | * 53 | * @param string|null $guard 54 | * @return $this 55 | */ 56 | public function assertGuest($guard = null) 57 | { 58 | $this->assertFalse($this->isAuthenticated($guard), 'The user is authenticated'); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Return true if the user is authenticated, false otherwise. 65 | * 66 | * @param string|null $guard 67 | * @return bool 68 | */ 69 | protected function isAuthenticated($guard = null) 70 | { 71 | return auth()->guard($guard)->check(); 72 | } 73 | 74 | /** 75 | * Assert that the user is authenticated as the given user. 76 | * 77 | * @param \Fluent\Auth\Contracts\AuthenticatorInterface $user 78 | * @param string|null $guard 79 | * @return $this 80 | */ 81 | public function assertAuthenticatedAs($user, $guard = null) 82 | { 83 | $expected = auth()->guard($guard)->user(); 84 | 85 | $this->assertNotNull($expected, 'The current user is not authenticated.'); 86 | 87 | $this->assertInstanceOf( 88 | get_class($expected), $user, 89 | 'The currently authenticated user is not who was expected' 90 | ); 91 | 92 | $this->assertSame( 93 | $expected->getAuthId(), $user->getAuthId(), 94 | 'The currently authenticated user is not who was expected' 95 | ); 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * Assert that the given credentials are valid. 102 | * 103 | * @param array $credentials 104 | * @param string|null $guard 105 | * @return $this 106 | */ 107 | public function assertCredentials(array $credentials, $guard = null) 108 | { 109 | $this->assertTrue( 110 | $this->hasCredentials($credentials, $guard), 'The given credentials are invalid.' 111 | ); 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Assert that the given credentials are invalid. 118 | * 119 | * @param array $credentials 120 | * @param string|null $guard 121 | * @return $this 122 | */ 123 | public function assertInvalidCredentials(array $credentials, $guard = null) 124 | { 125 | $this->assertFalse( 126 | $this->hasCredentials($credentials, $guard), 'The given credentials are valid.' 127 | ); 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Return true if the credentials are valid, false otherwise. 134 | * 135 | * @param array $credentials 136 | * @param string|null $guard 137 | * @return bool 138 | */ 139 | protected function hasCredentials(array $credentials, $guard = null) 140 | { 141 | $provider = auth()->guard($guard)->getProvider(); 142 | 143 | $user = $provider->findByCredentials($credentials); 144 | 145 | return $user && $provider->validateCredentials($user, $credentials); 146 | } 147 | } -------------------------------------------------------------------------------- /src/Passwords/Hash/AbstractManager.php: -------------------------------------------------------------------------------- 1 | config = $factory::config('Hashing', ['getShared' => $getShared]); 47 | } 48 | 49 | /** 50 | * Get the default driver name. 51 | * 52 | * @return string 53 | */ 54 | abstract public function getDefaultDriver(); 55 | 56 | /** 57 | * Get a driver instance. 58 | * 59 | * @param string|null $driver 60 | * @return mixed 61 | * @throws InvalidArgumentException 62 | */ 63 | public function driver($driver = null) 64 | { 65 | $driver = $driver ?: $this->getDefaultDriver(); 66 | 67 | if (is_null($driver)) { 68 | throw new InvalidArgumentException(sprintf( 69 | 'Unable to resolve NULL driver for [%s].', 70 | static::class 71 | )); 72 | } 73 | 74 | // If the given driver has not been created before, we will create the instances 75 | // here and cache it so we can return it next time very quickly. If there is 76 | // already a driver created by this name, we'll just return that instance. 77 | if (! isset($this->drivers[$driver])) { 78 | $this->drivers[$driver] = $this->createDriver($driver); 79 | } 80 | 81 | return $this->drivers[$driver]; 82 | } 83 | 84 | /** 85 | * Create a new driver instance. 86 | * 87 | * @param string $driver 88 | * @return mixed 89 | * @throws InvalidArgumentException 90 | */ 91 | protected function createDriver($driver) 92 | { 93 | // First, we will determine if a custom driver creator exists for the given driver and 94 | // if it does not we will check for a creator method for the driver. Custom creator 95 | // callbacks allow developers to build their own "drivers" easily using Closures. 96 | if (isset($this->customCreators[$driver])) { 97 | return $this->callCustomCreator($driver); 98 | } else { 99 | $method = 'create' . ucfirst($driver) . 'Driver'; 100 | 101 | if (method_exists($this, $method)) { 102 | return $this->$method(); 103 | } 104 | } 105 | 106 | throw new InvalidArgumentException("Driver [$driver] not supported."); 107 | } 108 | 109 | /** 110 | * Call a custom driver creator. 111 | * 112 | * @param string $driver 113 | * @return mixed 114 | */ 115 | protected function callCustomCreator($driver) 116 | { 117 | return $this->customCreators[$driver]($this->config); 118 | } 119 | 120 | /** 121 | * Register a custom driver creator Closure. 122 | * 123 | * @param string $driver 124 | * @return $this 125 | */ 126 | public function extend($driver, Closure $callback) 127 | { 128 | $this->customCreators[$driver] = $callback; 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Get all of the created "drivers". 135 | * 136 | * @return array 137 | */ 138 | public function getDrivers() 139 | { 140 | return $this->drivers; 141 | } 142 | 143 | /** 144 | * Dynamically call the default driver instance. 145 | * 146 | * @param string $method 147 | * @param array $parameters 148 | * @return mixed 149 | */ 150 | public function __call($method, $parameters) 151 | { 152 | return $this->driver()->$method(...$parameters); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Views/Auth/reset_password.php: -------------------------------------------------------------------------------- 1 | extend('Auth/layout') ?> 2 | 3 | section('content') ?> 4 |
5 |
6 |
7 |

8 | 9 | 10 | 11 | Back to log in 12 | 13 |

14 |
15 |
16 |

Reset password

17 | 18 | include('Auth/messages') ?> 19 |
20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 |
59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | endSection() ?> -------------------------------------------------------------------------------- /src/Traits/HasAccessTokensTrait.php: -------------------------------------------------------------------------------- 1 | insert(new AccessToken([ 33 | 'user_id' => $this->id, 34 | 'name' => $name, 35 | 'token' => hash('sha256', $rawToken = bin2hex(random_bytes(64))), 36 | 'scopes' => $scopes, 37 | ])); 38 | 39 | $token = $tokens->find($tokens->insertId()); 40 | 41 | $token->raw_token = $rawToken; 42 | 43 | return $token; 44 | } 45 | 46 | /** 47 | * Given the token, will retrieve the token to 48 | * verify it exists, then delete it. 49 | * 50 | * @return mixed 51 | */ 52 | public function revokeAccessToken(string $token) 53 | { 54 | $tokens = new AccessTokenModel(); 55 | 56 | return $tokens->where('user_id', $this->id) 57 | ->where('token', hash('sha256', $token)) 58 | ->delete(); 59 | } 60 | 61 | /** 62 | * Revokes all access tokens for this user. 63 | * 64 | * @return mixed 65 | */ 66 | public function revokeAllAccessTokens() 67 | { 68 | $tokens = new AccessTokenModel(); 69 | 70 | return $tokens->where('user_id', $this->id) 71 | ->delete(); 72 | } 73 | 74 | /** 75 | * Retrieves all personal access tokens for this user. 76 | * 77 | * @return array 78 | */ 79 | public function accessTokens(): array 80 | { 81 | $tokens = new AccessTokenModel(); 82 | 83 | return $tokens->where('user_id', $this->id) 84 | ->find(); 85 | } 86 | 87 | /** 88 | * Given a raw token, will hash it and attemp to 89 | * locate it within the system. 90 | * 91 | * @return AccessToken|null 92 | */ 93 | public function getAccessToken(?string $token) 94 | { 95 | if (empty($token)) { 96 | return null; 97 | } 98 | 99 | $tokens = new AccessTokenModel(); 100 | 101 | return $tokens->where('user_id', $this->id) 102 | ->where('token', hash('sha256', $token)) 103 | ->first(); 104 | } 105 | 106 | /** 107 | * Given the ID, returns the given access token. 108 | * 109 | * @return AccessToken|null 110 | */ 111 | public function getAccessTokenById(int $id) 112 | { 113 | $tokens = new AccessTokenModel(); 114 | 115 | return $tokens->where('user_id', $this->id) 116 | ->where('id', $id) 117 | ->first(); 118 | } 119 | 120 | /** 121 | * Determines whether the user's token grants permissions to $scope. 122 | * First checks against $this->activeToken, which is set during 123 | * authentication. If it hasn't been set, returns false. 124 | */ 125 | public function tokenCan(string $scope): bool 126 | { 127 | if (! $this->currentAccessToken() instanceof AccessToken) { 128 | return false; 129 | } 130 | 131 | return $this->currentAccessToken()->can($scope); 132 | } 133 | 134 | /** 135 | * Determines whether the user's token does NOT grant permissions to $scope. 136 | * First checks against $this->activeToken, which is set during 137 | * authentication. If it hasn't been set, returns true. 138 | */ 139 | public function tokenCant(string $scope): bool 140 | { 141 | if (! $this->currentAccessToken() instanceof AccessToken) { 142 | return true; 143 | } 144 | 145 | return $this->currentAccessToken()->cant($scope); 146 | } 147 | 148 | /** 149 | * Returns the current access token for the user. 150 | * 151 | * @return AccessToken 152 | */ 153 | public function currentAccessToken() 154 | { 155 | return $this->attributes['activeAccessToken'] ?? null; 156 | } 157 | 158 | /** 159 | * Sets the current active token for this user. 160 | * 161 | * @return $this 162 | */ 163 | public function withAccessToken(?AccessToken $accessToken) 164 | { 165 | $this->attributes['activeAccessToken'] = $accessToken; 166 | 167 | return $this; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Views/Auth/login.php: -------------------------------------------------------------------------------- 1 | extend('Auth/layout') ?> 2 | 3 | section('content') ?> 4 |
5 |
6 |

7 | 8 | 9 | 10 | 11 | Back to homepage 12 | 13 |

14 |
15 |
16 |
17 |
18 |

Sign in to our platform

19 |
20 | 21 | include('Auth/messages') ?> 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 |
39 | 40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 | 58 |
59 | 60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 | 68 | Not registered? 69 | Create account 70 | 71 |
72 |
73 |
74 |
75 |
76 |
77 | endSection() ?> -------------------------------------------------------------------------------- /src/Config/Auth.php: -------------------------------------------------------------------------------- 1 | 'web', 25 | 'provider' => 'users', 26 | 'password' => 'users', 27 | ]; 28 | 29 | /** 30 | * -------------------------------------------------------------------------- 31 | * Authentication Guards 32 | * -------------------------------------------------------------------------- 33 | * 34 | * Next, you may define every authentication adapter for your application. 35 | * Of course, a great default configuration has been defined for you 36 | * here which uses session storage and the user provider. 37 | * 38 | * All authentication drivers have a user provider. This defines how the 39 | * users are actually retrieved out of your database or other storage 40 | * mechanisms used by this application to persist your user's data. 41 | * 42 | * Supported: "session", "token" 43 | * 44 | * @var array 45 | */ 46 | public $guards = [ 47 | 'web' => [ 48 | 'driver' => SessionAdapter::class, 49 | 'provider' => 'users', 50 | ], 51 | 'token' => [ 52 | 'driver' => TokenAdapter::class, 53 | 'provider' => 'users', 54 | ], 55 | // etc your implementation 56 | ]; 57 | 58 | /** 59 | * -------------------------------------------------------------------------- 60 | * User Providers 61 | * -------------------------------------------------------------------------- 62 | * 63 | * All authentication drivers have a user provider. This defines how the 64 | * users are actually retrieved out of your database or other storage 65 | * mechanisms used by this application to persist your user's data. 66 | * 67 | * If you have multiple user tables or models you may configure multiple 68 | * sources which represent each model / table. These sources may then 69 | * be assigned to any extra authentication guards you have defined. 70 | * 71 | * Supported: "model", "database" 72 | * 73 | * @var array 74 | */ 75 | public $providers = [ 76 | 'users' => [ 77 | 'driver' => 'model', 78 | 'table' => UserModel::class, 79 | ], 80 | 'database' => [ 81 | 'connection' => 'default', 82 | 'driver' => 'connection', 83 | 'table' => 'users', 84 | ], 85 | ]; 86 | 87 | /** 88 | * -------------------------------------------------------------------------- 89 | * Resetting Passwords 90 | * -------------------------------------------------------------------------- 91 | * 92 | * You may specify multiple password reset configurations if you have more 93 | * than one user table or model in the application and you want to have 94 | * separate password reset settings based on the specific user types. 95 | * 96 | * The expire time is the number of minutes that the reset token should be 97 | * considered valid. This security feature keeps tokens short-lived so 98 | * they have less time to be guessed. You may change this as needed. 99 | * 100 | * @var array 101 | */ 102 | public $passwords = [ 103 | 'users' => [ 104 | 'provider' => 'users', 105 | 'connection' => 'default', 106 | 'table' => 'auth_password_resets', 107 | 'expire' => 60, 108 | 'throttle' => 60, 109 | ], 110 | ]; 111 | 112 | /** 113 | * -------------------------------------------------------------------------- 114 | * Password Confirmation Timeout 115 | * -------------------------------------------------------------------------- 116 | * 117 | * Here you may define the amount of seconds before a password confirmation 118 | * times out and the user is prompted to re-enter their password via the 119 | * confirmation screen. By default, the timeout lasts for three hours. 120 | * 121 | * @var int 122 | */ 123 | public $passwordTimeout = 3 * HOUR; 124 | 125 | /** 126 | * -------------------------------------------------------------------------- 127 | * Redirect Authenticated 128 | * -------------------------------------------------------------------------- 129 | * 130 | * Here you may define the redirect if authenticated success. 131 | * 132 | * @var string 133 | */ 134 | public $home = 'dashboard'; 135 | } 136 | -------------------------------------------------------------------------------- /src/Passwords/Hash/ArgonHasher.php: -------------------------------------------------------------------------------- 1 | time = $options['time'] ?? $this->time; 54 | $this->memory = $options['memory'] ?? $this->memory; 55 | $this->threads = $options['threads'] ?? $this->threads; 56 | $this->verifyAlgorithm = $options['verify'] ?? $this->verifyAlgorithm; 57 | } 58 | 59 | /** 60 | * Hash the given value. 61 | * 62 | * @param string $value 63 | * @param array $options 64 | * @return string 65 | * @throws RuntimeException 66 | */ 67 | public function make($value, array $options = []) 68 | { 69 | $hash = @password_hash($value, $this->algorithm(), [ 70 | 'memory_cost' => $this->memory($options), 71 | 'time_cost' => $this->time($options), 72 | 'threads' => $this->threads($options), 73 | ]); 74 | 75 | if (! is_string($hash)) { 76 | throw new RuntimeException('Argon2 hashing not supported.'); 77 | } 78 | 79 | return $hash; 80 | } 81 | 82 | /** 83 | * Get the algorithm that should be used for hashing. 84 | * 85 | * @return int 86 | */ 87 | protected function algorithm() 88 | { 89 | return PASSWORD_ARGON2I; 90 | } 91 | 92 | /** 93 | * Check the given plain value against a hash. 94 | * 95 | * @param string $value 96 | * @param string $hashedValue 97 | * @param array $options 98 | * @return bool 99 | * @throws RuntimeException 100 | */ 101 | public function check($value, $hashedValue, array $options = []) 102 | { 103 | if ($this->verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'argon2i') { 104 | throw new RuntimeException('This password does not use the Argon2i algorithm.'); 105 | } 106 | 107 | return parent::check($value, $hashedValue, $options); 108 | } 109 | 110 | /** 111 | * Check if the given hash has been hashed using the given options. 112 | * 113 | * @param string $hashedValue 114 | * @param array $options 115 | * @return bool 116 | */ 117 | public function needsRehash($hashedValue, array $options = []) 118 | { 119 | return password_needs_rehash($hashedValue, $this->algorithm(), [ 120 | 'memory_cost' => $this->memory($options), 121 | 'time_cost' => $this->time($options), 122 | 'threads' => $this->threads($options), 123 | ]); 124 | } 125 | 126 | /** 127 | * Set the default password memory factor. 128 | * 129 | * @return $this 130 | */ 131 | public function setMemory(int $memory) 132 | { 133 | $this->memory = $memory; 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * Set the default password timing factor. 140 | * 141 | * @return $this 142 | */ 143 | public function setTime(int $time) 144 | { 145 | $this->time = $time; 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Set the default password threads factor. 152 | * 153 | * @return $this 154 | */ 155 | public function setThreads(int $threads) 156 | { 157 | $this->threads = $threads; 158 | 159 | return $this; 160 | } 161 | 162 | /** 163 | * Extract the memory cost value from the options array. 164 | * 165 | * @param array $options 166 | * @return int 167 | */ 168 | protected function memory(array $options) 169 | { 170 | return $options['memory'] ?? $this->memory; 171 | } 172 | 173 | /** 174 | * Extract the time cost value from the options array. 175 | * 176 | * @param array $options 177 | * @return int 178 | */ 179 | protected function time(array $options) 180 | { 181 | return $options['time'] ?? $this->time; 182 | } 183 | 184 | /** 185 | * Extract the threads value from the options array. 186 | * 187 | * @param array $options 188 | * @return int 189 | */ 190 | protected function threads(array $options) 191 | { 192 | return $options['threads'] ?? $this->threads; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Views/Email/layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 29 | 30 | 31 | 32 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/Views/Auth/register.php: -------------------------------------------------------------------------------- 1 | extend('Auth/layout') ?> 2 | 3 | section('content') ?> 4 |
5 |
6 |

7 | 8 | 9 | 10 | 11 | Back to homepage 12 | 13 |

14 |
15 |
16 |
17 |
18 |

Create Account

19 |
20 | 21 | include('Auth/messages') ?> 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 | 44 |
45 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 |
60 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 |
70 | 71 |
72 |
73 | 74 |
75 |
76 |
77 | 78 | Already have an account? 79 | Login here 80 | 81 |
82 |
83 |
84 |
85 |
86 |
87 | endSection() ?> -------------------------------------------------------------------------------- /src/Passwords/PasswordBroker.php: -------------------------------------------------------------------------------- 1 | tokens = $tokens; 27 | $this->users = $users; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function sendResetLink(array $credentials, ?Closure $callback = null) 34 | { 35 | // First we will check to see if we found a user at the given credentials and 36 | // if we did not we will redirect back to this current URI with a piece of 37 | // "flash" data in the session to indicate to the developers the errors. 38 | $user = $this->getUser($credentials); 39 | 40 | if (is_null($user)) { 41 | return static::INVALID_USER; 42 | } 43 | 44 | if ($this->tokens->recentlyCreatedToken($user)) { 45 | return static::RESET_THROTTLED; 46 | } 47 | 48 | $token = $this->tokens->create($user); 49 | 50 | if ($callback) { 51 | $callback($user, $token); 52 | } else { 53 | // Once we have the reset token, we are ready to send the message out to this 54 | // user with a link to reset their password. We will then redirect back to 55 | // the current URI having nothing set in the session to indicate errors. 56 | $user->sendPasswordResetNotification($token); 57 | } 58 | 59 | return static::RESET_LINK_SENT; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function sendVerifyLink(array $credentials, ?Closure $callback = null) 66 | { 67 | // First we will check to see if we found a user at the given credentials and 68 | // if we did not we will redirect back to this current URI with a piece of 69 | // "flash" data in the session to indicate to the developers the errors. 70 | $user = $this->getUser($credentials); 71 | 72 | if (is_null($user)) { 73 | return static::INVALID_USER; 74 | } 75 | 76 | if (! $user instanceof VerifyEmailInterface) { 77 | return static::RESET_THROTTLED; 78 | } 79 | 80 | if ($callback) { 81 | $callback($user); 82 | } else { 83 | // We are ready to send verify the message out to this user with a link 84 | // to their email. We will then redirect back to the current URI 85 | // having nothing set in the session to indicate errors. 86 | $user->sendEmailVerificationNotification(); 87 | } 88 | 89 | return static::VERIFY_LINK_SENT; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function reset(array $credentials, Closure $callback) 96 | { 97 | $user = $this->validateReset($credentials); 98 | 99 | // If the responses from the validate method is not a user instance, we will 100 | // assume that it is a redirect and simply return it from this method and 101 | // the user is properly redirected having an error message on the post. 102 | if (! $user instanceof ResetPasswordInterface) { 103 | return $user; 104 | } 105 | 106 | $password = $credentials['password']; 107 | 108 | // Once the reset has been validated, we'll call the given callback with the 109 | // new password. This gives the user an opportunity to store the password 110 | // in their persistent storage. Then we'll delete the token and return. 111 | $callback($user, $password); 112 | 113 | $this->tokens->destroy($user); 114 | 115 | return static::PASSWORD_RESET; 116 | } 117 | 118 | /** 119 | * Validate a password reset for the given credentials. 120 | * 121 | * @param array $credentials 122 | * @return ResetPasswordInterface|string 123 | */ 124 | protected function validateReset(array $credentials) 125 | { 126 | if (is_null($user = $this->getUser($credentials))) { 127 | return static::INVALID_USER; 128 | } 129 | 130 | if (! $this->tokens->exists($user, $credentials['token'])) { 131 | return static::INVALID_TOKEN; 132 | } 133 | 134 | return $user; 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | public function getUser(array $credentials) 141 | { 142 | $credentials = Arr::except($credentials, ['token']); 143 | 144 | $user = $this->users->findByCredentials($credentials); 145 | 146 | if ($user && ! $user instanceof ResetPasswordInterface) { 147 | throw new UnexpectedValueException('User must implement ResetPasswordInterface.'); 148 | } 149 | 150 | return $user; 151 | } 152 | 153 | /** 154 | * {@inheritdoc} 155 | */ 156 | public function createToken(ResetPasswordInterface $user) 157 | { 158 | return $this->tokens->create($user); 159 | } 160 | 161 | /** 162 | * {@inheritdoc} 163 | */ 164 | public function deleteToken(ResetPasswordInterface $user) 165 | { 166 | $this->tokens->destroy($user); 167 | } 168 | 169 | /** 170 | * {@inheritdoc} 171 | */ 172 | public function tokenExists(ResetPasswordInterface $user, $token) 173 | { 174 | return $this->tokens->exists($user, $token); 175 | } 176 | 177 | /** 178 | * {@inheritdoc} 179 | */ 180 | public function getRepository() 181 | { 182 | return $this->tokens; 183 | } 184 | } 185 | --------------------------------------------------------------------------------