Get basic storage of webauthn tokens working
This commit is contained in:
parent
eaf12aec60
commit
1053b5d605
19 changed files with 531 additions and 211 deletions
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Account\RegisterWebauthnTokenRequest;
|
||||
use Pterodactyl\Services\Users\HardwareSecurityKeys\CreatePublicKeyCredentialsService;
|
||||
|
||||
class HardwareTokenController extends ClientApiController
|
||||
{
|
||||
private CreatePublicKeyCredentialsService $createPublicKeyCredentials;
|
||||
|
||||
private Repository $cache;
|
||||
|
||||
public function __construct(
|
||||
Repository $cache,
|
||||
CreatePublicKeyCredentialsService $createPublicKeyCredentials
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->cache = $cache;
|
||||
$this->createPublicKeyCredentials = $createPublicKeyCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the hardware security keys (WebAuthn) that exists for a user.
|
||||
*/
|
||||
public function index(Request $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data necessary for creating a new hardware security key for the
|
||||
* user.
|
||||
*/
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
$tokenId = Str::random(64);
|
||||
$credentials = $this->createPublicKeyCredentials->handle($request->user());
|
||||
|
||||
$this->cache->put("webauthn:$tokenId", [
|
||||
'credentials' => $credentials->jsonSerialize(),
|
||||
'user_entity' => $credentials->getUser()->jsonSerialize(),
|
||||
], CarbonImmutable::now()->addMinutes(10));
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
'token_id' => $tokenId,
|
||||
'credentials' => $credentials->jsonSerialize(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a new key for a user account.
|
||||
*/
|
||||
public function store(RegisterWebauthnTokenRequest $request): JsonResponse
|
||||
{
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a WebAuthn key from a user's account.
|
||||
*/
|
||||
public function delete(Request $request, int $webauthnKeyId): JsonResponse
|
||||
{
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
}
|
131
app/Http/Controllers/Api/Client/SecurityKeyController.php
Normal file
131
app/Http/Controllers/Api/Client/SecurityKeyController.php
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client;
|
||||
|
||||
use Exception;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
|
||||
use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Account\RegisterWebauthnTokenRequest;
|
||||
use Pterodactyl\Repositories\SecurityKeys\PublicKeyCredentialSourceRepository;
|
||||
use Pterodactyl\Services\Users\SecurityKeys\CreatePublicKeyCredentialsService;
|
||||
|
||||
class SecurityKeyController extends ClientApiController
|
||||
{
|
||||
protected CreatePublicKeyCredentialsService $createPublicKeyCredentials;
|
||||
|
||||
protected Repository $cache;
|
||||
|
||||
protected WebauthnServerRepository $webauthnServerRepository;
|
||||
|
||||
public function __construct(
|
||||
Repository $cache,
|
||||
WebauthnServerRepository $webauthnServerRepository,
|
||||
CreatePublicKeyCredentialsService $createPublicKeyCredentials
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->cache = $cache;
|
||||
$this->webauthnServerRepository = $webauthnServerRepository;
|
||||
$this->createPublicKeyCredentials = $createPublicKeyCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the hardware security keys (WebAuthn) that exists for a user.
|
||||
*/
|
||||
public function index(Request $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data necessary for creating a new hardware security key for the
|
||||
* user.
|
||||
*/
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
$tokenId = Str::random(64);
|
||||
$credentials = $this->createPublicKeyCredentials->handle($request->user(), $request->get('display_name'));
|
||||
|
||||
$this->cache->put(
|
||||
"register-security-key:$tokenId",
|
||||
serialize($credentials),
|
||||
CarbonImmutable::now()->addMinutes(10)
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
'token_id' => $tokenId,
|
||||
'credentials' => $credentials->jsonSerialize(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a new key for a user account.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
*/
|
||||
public function store(RegisterWebauthnTokenRequest $request): JsonResponse
|
||||
{
|
||||
$stored = $this->cache->pull("register-security-key:{$request->input('token_id')}");
|
||||
|
||||
if (!$stored) {
|
||||
throw new DisplayException('Could not register security key: no data present in session, please try your request again.');
|
||||
}
|
||||
|
||||
$credentials = unserialize($stored);
|
||||
if (!$credentials instanceof PublicKeyCredentialCreationOptions) {
|
||||
throw new Exception(sprintf('Unexpected security key data pulled from cache: expected "%s" but got "%s".', PublicKeyCredentialCreationOptions::class, get_class($credentials)));
|
||||
}
|
||||
|
||||
$server = $this->webauthnServerRepository->getServer($request->user());
|
||||
|
||||
$source = $server->loadAndCheckAttestationResponse(
|
||||
json_encode($request->input('registration')),
|
||||
$credentials,
|
||||
$this->getServerRequest($request),
|
||||
);
|
||||
|
||||
// Unfortunately this repository interface doesn't define a response — it is explicitly
|
||||
// void — so we need to just query the database immediately after this to pull the information
|
||||
// we just stored to return to the caller.
|
||||
PublicKeyCredentialSourceRepository::factory($request->user())->saveCredentialSource($source);
|
||||
|
||||
$created = $request->user()->securityKeys()
|
||||
->where('public_key_id', base64_encode($source->getPublicKeyCredentialId()))
|
||||
->first();
|
||||
|
||||
$created->update(['name' => $request->input('name')]);
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $created->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a WebAuthn key from a user's account.
|
||||
*/
|
||||
public function delete(Request $request, int $webauthnKeyId): JsonResponse
|
||||
{
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
|
||||
protected function getServerRequest(Request $request): ServerRequestInterface
|
||||
{
|
||||
$factory = new Psr17Factory();
|
||||
|
||||
$httpFactory = new PsrHttpFactory($factory, $factory, $factory, $factory);
|
||||
|
||||
return $httpFactory->createRequest($request);
|
||||
}
|
||||
}
|
|
@ -10,8 +10,12 @@ class RegisterWebauthnTokenRequest extends AccountApiRequest
|
|||
{
|
||||
return [
|
||||
'name' => ['string', 'required'],
|
||||
'register' => ['string', 'required'],
|
||||
'public_key' => ['string', 'required'],
|
||||
'token_id' => ['required', 'string'],
|
||||
'registration' => ['required', 'array'],
|
||||
'registration.id' => ['required', 'string'],
|
||||
'registration.type' => ['required', 'in:public-key'],
|
||||
'registration.response.attestationObject' => ['required', 'string'],
|
||||
'registration.response.clientDataJSON' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,19 +6,24 @@ use Webauthn\PublicKeyCredentialSource;
|
|||
use Webauthn\PublicKeyCredentialDescriptor;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class HardwareSecurityKey extends Model
|
||||
class SecurityKey extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const RESOURCE_NAME = 'hardware_security_key';
|
||||
public const RESOURCE_NAME = 'security_key';
|
||||
|
||||
protected $attributes = [
|
||||
protected $casts = [
|
||||
'user_id' => 'int',
|
||||
'transports' => 'array',
|
||||
'trust_path' => 'array',
|
||||
'other_ui' => 'array',
|
||||
];
|
||||
|
||||
protected $guarded = [
|
||||
'uuid',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
|
@ -210,9 +210,9 @@ class User extends Model implements
|
|||
return $this->hasMany(RecoveryToken::class);
|
||||
}
|
||||
|
||||
public function hardwareSecurityKeys(): HasMany
|
||||
public function securityKeys(): HasMany
|
||||
{
|
||||
return $this->hasMany(HardwareSecurityKey::class);
|
||||
return $this->hasMany(SecurityKey::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Webauthn;
|
||||
namespace Pterodactyl\Repositories\SecurityKeys;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Container\Container;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Pterodactyl\Models\HardwareSecurityKey;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyRepositoryInterface;
|
||||
|
||||
class PublicKeyCredentialSourceRepository implements PublicKeyRepositoryInterface
|
||||
|
@ -23,8 +24,8 @@ class PublicKeyCredentialSourceRepository implements PublicKeyRepositoryInterfac
|
|||
*/
|
||||
public function findOneByCredentialId(string $id): ?PublicKeyCredentialSource
|
||||
{
|
||||
/** @var \Pterodactyl\Models\HardwareSecurityKey $key */
|
||||
$key = $this->user->hardwareSecurityKeys()
|
||||
/** @var \Pterodactyl\Models\SecurityKey $key */
|
||||
$key = $this->user->securityKeys()
|
||||
->where('public_key_id', $id)
|
||||
->first();
|
||||
|
||||
|
@ -37,21 +38,36 @@ class PublicKeyCredentialSourceRepository implements PublicKeyRepositoryInterfac
|
|||
*/
|
||||
public function findAllForUserEntity(PublicKeyCredentialUserEntity $entity): array
|
||||
{
|
||||
$results = $this->user->hardwareSecurityKeys()
|
||||
$results = $this->user->securityKeys()
|
||||
->where('user_handle', $entity->getId())
|
||||
->get();
|
||||
|
||||
return $results->map(function (HardwareSecurityKey $key) {
|
||||
return $results->map(function (SecurityKey $key) {
|
||||
return $key->toCredentialSource();
|
||||
})->values()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a credential to the database and link it with the user.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function saveCredentialSource(PublicKeyCredentialSource $source): void
|
||||
{
|
||||
// todo: implement
|
||||
$this->user->securityKeys()->forceCreate([
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
'user_id' => $this->user->id,
|
||||
'public_key_id' => base64_encode($source->getPublicKeyCredentialId()),
|
||||
'public_key' => base64_encode($source->getCredentialPublicKey()),
|
||||
'aaguid' => $source->getAaguid()->toString(),
|
||||
'type' => $source->getType(),
|
||||
'transports' => $source->getTransports(),
|
||||
'attestation_type' => $source->getAttestationType(),
|
||||
'trust_path' => $source->getTrustPath()->jsonSerialize(),
|
||||
'user_handle' => $source->getUserHandle(),
|
||||
'counter' => $source->getCounter(),
|
||||
'other_ui' => $source->getOtherUI(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
28
app/Repositories/SecurityKeys/WebauthnServerRepository.php
Normal file
28
app/Repositories/SecurityKeys/WebauthnServerRepository.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\SecurityKeys;
|
||||
|
||||
use Webauthn\Server;
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Container\Container;
|
||||
use Webauthn\PublicKeyCredentialRpEntity;
|
||||
|
||||
final class WebauthnServerRepository
|
||||
{
|
||||
private PublicKeyCredentialRpEntity $rpEntity;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$url = str_replace(['http://', 'https://'], '', config('app.url'));
|
||||
|
||||
$this->rpEntity = new PublicKeyCredentialRpEntity(config('app.name'), trim($url, '/'));
|
||||
}
|
||||
|
||||
public function getServer(User $user)
|
||||
{
|
||||
return new Server(
|
||||
$this->rpEntity,
|
||||
PublicKeyCredentialSourceRepository::factory($user)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Users\HardwareSecurityKeys;
|
||||
|
||||
use Webauthn\Server;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Pterodactyl\Models\User;
|
||||
use Webauthn\PublicKeyCredentialRpEntity;
|
||||
use Pterodactyl\Models\HardwareSecurityKey;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Pterodactyl\Repositories\Webauthn\PublicKeyCredentialSourceRepository;
|
||||
|
||||
class CreatePublicKeyCredentialsService
|
||||
{
|
||||
protected PublicKeyCredentialRpEntity $rpEntity;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$url = str_replace(['http://', 'https://'], '', config('app.url'));
|
||||
|
||||
$this->rpEntity = new PublicKeyCredentialRpEntity(config('app.name'), trim($url, '/'));
|
||||
}
|
||||
|
||||
public function handle(User $user): PublicKeyCredentialCreationOptions
|
||||
{
|
||||
$id = Uuid::uuid4()->toString();
|
||||
|
||||
$entity = new PublicKeyCredentialUserEntity($user->uuid, $id, $user->email);
|
||||
|
||||
$excluded = $user->hardwareSecurityKeys->map(function (HardwareSecurityKey $key) {
|
||||
return $key->toCredentialsDescriptor();
|
||||
})->values()->toArray();
|
||||
|
||||
return $this->getServerInstance($user)->generatePublicKeyCredentialCreationOptions(
|
||||
$entity,
|
||||
PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
|
||||
$excluded
|
||||
);
|
||||
}
|
||||
|
||||
protected function getServerInstance(User $user)
|
||||
{
|
||||
return new Server(
|
||||
$this->rpEntity,
|
||||
PublicKeyCredentialSourceRepository::factory($user)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Users\SecurityKeys;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository;
|
||||
|
||||
class CreatePublicKeyCredentialsService
|
||||
{
|
||||
protected WebauthnServerRepository $webauthnServerRepository;
|
||||
|
||||
public function __construct(WebauthnServerRepository $webauthnServerRepository)
|
||||
{
|
||||
$this->webauthnServerRepository = $webauthnServerRepository;
|
||||
}
|
||||
|
||||
public function handle(User $user, ?string $displayName): PublicKeyCredentialCreationOptions
|
||||
{
|
||||
$id = Uuid::uuid4()->toString();
|
||||
|
||||
$entity = new PublicKeyCredentialUserEntity($user->uuid, $id, $name ?? $user->email);
|
||||
|
||||
$excluded = $user->securityKeys->map(function (SecurityKey $key) {
|
||||
return $key->toCredentialsDescriptor();
|
||||
})->values()->toArray();
|
||||
|
||||
$server = $this->webauthnServerRepository->getServer($user);
|
||||
|
||||
return $server->generatePublicKeyCredentialCreationOptions(
|
||||
$entity,
|
||||
PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
|
||||
$excluded
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Transformers\Api\Client;
|
||||
|
||||
use Pterodactyl\Models\HardwareSecurityKey;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Pterodactyl\Transformers\Api\Transformer;
|
||||
|
||||
class WebauthnKeyTransformer extends Transformer
|
||||
|
@ -12,13 +12,13 @@ class WebauthnKeyTransformer extends Transformer
|
|||
*/
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return HardwareSecurityKey::RESOURCE_NAME;
|
||||
return SecurityKey::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return basic information about the currently logged in user.
|
||||
*/
|
||||
public function transform(HardwareSecurityKey $model): array
|
||||
public function transform(SecurityKey $model): array
|
||||
{
|
||||
return [
|
||||
'id' => $model->id,
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"league/flysystem-aws-s3-v3": "^1.0",
|
||||
"league/flysystem-memory": "^1.0",
|
||||
"matriphe/iso-639": "^1.2",
|
||||
"nyholm/psr7": "^1.4",
|
||||
"pragmarx/google2fa": "^8.0",
|
||||
"predis/predis": "^1.1",
|
||||
"prologue/alerts": "^0.4",
|
||||
|
@ -40,6 +41,7 @@
|
|||
"spatie/laravel-fractal": "^5.8",
|
||||
"spatie/laravel-query-builder": "^3.5",
|
||||
"staudenmeir/belongs-to-through": "^2.11",
|
||||
"symfony/psr-http-message-bridge": "^2.1",
|
||||
"symfony/yaml": "^5.3",
|
||||
"web-auth/webauthn-lib": "^3.3",
|
||||
"webmozart/assert": "^1.10"
|
||||
|
|
221
composer.lock
generated
221
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f6da942b0b0b32416bca002dff11f403",
|
||||
"content-hash": "576d3b9784111373d9df4790023538d8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
|
@ -3314,6 +3314,83 @@
|
|||
},
|
||||
"time": "2021-07-21T10:44:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nyholm/psr7",
|
||||
"version": "1.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Nyholm/psr7.git",
|
||||
"reference": "2212385b47153ea71b1c1b1374f8cb5e4f7892ec"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Nyholm/psr7/zipball/2212385b47153ea71b1c1b1374f8cb5e4f7892ec",
|
||||
"reference": "2212385b47153ea71b1c1b1374f8cb5e4f7892ec",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1",
|
||||
"php-http/message-factory": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/http-message": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-factory-implementation": "1.0",
|
||||
"psr/http-message-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"http-interop/http-factory-tests": "^0.9",
|
||||
"php-http/psr7-integration-tests": "^1.0",
|
||||
"phpunit/phpunit": "^7.5 || 8.5 || 9.4",
|
||||
"symfony/error-handler": "^4.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.4-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Nyholm\\Psr7\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Tobias Nyholm",
|
||||
"email": "tobias.nyholm@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Martijn van der Ven",
|
||||
"email": "martijn@vanderven.se"
|
||||
}
|
||||
],
|
||||
"description": "A fast PHP7 implementation of PSR-7",
|
||||
"homepage": "https://tnyholm.se",
|
||||
"keywords": [
|
||||
"psr-17",
|
||||
"psr-7"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Nyholm/psr7/issues",
|
||||
"source": "https://github.com/Nyholm/psr7/tree/1.4.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/Zegnat",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nyholm",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2021-07-02T08:32:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "opis/closure",
|
||||
"version": "3.6.2",
|
||||
|
@ -3446,6 +3523,60 @@
|
|||
},
|
||||
"time": "2020-12-06T15:14:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/message-factory",
|
||||
"version": "v1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/message-factory.git",
|
||||
"reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1",
|
||||
"reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4",
|
||||
"psr/http-message": "^1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Factory interfaces for PSR-7 HTTP Message",
|
||||
"homepage": "http://php-http.org",
|
||||
"keywords": [
|
||||
"factory",
|
||||
"http",
|
||||
"message",
|
||||
"stream",
|
||||
"uri"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/message-factory/issues",
|
||||
"source": "https://github.com/php-http/message-factory/tree/master"
|
||||
},
|
||||
"time": "2015-12-19T14:08:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.7.5",
|
||||
|
@ -6587,6 +6718,94 @@
|
|||
],
|
||||
"time": "2021-07-23T15:54:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/psr-http-message-bridge",
|
||||
"version": "v2.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/psr-http-message-bridge.git",
|
||||
"reference": "c9012994c4b4fb23e7c57dd86b763a417a04feba"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/c9012994c4b4fb23e7c57dd86b763a417a04feba",
|
||||
"reference": "c9012994c4b4fb23e7c57dd86b763a417a04feba",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1",
|
||||
"psr/http-message": "^1.0",
|
||||
"symfony/http-foundation": "^4.4 || ^5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"nyholm/psr7": "^1.1",
|
||||
"psr/log": "^1.1 || ^2 || ^3",
|
||||
"symfony/browser-kit": "^4.4 || ^5.0",
|
||||
"symfony/config": "^4.4 || ^5.0",
|
||||
"symfony/event-dispatcher": "^4.4 || ^5.0",
|
||||
"symfony/framework-bundle": "^4.4 || ^5.0",
|
||||
"symfony/http-kernel": "^4.4 || ^5.0",
|
||||
"symfony/phpunit-bridge": "^4.4.19 || ^5.2"
|
||||
},
|
||||
"suggest": {
|
||||
"nyholm/psr7": "For a super lightweight PSR-7/17 implementation"
|
||||
},
|
||||
"type": "symfony-bridge",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Bridge\\PsrHttpMessage\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "http://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "PSR HTTP message bridge",
|
||||
"homepage": "http://symfony.com",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr-17",
|
||||
"psr-7"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/symfony/psr-http-message-bridge/issues",
|
||||
"source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2021-07-27T17:25:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/routing",
|
||||
"version": "v5.3.4",
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Pterodactyl\Models\HardwareSecurityKey;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class WebauthnKeyFactory extends Factory
|
||||
|
@ -10,7 +10,7 @@ class WebauthnKeyFactory extends Factory
|
|||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*/
|
||||
protected $model = HardwareSecurityKey::class;
|
||||
protected $model = SecurityKey::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
|
|
|
@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateHardwareSecurityKeysTable extends Migration
|
||||
class CreateSecurityKeysTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
@ -13,10 +13,11 @@ class CreateHardwareSecurityKeysTable extends Migration
|
|||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('hardware_security_keys', function (Blueprint $table) {
|
||||
Schema::create('security_keys', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->char('uuid', 36);
|
||||
$table->unsignedInteger('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
$table->char('uuid', 36)->unique();
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->string('name');
|
||||
$table->text('public_key_id');
|
||||
$table->text('public_key');
|
||||
$table->char('aaguid', 36);
|
||||
|
@ -26,8 +27,10 @@ class CreateHardwareSecurityKeysTable extends Migration
|
|||
$table->json('trust_path');
|
||||
$table->text('user_handle');
|
||||
$table->unsignedInteger('counter');
|
||||
$table->json('other_ui');
|
||||
$table->json('other_ui')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -38,6 +41,6 @@ class CreateHardwareSecurityKeysTable extends Migration
|
|||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('hardware_security_keys');
|
||||
Schema::dropIfExists('security_keys');
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ export const rawDataToWebauthnKey = (data: any): WebauthnKey => ({
|
|||
|
||||
export default (): Promise<WebauthnKey[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('/api/client/account/webauthn')
|
||||
http.get('/api/client/account/security-keys')
|
||||
.then(({ data }) => resolve((data.data || []).map((d: any) => rawDataToWebauthnKey(d.attributes))))
|
||||
.catch(reject);
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import http from '@/api/http';
|
||||
import { rawDataToWebauthnKey, WebauthnKey } from '@/api/account/webauthn/getWebauthnKeys';
|
||||
|
||||
export const base64Decode = (input: string): string => {
|
||||
input = input.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
@ -13,14 +12,10 @@ export const base64Decode = (input: string): string => {
|
|||
return input;
|
||||
};
|
||||
|
||||
export const bufferDecode = (value: string): ArrayBuffer => {
|
||||
return Uint8Array.from(window.atob(value), c => c.charCodeAt(0));
|
||||
};
|
||||
export const bufferDecode = (value: string): ArrayBuffer => Uint8Array.from(window.atob(value), c => c.charCodeAt(0));
|
||||
|
||||
export const bufferEncode = (value: ArrayBuffer): string => {
|
||||
// @ts-ignore
|
||||
return window.btoa(String.fromCharCode.apply(null, new Uint8Array(value)));
|
||||
};
|
||||
// @ts-ignore
|
||||
export const bufferEncode = (value: ArrayBuffer): string => window.btoa(String.fromCharCode.apply(null, new Uint8Array(value)));
|
||||
|
||||
export const decodeCredentials = (credentials: PublicKeyCredentialDescriptor[]) => {
|
||||
return credentials.map(c => {
|
||||
|
@ -32,42 +27,44 @@ export const decodeCredentials = (credentials: PublicKeyCredentialDescriptor[])
|
|||
});
|
||||
};
|
||||
|
||||
export default (name: string): Promise<WebauthnKey> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('/api/client/account/webauthn/register').then((res) => {
|
||||
const publicKey = res.data.public_key;
|
||||
const publicKeyCredential = Object.assign({}, publicKey);
|
||||
|
||||
publicKeyCredential.user.id = bufferDecode(publicKey.user.id);
|
||||
publicKeyCredential.challenge = bufferDecode(base64Decode(publicKey.challenge));
|
||||
if (publicKey.excludeCredentials) {
|
||||
publicKeyCredential.excludeCredentials = decodeCredentials(publicKey.excludeCredentials);
|
||||
}
|
||||
|
||||
return navigator.credentials.create({
|
||||
publicKey: publicKeyCredential,
|
||||
});
|
||||
}).then((c) => {
|
||||
if (c === null) {
|
||||
return;
|
||||
}
|
||||
const credential = c as PublicKeyCredential;
|
||||
const response = credential.response as AuthenticatorAttestationResponse;
|
||||
|
||||
http.post('/api/client/account/webauthn/register', {
|
||||
name: name,
|
||||
|
||||
register: JSON.stringify({
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: bufferEncode(credential.rawId),
|
||||
|
||||
response: {
|
||||
attestationObject: bufferEncode(response.attestationObject),
|
||||
clientDataJSON: bufferEncode(response.clientDataJSON),
|
||||
},
|
||||
}),
|
||||
}).then(({ data }) => resolve(rawDataToWebauthnKey(data.attributes))).catch(reject);
|
||||
}).catch(reject);
|
||||
const registerCredentialForAccount = async (name: string, tokenId: string, credential: PublicKeyCredential) => {
|
||||
const { data } = await http.post('/api/client/account/security-keys/register', {
|
||||
name,
|
||||
token_id: tokenId,
|
||||
registration: {
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: bufferEncode(credential.rawId),
|
||||
response: {
|
||||
attestationObject: bufferEncode((credential.response as AuthenticatorAttestationResponse).attestationObject),
|
||||
clientDataJSON: bufferEncode(credential.response.clientDataJSON),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(data.data);
|
||||
};
|
||||
|
||||
export const register = async (name: string): Promise<void> => {
|
||||
const { data } = await http.get('/api/client/account/security-keys/register', {
|
||||
params: {
|
||||
display_name: name,
|
||||
},
|
||||
});
|
||||
|
||||
const publicKey = data.data.credentials;
|
||||
publicKey.challenge = bufferDecode(base64Decode(publicKey.challenge));
|
||||
publicKey.user.id = bufferDecode(publicKey.user.id);
|
||||
|
||||
if (publicKey.excludeCredentials) {
|
||||
publicKey.excludeCredentials = decodeCredentials(publicKey.excludeCredentials);
|
||||
}
|
||||
|
||||
const credentials = await navigator.credentials.create({ publicKey });
|
||||
|
||||
if (!credentials || credentials.type !== 'public-key') {
|
||||
throw new Error(`Unexpected type returned by navigator.credentials.create(): expected "public-key", got "${credentials?.type}"`);
|
||||
}
|
||||
|
||||
await registerCredentialForAccount(name, data.data.token_id, credentials);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import deleteWebauthnKey from '@/api/account/webauthn/deleteWebauthnKey';
|
||||
import getWebauthnKeys, { WebauthnKey } from '@/api/account/webauthn/getWebauthnKeys';
|
||||
import registerWebauthnKey from '@/api/account/webauthn/registerWebauthnKey';
|
||||
import { register } from '@/api/account/webauthn/registerWebauthnKey';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import Button from '@/components/elements/Button';
|
||||
import ContentBox from '@/components/elements/ContentBox';
|
||||
|
@ -28,14 +28,13 @@ const AddSecurityKeyForm = ({ onKeyAdded }: { onKeyAdded: (key: WebauthnKey) =>
|
|||
const submit = ({ name }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
|
||||
clearFlashes('security_keys');
|
||||
|
||||
registerWebauthnKey(name)
|
||||
.then(key => {
|
||||
register(name)
|
||||
.then(() => {
|
||||
resetForm();
|
||||
onKeyAdded(key);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'security_keys', error });
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
clearAndAddHttpError({ key: 'security_keys', error: err });
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
|
|
|
@ -30,10 +30,10 @@ Route::group(['prefix' => '/account'], function () {
|
|||
Route::post('/api-keys', [Client\ApiKeyController::class, 'store']);
|
||||
Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']);
|
||||
|
||||
Route::get('/webauthn', [Client\HardwareTokenController::class, 'index'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::get('/webauthn/register', [Client\HardwareTokenController::class, 'create'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::post('/webauthn/register', [Client\HardwareTokenController::class, 'store'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::delete('/webauthn/{id}', [Client\HardwareTokenController::class, 'delete'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::get('/security-keys', [Client\SecurityKeyController::class, 'index'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::get('/security-keys/register', [Client\SecurityKeyController::class, 'create'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::post('/security-keys/register', [Client\SecurityKeyController::class, 'store'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::delete('/security-keys/{securityKey}', [Client\SecurityKeyController::class, 'delete'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
|
||||
Route::get('/ssh', 'SSHKeyController@index');
|
||||
Route::post('/ssh', 'SSHKeyController@store');
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace Pterodactyl\Tests\Unit\Http\Middleware;
|
|||
|
||||
use Mockery as m;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\HardwareSecurityKey;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Exceptions\Http\TwoFactorAuthRequiredException;
|
||||
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
|
@ -66,7 +66,7 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
|
|||
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::factory()
|
||||
->has(HardwareSecurityKey::factory()->count(1))
|
||||
->has(SecurityKey::factory()->count(1))
|
||||
->create(['use_totp' => false]);
|
||||
$this->setRequestUserModel($user);
|
||||
|
||||
|
@ -141,7 +141,7 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
|
|||
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::factory()
|
||||
->has(HardwareSecurityKey::factory()->count(1))
|
||||
->has(SecurityKey::factory()->count(1))
|
||||
->create(['use_totp' => false]);
|
||||
$this->setRequestUserModel($user);
|
||||
|
||||
|
@ -255,7 +255,7 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
|
|||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
||||
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::factory()->has(HardwareSecurityKey::factory()->count(1))->create(['use_totp' => false]);
|
||||
$user = User::factory()->has(SecurityKey::factory()->count(1))->create(['use_totp' => false]);
|
||||
$this->setRequestUserModel($user);
|
||||
|
||||
$this->assertFalse($user->use_totp);
|
||||
|
@ -278,7 +278,7 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
|
|||
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::factory()
|
||||
->has(HardwareSecurityKey::factory()->count(1))
|
||||
->has(SecurityKey::factory()->count(1))
|
||||
->create(['use_totp' => false, 'root_admin' => true]);
|
||||
$this->setRequestUserModel($user);
|
||||
|
||||
|
|
Loading…
Reference in a new issue