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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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'));
Comment thread
datamweb marked this conversation as resolved.
}

return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/Filters/TokenAuth.php
Original file line numberDiff line numberDiff 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
Copy Markdown
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
Copy Markdown
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.

@jozefrebjakjozefrebjakJan 5, 2023

Copy link
Copy Markdown
Contributor

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.

@kenjiskenjisJan 5, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown
MemberAuthor

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')]);
Comment thread
datamweb marked this conversation as resolved.
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Comment thread
datamweb marked this conversation as resolved.

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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'));
Comment thread
datamweb marked this conversation as resolved.
}

return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/Filters/TokenAuth.php
Original file line numberDiff line numberDiff 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
Copy Markdown
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
Copy Markdown
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.

@jozefrebjakjozefrebjakJan 5, 2023

Copy link
Copy Markdown
Contributor

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.

@kenjiskenjisJan 5, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown
MemberAuthor

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')]);
Comment thread
datamweb marked this conversation as resolved.
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Comment thread
datamweb marked this conversation as resolved.

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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'));
Comment thread
datamweb marked this conversation as resolved.
}

return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/Filters/TokenAuth.php
Original file line numberDiff line numberDiff 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
Copy Markdown
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
Copy Markdown
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.

@jozefrebjakjozefrebjakJan 5, 2023

Copy link
Copy Markdown
Contributor

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.

@kenjiskenjisJan 5, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown
MemberAuthor

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')]);
Comment thread
datamweb marked this conversation as resolved.
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Comment thread
datamweb marked this conversation as resolved.

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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'));
Comment thread
datamweb marked this conversation as resolved.
}

return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/Filters/TokenAuth.php
Original file line numberDiff line numberDiff 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
Copy Markdown
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
Copy Markdown
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.

@jozefrebjakjozefrebjakJan 5, 2023

Copy link
Copy Markdown
Contributor

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.

@kenjiskenjisJan 5, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown
MemberAuthor

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')]);
Comment thread
datamweb marked this conversation as resolved.
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Comment thread
datamweb marked this conversation as resolved.

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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'));
Comment thread
datamweb marked this conversation as resolved.
}

return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/Filters/TokenAuth.php
Original file line numberDiff line numberDiff 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
Copy Markdown
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
Copy Markdown
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.

@jozefrebjakjozefrebjakJan 5, 2023

Copy link
Copy Markdown
Contributor

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.

@kenjiskenjisJan 5, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown
MemberAuthor

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')]);
Comment thread
datamweb marked this conversation as resolved.
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Comment thread
datamweb marked this conversation as resolved.

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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'));
Comment thread
datamweb marked this conversation as resolved.
}

return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/Filters/TokenAuth.php
Original file line numberDiff line numberDiff 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
Copy Markdown
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
Copy Markdown
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.

@jozefrebjakjozefrebjakJan 5, 2023

Copy link
Copy Markdown
Contributor

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.

@kenjiskenjisJan 5, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown
MemberAuthor

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')]);
Comment thread
datamweb marked this conversation as resolved.
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Comment thread
datamweb marked this conversation as resolved.

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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'));
Comment thread
datamweb marked this conversation as resolved.
}

return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/Filters/TokenAuth.php
Original file line numberDiff line numberDiff 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
Copy Markdown
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
Copy Markdown
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.

@jozefrebjakjozefrebjakJan 5, 2023

Copy link
Copy Markdown
Contributor

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.

@kenjiskenjisJan 5, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown
MemberAuthor

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')]);
Comment thread
datamweb marked this conversation as resolved.
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Comment thread
datamweb marked this conversation as resolved.

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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'));
Comment thread
datamweb marked this conversation as resolved.
}

return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/Filters/TokenAuth.php
Original file line numberDiff line numberDiff 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
Copy Markdown
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
Copy Markdown
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.

@jozefrebjakjozefrebjakJan 5, 2023

Copy link
Copy Markdown
Contributor

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.

@kenjiskenjisJan 5, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown
MemberAuthor

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')]);
Comment thread
datamweb marked this conversation as resolved.
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Comment thread
datamweb marked this conversation as resolved.

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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