Get basic storage of webauthn tokens working

This commit is contained in:
Dane Everitt 2021-08-08 10:48:35 -07:00
parent eaf12aec60
commit 1053b5d605
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
19 changed files with 531 additions and 211 deletions

View file

@ -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([]);
}
}

View 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);
}
}

View file

@ -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'],
];
}
}

View file

@ -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);

View file

@ -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);
}
/**

View file

@ -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(),
]);
}
/**

View 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)
);
}
}

View file

@ -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)
);
}
}

View file

@ -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
);
}
}

View file

@ -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,

View file

@ -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
View file

@ -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",

View file

@ -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.

View file

@ -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');
}
}

View file

@ -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);
});

View file

@ -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);
};

View file

@ -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));
};

View file

@ -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');

View file

@ -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);