Clean out existing webauthn logic, implement base logic for base package
This commit is contained in:
parent
0103a0c31e
commit
eaf12aec60
19 changed files with 584 additions and 1541 deletions
74
app/Http/Controllers/Api/Client/HardwareTokenController.php
Normal file
74
app/Http/Controllers/Api/Client/HardwareTokenController.php
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?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([]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,127 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Http\Controllers\Api\Client;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Pterodactyl\Models\WebauthnKey;
|
|
||||||
use LaravelWebauthn\Facades\Webauthn;
|
|
||||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
||||||
use Pterodactyl\Transformers\Api\Client\WebauthnKeyTransformer;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
|
|
||||||
class WebauthnController extends ClientApiController
|
|
||||||
{
|
|
||||||
private const SESSION_PUBLICKEY_CREATION = 'webauthn.publicKeyCreation';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ?
|
|
||||||
*
|
|
||||||
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
|
||||||
*/
|
|
||||||
public function index(Request $request): array
|
|
||||||
{
|
|
||||||
return $this->fractal->collection(WebauthnKey::query()->where('user_id', '=', $request->user()->id)->get())
|
|
||||||
->transformWith(WebauthnKeyTransformer::class)
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ?
|
|
||||||
*/
|
|
||||||
public function register(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
if (!Webauthn::canRegister($request->user())) {
|
|
||||||
return new JsonResponse([
|
|
||||||
'error' => [
|
|
||||||
'message' => trans('webauthn::errors.cannot_register_new_key'),
|
|
||||||
],
|
|
||||||
], JsonResponse::HTTP_FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
$publicKey = Webauthn::getRegisterData($request->user());
|
|
||||||
|
|
||||||
$request->session()->put(self::SESSION_PUBLICKEY_CREATION, $publicKey);
|
|
||||||
$request->session()->save();
|
|
||||||
|
|
||||||
return new JsonResponse([
|
|
||||||
'public_key' => $publicKey,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ?
|
|
||||||
*
|
|
||||||
* @return array|JsonResponse
|
|
||||||
*/
|
|
||||||
public function create(Request $request)
|
|
||||||
{
|
|
||||||
if (!Webauthn::canRegister($request->user())) {
|
|
||||||
return new JsonResponse([
|
|
||||||
'error' => [
|
|
||||||
'message' => trans('webauthn::errors.cannot_register_new_key'),
|
|
||||||
],
|
|
||||||
], JsonResponse::HTTP_FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->input('register') === null) {
|
|
||||||
throw new BadRequestHttpException('Missing register data in request body.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->input('name') === null) {
|
|
||||||
throw new BadRequestHttpException('Missing name in request body.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$publicKey = $request->session()->pull(self::SESSION_PUBLICKEY_CREATION);
|
|
||||||
if (!$publicKey instanceof PublicKeyCredentialCreationOptions) {
|
|
||||||
throw new ModelNotFoundException(trans('webauthn::errors.create_data_not_found'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$webauthnKey = Webauthn::doRegister(
|
|
||||||
$request->user(),
|
|
||||||
$publicKey,
|
|
||||||
$request->input('register'),
|
|
||||||
$request->input('name'),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return $this->fractal->item($webauthnKey)
|
|
||||||
->transformWith(WebauthnKeyTransformer::class)
|
|
||||||
->toArray();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return new JsonResponse([
|
|
||||||
'error' => [
|
|
||||||
'message' => $e->getMessage(),
|
|
||||||
],
|
|
||||||
], JsonResponse::HTTP_FORBIDDEN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ?
|
|
||||||
*/
|
|
||||||
public function deleteKey(Request $request, int $webauthnKeyId): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
WebauthnKey::query()
|
|
||||||
->where('user_id', $request->user()->getAuthIdentifier())
|
|
||||||
->findOrFail($webauthnKeyId)
|
|
||||||
->delete();
|
|
||||||
|
|
||||||
return new JsonResponse([
|
|
||||||
'deleted' => true,
|
|
||||||
'id' => $webauthnKeyId,
|
|
||||||
]);
|
|
||||||
} catch (ModelNotFoundException $e) {
|
|
||||||
return new JsonResponse([
|
|
||||||
'error' => [
|
|
||||||
'message' => trans('webauthn::errors.object_not_found'),
|
|
||||||
],
|
|
||||||
], JsonResponse::HTTP_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@ use Illuminate\Http\Request;
|
||||||
use Illuminate\Auth\AuthManager;
|
use Illuminate\Auth\AuthManager;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use LaravelWebauthn\Facades\Webauthn;
|
|
||||||
use Illuminate\Contracts\Config\Repository;
|
use Illuminate\Contracts\Config\Repository;
|
||||||
use Illuminate\Contracts\View\Factory as ViewFactory;
|
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||||
|
@ -92,30 +91,7 @@ class LoginController extends AbstractLoginController
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$webauthnKeys = $user->webauthnKeys()->get();
|
if ($user->use_totp) {
|
||||||
|
|
||||||
if (count($webauthnKeys) > 0) {
|
|
||||||
$token = Str::random(64);
|
|
||||||
$this->cache->put($token, $user->id, CarbonImmutable::now()->addMinutes(5));
|
|
||||||
|
|
||||||
$publicKey = Webauthn::getAuthenticateData($user);
|
|
||||||
$request->session()->put(self::SESSION_PUBLICKEY_REQUEST, $publicKey);
|
|
||||||
$request->session()->save();
|
|
||||||
|
|
||||||
$methods = [self::METHOD_WEBAUTHN];
|
|
||||||
if ($user->use_totp) {
|
|
||||||
$methods[] = self::METHOD_TOTP;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonResponse([
|
|
||||||
'complete' => false,
|
|
||||||
'methods' => $methods,
|
|
||||||
'confirmation_token' => $token,
|
|
||||||
'webauthn' => [
|
|
||||||
'public_key' => $publicKey,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
} elseif ($user->use_totp) {
|
|
||||||
$token = Str::random(64);
|
$token = Str::random(64);
|
||||||
$this->cache->put($token, $user->id, CarbonImmutable::now()->addMinutes(5));
|
$this->cache->put($token, $user->id, CarbonImmutable::now()->addMinutes(5));
|
||||||
|
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Http\Controllers\Auth;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Pterodactyl\Models\User;
|
|
||||||
use Illuminate\Auth\AuthManager;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use LaravelWebauthn\Facades\Webauthn;
|
|
||||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
||||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
|
||||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
|
||||||
|
|
||||||
class WebauthnController extends AbstractLoginController
|
|
||||||
{
|
|
||||||
private const SESSION_PUBLICKEY_REQUEST = 'webauthn.publicKeyRequest';
|
|
||||||
|
|
||||||
private CacheRepository $cache;
|
|
||||||
|
|
||||||
public function __construct(AuthManager $auth, ConfigRepository $config, CacheRepository $cache)
|
|
||||||
{
|
|
||||||
parent::__construct($auth, $config);
|
|
||||||
|
|
||||||
$this->cache = $cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return JsonResponse|void
|
|
||||||
*
|
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
|
||||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
|
||||||
*/
|
|
||||||
public function auth(Request $request)
|
|
||||||
{
|
|
||||||
if ($this->hasTooManyLoginAttempts($request)) {
|
|
||||||
$this->sendLockoutResponse($request);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $request->input('confirmation_token');
|
|
||||||
try {
|
|
||||||
/** @var \Pterodactyl\Models\User $user */
|
|
||||||
$user = User::query()->findOrFail($this->cache->get($token, 0));
|
|
||||||
} catch (ModelNotFoundException $exception) {
|
|
||||||
$this->incrementLoginAttempts($request);
|
|
||||||
|
|
||||||
$this->sendFailedLoginResponse(
|
|
||||||
$request,
|
|
||||||
null,
|
|
||||||
'The authentication token provided has expired, please refresh the page and try again.'
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->auth->guard()->onceUsingId($user->id);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$publicKey = $request->session()->pull(self::SESSION_PUBLICKEY_REQUEST);
|
|
||||||
if (!$publicKey instanceof PublicKeyCredentialRequestOptions) {
|
|
||||||
throw new ModelNotFoundException(trans('webauthn::errors.auth_data_not_found'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = Webauthn::doAuthenticate(
|
|
||||||
$user,
|
|
||||||
$publicKey,
|
|
||||||
$this->input($request, 'data'),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$result) {
|
|
||||||
return new JsonResponse([
|
|
||||||
'error' => [
|
|
||||||
'message' => 'Nice attempt, you didn\'t pass the challenge.',
|
|
||||||
],
|
|
||||||
], JsonResponse::HTTP_I_AM_A_TEAPOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->cache->delete($token);
|
|
||||||
|
|
||||||
return $this->sendLoginResponse($user, $request);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return new JsonResponse([
|
|
||||||
'error' => [
|
|
||||||
'message' => $e->getMessage(),
|
|
||||||
],
|
|
||||||
], JsonResponse::HTTP_FORBIDDEN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the input with a string result.
|
|
||||||
*/
|
|
||||||
private function input(Request $request, string $name, string $default = ''): string
|
|
||||||
{
|
|
||||||
$result = $request->input($name);
|
|
||||||
|
|
||||||
return is_string($result) ? $result : $default;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Requests\Api\Client\Account;
|
||||||
|
|
||||||
|
use Pterodactyl\Http\Requests\Api\Client\AccountApiRequest;
|
||||||
|
|
||||||
|
class RegisterWebauthnTokenRequest extends AccountApiRequest
|
||||||
|
{
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['string', 'required'],
|
||||||
|
'register' => ['string', 'required'],
|
||||||
|
'public_key' => ['string', 'required'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
51
app/Models/HardwareSecurityKey.php
Normal file
51
app/Models/HardwareSecurityKey.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Models;
|
||||||
|
|
||||||
|
use Webauthn\PublicKeyCredentialSource;
|
||||||
|
use Webauthn\PublicKeyCredentialDescriptor;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class HardwareSecurityKey extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public const RESOURCE_NAME = 'hardware_security_key';
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'user_id' => 'int',
|
||||||
|
'transports' => 'array',
|
||||||
|
'trust_path' => 'array',
|
||||||
|
'other_ui' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toCredentialsDescriptor()
|
||||||
|
{
|
||||||
|
return new PublicKeyCredentialDescriptor(
|
||||||
|
$this->type,
|
||||||
|
$this->public_key_id,
|
||||||
|
$this->transports
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toCredentialSource(): PublicKeyCredentialSource
|
||||||
|
{
|
||||||
|
return PublicKeyCredentialSource::createFromArray([
|
||||||
|
'publicKeyCredentialId' => $this->public_key_id,
|
||||||
|
'type' => $this->type,
|
||||||
|
'transports' => $this->transports,
|
||||||
|
'attestationType' => $this->attestation_type,
|
||||||
|
// 'trustPath' => $key->trustPath->jsonSerialize(),
|
||||||
|
'aaguid' => $this->aaguid,
|
||||||
|
'credentialPublicKey' => $this->public_key,
|
||||||
|
'userHandle' => $this->user_handle,
|
||||||
|
'counter' => $this->counter,
|
||||||
|
'otherUI' => $this->other_ui,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -210,9 +210,9 @@ class User extends Model implements
|
||||||
return $this->hasMany(RecoveryToken::class);
|
return $this->hasMany(RecoveryToken::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function webauthnKeys(): HasMany
|
public function hardwareSecurityKeys(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(WebauthnKey::class);
|
return $this->hasMany(HardwareSecurityKey::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
|
|
||||||
class WebauthnKey extends \LaravelWebauthn\Models\WebauthnKey
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
public const RESOURCE_NAME = 'webauthn_key';
|
|
||||||
|
|
||||||
public function user(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Repositories\Webauthn;
|
||||||
|
|
||||||
|
use Pterodactyl\Models\User;
|
||||||
|
use Illuminate\Container\Container;
|
||||||
|
use Webauthn\PublicKeyCredentialSource;
|
||||||
|
use Webauthn\PublicKeyCredentialUserEntity;
|
||||||
|
use Pterodactyl\Models\HardwareSecurityKey;
|
||||||
|
use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyRepositoryInterface;
|
||||||
|
|
||||||
|
class PublicKeyCredentialSourceRepository implements PublicKeyRepositoryInterface
|
||||||
|
{
|
||||||
|
protected User $user;
|
||||||
|
|
||||||
|
public function __construct(User $user)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a single hardware security token for a user by uzing the credential ID.
|
||||||
|
*/
|
||||||
|
public function findOneByCredentialId(string $id): ?PublicKeyCredentialSource
|
||||||
|
{
|
||||||
|
/** @var \Pterodactyl\Models\HardwareSecurityKey $key */
|
||||||
|
$key = $this->user->hardwareSecurityKeys()
|
||||||
|
->where('public_key_id', $id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $key ? $key->toCredentialSource() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all of the hardware tokens that exist for the user using the given
|
||||||
|
* entity handle.
|
||||||
|
*/
|
||||||
|
public function findAllForUserEntity(PublicKeyCredentialUserEntity $entity): array
|
||||||
|
{
|
||||||
|
$results = $this->user->hardwareSecurityKeys()
|
||||||
|
->where('user_handle', $entity->getId())
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $results->map(function (HardwareSecurityKey $key) {
|
||||||
|
return $key->toCredentialSource();
|
||||||
|
})->values()->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a credential to the database and link it with the user.
|
||||||
|
*/
|
||||||
|
public function saveCredentialSource(PublicKeyCredentialSource $source): void
|
||||||
|
{
|
||||||
|
// todo: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new instance of the repository with the provided user attached.
|
||||||
|
*/
|
||||||
|
public static function factory(User $user): self
|
||||||
|
{
|
||||||
|
return Container::getInstance()->make(static::class, ['user' => $user]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace Pterodactyl\Transformers\Api\Client;
|
namespace Pterodactyl\Transformers\Api\Client;
|
||||||
|
|
||||||
use Pterodactyl\Models\WebauthnKey;
|
use Pterodactyl\Models\HardwareSecurityKey;
|
||||||
use Pterodactyl\Transformers\Api\Transformer;
|
use Pterodactyl\Transformers\Api\Transformer;
|
||||||
|
|
||||||
class WebauthnKeyTransformer extends Transformer
|
class WebauthnKeyTransformer extends Transformer
|
||||||
|
@ -12,15 +12,13 @@ class WebauthnKeyTransformer extends Transformer
|
||||||
*/
|
*/
|
||||||
public function getResourceName(): string
|
public function getResourceName(): string
|
||||||
{
|
{
|
||||||
return WebauthnKey::RESOURCE_NAME;
|
return HardwareSecurityKey::RESOURCE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return basic information about the currently logged in user.
|
* Return basic information about the currently logged in user.
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Models\WebauthnKey|\LaravelWebauthn\Models\WebauthnKey $model
|
|
||||||
*/
|
*/
|
||||||
public function transform($model): array
|
public function transform(HardwareSecurityKey $model): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $model->id,
|
'id' => $model->id,
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"ext-pdo_mysql": "*",
|
"ext-pdo_mysql": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"asbiin/laravel-webauthn": "^1.1",
|
"aws/aws-sdk-php": "^3.186",
|
||||||
"aws/aws-sdk-php": "^3.192",
|
|
||||||
"doctrine/dbal": "^3.1",
|
"doctrine/dbal": "^3.1",
|
||||||
"fideloper/proxy": "^4.4",
|
"fideloper/proxy": "^4.4",
|
||||||
"guzzlehttp/guzzle": "^7.3",
|
"guzzlehttp/guzzle": "^7.3",
|
||||||
|
@ -42,6 +41,7 @@
|
||||||
"spatie/laravel-query-builder": "^3.5",
|
"spatie/laravel-query-builder": "^3.5",
|
||||||
"staudenmeir/belongs-to-through": "^2.11",
|
"staudenmeir/belongs-to-through": "^2.11",
|
||||||
"symfony/yaml": "^5.3",
|
"symfony/yaml": "^5.3",
|
||||||
|
"web-auth/webauthn-lib": "^3.3",
|
||||||
"webmozart/assert": "^1.10"
|
"webmozart/assert": "^1.10"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|
1294
composer.lock
generated
1294
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,222 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| LaravelWebauthn Master Switch
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option may be used to disable LaravelWebauthn.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'enable' => true,
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Route Middleware
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| These middleware will be assigned to Webauthn routes, giving you
|
|
||||||
| the chance to add your own middleware to this list or change any of
|
|
||||||
| the existing middleware. Or, you can simply stick with this list.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'middleware' => [
|
|
||||||
'web',
|
|
||||||
'auth',
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Prefix path
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The uri prefix for all webauthn requests.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'prefix' => 'webauthn',
|
|
||||||
|
|
||||||
'authenticate' => [
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| View to load after middleware login request.
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The name of blade template to load whe a user login and it request to validate
|
|
||||||
| the Webauthn 2nd factor.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'view' => 'webauthn::authenticate',
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Redirect with callback url after login.
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Save the destination url, then after a successful login, redirect to this
|
|
||||||
| url.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'postSuccessCallback' => true,
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Redirect route
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| If postSuccessCallback if false, redirect to this route after login
|
|
||||||
| request is complete.
|
|
||||||
| If empty, send a json response to let the client side redirection.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'postSuccessRedirectRoute' => '',
|
|
||||||
],
|
|
||||||
|
|
||||||
'register' => [
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| View to load on register request.
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The name of blade template to load when a user request a creation of
|
|
||||||
| Webauthn key.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'view' => 'webauthn::register',
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Redirect route
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The route to redirect to after register key request is complete.
|
|
||||||
| If empty, send a json response to let the client side redirection.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'postSuccessRedirectRoute' => '',
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session name
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Name of the session parameter to store the successful login.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'sessionName' => 'webauthn_auth',
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Webauthn challenge length
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Length of the random string used in the challenge request.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'challenge_length' => 32,
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Webauthn timeout (milliseconds)
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Time that the caller is willing to wait for the call to complete.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'timeout' => 60000,
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Webauthn extension client input
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Optional authentication extension.
|
|
||||||
| See https://www.w3.org/TR/webauthn/#client-extension-input
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'extensions' => [],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Webauthn icon
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Url which resolves to an image associated with the entity.
|
|
||||||
| See https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-icon
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'icon' => null,
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Webauthn Attestation Conveyance
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This parameter specify the preference regarding the attestation conveyance
|
|
||||||
| during credential generation.
|
|
||||||
| See https://www.w3.org/TR/webauthn/#attestation-convey
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'attestation_conveyance' => \Webauthn\PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Google Safetynet ApiKey
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Api key to use Google Safetynet.
|
|
||||||
| See https://developer.android.com/training/safetynet/attestation
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'google_safetynet_api_key' => '',
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Webauthn Public Key Credential Parameters
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| List of allowed Cryptographic Algorithm Identifier.
|
|
||||||
| See https://www.w3.org/TR/webauthn/#alg-identifier
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'public_key_credential_parameters' => [
|
|
||||||
\Cose\Algorithms::COSE_ALGORITHM_ES256,
|
|
||||||
\Cose\Algorithms::COSE_ALGORITHM_RS256,
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Webauthn Authenticator Selection Criteria
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Requirement for the creation operation.
|
|
||||||
| See https://www.w3.org/TR/webauthn/#authenticatorSelection
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'authenticator_selection_criteria' => [
|
|
||||||
/*
|
|
||||||
| See https://www.w3.org/TR/webauthn/#attachment
|
|
||||||
*/
|
|
||||||
'attachment_mode' => \Webauthn\AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
|
|
||||||
|
|
||||||
'require_resident_key' => false,
|
|
||||||
|
|
||||||
/*
|
|
||||||
| See https://www.w3.org/TR/webauthn/#userVerificationRequirement
|
|
||||||
*/
|
|
||||||
'user_verification' => \Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED,
|
|
||||||
],
|
|
||||||
];
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use Pterodactyl\Models\WebauthnKey;
|
use Pterodactyl\Models\HardwareSecurityKey;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
class WebauthnKeyFactory extends Factory
|
class WebauthnKeyFactory extends Factory
|
||||||
|
@ -10,7 +10,7 @@ class WebauthnKeyFactory extends Factory
|
||||||
/**
|
/**
|
||||||
* The name of the factory's corresponding model.
|
* The name of the factory's corresponding model.
|
||||||
*/
|
*/
|
||||||
protected $model = WebauthnKey::class;
|
protected $model = HardwareSecurityKey::class;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the model's default state.
|
* Define the model's default state.
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateHardwareSecurityKeysTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('hardware_security_keys', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->char('uuid', 36);
|
||||||
|
$table->unsignedInteger('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||||
|
$table->text('public_key_id');
|
||||||
|
$table->text('public_key');
|
||||||
|
$table->char('aaguid', 36);
|
||||||
|
$table->string('type');
|
||||||
|
$table->json('transports');
|
||||||
|
$table->string('attestation_type');
|
||||||
|
$table->json('trust_path');
|
||||||
|
$table->text('user_handle');
|
||||||
|
$table->unsignedInteger('counter');
|
||||||
|
$table->json('other_ui');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('hardware_security_keys');
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,10 +30,10 @@ Route::group(['prefix' => '/account'], function () {
|
||||||
Route::post('/api-keys', [Client\ApiKeyController::class, 'store']);
|
Route::post('/api-keys', [Client\ApiKeyController::class, 'store']);
|
||||||
Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']);
|
Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']);
|
||||||
|
|
||||||
Route::get('/webauthn', 'WebauthnController@index')->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
Route::get('/webauthn', [Client\HardwareTokenController::class, 'index'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||||
Route::get('/webauthn/register', 'WebauthnController@register')->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
Route::get('/webauthn/register', [Client\HardwareTokenController::class, 'create'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||||
Route::post('/webauthn/register', 'WebauthnController@create')->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
Route::post('/webauthn/register', [Client\HardwareTokenController::class, 'store'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||||
Route::delete('/webauthn/{id}', 'WebauthnController@deleteKey')->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
Route::delete('/webauthn/{id}', [Client\HardwareTokenController::class, 'delete'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||||
|
|
||||||
Route::get('/ssh', 'SSHKeyController@index');
|
Route::get('/ssh', 'SSHKeyController@index');
|
||||||
Route::post('/ssh', 'SSHKeyController@store');
|
Route::post('/ssh', 'SSHKeyController@store');
|
||||||
|
|
|
@ -20,7 +20,6 @@ Route::group(['middleware' => 'guest'], function () {
|
||||||
// Login endpoints.
|
// Login endpoints.
|
||||||
Route::post('/login', 'LoginController@login')->middleware('recaptcha');
|
Route::post('/login', 'LoginController@login')->middleware('recaptcha');
|
||||||
Route::post('/login/checkpoint', 'LoginCheckpointController')->name('auth.login-checkpoint');
|
Route::post('/login/checkpoint', 'LoginCheckpointController')->name('auth.login-checkpoint');
|
||||||
Route::post('/login/checkpoint/key', 'WebauthnController@auth');
|
|
||||||
|
|
||||||
// Forgot password route. A post to this endpoint will trigger an
|
// Forgot password route. A post to this endpoint will trigger an
|
||||||
// email to be sent containing a reset token.
|
// email to be sent containing a reset token.
|
||||||
|
|
|
@ -4,7 +4,7 @@ namespace Pterodactyl\Tests\Unit\Http\Middleware;
|
||||||
|
|
||||||
use Mockery as m;
|
use Mockery as m;
|
||||||
use Pterodactyl\Models\User;
|
use Pterodactyl\Models\User;
|
||||||
use Pterodactyl\Models\WebauthnKey;
|
use Pterodactyl\Models\HardwareSecurityKey;
|
||||||
use Prologue\Alerts\AlertsMessageBag;
|
use Prologue\Alerts\AlertsMessageBag;
|
||||||
use Pterodactyl\Exceptions\Http\TwoFactorAuthRequiredException;
|
use Pterodactyl\Exceptions\Http\TwoFactorAuthRequiredException;
|
||||||
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
||||||
|
@ -66,7 +66,7 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
|
||||||
|
|
||||||
/** @var \Pterodactyl\Models\User $user */
|
/** @var \Pterodactyl\Models\User $user */
|
||||||
$user = User::factory()
|
$user = User::factory()
|
||||||
->has(WebauthnKey::factory()->count(1))
|
->has(HardwareSecurityKey::factory()->count(1))
|
||||||
->create(['use_totp' => false]);
|
->create(['use_totp' => false]);
|
||||||
$this->setRequestUserModel($user);
|
$this->setRequestUserModel($user);
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
|
||||||
|
|
||||||
/** @var \Pterodactyl\Models\User $user */
|
/** @var \Pterodactyl\Models\User $user */
|
||||||
$user = User::factory()
|
$user = User::factory()
|
||||||
->has(WebauthnKey::factory()->count(1))
|
->has(HardwareSecurityKey::factory()->count(1))
|
||||||
->create(['use_totp' => false]);
|
->create(['use_totp' => false]);
|
||||||
$this->setRequestUserModel($user);
|
$this->setRequestUserModel($user);
|
||||||
|
|
||||||
|
@ -255,7 +255,7 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
|
||||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
||||||
|
|
||||||
/** @var \Pterodactyl\Models\User $user */
|
/** @var \Pterodactyl\Models\User $user */
|
||||||
$user = User::factory()->has(WebauthnKey::factory()->count(1))->create(['use_totp' => false]);
|
$user = User::factory()->has(HardwareSecurityKey::factory()->count(1))->create(['use_totp' => false]);
|
||||||
$this->setRequestUserModel($user);
|
$this->setRequestUserModel($user);
|
||||||
|
|
||||||
$this->assertFalse($user->use_totp);
|
$this->assertFalse($user->use_totp);
|
||||||
|
@ -278,7 +278,7 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
|
||||||
|
|
||||||
/** @var \Pterodactyl\Models\User $user */
|
/** @var \Pterodactyl\Models\User $user */
|
||||||
$user = User::factory()
|
$user = User::factory()
|
||||||
->has(WebauthnKey::factory()->count(1))
|
->has(HardwareSecurityKey::factory()->count(1))
|
||||||
->create(['use_totp' => false, 'root_admin' => true]);
|
->create(['use_totp' => false, 'root_admin' => true]);
|
||||||
$this->setRequestUserModel($user);
|
$this->setRequestUserModel($user);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue