Skip to content

Commit

Permalink
add IP allowlist, fix empty credentials table
Browse files Browse the repository at this point in the history
  • Loading branch information
kringkaste committed Jun 2, 2021
1 parent 0275031 commit 4275ee3
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 26 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# Craft Basic Authentication Changelog

## 1.1.0 - 2021-06-02

### Added
- Add an allowlist of IP addresses or subnets (CIDR).

### Fixed
- Fixed an error when storing an empty credentials table.

## 1.0.1 - 2020-02-04

### Fixed
- A newly entered password was not hashed in some cases.

## 1.0.0 - 2020-02-02

### Added
- Initial release
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ I you are using Apache to host your CraftCMS, you have to tell Apache to pass th
CGIPassAuth on
```

## Credentials
## Settings

On the settings page in the control panel you can add credentials to use for authentication.

![Screenshot](resources/credentials.png)
![Screenshot](resources/settings.png)

You can add a list of IP addresses and subnets (v4 and v6) that have access without any credentials. Use the [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation) for subnets.

These settings supports the project config if enabled.

Expand Down Expand Up @@ -91,13 +93,21 @@ Only activates the BasicAuth if a certain condition is met.
{% basicauth require valid if craft.app.request.isMobileBrowser() %}
```

### Globals
## Globals

This plugin provides als two global variables with the credentials of the user:
This plugin provides two global variables with the credentials of the user:

```twig
<p>Hello {{ basicAuthUsername }}!</p>
<p>Your password is: {{ basicAuthPassword }}</p>
```

## Why should I use this plugin and not the webserver module?

May you ask yourself why you should use this plugin and not the Basic Authentication provided by the webserver? Here are some aspects:

1. You have full control of the Basic Authentication without your DevOps friends.
2. You can add the `{% basicauth %}` wherever you need it: In your central layout for all pages or only in one special template with some fancy conditions.
3. You can use conditions from Craft. For example: `{% basicauth require valid if not currentUser %}`

With ❤ by [codemonauts](https://codemonauts.com)
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "codemonauts/craft-basicauth",
"description": "Craft CMS plugin for HTTP Basic Authentication within templates.",
"version": "1.0.1",
"version": "1.1.0",
"type": "craft-plugin",
"keywords": [
"craft",
Expand All @@ -23,6 +23,7 @@
},
"require": {
"craftcms/cms": "^3.0.0",
"symfony/http-foundation": "^5.3.1",
"ext-json": "*"
},
"autoload": {
Expand Down
Binary file modified resources/basicauth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed resources/credentials.png
Binary file not shown.
Binary file added resources/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 29 additions & 20 deletions src/BasicAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,33 @@ public function init()
Event::on(Plugin::class, Plugin::EVENT_BEFORE_SAVE_SETTINGS, function(ModelEvent $event) {
$settings = $this->getSettings();

// Set new entered passwords
foreach ($settings->newPasswords as $key => $newPassword) {
if (trim($newPassword) != '') {
$settings->credentials[$key][1] = Craft::$app->security->hashPassword($newPassword);
}
}
if (is_array($settings->credentials) && !empty($settings->credentials)) {

// Set passwords for new rows
foreach ($settings->credentials as $key => $cred) {
if (preg_match('/^\$2.\$/i', $cred[1]) !== 1) {
$settings->credentials[$key][1] = Craft::$app->security->hashPassword($cred[1]);
// Set new entered passwords
foreach ($settings->newPasswords as $key => $newPassword) {
if (trim($newPassword) != '') {
$settings->credentials[$key][1] = Craft::$app->security->hashPassword($newPassword);
}
}
}

// Set passwords for new rows
foreach ($settings->credentials as $key => $cred) {
if (preg_match('/^\$2.\$/i', $cred[1]) !== 1) {
$settings->credentials[$key][1] = Craft::$app->security->hashPassword($cred[1]);
}
}

$settings->newPasswords = [];
$settings->newPasswords = [];

$this->getSettings()->setAttributes($settings->toArray());
$this->getSettings()->setAttributes($settings->toArray());

// Check values
if ($this->getSettings()->credentials) {
foreach ($this->getSettings()->credentials as $cred) {
if ($cred[0] == '' || $cred[1] == '') {
$event->isValid = false;
return;
// Check values
if ($this->getSettings()->credentials) {
foreach ($this->getSettings()->credentials as $cred) {
if ($cred[0] == '' || $cred[1] == '') {
$event->isValid = false;
return;
}
}
}
}
Expand Down Expand Up @@ -112,7 +114,7 @@ protected function settingsHtml()
return Craft::$app->getView()->renderTemplate('basicauth/settings', [
'settings' => $this->getSettings(),
'creds' => $creds,
'cols' => [
'credentialsCols' => [
[
'heading' => 'Username*',
'type' => 'singleline',
Expand All @@ -128,6 +130,13 @@ protected function settingsHtml()
'type' => 'singleline',
]
],
'allowlistCols' => [
[
'heading' => 'IP address or subnet*',
'info' => 'IPv4 or IPv6 address or subnet in CIDR notation',
'type' => 'singleline',
],
],
]
);
}
Expand Down
23 changes: 23 additions & 0 deletions src/models/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace codemonauts\basicauth\models;

use craft\base\Model;
use yii\validators\IpValidator;

class Settings extends Model
{
Expand All @@ -11,6 +12,11 @@ class Settings extends Model
*/
public $credentials = [];

/**
* @var array The allowlist of IP addresses or ranges that overwrites credentials.
*/
public $allowlist = [];

/**
* @var array The new passwords set by user
*/
Expand All @@ -23,6 +29,23 @@ public function rules()
{
return [
[['credentials', 'newPasswords'], 'safe'],
['allowlist', 'validateIps']

];
}

public function validateIps($attribute)
{
$values = $this->$attribute;

if (is_array($values)) {
$ipValidator = new IpValidator(['subnet' => null]);
foreach ($values as $ip) {
$error = [];
if (!$ipValidator->validate($ip[0], $error)) {
$this->addError($attribute, $ip[0] . ': ' . $error);
}
}
}
}
}
11 changes: 11 additions & 0 deletions src/services/AuthService.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Craft;
use craft\base\Component;
use craft\helpers\StringHelper;
use Symfony\Component\HttpFoundation\IpUtils;

class AuthService extends Component
{
Expand All @@ -28,6 +29,16 @@ public function check($type, $entity = null, $siteHandle = null, $env = null, $r

if ($matchedSite && $matchedEnv) {

// Check if IP address matches allowlist
$allowlist = BasicAuth::getInstance()->getSettings()->allowlist;
if (!empty($allowlist)) {
$allowlist = array_merge([], ...$allowlist);
$ip = Craft::$app->request->getRemoteIP();
if (IpUtils::checkIp($ip, $allowlist)) {
return;
}
}

$isAuthenticated = false;

$hasCredentials = (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']));
Expand Down
14 changes: 13 additions & 1 deletion src/templates/settings.twig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
label: "Credentials",
id: 'credentials',
name: 'credentials',
cols: cols,
cols: credentialsCols,
rows: creds
}) }}

{{ forms.editableTableField({
first: true,
label: "IP Allowlist",
instructions: 'IP addresses and subnets that have access without any credentials.',
id: 'allowlist',
name: 'allowlist',
cols: allowlistCols,
rows: settings.allowlist,
errors: settings.getErrors('allowlist'),
tip: "Your current IP accessing the CP is: " ~ craft.app.request.getRemoteIp()
}) }}

0 comments on commit 4275ee3

Please sign in to comment.