Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ user_id();
## Authenticator Responses

Many of the authenticator methods will return a `CodeIgniter\Shield\Result` class. This provides a consistent
way of checking the results and can have additional information return along with it. The class
way of checking the results and can have additional information returned along with it. The class
has the following methods:

### isOK()
Expand Down
44 changes: 44 additions & 0 deletions docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
- [removeGroup()](#removegroup)
- [syncGroups()](#syncgroups)
- [getGroups()](#getgroups)
- [User Activation](#user-activation)
- [Checking Activation Status](#checking-activation-status)
- [Activating a User](#activating-a-user)
- [Deactivating a User](#deactivating-a-user)

Authorization happens once a user has been identified through authentication. It is the process of
determining what actions a user is allowed to do within your site.
Expand Down Expand Up @@ -233,3 +237,43 @@ Returns all groups this user is a part of.
```php
$user->getGroups();
```

## User Activation

All users have an `active` flag. This is only used when the [`EmailActivation` action](./auth_actions.md), or a custom action used to activate a user, is enabled.

### Checking Activation Status

You can determine if a user has been activated with the `isActivated()` method.

```php
if ($user->isActivated()) {
//
}
```

> **Note** If no activator is specified in the `Auth` config file, `actions['register']` property, then this will always return `true`.

You can check if a user has not been activated yet via the `isNotActivated()` method.

```php
if ($user->isNotActivated()) {
//
}
```

## Activating a User

Users are automatically activated withih the `EmailActivator` action. They can be manually activated via the `activate()` method on the User entity.

```php
$user->activate();
```

## Deactivating a User

Users can be manually deactivated via the `deactivate()` method on the User entity.

```php
$user->deactivate();
```
1 change: 0 additions & 1 deletion src/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use CodeIgniter\Shield\Models\UserModel;

/**
* @method void activateUser(User $user) [Session]
* @method Result attempt(array $credentials)
* @method Result check(array $credentials)
* @method bool checkAction(string $token, string $type) [Session]
Expand Down
2 changes: 1 addition & 1 deletion src/Authentication/Actions/EmailActivator.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public function verify(IncomingRequest $request)
$user = $authenticator->getUser();

// Set the user active now
$authenticator->activateUser($user);
$user->activate();

// Success!
return redirect()->to(config('Auth')->registerRedirect())
Expand Down
8 changes: 0 additions & 8 deletions src/Authentication/Authenticators/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,14 +254,6 @@ public function completeLogin(User $user): void
Events::trigger('login', $user);
}

/**
* Activate a User
*/
public function activateUser(User $user): void
{
$this->provider->activate($user);
}

/**
* @param int|string|null $userId
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Controllers/RegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public function registerAction(): RedirectResponse
}

// Set the user active
$authenticator->activateUser($user);
$user->activate();

$authenticator->completeLogin($user);

Expand Down
2 changes: 2 additions & 0 deletions src/Entities/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use CodeIgniter\Shield\Authorization\Traits\Authorizable;
use CodeIgniter\Shield\Models\LoginModel;
use CodeIgniter\Shield\Models\UserIdentityModel;
use CodeIgniter\Shield\Traits\Activatable;
use CodeIgniter\Shield\Traits\Resettable;

/**
Expand All @@ -27,6 +28,7 @@ class User extends Entity
use Authorizable;
use HasAccessTokens;
use Resettable;
use Activatable;

/**
* @var UserIdentity[]|null
Expand Down
9 changes: 9 additions & 0 deletions src/Filters/SessionAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ public function before(RequestInterface $request, $arguments = null)
$authenticator->recordActiveDate();
}

// Block inactive users when Email Activation is enabled
$user = $authenticator->getUser();
if ($user !== null && ! $user->isActivated()) {
$authenticator->logout();

return redirect()->route('login')
->with('error', lang('Auth.activationBlocked'));
}

return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/Filters/TokenAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,24 @@ public function before(RequestInterface $request, $arguments = null)
]);

if (! $result->isOK() || (! empty($arguments) && $result->extraInfo()->tokenCant($arguments[0]))) {
return redirect()->to('/login');
return service('response')
->setStatusCode(Response::HTTP_UNAUTHORIZED)
->setJson(['message' => lang('Auth.badToken')]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change. It is better to document it.

#433 suggests 401, not 403. Which is better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for 401. For example, I already using it in few deployments with own api-filter. I would like to switch to this one if it's will be suitable for me.

Copy link
Contributor

@jozefrebjak jozefrebjak Jan 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

401 Unauthorized is the status code to return when the client provides no credentials or invalid credentials. 403 Forbidden is the status code to return when a client has valid credentials but not enough privileges to perform an action on a resource.

Receiving a 403 response is the server telling you, “I’m sorry. I know who you are–I believe who you say you are–but you just don’t have permission to access this resource. Maybe if you ask the system administrator nicely, you’ll get permission. But please don’t bother me again until your predicament changes.”

In summary, a 401 Unauthorized response should be used for missing or bad authentication, and a 403 Forbidden response should be used afterwards, when the user is authenticated but isn’t authorized to perform the requested operation on the given resource.

@lonnieezell Here we are mixing authentication and authorization. Maybe we need to first check if a client provided a valid token and if not, return 401 and after that, check, scopes and if there is no valid scope, return 403.

Copy link
Member

@kenjis kenjis Jan 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HyperText Transfer Protocol (HTTP) 401 Unauthorized response status code indicates that the client request has not been completed because it lacks valid authentication credentials for the requested resource.

This status code is similar to the 403 Forbidden status code, except that in situations resulting in this status code, user authentication can allow access to the resource.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401

The HTTP 403 Forbidden response status code indicates that the server understands the request but refuses to authorize it.

This status is similar to 401, but for the 403 Forbidden status code, re-authenticating makes no difference. The access is tied to the application logic, such as insufficient rights to a resource.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These make sense. First check will result in a 401, with the not-activated check being a 403.

}

if (setting('Auth.recordActiveDate')) {
$authenticator->recordActiveDate();
}

// Block inactive users when Email Activation is enabled
$user = $authenticator->getUser();
if ($user !== null && ! $user->isActivated()) {
$authenticator->logout();

return service('response')
->setStatusCode(Response::HTTP_FORBIDDEN)
->setJson(['message' => lang('Auth.activationBlocked')]);
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'Bitte verwenden Sie den unten stehenden Code, um Ihr Konto zu aktivieren und die Website zu nutzen.',
'invalidActivateToken' => 'Der Code war falsch.',
'needActivate' => '(To be translated) You must complete your registration by confirming the code sent to your email address.',
'activationBlocked' => '(to be translated) You must activate your account before logging in.',

// Groups
'unknownGroup' => '{0} ist eine ungültige Gruppe.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/en/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'Please use the code below to activate your account and start using the site.',
'invalidActivateToken' => 'The code was incorrect.',
'needActivate' => 'You must complete your registration by confirming the code sent to your email address.',
'activationBlocked' => 'You must activate your account before logging in.',

// Groups
'unknownGroup' => '{0} is not a valid group.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/es/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'Por favor, usa el código de abajo para activar tu cuenta y empezar a usar el sitio.',
'invalidActivateToken' => 'El código no es correcto.',
'needActivate' => '(To be translated) You must complete your registration by confirming the code sent to your email address.',
'activationBlocked' => '(to be translated) You must activate your account before logging in.',

// Grupos
'unknownGroup' => '{0} no es un grupo válido.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/fa/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'لطفا برای فعالسازی حساب کاربری و استفاده از سایت از کد زیر استفاده کنید.',
'invalidActivateToken' => 'کد صحیح نمی باشد.',
'needActivate' => 'شما باید با ارائه کد ارسال شده به ایمیلتان، ثبت نام را تکمیل کنید.',
'activationBlocked' => 'قبل از تلاش برای ورود، باید اکانت خود را فعال کنید.',

// Groups
'unknownGroup' => '{0} گروهی معتبر نیست.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/fr/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'Veuillez utiliser le code suivant pour activer votre compte et commencer à utiliser le site.',
'invalidActivateToken' => 'Le code était incorrect.',
'needActivate' => 'Complétez votre inscription en confirmant le code envoyé à votre email.',
'activationBlocked' => '(to be translated) You must activate your account before logging in.',

// Groups
'unknownGroup' => '{0} n\'est pas un groupe valide.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/id/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'Silahkan gunakan kode dibawah ini untuk mengaktivasi akun Anda.',
'invalidActivateToken' => 'Kode tidak sesuai.',
'needActivate' => 'Anda harus menyelesaikan registrasi Anda dengan mengonfirmasi kode yang dikirim ke alamat email Anda.',
'activationBlocked' => '(to be translated) You must activate your account before logging in.',

// Groups
'unknownGroup' => '{0} bukan grup yang sah.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/it/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'Perfavore usa il codice qui sotto per attivare il tuo acccount ed iniziare ad usare il sito.',
'invalidActivateToken' => 'Il codice era sbagliato.',
'needActivate' => 'Devi completare la registrazione confermando il codice inviato al tuo indrizzo email.',
'activationBlocked' => '(to be translated) You must activate your account before logging in.',

// Groups
'unknownGroup' => '{0} non è un gruppo valido.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/ja/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => '以下のコードを使用してアカウントを有効化し、サイトの利用を開始してください。', // 'Please use the code below to activate your account and start using the site.',
'invalidActivateToken' => 'コードが間違っています。', // 'The code was incorrect.',
'needActivate' => 'メールアドレスに送信されたコードを確認し、登録を完了する必要があります。', // 'You must complete your registration by confirming the code sent to your email address.',
'activationBlocked' => 'ログインする前にアカウントを有効化する必要があります。',

// Groups
'unknownGroup' => '{0} は有効なグループではありません。', // '{0} is not a valid group.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/pt-BR/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'Use o código abaixo para ativar sua conta e começar a usar o site.',
'invalidActivateToken' => 'O código estava incorreto.',
'needActivate' => 'Você deve concluir seu registro confirmando o código enviado para seu endereço de e-mail.',
'activationBlocked' => '(to be translated) You must activate your account before logging in.',

// Grupos
'unknownGroup' => '{0} não é um grupo válido.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/sk/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'Pomocou nižšie uvedeného kódu aktivujte svoj účet a môžete začať používať stránku.',
'invalidActivateToken' => 'Kód bol nesprávny',
'needActivate' => 'Registráciu musíte dokončiť potvrdením kódu zaslaného na vašu e-mailovú adresu.',
'activationBlocked' => '(to be translated) You must activate your account before logging in.',

// Groups
'unknownGroup' => '{0} nie je platná skupina.',
Expand Down
1 change: 1 addition & 0 deletions src/Language/tr/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'emailActivateMailBody' => 'Hesabınızı etkinleştirmek ve siteyi kullanmaya başlamak için lütfen aşağıdaki kodu kullanın.',
'invalidActivateToken' => 'Kod yanlıştı.',
'needActivate' => 'E-posta adresinize gönderilen kodu onaylayarak kaydınızı tamamlamanız gerekmektedir.',
'activationBlocked' => '(to be translated) You must activate your account before logging in.',

// Groups
'unknownGroup' => '{0} geçerli bir grup değil.',
Expand Down
56 changes: 56 additions & 0 deletions src/Traits/Activatable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace CodeIgniter\Shield\Traits;

trait Activatable
{
/**
* Returns true if the user has been activated
* and activation is required after registration.
*/
public function isActivated(): bool
{
// If activation is not required, then we're always active.
return ! $this->shouldActivate() || $this->active;
}

/**
* Returns true if the user has not been activated.
*/
public function isNotActivated(): bool
{
return ! $this->isActivated();
}

/**
* Activates the user.
*/
public function activate(): bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change the return type to void?
The UserModel::update() throws an exception when the update fails.
So returning bool does not make sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless it's changing in a PR, update() currently returns a boolean. Changing this to void throws errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sent a PR #626

{
$model = auth()->getProvider();

return $model->update($this->id, ['active' => 1]);
}

/**
* Deactivates the user.
*/
public function deactivate(): bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same above.

{
$model = auth()->getProvider();

return $model->update($this->id, ['active' => 0]);
}

/**
* Does the Auth actions require activation?
* Check for the generic 'Activator' class name to allow
* for custom implementations, provided they follow the naming convention.
*/
private function shouldActivate(): bool
{
return strpos(setting('Auth.actions')['register'] ?? '', 'Activator') !== false;
}
}
26 changes: 26 additions & 0 deletions tests/Authentication/Filters/SessionFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,30 @@ public function testRecordActiveDate(): void
// Last Active should be greater than 'updated_at' column
$this->assertGreaterThan(auth('session')->user()->updated_at, auth('session')->user()->last_active);
}

public function testBlocksInactiveUsers(): void
{
$user = fake(UserModel::class, ['active' => false]);

// Activation only required with email activation
setting('Auth.actions', ['register' => null]);

$result = $this->actingAs($user)
->get('protected-route');

$result->assertStatus(200);
$result->assertSee('Protected');

// Now require user activation and try again
setting('Auth.actions', ['register' => '\CodeIgniter\Shield\Authentication\Actions\EmailActivator']);

$result = $this->actingAs($user)
->get('protected-route');

$result->assertRedirectTo('/login');
// User should be logged out
$this->assertNull(auth('session')->id());

setting('Auth.actions', ['register' => null]);
}
}
30 changes: 28 additions & 2 deletions tests/Authentication/Filters/TokenFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function testFilterNotAuthorized(): void
{
$result = $this->call('get', 'protected-route');

$result->assertRedirectTo('/login');
$result->assertStatus(401);

$result = $this->get('open-route');
$result->assertStatus(200);
Expand Down Expand Up @@ -84,6 +84,32 @@ public function testFiltersProtectsWithScopes(): void
$result = $this->withHeaders(['Authorization' => 'Bearer ' . $token2->raw_token])
->get('protected-user-route');

$result->assertRedirectTo('/login');
$result->assertStatus(401);
}

public function testBlocksInactiveUsers(): void
{
/** @var User $user */
$user = fake(UserModel::class, ['active' => false]);
$token = $user->generateAccessToken('foo');

// Activation only required with email activation
setting('Auth.actions', ['register' => null]);

$result = $this->withHeaders(['Authorization' => 'Bearer ' . $token->raw_token])
->get('protected-route');

$result->assertStatus(200);
$result->assertSee('Protected');

// Now require user activation and try again
setting('Auth.actions', ['register' => '\CodeIgniter\Shield\Authentication\Actions\EmailActivator']);

$result = $this->withHeaders(['Authorization' => 'Bearer ' . $token->raw_token])
->get('protected-route');

$result->assertStatus(403);

setting('Auth.actions', ['register' => null]);
}
}
Loading