new [Authkey] implementation ready
- users can have multiple keys - keys are hashed with bcrypt - each key can have its own expiration - each key can have a contextual comment - authentication via API requests happens with the Authorization headerpull/32/head
parent
4a3daabf11
commit
b027007618
|
@ -114,13 +114,7 @@ class AppController extends Controller
|
|||
{
|
||||
if (!empty($_SERVER['HTTP_AUTHORIZATION']) && strlen($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
$this->loadModel('AuthKeys');
|
||||
$authKey = $this->AuthKeys->find()->where([
|
||||
'authkey' => $_SERVER['HTTP_AUTHORIZATION'],
|
||||
'OR' => [
|
||||
'valid_until' => 0,
|
||||
'valid_until >' => time()
|
||||
]
|
||||
])->first();
|
||||
$authKey = $this->AuthKeys->checkKey($_SERVER['HTTP_AUTHORIZATION']);
|
||||
if (!empty($authKey)) {
|
||||
$this->loadModel('Users');
|
||||
$user = $this->Users->get($authKey['user_id']);
|
||||
|
|
|
@ -19,12 +19,13 @@ class AuthKeysController extends AppController
|
|||
$this->CRUD->index([
|
||||
'filters' => ['users.username', 'authkey', 'comment', 'users.id'],
|
||||
'quickFilters' => ['authkey', 'comment'],
|
||||
'contain' => ['Users']
|
||||
'contain' => ['Users'],
|
||||
'exclude_fields' => ['authkey']
|
||||
]);
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->restResponsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'ContactDB');
|
||||
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
|
@ -33,12 +34,15 @@ class AuthKeysController extends AppController
|
|||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->restResponsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'ContactDB');
|
||||
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$this->CRUD->add();
|
||||
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
|
||||
$this->CRUD->add([
|
||||
'displayOnSuccess' => 'authkey_display'
|
||||
]);
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->restResponsePayload;
|
||||
}
|
||||
|
@ -49,6 +53,5 @@ class AuthKeysController extends AppController
|
|||
])
|
||||
];
|
||||
$this->set(compact('dropdownData'));
|
||||
$this->set('metaGroup', 'ContactDB');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Controller\Component;
|
|||
|
||||
use Cake\Controller\Component;
|
||||
use Cake\Error\Debugger;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Inflector;
|
||||
|
||||
class CRUDComponent extends Component
|
||||
|
@ -62,10 +63,15 @@ class CRUDComponent extends Component
|
|||
if ($this->Table->save($data)) {
|
||||
$message = __('{0} added.', $this->ObjectAlias);
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$data = $this->Table->get($id);
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
} else {
|
||||
$this->Controller->Flash->success($message);
|
||||
if (!empty($params['displayOnSuccess'])) {
|
||||
$this->Controller->set('entity', $data);
|
||||
$this->Controller->set('referer', $this->Controller->referer());
|
||||
$this->Controller->render($params['displayOnSuccess']);
|
||||
return;
|
||||
}
|
||||
$this->Controller->redirect(['action' => 'index']);
|
||||
}
|
||||
} else {
|
||||
|
@ -97,7 +103,6 @@ class CRUDComponent extends Component
|
|||
if ($this->Table->save($data)) {
|
||||
$message = __('{0} updated.', $this->ObjectAlias);
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$data = $this->Table->get($id);
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
} else {
|
||||
$this->Controller->Flash->success($message);
|
||||
|
|
|
@ -37,7 +37,7 @@ class EncryptionKeysController extends AppController
|
|||
|
||||
public function add()
|
||||
{
|
||||
$this->CRUD->add();
|
||||
$this->CRUD->add(['displayOnSuccess' => 'add_success']);
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->restResponsePayload;
|
||||
}
|
||||
|
|
|
@ -8,4 +8,5 @@ use Cake\ORM\Entity;
|
|||
class AuthKey extends AppModel
|
||||
{
|
||||
|
||||
protected $_hidden = ['authkey'];
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use Authentication\PasswordHasher\DefaultPasswordHasher;
|
|||
|
||||
class User extends AppModel
|
||||
{
|
||||
protected $_hidden = ['password'];
|
||||
protected function _setPassword(string $password) : ?string
|
||||
{
|
||||
if (strlen($password) > 0) {
|
||||
|
|
|
@ -5,8 +5,12 @@ namespace App\Model\Table;
|
|||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Datasource\EntityInterface;
|
||||
use Cake\Event\Event;
|
||||
use Cake\Event\EventInterface;
|
||||
use Cake\Auth\DefaultPasswordHasher;
|
||||
use Cake\Utility\Security;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use ArrayObject;
|
||||
|
||||
class AuthKeysTable extends AppTable
|
||||
|
@ -24,11 +28,21 @@ class AuthKeysTable extends AppTable
|
|||
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
|
||||
{
|
||||
$data['created'] = time();
|
||||
if (empty($data['valid_until'])) {
|
||||
$data['valid_until'] = 0;
|
||||
if (empty($data['expiration'])) {
|
||||
$data['expiration'] = 0;
|
||||
} else {
|
||||
$data['expiration'] = strtotime($data['expiration']);
|
||||
}
|
||||
if (empty($data['authkey'])) {
|
||||
$data['authkey'] = $this->generateAuthKey();
|
||||
}
|
||||
|
||||
public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
|
||||
{
|
||||
if (empty($entity->authkey)) {
|
||||
$authkey = $this->generateAuthKey();
|
||||
$entity->authkey_start = substr($authkey, 0, 4);
|
||||
$entity->authkey_end = substr($authkey, -4);
|
||||
$entity->authkey = (new DefaultPasswordHasher())->hash($authkey);
|
||||
$entity->authkey_raw = $authkey;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,9 +54,33 @@ class AuthKeysTable extends AppTable
|
|||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
$validator
|
||||
->notEmptyString('authkey')
|
||||
->notEmptyString('user_id')
|
||||
->requirePresence(['authkey', 'user_id'], 'create');
|
||||
->requirePresence(['user_id'], 'create');
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function checkKey($authkey)
|
||||
{
|
||||
if (strlen($authkey) != 40) {
|
||||
return [];
|
||||
}
|
||||
$start = substr($authkey, 0, 4);
|
||||
$end = substr($authkey, -4);
|
||||
$candidates = $this->find()->where([
|
||||
'authkey_start' => $start,
|
||||
'authkey_end' => $end,
|
||||
'OR' => [
|
||||
'expiration' => 0,
|
||||
'expiration >' => time()
|
||||
]
|
||||
]);
|
||||
if (!empty($candidates)) {
|
||||
foreach ($candidates as $candidate) {
|
||||
if ((new DefaultPasswordHasher())->check($authkey, $candidate['authkey'])) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ echo $this->element('genericElements/Form/genericForm', array(
|
|||
'field' => 'comment'
|
||||
),
|
||||
array(
|
||||
'field' => 'valid_until',
|
||||
'label' => 'Validity'
|
||||
'field' => 'expiration',
|
||||
'label' => 'Expiration'
|
||||
)
|
||||
),
|
||||
'submit' => array(
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<h4><?= __('Authkey created'); ?></h4>
|
||||
<p><?= __('Please make sure that you note down the authkey below, this is the only time the authkey is shown in plain text, so make sure you save it. If you lose the key, simply remove the entry and generate a new one.'); ?></p>
|
||||
<p><?=__('Cerebrate will use the first and the last 4 digit for identification purposes.')?></p>
|
||||
<p><?= sprintf('%s: <span class="text-weight-bold">%s</span>', __('Authkey'), h($entity->authkey_raw)) ?></p>
|
||||
<a href="<?= $referer ?>" class="btn btn-primary"><?= __('I have noted down my key, take me back now') ?></a>
|
|
@ -39,16 +39,33 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
[
|
||||
'name' => __('Auth key'),
|
||||
'sort' => 'authkey',
|
||||
'data_path' => 'authkey',
|
||||
'element' => 'authkey',
|
||||
'privacy' => 1
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => __('Expiration'),
|
||||
'sort' => 'expiration',
|
||||
'data_path' => 'expiration',
|
||||
'element' => 'expiration'
|
||||
],
|
||||
[
|
||||
'name' => __('Disabled'),
|
||||
'sort' => 'disabled',
|
||||
'data_path' => 'disabled',
|
||||
'element' => 'boolean'
|
||||
],
|
||||
[
|
||||
'name' => __('Comment'),
|
||||
'sort' => 'comment',
|
||||
'data_path' => 'comment',
|
||||
],
|
||||
],
|
||||
'title' => __('Authentication key Index'),
|
||||
'description' => __('A list of API keys bound to a user.'),
|
||||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'onclick' => 'populateAndLoadModal(\'/encryptionKeys/delete/[onclick_params_data_path]\');',
|
||||
'onclick' => 'populateAndLoadModal(\'/authKeys/delete/[onclick_params_data_path]\');',
|
||||
'onclick_params_data_path' => 'id',
|
||||
'icon' => 'trash'
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
echo sprintf(
|
||||
'%s',
|
||||
sprintf(
|
||||
'<button id="submitButton" class="btn btn-primary" onClick="%s">%s</button>',
|
||||
'<button id="submitButton" class="btn btn-primary" onClick="%s" autofocus>%s</button>',
|
||||
"$('#form-" . h($formRandomValue) . "').submit()",
|
||||
__('Submit')
|
||||
)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
$start = $this->Hash->extract($row, 'authkey_start')[0];
|
||||
$end = $this->Hash->extract($row, 'authkey_end')[0];
|
||||
echo sprintf(
|
||||
'<div>%s: <span class="font-weight-bold text-info">%s</span></div>',
|
||||
__('Starts with'),
|
||||
h($start)
|
||||
);
|
||||
echo sprintf(
|
||||
'<div>%s: <span class="font-weight-bold text-info">%s</span></div>',
|
||||
__('Ends with'),
|
||||
h($end)
|
||||
);
|
||||
?>
|
|
@ -13,7 +13,11 @@
|
|||
}
|
||||
$data = h($data);
|
||||
if (is_numeric($data)) {
|
||||
$data = date('Y-m-d H:i:s', $data);
|
||||
if ($data == 0) {
|
||||
__('N/A');
|
||||
} else {
|
||||
$data = date('Y-m-d H:i:s', $data);
|
||||
}
|
||||
}
|
||||
if (!empty($field['onClick'])) {
|
||||
$data = sprintf(
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
$data = $this->Hash->extract($row, $field['data_path']);
|
||||
if (is_array($data)) {
|
||||
if (count($data) > 1) {
|
||||
$data = implode(', ', $data);
|
||||
} else {
|
||||
if (count($data) > 0) {
|
||||
$data = $data[0];
|
||||
} else {
|
||||
$data = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
$data = h($data);
|
||||
if (is_numeric($data)) {
|
||||
if ($data == 0) {
|
||||
$data = '<span class="text-primary font-weight-bold">' . __('Indefinite') . '</span>';
|
||||
} else {
|
||||
if ($data <= time()) {
|
||||
$data = '<span class="text-danger font-weight-bold">' . __('Expired') . '</span>';
|
||||
} else {
|
||||
$data = '<span class="text-success font-weight-bold">' . date('Y-m-d H:i:s', $data) . '</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($field['onClick'])) {
|
||||
$data = sprintf(
|
||||
'<span onClick="%s">%s</span>',
|
||||
$field['onClick'],
|
||||
$data
|
||||
);
|
||||
}
|
||||
echo $data;
|
||||
?>
|
Loading…
Reference in New Issue