Skip to content

Commit

Permalink
added authentication for api
Browse files Browse the repository at this point in the history
firstly a user has to enable api in settings, then they use basic authentication to obtain a bearer token (valid for 1 hour) with which they can query private/sensitive data
tokens can be invalidated prematurely by api itself or from settings (new page api tokens)
the only private data that can be accessed with tokens at the moment is tokens themselves (reading and deleting)
  • Loading branch information
konecnyjakub committed Apr 10, 2023
1 parent c310e34 commit f546f52
Show file tree
Hide file tree
Showing 25 changed files with 651 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Version 1.0.0-beta9+dev
- added pagination to homepage and articles' categories
- added notifications
- merged search types articles title and articles text
- added authentication for api

Version 1.0.0-beta9
- removed user setting infomails
Expand Down
9 changes: 9 additions & 0 deletions app/Api/ApiNotEnabledException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);

namespace Nexendrie\Api;

class ApiNotEnabledException extends \Nexendrie\Model\AccessDeniedException {

}
?>
4 changes: 4 additions & 0 deletions app/Api/DI/ApiExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public function getConfigSchema(): \Nette\Schema\Schema {
return Expect::structure([
"transformers" => Expect::arrayOf("class")->default([]),
"maxDepth" => Expect::int(2),
"tokenTtl" => Expect::int(60 * 60),
"tokenLength" => Expect::int(20),
]);
}

Expand All @@ -24,6 +26,8 @@ public function loadConfiguration(): void {
$config = $this->getConfig();
$builder->addDefinition($this->prefix("entityConverter"))
->setFactory(\Nexendrie\Api\EntityConverter::class, [$config->maxDepth]);
$builder->addDefinition($this->prefix("tokens"))
->setFactory(\Nexendrie\Api\Tokens::class, [$config->tokenTtl, $config->tokenLength,]);
foreach($config->transformers as $index => $transformer) {
$builder->addDefinition($this->prefix("transformer.$index"))
->setType($transformer);
Expand Down
9 changes: 9 additions & 0 deletions app/Api/TokenExpiredException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);

namespace Nexendrie\Api;

class TokenExpiredException extends \RuntimeException {

}
?>
9 changes: 9 additions & 0 deletions app/Api/TokenNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);

namespace Nexendrie\Api;

class TokenNotFoundException extends \Nexendrie\Model\RecordNotFoundException {

}
?>
73 changes: 73 additions & 0 deletions app/Api/Tokens.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);

namespace Nexendrie\Api;

use Nette\Security\User;
use Nette\Utils\Random;
use Nexendrie\Model\AuthenticationNeededException;
use Nexendrie\Orm\ApiToken;
use Nexendrie\Orm\Model as ORM;

/**
* @property-read int $length
*/
final class Tokens {
use \Nette\SmartObject;

private int $ttl;
private int $length;
private ORM $orm;
private User $user;

public function __construct(int $ttl, int $length, ORM $orm, User $user) {
$this->ttl = $ttl;
$this->length = $length;
$this->orm = $orm;
$this->user = $user;
}

public function create(): ApiToken {
if(!$this->user->isLoggedIn()) {
throw new AuthenticationNeededException();
}

/** @var \Nexendrie\Orm\User $user */
$user = $this->orm->users->getById($this->user->id);
if(!$user->api) {
throw new ApiNotEnabledException();
}

$token = new ApiToken();
$token->user = $user;
$token->token = Random::generate(max(1, $this->length));
$token->expire = time() + $this->ttl;
$this->orm->apiTokens->persistAndFlush($token);

return $token;
}

public function invalidate(string $token): void {
if(!$this->user->isLoggedIn()) {
throw new AuthenticationNeededException();
}

$tokenEntity = $this->orm->apiTokens->getByToken($token);
if($tokenEntity === null || $tokenEntity->user->id !== $this->user->id) {
throw new TokenNotFoundException();
}

$dt = new \DateTime();
if($tokenEntity->expire <= $dt->getTimestamp()) {
throw new TokenExpiredException();
}

$tokenEntity->expire = $dt->getTimestamp();
$this->orm->apiTokens->persistAndFlush($tokenEntity);
}

public function getLength(): int {
return $this->length;
}
}
?>
18 changes: 18 additions & 0 deletions app/Api/Transformers/ApiTokenTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

namespace Nexendrie\Api\Transformers;

class ApiTokenTransformer extends BaseTransformer {
protected array $fields = ["id", "token", "expireAt", "createdAt", "user",];
protected array $fieldsRename = ["createdAt" => "created", "expireAt" => "expire",];

public function getEntityClassName(): string {
return \Nexendrie\Orm\ApiToken::class;
}

public function getCollectionName(): string {
return "tokens";
}
}
?>
12 changes: 10 additions & 2 deletions app/Forms/UserSettingsFormFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

namespace Nexendrie\Forms;

use Nette\Application\LinkGenerator;
use Nette\Application\UI\Form;
use Nette\Utils\Html;
use Nexendrie\Model\ThemesManager;
use Nexendrie\Model\UserManager;
use Nette\Security\User;
Expand All @@ -19,13 +21,15 @@ final class UserSettingsFormFactory {
protected UserManager $model;
protected User $user;
protected ThemesManager $themesManager;
protected LinkGenerator $linkGenerator;

public function __construct(UserManager $model, User $user, ThemesManager $themesManager) {
public function __construct(UserManager $model, User $user, ThemesManager $themesManager, LinkGenerator $linkGenerator) {
$this->model = $model;
$this->user = $user;
$this->themesManager = $themesManager;
$this->linkGenerator = $linkGenerator;
}

public function create(): Form {
$defaultValues = $this->model->getSettings();
$form = new Form();
Expand Down Expand Up @@ -57,6 +61,10 @@ public function create(): Form {
->setHtmlId("notifications_browser")
->setHtmlAttribute("onclick", "notificationsSetup()");
}
$apiTokensLink = $this->linkGenerator->link("Front:User:apiTokens");
$form->addGroup("API")
->setOption("description", Html::el("p")->setHtml("Povolí používání API pro tento účet. Svoje platné tokeny můžeš spravovat <a href=\"$apiTokensLink\">zde</a>."));
$form->addCheckbox("api", "Zapnout API pro tento účet");
$form->setCurrentGroup(null);
$form->addSubmit("save", "Uložit změny");
$form->setDefaults($defaultValues);
Expand Down
2 changes: 1 addition & 1 deletion app/Model/UserManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public function getSettings(): array {
$user = $this->orm->users->getById($this->user->id);
$settings = [
"publicname" => $user->publicname, "email" => $user->email, "style" => $user->style, "gender" => $user->gender,
"notifications" => $user->notifications,
"notifications" => $user->notifications, "api" => $user->api,
];
return $settings;
}
Expand Down
33 changes: 33 additions & 0 deletions app/Orm/ApiToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);

namespace Nexendrie\Orm;

/**
* Api token
*
* @author Jakub Konečný
* @property int $id {primary}
* @property string $token
* @property User $user {m:1 User::$apiTokens}
* @property int $expire
* @property-read string $expireAt {virtual}
* @property int $created
* @property-read string $createdAt {virtual}
*/
final class ApiToken extends BaseEntity {
protected \Nexendrie\Model\Locale $localeModel;

public function injectLocaleModel(\Nexendrie\Model\Locale $localeModel): void {
$this->localeModel = $localeModel;
}

protected function getterExpireAt(): string {
return $this->localeModel->formatDateTime($this->expire);
}

protected function getterCreatedAt(): string {
return $this->localeModel->formatDateTime($this->created);
}
}
?>
12 changes: 12 additions & 0 deletions app/Orm/ApiTokensMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);

namespace Nexendrie\Orm;

/**
* @author Jakub Konečný
*/
final class ApiTokensMapper extends \Nextras\Orm\Mapper\Mapper {

}
?>
40 changes: 40 additions & 0 deletions app/Orm/ApiTokensRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);

namespace Nexendrie\Orm;

use Nextras\Orm\Collection\ICollection;

/**
* @author Jakub Konečný
* @method ApiToken|null getById(int $id)
* @method ApiToken|null getBy(array $conds)
* @method ICollection|ApiToken[] findBy(array $conds)
* @method ICollection|ApiToken[] findAll()
*/
final class ApiTokensRepository extends \Nextras\Orm\Repository\Repository {
public static function getEntityClassNames(): array {
return [ApiToken::class];
}

/**
* @param User|int $user
* @return ICollection|ApiToken[]
*/
public function findByUser($user): ICollection {
return $this->findBy(["user" => $user]);
}

public function getByToken(string $token): ?ApiToken {
return $this->getBy(["token" => $token]);
}

/**
* @param User|int $user
* @return ICollection|ApiToken[]
*/
public function findActiveForUser($user): ICollection {
return $this->findBy(["user" => $user, "expire>=" => time(),]);
}
}
?>
1 change: 1 addition & 0 deletions app/Orm/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
* @property-read ChatMessagesRepository $chatMessages
* @property-read ContentReportsRepository $contentReports
* @property-read NotificationsRepository $notifications
* @property-read ApiTokensRepository $apiTokens
*/
final class Model extends \Nextras\Orm\Model\Model {

Expand Down
4 changes: 3 additions & 1 deletion app/Orm/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
* @property Order|null $order {m:1 Order::$members} {default null}
* @property OrderRank|null $orderRank {m:1 OrderRank::$people} {default null}
* @property bool $notifications {default false}
* @property bool $api {default false}
* @property-read float $adventureBonusIncome {virtual}
* @property OneHasMany|Comment[] $comments {1:m Comment::$author}
* @property OneHasMany|Article[] $articles {1:m Article::$author}
Expand Down Expand Up @@ -68,7 +69,8 @@
* @property OneHasMany|GuildFee[] $guildFees {1:m GuildFee::$user}
* @property OneHasMany|OrderFee[] $orderFees {1:m OrderFee::$user}
* @property OneHasMany|ChatMessage[] $chatMessages {1:m ChatMessage::$user}
* @property OneHasMany|Notification $notificationQueue {1:m Notification::$user}
* @property OneHasMany|Notification[] $notificationQueue {1:m Notification::$user}
* @property OneHasMany|ApiToken[] $apiTokens {1:m ApiToken::$user}
* @property-read string $title {virtual}
* @property-read int $completedAdventures {virtual}
* @property-read int $completedJobs {virtual}
Expand Down

0 comments on commit f546f52

Please sign in to comment.