Support authenticating the provided key when loggin in
This commit is contained in:
parent
54c7207836
commit
09497c234a
9 changed files with 117 additions and 55 deletions
|
@ -6,6 +6,7 @@ use Carbon\CarbonImmutable;
|
|||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
@ -97,7 +98,7 @@ class SecurityKeyController extends ClientApiController
|
|||
}
|
||||
|
||||
$key = $this->storeSecurityKeyService
|
||||
->setRequest($this->getServerRequest($request))
|
||||
->setRequest(SecurityKey::getPsrRequestFactory($request))
|
||||
->setKeyName($request->input('name'))
|
||||
->handle($request->user(), $request->input('registration'), $credentials);
|
||||
|
||||
|
@ -115,13 +116,4 @@ class SecurityKeyController extends ClientApiController
|
|||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
protected function getServerRequest(Request $request): ServerRequestInterface
|
||||
{
|
||||
$factory = new Psr17Factory();
|
||||
|
||||
$httpFactory = new PsrHttpFactory($factory, $factory, $factory, $factory);
|
||||
|
||||
return $httpFactory->createRequest($request);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,32 +5,44 @@ namespace Pterodactyl\Http\Controllers\Auth;
|
|||
use Carbon\CarbonImmutable;
|
||||
use Carbon\CarbonInterface;
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest;
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class LoginCheckpointController extends AbstractLoginController
|
||||
{
|
||||
public const TOKEN_EXPIRED_MESSAGE = 'The authentication token provided has expired, please refresh the page and try again.';
|
||||
|
||||
private Encrypter $encrypter;
|
||||
protected Encrypter $encrypter;
|
||||
|
||||
private Google2FA $google2FA;
|
||||
protected Google2FA $google2FA;
|
||||
|
||||
private ValidationFactory $validation;
|
||||
protected WebauthnServerRepository $repository;
|
||||
|
||||
protected ValidationFactory $validation;
|
||||
|
||||
/**
|
||||
* LoginCheckpointController constructor.
|
||||
*/
|
||||
public function __construct(Encrypter $encrypter, Google2FA $google2FA, ValidationFactory $validation)
|
||||
{
|
||||
public function __construct(
|
||||
Encrypter $encrypter,
|
||||
Google2FA $google2FA,
|
||||
ValidationFactory $validation,
|
||||
WebauthnServerRepository $repository
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->encrypter = $encrypter;
|
||||
$this->google2FA = $google2FA;
|
||||
$this->validation = $validation;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,35 +58,9 @@ class LoginCheckpointController extends AbstractLoginController
|
|||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
*/
|
||||
public function __invoke(LoginCheckpointRequest $request)
|
||||
public function token(LoginCheckpointRequest $request)
|
||||
{
|
||||
if ($this->hasTooManyLoginAttempts($request)) {
|
||||
$this->sendLockoutResponse($request);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$details = $request->session()->get('auth_confirmation_token');
|
||||
if (!$this->hasValidSessionData($details)) {
|
||||
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hash_equals($request->input('confirmation_token') ?? '', $details['token_value'])) {
|
||||
$this->sendFailedLoginResponse($request);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::query()->findOrFail($details['user_id']);
|
||||
} catch (ModelNotFoundException $exception) {
|
||||
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
|
||||
|
||||
return;
|
||||
}
|
||||
$user = $this->extractUserFromRequest($request);
|
||||
|
||||
// Recovery tokens go through a slightly different pathway for usage.
|
||||
if (!is_null($recoveryToken = $request->input('recovery_token'))) {
|
||||
|
@ -92,6 +78,65 @@ class LoginCheckpointController extends AbstractLoginController
|
|||
$this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a login request using a security key for a user.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
*/
|
||||
public function key(Request $request)
|
||||
{
|
||||
$key = $request->session()->get(SecurityKey::PK_SESSION_NAME);
|
||||
if (!$key instanceof PublicKeyCredentialRequestOptions) {
|
||||
throw new BadRequestHttpException('No security keys configured in session.');
|
||||
}
|
||||
|
||||
$user = $this->extractUserFromRequest($request);
|
||||
|
||||
$source = $this->repository->getServer($user)->loadAndCheckAssertionResponse(
|
||||
$request->input('data'),
|
||||
$key,
|
||||
$user->toPublicKeyCredentialEntity(),
|
||||
SecurityKey::getPsrRequestFactory($request)
|
||||
);
|
||||
|
||||
dd($source->getUserHandle());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Pterodactyl\Models\User
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
*/
|
||||
protected function extractUserFromRequest(Request $request): User
|
||||
{
|
||||
if ($this->hasTooManyLoginAttempts($request)) {
|
||||
$this->sendLockoutResponse($request);
|
||||
}
|
||||
|
||||
$details = $request->session()->get('auth_confirmation_token');
|
||||
if (!$this->hasValidSessionData($details)) {
|
||||
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
|
||||
}
|
||||
|
||||
if (!hash_equals($request->input('confirmation_token') ?? '', $details['token_value'])) {
|
||||
$this->sendFailedLoginResponse($request);
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::query()->findOrFail($details['user_id']);
|
||||
} catch (ModelNotFoundException $exception) {
|
||||
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given recovery token is valid for the user account. If we find a matching token
|
||||
* it will be deleted from the database.
|
||||
|
|
|
@ -16,7 +16,6 @@ class LoginController extends AbstractLoginController
|
|||
{
|
||||
private const METHOD_TOTP = 'totp';
|
||||
private const METHOD_WEBAUTHN = 'webauthn';
|
||||
private const SESSION_PUBLICKEY_REQUEST = 'webauthn.publicKeyRequest';
|
||||
|
||||
protected GeneratePublicKeyCredentialsRequestService $service;
|
||||
|
||||
|
@ -98,7 +97,7 @@ class LoginController extends AbstractLoginController
|
|||
if (!empty($user->securityKeys)) {
|
||||
$key = $this->service->handle($user);
|
||||
|
||||
$request->session()->put(self::SESSION_PUBLICKEY_REQUEST, $key);
|
||||
$request->session()->put(SecurityKey::PK_SESSION_NAME, $key);
|
||||
|
||||
$response['webauthn'] = ['public_key' => $key];
|
||||
}
|
||||
|
|
|
@ -3,19 +3,24 @@
|
|||
namespace Pterodactyl\Models;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Http\Request;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Webauthn\TrustPath\TrustPath;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\TrustPath\TrustPathLoader;
|
||||
use Webauthn\PublicKeyCredentialDescriptor;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
|
||||
|
||||
class SecurityKey extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const RESOURCE_NAME = 'security_key';
|
||||
public const PK_SESSION_NAME = 'security_key_pk_request';
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'int',
|
||||
|
@ -109,4 +114,19 @@ class SecurityKey extends Model
|
|||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a PSR17 Request factory to be used by different Webauthn tooling.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Psr\Http\Message\ServerRequestInterface
|
||||
*/
|
||||
public static function getPsrRequestFactory(Request $request): ServerRequestInterface
|
||||
{
|
||||
$factory = new Psr17Factory();
|
||||
|
||||
$httpFactory = new PsrHttpFactory($factory, $factory, $factory, $factory);
|
||||
|
||||
return $httpFactory->createRequest($request);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use Illuminate\Support\Collection;
|
|||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Pterodactyl\Models\Traits\HasAccessTokens;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
@ -229,4 +230,12 @@ class User extends Model implements
|
|||
})
|
||||
->groupBy('servers.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Webauthn\PublicKeyCredentialUserEntity
|
||||
*/
|
||||
public function toPublicKeyCredentialEntity(): PublicKeyCredentialUserEntity
|
||||
{
|
||||
return new PublicKeyCredentialUserEntity($this->username, $this->uuid, $this->email, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace Pterodactyl\Repositories\SecurityKeys;
|
|||
|
||||
use Webauthn\Server;
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Container\Container;
|
||||
use Webauthn\PublicKeyCredentialRpEntity;
|
||||
|
||||
final class WebauthnServerRepository
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace Pterodactyl\Services\Users\SecurityKeys;
|
|||
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository;
|
||||
|
||||
|
@ -19,8 +18,6 @@ class CreatePublicKeyCredentialsService
|
|||
|
||||
public function handle(User $user): PublicKeyCredentialCreationOptions
|
||||
{
|
||||
$entity = new PublicKeyCredentialUserEntity($user->username, $user->uuid, $user->email, null);
|
||||
|
||||
$excluded = $user->securityKeys->map(function (SecurityKey $key) {
|
||||
return $key->getPublicKeyCredentialDescriptor();
|
||||
})->values()->toArray();
|
||||
|
@ -28,7 +25,7 @@ class CreatePublicKeyCredentialsService
|
|||
$server = $this->webauthnServerRepository->getServer($user);
|
||||
|
||||
return $server->generatePublicKeyCredentialCreationOptions(
|
||||
$entity,
|
||||
$user->toPublicKeyCredentialEntity(),
|
||||
PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
|
||||
$excluded
|
||||
);
|
||||
|
|
|
@ -22,12 +22,10 @@ export default (token: string, publicKey: PublicKeyCredentialRequestOptions): Pr
|
|||
|
||||
const data = {
|
||||
confirmation_token: token,
|
||||
|
||||
data: JSON.stringify({
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: bufferEncode(credential.rawId),
|
||||
|
||||
response: {
|
||||
authenticatorData: bufferEncode(response.authenticatorData),
|
||||
clientDataJSON: bufferEncode(response.clientDataJSON),
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
| Endpoint: /auth
|
||||
|
|
||||
*/
|
||||
|
||||
use Pterodactyl\Http\Controllers\Auth;
|
||||
|
||||
Route::group(['middleware' => 'guest'], function () {
|
||||
// These routes are defined so that we can continue to reference them programmatically.
|
||||
// They all route to the same controller function which passes off to React.
|
||||
|
@ -21,9 +24,9 @@ Route::group(['middleware' => 'guest'], function () {
|
|||
// @see \Pterodactyl\Providers\RouteServiceProvider
|
||||
Route::middleware(['throttle:authentication'])->group(function () {
|
||||
// Login endpoints.
|
||||
Route::post('/login', 'LoginController@login')->middleware('recaptcha');
|
||||
Route::post('/login/checkpoint', 'LoginCheckpointController')->name('auth.login-checkpoint');
|
||||
Route::post('/login/checkpoint/key', 'WebauthnController@auth')->name('auth.login-checkpoint-key');
|
||||
Route::post('/login', [Auth\LoginController::class, 'login'])->middleware('recaptcha');
|
||||
Route::post('/login/checkpoint', [Auth\LoginCheckpointController::class, 'token'])->name('auth.login-checkpoint');
|
||||
Route::post('/login/checkpoint/key', [Auth\LoginCheckpointController::class, 'key'])->name('auth.login-checkpoint-key');
|
||||
|
||||
// Forgot password route. A post to this endpoint will trigger an
|
||||
// email to be sent containing a reset token.
|
||||
|
|
Loading…
Reference in a new issue