From 06f692e649f31018ec0dca5d42a0d140302ca958 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Mon, 24 Oct 2022 09:44:16 -0600 Subject: [PATCH] Add controllers and packages for security keys --- .../Api/Client/SecurityKeyController.php | 105 ++ .../Auth/AbstractLoginController.php | 18 +- .../Auth/LoginCheckpointController.php | 107 +- app/Http/Controllers/Auth/LoginController.php | 39 +- .../RequireTwoFactorAuthentication.php | 2 +- .../Account/RegisterSecurityKeyRequest.php | 21 + .../Requests/Api/Client/ClientApiRequest.php | 2 +- app/Models/SecurityKey.php | 125 ++ app/Models/User.php | 25 +- .../PublicKeyCredentialSourceRepository.php | 68 + .../SecurityKeys/WebauthnServerRepository.php | 161 +++ .../CreatePublicKeyCredentialService.php | 25 + .../SecurityKeys/StoreSecurityKeyService.php | 79 ++ .../Api/Client/SecurityKeyTransformer.php | 25 + composer.json | 5 +- composer.lock | 1225 +++++++++++++---- database/Factories/SecurityKeyFactory.php | 43 + ...8_07_170141_create_security_keys_table.php | 42 + .../scripts/api/account/security-keys.ts | 85 ++ .../scripts/api/definitions/user/models.d.ts | 45 +- .../api/definitions/user/transformers.ts | 49 +- resources/scripts/lib/base64.spec.ts | 12 + resources/scripts/lib/base64.ts | 16 + resources/scripts/lib/buffer.spec.ts | 21 + resources/scripts/lib/buffer.ts | 9 + routes/api-client.php | 5 + routes/auth.php | 10 +- .../RequireTwoFactorAuthenticationTest.php | 328 +++++ tsconfig.json | 84 +- 29 files changed, 2398 insertions(+), 383 deletions(-) create mode 100644 app/Http/Controllers/Api/Client/SecurityKeyController.php create mode 100644 app/Http/Requests/Api/Client/Account/RegisterSecurityKeyRequest.php create mode 100644 app/Models/SecurityKey.php create mode 100644 app/Repositories/SecurityKeys/PublicKeyCredentialSourceRepository.php create mode 100644 app/Repositories/SecurityKeys/WebauthnServerRepository.php create mode 100644 app/Services/Users/SecurityKeys/CreatePublicKeyCredentialService.php create mode 100644 app/Services/Users/SecurityKeys/StoreSecurityKeyService.php create mode 100644 app/Transformers/Api/Client/SecurityKeyTransformer.php create mode 100644 database/Factories/SecurityKeyFactory.php create mode 100644 database/migrations/2021_08_07_170141_create_security_keys_table.php create mode 100644 resources/scripts/api/account/security-keys.ts create mode 100644 resources/scripts/lib/base64.spec.ts create mode 100644 resources/scripts/lib/base64.ts create mode 100644 resources/scripts/lib/buffer.spec.ts create mode 100644 resources/scripts/lib/buffer.ts create mode 100644 tests/Unit/Http/Middleware/RequireTwoFactorAuthenticationTest.php diff --git a/app/Http/Controllers/Api/Client/SecurityKeyController.php b/app/Http/Controllers/Api/Client/SecurityKeyController.php new file mode 100644 index 000000000..ea72d9a8b --- /dev/null +++ b/app/Http/Controllers/Api/Client/SecurityKeyController.php @@ -0,0 +1,105 @@ +fractal->collection($request->user()->securityKeys) + ->transformWith(SecurityKeyTransformer::class) + ->toArray(); + } + + /** + * Returns the data necessary for creating a new hardware security key for the + * user. + * + * @throws \Webauthn\Exception\InvalidDataException + */ + public function create(Request $request): JsonResponse + { + $tokenId = Str::random(64); + $credentials = $this->createPublicKeyCredentialService->handle($request->user()); + + // TODO: session + $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 \Pterodactyl\Exceptions\DisplayException + * @throws \Throwable + */ + public function store(RegisterSecurityKeyRequest $request): array + { + $credentials = unserialize( + $this->cache->pull("register-security-key:{$request->input('token_id')}", serialize(null)) + ); + + if ( + !is_object($credentials) || + !$credentials instanceof PublicKeyCredentialCreationOptions || + $credentials->getUser()->getId() !== $request->user()->uuid + ) { + throw new DisplayException('Could not register security key: invalid data present in session, please try again.'); + } + + $key = $this->storeSecurityKeyService + ->setRequest(SecurityKey::getPsrRequestFactory($request)) + ->setKeyName($request->input('name')) + ->handle($request->user(), $request->input('registration'), $credentials); + + return $this->fractal->item($key) + ->transformWith(SecurityKeyTransformer::class) + ->toArray(); + } + + /** + * Removes a WebAuthn key from a user's account. + */ + public function delete(Request $request, string $securityKey): JsonResponse + { + $request->user()->securityKeys()->where('uuid', $securityKey)->delete(); + + return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + } +} diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php index f07282fba..8faabbc08 100644 --- a/app/Http/Controllers/Auth/AbstractLoginController.php +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -8,7 +8,6 @@ use Illuminate\Auth\AuthManager; use Illuminate\Http\JsonResponse; use Illuminate\Auth\Events\Failed; use Illuminate\Container\Container; -use Illuminate\Support\Facades\Event; use Pterodactyl\Events\Auth\DirectLogin; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; @@ -37,7 +36,7 @@ abstract class AbstractLoginController extends Controller protected string $redirectTo = '/'; /** - * LoginController constructor. + * AbstractLoginController constructor. */ public function __construct() { @@ -58,7 +57,7 @@ abstract class AbstractLoginController extends Controller $this->getField($request->input('user')) => $request->input('user'), ]); - if ($request->route()->named('auth.login-checkpoint')) { + if ($request->route()->named('auth.checkpoint') || $request->route()->named('auth.checkpoint.key')) { throw new DisplayException($message ?? trans('auth.two_factor.checkpoint_failed')); } @@ -77,14 +76,13 @@ abstract class AbstractLoginController extends Controller $this->auth->guard()->login($user, true); - Event::dispatch(new DirectLogin($user, true)); + event(new DirectLogin($user, true)); return new JsonResponse([ - 'data' => [ - 'complete' => true, - 'intended' => $this->redirectPath(), - 'user' => $user->toVueObject(), - ], + 'complete' => true, + 'methods' => [], + 'intended' => $this->redirectPath(), + 'user' => $user->toReactObject(), ]); } @@ -101,6 +99,6 @@ abstract class AbstractLoginController extends Controller */ protected function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = []) { - Event::dispatch(new Failed('auth', $user, $credentials)); + event(new Failed('auth', $user, $credentials)); } } diff --git a/app/Http/Controllers/Auth/LoginCheckpointController.php b/app/Http/Controllers/Auth/LoginCheckpointController.php index af05c55ef..f0b924a5c 100644 --- a/app/Http/Controllers/Auth/LoginCheckpointController.php +++ b/app/Http/Controllers/Auth/LoginCheckpointController.php @@ -5,14 +5,17 @@ namespace Pterodactyl\Http\Controllers\Auth; use Carbon\CarbonImmutable; use Carbon\CarbonInterface; use Pterodactyl\Models\User; +use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use PragmaRX\Google2FA\Google2FA; -use Illuminate\Support\Facades\Event; +use Pterodactyl\Models\SecurityKey; use Illuminate\Contracts\Encryption\Encrypter; +use Webauthn\PublicKeyCredentialRequestOptions; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Pterodactyl\Events\Auth\ProvidedAuthenticationToken; 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 { @@ -24,6 +27,7 @@ class LoginCheckpointController extends AbstractLoginController public function __construct( private Encrypter $encrypter, private Google2FA $google2FA, + private WebauthnServerRepository $webauthnServerRepository, private ValidationFactory $validation ) { parent::__construct(); @@ -34,13 +38,80 @@ class LoginCheckpointController extends AbstractLoginController * token. Once a user has reached this stage it is assumed that they have already * provided a valid username and password. * + * @return \Illuminate\Http\JsonResponse|void + * * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException - * @throws \Exception * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Exception */ - public function __invoke(LoginCheckpointRequest $request): JsonResponse + public function token(LoginCheckpointRequest $request) + { + $user = $this->extractUserFromRequest($request); + + // Recovery tokens go through a slightly different pathway for usage. + if (!is_null($recoveryToken = $request->input('recovery_token'))) { + if ($this->isValidRecoveryToken($user, $recoveryToken)) { + return $this->sendLoginResponse($user, $request); + } + } else { + if (!$user->use_totp) { + $this->sendFailedLoginResponse($request, $user); + } + + $decrypted = $this->encrypter->decrypt($user->totp_secret); + + if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { + return $this->sendLoginResponse($user, $request); + } + } + + $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. + * + * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function key(Request $request): JsonResponse + { + $options = $request->session()->get(SecurityKey::PK_SESSION_NAME); + if (!$options instanceof PublicKeyCredentialRequestOptions) { + throw new BadRequestHttpException('No security keys configured in session.'); + } + + $user = $this->extractUserFromRequest($request); + + try { + $source = $this->webauthnServerRepository->loadAndCheckAssertionResponse( + $user, + // TODO: we may have to `json_encode` this so it will be decoded properly. + $request->input('data'), + $options, + SecurityKey::getPsrRequestFactory($request) + ); + } catch (\Exception|\Throwable $e) { + throw $e; + } + + if (hash_equals($user->uuid, $source->getUserHandle())) { + return $this->sendLoginResponse($user, $request); + } + + throw new BadRequestHttpException('An unexpected error was encountered while validating that security key.'); + } + + /** + * Extracts the user from the session data using the provided confirmation token. + * + * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\DisplayException + */ + protected function extractUserFromRequest(Request $request): User { if ($this->hasTooManyLoginAttempts($request)) { $this->sendLockoutResponse($request); @@ -62,24 +133,7 @@ class LoginCheckpointController extends AbstractLoginController $this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE); } - // Recovery tokens go through a slightly different pathway for usage. - if (!is_null($recoveryToken = $request->input('recovery_token'))) { - if ($this->isValidRecoveryToken($user, $recoveryToken)) { - Event::dispatch(new ProvidedAuthenticationToken($user, true)); - - return $this->sendLoginResponse($user, $request); - } - } else { - $decrypted = $this->encrypter->decrypt($user->totp_secret); - - if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { - Event::dispatch(new ProvidedAuthenticationToken($user)); - - return $this->sendLoginResponse($user, $request); - } - } - - $this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null); + return $user; } /** @@ -101,14 +155,19 @@ class LoginCheckpointController extends AbstractLoginController return false; } + protected function hasValidSessionData(array $data): bool + { + return static::isValidSessionData($this->validation, $data); + } + /** * Determines if the data provided from the session is valid or not. This * will return false if the data is invalid, or if more time has passed than * was configured when the session was written. */ - protected function hasValidSessionData(array $data): bool + protected static function isValidSessionData(ValidationFactory $validation, array $data): bool { - $validator = $this->validation->make($data, [ + $validator = $validation->make($data, [ 'user_id' => 'required|integer|min:1', 'token_value' => 'required|string', 'expires_at' => 'required', diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 2dbb34ee3..9b7d7c7a4 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -9,15 +9,19 @@ use Pterodactyl\Models\User; use Illuminate\Http\JsonResponse; use Pterodactyl\Facades\Activity; use Illuminate\Contracts\View\View; -use Illuminate\Contracts\View\Factory as ViewFactory; +use Pterodactyl\Models\SecurityKey; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository; class LoginController extends AbstractLoginController { + private const METHOD_TOTP = 'totp'; + private const METHOD_WEBAUTHN = 'webauthn'; + /** * LoginController constructor. */ - public function __construct(private ViewFactory $view) + public function __construct(protected WebauthnServerRepository $webauthnServerRepository) { parent::__construct(); } @@ -29,7 +33,7 @@ class LoginController extends AbstractLoginController */ public function index(): View { - return $this->view->make('templates/auth.core'); + return view('templates/auth.core'); } /** @@ -37,6 +41,7 @@ class LoginController extends AbstractLoginController * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Illuminate\Validation\ValidationException + * @throws \Webauthn\Exception\InvalidDataException */ public function login(Request $request): JsonResponse { @@ -62,7 +67,9 @@ class LoginController extends AbstractLoginController $this->sendFailedLoginResponse($request, $user); } - if (!$user->use_totp) { + // Return early if the user does not have 2FA enabled, otherwise we will require them + // to complete a secondary challenge before they can log in. + if (!$user->has2FAEnabled()) { return $this->sendLoginResponse($user, $request); } @@ -74,11 +81,23 @@ class LoginController extends AbstractLoginController 'expires_at' => CarbonImmutable::now()->addMinutes(5), ]); - return new JsonResponse([ - 'data' => [ - 'complete' => false, - 'confirmation_token' => $token, - ], - ]); + $response = [ + 'complete' => false, + 'methods' => array_values(array_filter([ + $user->use_totp ? self::METHOD_TOTP : null, + $user->securityKeys->isNotEmpty() ? self::METHOD_WEBAUTHN : null, + ])), + 'confirm_token' => $token, + ]; + + if ($user->securityKeys->isNotEmpty()) { + $key = $this->webauthnServerRepository->generatePublicKeyCredentialRequestOptions($user); + + $request->session()->put(SecurityKey::PK_SESSION_NAME, $key); + + $request['webauthn'] = ['public_key' => $key]; + } + + return new JsonResponse($response); } } diff --git a/app/Http/Middleware/RequireTwoFactorAuthentication.php b/app/Http/Middleware/RequireTwoFactorAuthentication.php index e3307727f..2f966c4cc 100644 --- a/app/Http/Middleware/RequireTwoFactorAuthentication.php +++ b/app/Http/Middleware/RequireTwoFactorAuthentication.php @@ -50,7 +50,7 @@ class RequireTwoFactorAuthentication // send them right through, nothing else needs to be checked. // // If the level is set as admin and the user is not an admin, pass them through as well. - if ($level === self::LEVEL_NONE || $user->use_totp) { + if ($level === self::LEVEL_NONE || $user->has2FAEnabled()) { return $next($request); } elseif ($level === self::LEVEL_ADMIN && !$user->root_admin) { return $next($request); diff --git a/app/Http/Requests/Api/Client/Account/RegisterSecurityKeyRequest.php b/app/Http/Requests/Api/Client/Account/RegisterSecurityKeyRequest.php new file mode 100644 index 000000000..959a8951d --- /dev/null +++ b/app/Http/Requests/Api/Client/Account/RegisterSecurityKeyRequest.php @@ -0,0 +1,21 @@ + ['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'], + ]; + } +} diff --git a/app/Http/Requests/Api/Client/ClientApiRequest.php b/app/Http/Requests/Api/Client/ClientApiRequest.php index 5ae1680a4..8b703f059 100644 --- a/app/Http/Requests/Api/Client/ClientApiRequest.php +++ b/app/Http/Requests/Api/Client/ClientApiRequest.php @@ -23,7 +23,7 @@ class ClientApiRequest extends ApplicationApiRequest return $this->user()->can($this->permission(), $server); } - // If there is no server available on the reqest, trigger a failure since + // If there is no server available on the request, trigger a failure since // we expect there to be one at this point. return false; } diff --git a/app/Models/SecurityKey.php b/app/Models/SecurityKey.php new file mode 100644 index 000000000..9dc89543d --- /dev/null +++ b/app/Models/SecurityKey.php @@ -0,0 +1,125 @@ + 'int', + 'transports' => 'array', + 'other_ui' => 'array', + ]; + + protected $guarded = [ + 'uuid', + 'user_id', + ]; + + public function getPublicKeyAttribute(string $value): string + { + return base64_decode($value); + } + + public function setPublicKeyAttribute(string $value): void + { + $this->attributes['public_key'] = base64_encode($value); + } + + public function getPublicKeyIdAttribute(string $value): string + { + return base64_decode($value); + } + + public function setPublicKeyIdAttribute(string $value): void + { + $this->attributes['public_key_id'] = base64_encode($value); + } + + public function getTrustPathAttribute(?string $value): ?TrustPath + { + if (is_null($value)) { + return null; + } + + return TrustPathLoader::loadTrustPath(json_decode($value, true)); + } + + public function setTrustPathAttribute(?TrustPath $value): void + { + $this->attributes['trust_path'] = json_encode($value); + } + + /** + * @param \Ramsey\Uuid\UuidInterface|string|null $value + */ + public function setAaguidAttribute($value): void + { + $value = $value instanceof UuidInterface ? $value->__toString() : $value; + + $this->attributes['aaguid'] = (is_null($value) || $value === Uuid::NIL) ? null : $value; + } + + public function getAaguidAttribute(?string $value): ?UuidInterface + { + if (!is_null($value) && Uuid::isValid($value)) { + return Uuid::fromString($value); + } + + return null; + } + + public function getPublicKeyCredentialDescriptor(): PublicKeyCredentialDescriptor + { + return new PublicKeyCredentialDescriptor($this->type, $this->public_key_id, $this->transports); + } + + public function getPublicKeyCredentialSource(): PublicKeyCredentialSource + { + return new PublicKeyCredentialSource( + $this->public_key_id, + $this->type, + $this->transports, + $this->attestation_type, + $this->trust_path, + $this->aaguid ?? Uuid::fromString(Uuid::NIL), + $this->public_key, + $this->user_handle, + $this->counter + ); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Returns a PSR17 Request factory to be used by different Webauthn tooling. + */ + public static function getPsrRequestFactory(Request $request): ServerRequestInterface + { + $factory = new Psr17Factory(); + + $httpFactory = new PsrHttpFactory($factory, $factory, $factory, $factory); + + return $httpFactory->createRequest($request); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index df8271cf4..31be69412 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,6 +9,7 @@ use Illuminate\Validation\Rules\In; 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 Pterodactyl\Traits\Helpers\AvailableLanguages; @@ -47,6 +48,8 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification; * @property int|null $notifications_count * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\RecoveryToken[] $recoveryTokens * @property int|null $recovery_tokens_count + * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\SecurityKey[] $securityKeys + * @property int|null $security_keys_count * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Server[] $servers * @property int|null $servers_count * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\UserSSHKey[] $sshKeys @@ -186,9 +189,9 @@ class User extends Model implements } /** - * Return the user model in a format that can be passed over to Vue templates. + * Return the user model in a format that can be passed over to React templates. */ - public function toVueObject(): array + public function toReactObject(): array { return Collection::make($this->toArray())->except(['id', 'external_id'])->toArray(); } @@ -248,6 +251,11 @@ class User extends Model implements return $this->hasMany(UserSSHKey::class); } + public function securityKeys(): HasMany + { + return $this->hasMany(SecurityKey::class); + } + /** * Returns all the activity logs where this user is the subject — not to * be confused by activity logs where this user is the _actor_. @@ -271,4 +279,17 @@ class User extends Model implements }) ->groupBy('servers.id'); } + + public function toPublicKeyCredentialEntity(): PublicKeyCredentialUserEntity + { + return PublicKeyCredentialUserEntity::create($this->username, $this->uuid, $this->email); + } + + /** + * Returns true if the user has two-factor authentication enabled. + */ + public function has2FAEnabled(): bool + { + return $this->use_totp || $this->securityKeys->isNotEmpty(); + } } diff --git a/app/Repositories/SecurityKeys/PublicKeyCredentialSourceRepository.php b/app/Repositories/SecurityKeys/PublicKeyCredentialSourceRepository.php new file mode 100644 index 000000000..6715eb167 --- /dev/null +++ b/app/Repositories/SecurityKeys/PublicKeyCredentialSourceRepository.php @@ -0,0 +1,68 @@ +user = $user; + } + + /** + * Find a single hardware security token for a user by using the credential ID. + */ + public function findOneByCredentialId(string $id): ?PublicKeyCredentialSource + { + /** @var \Pterodactyl\Models\SecurityKey $key */ + $key = $this->user->securityKeys() + ->where('public_key_id', base64_encode($id)) + ->first(); + + return optional($key)->getPublicKeyCredentialSource(); + } + + /** + * Find all the hardware tokens that exist for the user using the given + * entity handle. + */ + public function findAllForUserEntity(PublicKeyCredentialUserEntity $entity): array + { + $results = $this->user->securityKeys() + ->where('user_handle', $entity->getId()) + ->get(); + + return $results->map(function (SecurityKey $key) { + return $key->getPublicKeyCredentialSource(); + })->values()->toArray(); + } + + /** + * Save a credential to the database and link it with the user. + * + * @throws \Throwable + */ + public function saveCredentialSource(PublicKeyCredentialSource $source): void + { + // no-op — we handle creation of the keys in StoreSecurityKeyService + // + // If you put logic in here it is triggered on each login. + } + + /** + * 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]); + } +} diff --git a/app/Repositories/SecurityKeys/WebauthnServerRepository.php b/app/Repositories/SecurityKeys/WebauthnServerRepository.php new file mode 100644 index 000000000..935cd2cda --- /dev/null +++ b/app/Repositories/SecurityKeys/WebauthnServerRepository.php @@ -0,0 +1,161 @@ +publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository; + + $this->rpEntity = new PublicKeyCredentialRpEntity(config('app.name'), trim($url, '/')); + $this->credentialLoader = new PublicKeyCredentialLoader(new AttestationObjectLoader(new AttestationStatementSupportManager())); + $this->assertionValidator = new AuthenticatorAssertionResponseValidator( + $this->publicKeyCredentialSourceRepository, + null, + ExtensionOutputCheckerHandler::create(), + AlgorithmManager::create(), + ); + $this->attestationValidator = new AuthenticatorAttestationResponseValidator( + new AttestationStatementSupportManager(), + $this->publicKeyCredentialSourceRepository, + null, + new ExtensionOutputCheckerHandler(), + ); + } + + /** + * @throws \Webauthn\Exception\InvalidDataException + */ + public function getPublicKeyCredentialCreationOptions(User $user): PublicKeyCredentialCreationOptions + { + $excluded = $user->securityKeys->map(function (SecurityKey $key) { + return $key->getPublicKeyCredentialDescriptor(); + })->values()->toArray(); + + $challenge = Str::random(16); + + return (new PublicKeyCredentialCreationOptions( + $this->rpEntity, + $user->toPublicKeyCredentialEntity(), + $challenge, + [ + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256K), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES384), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES512), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS256), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS384), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS512), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS256), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS384), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS512), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ED256), + PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ED512), + ], + )) + ->setTimeout(30_000) + ->excludeCredentials(...$excluded) + ->setAttestation(PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) + ->setAuthenticatorSelection(AuthenticatorSelectionCriteria::create()); + } + + /** + * @throws \Webauthn\Exception\InvalidDataException + */ + public function generatePublicKeyCredentialRequestOptions(User $user): PublicKeyCredentialRequestOptions + { + $allowedCredentials = $user->securityKeys->map(function (SecurityKey $key) { + return $key->getPublicKeyCredentialDescriptor(); + })->values()->toArray(); + + return (new PublicKeyCredentialRequestOptions(Str::random(32))) + ->allowCredentials(...$allowedCredentials) + ->setUserVerification(PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED); + } + + /** + * @throws \Throwable + * @throws \JsonException + */ + public function loadAndCheckAssertionResponse( + User $user, + array $data, + PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, + ServerRequestInterface $request + ): PublicKeyCredentialSource { + $credential = $this->credentialLoader->loadArray($data); + + $authenticatorAssertionResponse = $credential->getResponse(); + if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) { + // TODO + throw new \Exception(''); + } + + return $this->assertionValidator->check( + $credential->getRawId(), + $authenticatorAssertionResponse, + $publicKeyCredentialRequestOptions, + $request, + null, // TODO: use handle? +// $user->toPublicKeyCredentialEntity() + ); + } + + /** + * Register a new security key for a user. + * + * @throws \Throwable + * @throws \JsonException + */ + public function loadAndCheckAttestationResponse( + User $user, + array $data, + PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, + ServerRequestInterface $request + ): PublicKeyCredentialSource { + $credential = $this->credentialLoader->loadArray($data); + + $authenticatorAttestationResponse = $credential->getResponse(); + if (!$authenticatorAttestationResponse instanceof AuthenticatorAttestationResponse) { + // TODO + throw new \Exception(''); + } + + return $this->attestationValidator->check( + $authenticatorAttestationResponse, + $publicKeyCredentialCreationOptions, + $request, + ); + } +} diff --git a/app/Services/Users/SecurityKeys/CreatePublicKeyCredentialService.php b/app/Services/Users/SecurityKeys/CreatePublicKeyCredentialService.php new file mode 100644 index 000000000..2fbf7668e --- /dev/null +++ b/app/Services/Users/SecurityKeys/CreatePublicKeyCredentialService.php @@ -0,0 +1,25 @@ +webauthnServerRepository = $webauthnServerRepository; + } + + /** + * @throws \Webauthn\Exception\InvalidDataException + */ + public function handle(User $user): PublicKeyCredentialCreationOptions + { + return $this->webauthnServerRepository->getPublicKeyCredentialCreationOptions($user); + } +} diff --git a/app/Services/Users/SecurityKeys/StoreSecurityKeyService.php b/app/Services/Users/SecurityKeys/StoreSecurityKeyService.php new file mode 100644 index 000000000..c7fc74038 --- /dev/null +++ b/app/Services/Users/SecurityKeys/StoreSecurityKeyService.php @@ -0,0 +1,79 @@ +request = $request; + + return $this; + } + + /** + * Sets the security key's name. If not provided a random string will be used. + */ + public function setKeyName(?string $name): self + { + $this->keyName = $name; + + return $this; + } + + /** + * Validates and stores a new hardware security key on a user's account. + * + * @throws \Throwable + */ + public function handle(User $user, array $registration, PublicKeyCredentialCreationOptions $options): SecurityKey + { + Assert::notNull($this->request, 'A request interface must be set on the service before it can be called.'); + + $source = $this->webauthnServerRepository->loadAndCheckAttestationResponse($user, $registration, $options, $this->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. + /** @var \Pterodactyl\Models\SecurityKey $key */ + $key = $user->securityKeys()->make()->forceFill([ + 'uuid' => Uuid::uuid4(), + 'name' => $this->keyName ?? 'Security Key (' . Str::random() . ')', + 'public_key_id' => $source->getPublicKeyCredentialId(), + 'public_key' => $source->getCredentialPublicKey(), + 'aaguid' => $source->getAaguid(), + 'type' => $source->getType(), + 'transports' => $source->getTransports(), + 'attestation_type' => $source->getAttestationType(), + 'trust_path' => $source->getTrustPath(), + 'user_handle' => $user->uuid, + 'counter' => $source->getCounter(), + 'other_ui' => $source->getOtherUI(), + ]); + + $key->saveOrFail(); + + return $key; + } +} diff --git a/app/Transformers/Api/Client/SecurityKeyTransformer.php b/app/Transformers/Api/Client/SecurityKeyTransformer.php new file mode 100644 index 000000000..aaf36fca5 --- /dev/null +++ b/app/Transformers/Api/Client/SecurityKeyTransformer.php @@ -0,0 +1,25 @@ + $model->uuid, + 'name' => $model->name, + 'type' => $model->type, + 'public_key_id' => base64_encode($model->public_key_id), + 'created_at' => self::formatTimestamp($model->created_at), + 'updated_at' => self::formatTimestamp($model->updated_at), + ]; + } +} diff --git a/composer.json b/composer.json index 160537b6b..cadbabb62 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "league/flysystem-aws-s3-v3": "~3.5.0", "league/flysystem-memory": "~3.3.0", "matriphe/iso-639": "~1.2", + "nyholm/psr7": "~1.5", "phpseclib/phpseclib": "~3.0", "pragmarx/google2fa": "~5.0.0", "predis/predis": "~2.0.2", @@ -50,7 +51,9 @@ "symfony/http-client": "~6.0", "symfony/mailgun-mailer": "~6.0", "symfony/postmark-mailer": "~6.0", + "symfony/psr-http-message-bridge": "~2.1", "symfony/yaml": "~5.4", + "web-auth/webauthn-lib": "~4.3", "webmozart/assert": "~1.11" }, "require-dev": { @@ -100,7 +103,7 @@ "preferred-install": "dist", "sort-packages": true, "platform": { - "php": "8.0.2" + "php": "8.1.0" } } } diff --git a/composer.lock b/composer.lock index 9f4b73f6f..aa7a2ee8d 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "ae61e7d6e405e3a59c8a54f3eefa2c50", + "content-hash": "1e13394cf95fb955fbf12cb9f71e215d", "packages": [ { "name": "aws/aws-crt-php", @@ -208,16 +208,16 @@ }, { "name": "dflydev/dot-access-data", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "0992cc19268b259a39e86f296da5f0677841f42c" + "reference": "f41715465d65213d644d3141a6a93081be5d3549" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/0992cc19268b259a39e86f296da5f0677841f42c", - "reference": "0992cc19268b259a39e86f296da5f0677841f42c", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", + "reference": "f41715465d65213d644d3141a6a93081be5d3549", "shasum": "" }, "require": { @@ -228,7 +228,7 @@ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", "scrutinizer/ocular": "1.6.0", "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^3.14" + "vimeo/psalm": "^4.0.0" }, "type": "library", "extra": { @@ -277,9 +277,9 @@ ], "support": { "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", - "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.1" + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2" }, - "time": "2021-08-13T13:06:58+00:00" + "time": "2022-10-27T11:44:00+00:00" }, { "name": "doctrine/cache", @@ -916,6 +916,81 @@ ], "time": "2022-06-18T20:57:19+00:00" }, + { + "name": "fgrosse/phpasn1", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/fgrosse/PHPASN1.git", + "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/eef488991d53e58e60c9554b09b1201ca5ba9296", + "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296", + "shasum": "" + }, + "require": { + "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "~2.0", + "phpunit/phpunit": "^6.3 || ^7.0 || ^8.0" + }, + "suggest": { + "ext-bcmath": "BCmath is the fallback extension for big integer calculations", + "ext-curl": "For loading OID information from the web if they have not bee defined statically", + "ext-gmp": "GMP is the preferred extension for big integer calculations", + "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "FG\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Friedrich Große", + "email": "friedrich.grosse@gmail.com", + "homepage": "https://github.com/FGrosse", + "role": "Author" + }, + { + "name": "All contributors", + "homepage": "https://github.com/FGrosse/PHPASN1/contributors" + } + ], + "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", + "homepage": "https://github.com/FGrosse/PHPASN1", + "keywords": [ + "DER", + "asn.1", + "asn1", + "ber", + "binary", + "decoding", + "encoding", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/fgrosse/PHPASN1/issues", + "source": "https://github.com/fgrosse/PHPASN1/tree/v2.4.0" + }, + "time": "2021-12-11T12:41:06+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.2.0", @@ -1263,16 +1338,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.4.2", + "version": "2.4.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "3148458748274be1546f8f2809a6c09fe66f44aa" + "reference": "67c26b443f348a51926030c83481b85718457d3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/3148458748274be1546f8f2809a6c09fe66f44aa", - "reference": "3148458748274be1546f8f2809a6c09fe66f44aa", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d", + "reference": "67c26b443f348a51926030c83481b85718457d3d", "shasum": "" }, "require": { @@ -1362,7 +1437,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.2" + "source": "https://github.com/guzzle/psr7/tree/2.4.3" }, "funding": [ { @@ -1378,7 +1453,7 @@ "type": "tidelift" } ], - "time": "2022-10-25T13:49:28+00:00" + "time": "2022-10-26T14:07:24+00:00" }, { "name": "hashids/hashids", @@ -2144,16 +2219,16 @@ }, { "name": "league/commonmark", - "version": "2.3.5", + "version": "2.3.6", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "84d74485fdb7074f4f9dd6f02ab957b1de513257" + "reference": "857afc47ce113454bd629037213378ba3219dd40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/84d74485fdb7074f4f9dd6f02ab957b1de513257", - "reference": "84d74485fdb7074f4f9dd6f02ab957b1de513257", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/857afc47ce113454bd629037213378ba3219dd40", + "reference": "857afc47ce113454bd629037213378ba3219dd40", "shasum": "" }, "require": { @@ -2173,7 +2248,7 @@ "erusev/parsedown": "^1.0", "ext-json": "*", "github/gfm": "0.29.0", - "michelf/php-markdown": "^1.4", + "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21", @@ -2246,7 +2321,7 @@ "type": "tidelift" } ], - "time": "2022-07-29T10:59:45+00:00" + "time": "2022-10-30T16:45:38+00:00" }, { "name": "league/config", @@ -3198,16 +3273,16 @@ }, { "name": "nunomaduro/termwind", - "version": "v1.14.1", + "version": "v1.14.2", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "86fc30eace93b9b6d4c844ba6de76db84184e01b" + "reference": "9a8218511eb1a0965629ff820dda25985440aefc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/86fc30eace93b9b6d4c844ba6de76db84184e01b", - "reference": "86fc30eace93b9b6d4c844ba6de76db84184e01b", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/9a8218511eb1a0965629ff820dda25985440aefc", + "reference": "9a8218511eb1a0965629ff820dda25985440aefc", "shasum": "" }, "require": { @@ -3264,7 +3339,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v1.14.1" + "source": "https://github.com/nunomaduro/termwind/tree/v1.14.2" }, "funding": [ { @@ -3280,7 +3355,84 @@ "type": "github" } ], - "time": "2022-10-17T15:20:29+00:00" + "time": "2022-10-28T22:51:32+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "f734364e38a876a23be4d906a2a089e1315be18a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/f734364e38a876a23be4d906a2a089e1315be18a", + "reference": "f734364e38a876a23be4d906a2a089e1315be18a", + "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.5.1" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2022-06-22T07:13:36+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -3399,6 +3551,60 @@ }, "time": "2020-10-15T08:29:30+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.9.0", @@ -4816,6 +5022,186 @@ ], "time": "2022-07-29T14:19:59+00:00" }, + { + "name": "spomky-labs/cbor-php", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "de06c1be866bade5270f4cf5ef3b3746ba3f26eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/de06c1be866bade5270f4cf5ef3b3746ba3f26eb", + "reference": "de06c1be866bade5270f4cf5ef3b3746ba3f26eb", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10", + "ext-mbstring": "*", + "php": ">=8.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-json": "*", + "infection/infection": "^0.26", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13", + "roave/security-advisories": "dev-latest", + "symfony/var-dumper": "^6.0", + "symplify/easy-coding-standard": "^11.0" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", + "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "CBOR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + } + ], + "description": "CBOR Encoder/Decoder for PHP", + "keywords": [ + "Concise Binary Object Representation", + "RFC7049", + "cbor" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/cbor-php/issues", + "source": "https://github.com/Spomky-Labs/cbor-php/tree/v3.0.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-06-26T07:20:40+00:00" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "4dc75ffcdaad63b3512c30bdae8a8d862cf1b2cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/4dc75ffcdaad63b3512c30bdae8a8d862cf1b2cb", + "reference": "4dc75ffcdaad63b3512c30bdae8a8d862cf1b2cb", + "shasum": "" + }, + "require": { + "brick/math": "^0.10", + "ext-mbstring": "*", + "php": ">=8.1" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-gmp": "*", + "infection/infection": "^0.26", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^9.5.5", + "rector/rector": "^0.14", + "roave/security-advisories": "dev-latest", + "symfony/phpunit-bridge": "^6.1", + "symfony/var-dumper": "^6.1", + "symplify/easy-coding-standard": "^11.1", + "thecodingmachine/phpstan-safe-rule": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "SpomkyLabs\\Pki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" + } + ], + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-08-22T11:26:16+00:00" + }, { "name": "staudenmeir/belongs-to-through", "version": "v2.12.1", @@ -4919,20 +5305,21 @@ }, { "name": "symfony/console", - "version": "v6.0.14", + "version": "v6.1.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1f89cab8d52c84424f798495b3f10342a7b1a070" + "reference": "a1282bd0c096e0bdb8800b104177e2ce404d8815" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1f89cab8d52c84424f798495b3f10342a7b1a070", - "reference": "1f89cab8d52c84424f798495b3f10342a7b1a070", + "url": "https://api.github.com/repos/symfony/console/zipball/a1282bd0c096e0bdb8800b104177e2ce404d8815", + "reference": "a1282bd0c096e0bdb8800b104177e2ce404d8815", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^1.1|^2|^3", "symfony/string": "^5.4|^6.0" @@ -4994,7 +5381,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.0.14" + "source": "https://github.com/symfony/console/tree/v6.1.7" }, "funding": [ { @@ -5010,24 +5397,24 @@ "type": "tidelift" } ], - "time": "2022-10-07T08:02:12+00:00" + "time": "2022-10-26T21:42:49+00:00" }, { "name": "symfony/css-selector", - "version": "v6.0.11", + "version": "v6.1.3", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "ab2746acddc4f03a7234c8441822ac5d5c63efe9" + "reference": "0dd5e36b80e1de97f8f74ed7023ac2b837a36443" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/ab2746acddc4f03a7234c8441822ac5d5c63efe9", - "reference": "ab2746acddc4f03a7234c8441822ac5d5c63efe9", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/0dd5e36b80e1de97f8f74ed7023ac2b837a36443", + "reference": "0dd5e36b80e1de97f8f74ed7023ac2b837a36443", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "type": "library", "autoload": { @@ -5059,7 +5446,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.0.11" + "source": "https://github.com/symfony/css-selector/tree/v6.1.3" }, "funding": [ { @@ -5075,29 +5462,29 @@ "type": "tidelift" } ], - "time": "2022-06-27T17:10:44+00:00" + "time": "2022-06-27T17:24:16+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.0.2", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c" + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", - "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.1-dev" }, "thanks": { "name": "symfony/contracts", @@ -5126,7 +5513,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1" }, "funding": [ { @@ -5142,24 +5529,24 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2022-02-25T11:15:52+00:00" }, { "name": "symfony/error-handler", - "version": "v6.0.14", + "version": "v6.1.7", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "81e57c793d9a573f29f8b5296d5d8ee4602badcb" + "reference": "699a26ce5ec656c198bf6e26398b0f0818c7e504" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/81e57c793d9a573f29f8b5296d5d8ee4602badcb", - "reference": "81e57c793d9a573f29f8b5296d5d8ee4602badcb", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/699a26ce5ec656c198bf6e26398b0f0818c7e504", + "reference": "699a26ce5ec656c198bf6e26398b0f0818c7e504", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/var-dumper": "^5.4|^6.0" }, @@ -5197,7 +5584,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.0.14" + "source": "https://github.com/symfony/error-handler/tree/v6.1.7" }, "funding": [ { @@ -5213,24 +5600,24 @@ "type": "tidelift" } ], - "time": "2022-10-07T08:02:12+00:00" + "time": "2022-10-28T16:23:08+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.0.9", + "version": "v6.1.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "5c85b58422865d42c6eb46f7693339056db098a8" + "reference": "a0449a7ad7daa0f7c0acd508259f80544ab5a347" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5c85b58422865d42c6eb46f7693339056db098a8", - "reference": "5c85b58422865d42c6eb46f7693339056db098a8", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a0449a7ad7daa0f7c0acd508259f80544ab5a347", + "reference": "a0449a7ad7daa0f7c0acd508259f80544ab5a347", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/event-dispatcher-contracts": "^2|^3" }, "conflict": { @@ -5280,7 +5667,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.0.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.1.0" }, "funding": [ { @@ -5296,24 +5683,24 @@ "type": "tidelift" } ], - "time": "2022-05-05T16:45:52+00:00" + "time": "2022-05-05T16:51:07+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.0.2", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7bc61cc2db649b4637d331240c5346dcc7708051" + "reference": "02ff5eea2f453731cfbc6bc215e456b781480448" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7bc61cc2db649b4637d331240c5346dcc7708051", - "reference": "7bc61cc2db649b4637d331240c5346dcc7708051", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/02ff5eea2f453731cfbc6bc215e456b781480448", + "reference": "02ff5eea2f453731cfbc6bc215e456b781480448", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/event-dispatcher": "^1" }, "suggest": { @@ -5322,7 +5709,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.1-dev" }, "thanks": { "name": "symfony/contracts", @@ -5359,7 +5746,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.1.1" }, "funding": [ { @@ -5375,24 +5762,27 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2022-02-25T11:15:52+00:00" }, { "name": "symfony/finder", - "version": "v6.0.11", + "version": "v6.1.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "09cb683ba5720385ea6966e5e06be2a34f2568b1" + "reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/09cb683ba5720385ea6966e5e06be2a34f2568b1", - "reference": "09cb683ba5720385ea6966e5e06be2a34f2568b1", + "url": "https://api.github.com/repos/symfony/finder/zipball/39696bff2c2970b3779a5cac7bf9f0b88fc2b709", + "reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0" }, "type": "library", "autoload": { @@ -5420,7 +5810,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.0.11" + "source": "https://github.com/symfony/finder/tree/v6.1.3" }, "funding": [ { @@ -5436,24 +5826,24 @@ "type": "tidelift" } ], - "time": "2022-07-29T07:39:48+00:00" + "time": "2022-07-29T07:42:06+00:00" }, { "name": "symfony/http-client", - "version": "v6.0.14", + "version": "v6.1.7", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "ec183a587e3ad47f03cf1572d4b8437e0fc3e923" + "reference": "f515d066728774efb34347a87580621416ca8968" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/ec183a587e3ad47f03cf1572d4b8437e0fc3e923", - "reference": "ec183a587e3ad47f03cf1572d4b8437e0fc3e923", + "url": "https://api.github.com/repos/symfony/http-client/zipball/f515d066728774efb34347a87580621416ca8968", + "reference": "f515d066728774efb34347a87580621416ca8968", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/http-client-contracts": "^3", "symfony/service-contracts": "^1.0|^2|^3" @@ -5504,7 +5894,7 @@ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-client/tree/v6.0.14" + "source": "https://github.com/symfony/http-client/tree/v6.1.7" }, "funding": [ { @@ -5520,24 +5910,24 @@ "type": "tidelift" } ], - "time": "2022-10-11T15:20:43+00:00" + "time": "2022-10-28T16:23:08+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.0.2", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "4184b9b63af1edaf35b6a7974c6f1f9f33294129" + "reference": "fd038f08c623ab5d22b26e9ba35afe8c79071800" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4184b9b63af1edaf35b6a7974c6f1f9f33294129", - "reference": "4184b9b63af1edaf35b6a7974c6f1f9f33294129", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/fd038f08c623ab5d22b26e9ba35afe8c79071800", + "reference": "fd038f08c623ab5d22b26e9ba35afe8c79071800", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "suggest": { "symfony/http-client-implementation": "" @@ -5545,7 +5935,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.1-dev" }, "thanks": { "name": "symfony/contracts", @@ -5555,7 +5945,10 @@ "autoload": { "psr-4": { "Symfony\\Contracts\\HttpClient\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5582,7 +5975,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.1.1" }, "funding": [ { @@ -5598,24 +5991,24 @@ "type": "tidelift" } ], - "time": "2022-04-12T16:11:42+00:00" + "time": "2022-04-22T07:30:54+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.0.14", + "version": "v6.1.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e8aa505d35660877e6695d68be53df2ceac7cf57" + "reference": "792a1856d2b95273f0e1c3435785f1d01a60ecc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e8aa505d35660877e6695d68be53df2ceac7cf57", - "reference": "e8aa505d35660877e6695d68be53df2ceac7cf57", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/792a1856d2b95273f0e1c3435785f1d01a60ecc6", + "reference": "792a1856d2b95273f0e1c3435785f1d01a60ecc6", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.1" }, @@ -5657,7 +6050,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.0.14" + "source": "https://github.com/symfony/http-foundation/tree/v6.1.7" }, "funding": [ { @@ -5673,26 +6066,26 @@ "type": "tidelift" } ], - "time": "2022-10-02T08:16:40+00:00" + "time": "2022-10-12T09:44:59+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.0.14", + "version": "v6.1.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f9fc93c4f12e2fd7dea37f7b5840deb34e9037fc" + "reference": "8fc1ffe753948c47a103a809cdd6a4a8458b3254" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f9fc93c4f12e2fd7dea37f7b5840deb34e9037fc", - "reference": "f9fc93c4f12e2fd7dea37f7b5840deb34e9037fc", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/8fc1ffe753948c47a103a809cdd6a4a8458b3254", + "reference": "8fc1ffe753948c47a103a809cdd6a4a8458b3254", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/log": "^1|^2|^3", - "symfony/error-handler": "^5.4|^6.0", + "symfony/error-handler": "^6.1", "symfony/event-dispatcher": "^5.4|^6.0", "symfony/http-foundation": "^5.4|^6.0", "symfony/polyfill-ctype": "^1.8" @@ -5700,9 +6093,9 @@ "conflict": { "symfony/browser-kit": "<5.4", "symfony/cache": "<5.4", - "symfony/config": "<5.4", + "symfony/config": "<6.1", "symfony/console": "<5.4", - "symfony/dependency-injection": "<5.4", + "symfony/dependency-injection": "<6.1", "symfony/doctrine-bridge": "<5.4", "symfony/form": "<5.4", "symfony/http-client": "<5.4", @@ -5719,10 +6112,10 @@ "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", "symfony/browser-kit": "^5.4|^6.0", - "symfony/config": "^5.4|^6.0", + "symfony/config": "^6.1", "symfony/console": "^5.4|^6.0", "symfony/css-selector": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", + "symfony/dependency-injection": "^6.1", "symfony/dom-crawler": "^5.4|^6.0", "symfony/expression-language": "^5.4|^6.0", "symfony/finder": "^5.4|^6.0", @@ -5732,6 +6125,7 @@ "symfony/stopwatch": "^5.4|^6.0", "symfony/translation": "^5.4|^6.0", "symfony/translation-contracts": "^1.1|^2|^3", + "symfony/uid": "^5.4|^6.0", "twig/twig": "^2.13|^3.0.4" }, "suggest": { @@ -5766,7 +6160,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.0.14" + "source": "https://github.com/symfony/http-kernel/tree/v6.1.7" }, "funding": [ { @@ -5782,25 +6176,25 @@ "type": "tidelift" } ], - "time": "2022-10-12T07:43:45+00:00" + "time": "2022-10-28T18:06:36+00:00" }, { "name": "symfony/mailer", - "version": "v6.0.13", + "version": "v6.1.7", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "6269c872ab4792e8facbf8af27a2fbee8429f217" + "reference": "7e19813c0b43387c55665780c4caea505cc48391" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/6269c872ab4792e8facbf8af27a2fbee8429f217", - "reference": "6269c872ab4792e8facbf8af27a2fbee8429f217", + "url": "https://api.github.com/repos/symfony/mailer/zipball/7e19813c0b43387c55665780c4caea505cc48391", + "reference": "7e19813c0b43387c55665780c4caea505cc48391", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3", - "php": ">=8.0.2", + "php": ">=8.1", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", "symfony/event-dispatcher": "^5.4|^6.0", @@ -5840,7 +6234,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.0.13" + "source": "https://github.com/symfony/mailer/tree/v6.1.7" }, "funding": [ { @@ -5856,24 +6250,24 @@ "type": "tidelift" } ], - "time": "2022-08-29T06:49:22+00:00" + "time": "2022-10-28T16:23:08+00:00" }, { "name": "symfony/mailgun-mailer", - "version": "v6.0.7", + "version": "v6.1.0", "source": { "type": "git", "url": "https://github.com/symfony/mailgun-mailer.git", - "reference": "f0d032c26683b26f4bc26864e09b1e08fa55226e" + "reference": "44d3c15049d84f5165917a6190f06adbe64d71dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/f0d032c26683b26f4bc26864e09b1e08fa55226e", - "reference": "f0d032c26683b26f4bc26864e09b1e08fa55226e", + "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/44d3c15049d84f5165917a6190f06adbe64d71dd", + "reference": "44d3c15049d84f5165917a6190f06adbe64d71dd", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/mailer": "^5.4|^6.0" }, "require-dev": { @@ -5905,7 +6299,7 @@ "description": "Symfony Mailgun Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailgun-mailer/tree/v6.0.7" + "source": "https://github.com/symfony/mailgun-mailer/tree/v6.1.0" }, "funding": [ { @@ -5921,24 +6315,24 @@ "type": "tidelift" } ], - "time": "2022-03-24T17:11:42+00:00" + "time": "2022-04-01T07:15:35+00:00" }, { "name": "symfony/mime", - "version": "v6.0.14", + "version": "v6.1.7", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "c01b88b63418131daf2edd0bdc17fc8a6d1c939a" + "reference": "f440f066d57691088d998d6e437ce98771144618" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/c01b88b63418131daf2edd0bdc17fc8a6d1c939a", - "reference": "c01b88b63418131daf2edd0bdc17fc8a6d1c939a", + "url": "https://api.github.com/repos/symfony/mime/zipball/f440f066d57691088d998d6e437ce98771144618", + "reference": "f440f066d57691088d998d6e437ce98771144618", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -5946,8 +6340,7 @@ "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<5.4", - "symfony/serializer": "<5.4.14|>=6.0,<6.0.14|>=6.1,<6.1.6" + "symfony/mailer": "<5.4" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1", @@ -5955,7 +6348,7 @@ "symfony/dependency-injection": "^5.4|^6.0", "symfony/property-access": "^5.4|^6.0", "symfony/property-info": "^5.4|^6.0", - "symfony/serializer": "^5.4.14|~6.0.14|^6.1.6" + "symfony/serializer": "^5.2|^6.0" }, "type": "library", "autoload": { @@ -5987,7 +6380,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.0.14" + "source": "https://github.com/symfony/mime/tree/v6.1.7" }, "funding": [ { @@ -6003,7 +6396,7 @@ "type": "tidelift" } ], - "time": "2022-10-07T08:02:12+00:00" + "time": "2022-10-19T08:10:53+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6812,20 +7205,20 @@ }, { "name": "symfony/postmark-mailer", - "version": "v6.0.7", + "version": "v6.1.0", "source": { "type": "git", "url": "https://github.com/symfony/postmark-mailer.git", - "reference": "8405569233efb0140e55eb6236c9e55693f058ff" + "reference": "5f38d688df43bea507bf0dfe0d0ca6f99221b708" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/postmark-mailer/zipball/8405569233efb0140e55eb6236c9e55693f058ff", - "reference": "8405569233efb0140e55eb6236c9e55693f058ff", + "url": "https://api.github.com/repos/symfony/postmark-mailer/zipball/5f38d688df43bea507bf0dfe0d0ca6f99221b708", + "reference": "5f38d688df43bea507bf0dfe0d0ca6f99221b708", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/mailer": "^5.4|^6.0" }, "require-dev": { @@ -6857,7 +7250,7 @@ "description": "Symfony Postmark Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/postmark-mailer/tree/v6.0.7" + "source": "https://github.com/symfony/postmark-mailer/tree/v6.1.0" }, "funding": [ { @@ -6873,24 +7266,24 @@ "type": "tidelift" } ], - "time": "2022-03-24T17:11:42+00:00" + "time": "2022-04-01T07:15:35+00:00" }, { "name": "symfony/process", - "version": "v6.0.11", + "version": "v6.1.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "44270a08ccb664143dede554ff1c00aaa2247a43" + "reference": "a6506e99cfad7059b1ab5cab395854a0a0c21292" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/44270a08ccb664143dede554ff1c00aaa2247a43", - "reference": "44270a08ccb664143dede554ff1c00aaa2247a43", + "url": "https://api.github.com/repos/symfony/process/zipball/a6506e99cfad7059b1ab5cab395854a0a0c21292", + "reference": "a6506e99cfad7059b1ab5cab395854a0a0c21292", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "type": "library", "autoload": { @@ -6918,7 +7311,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.0.11" + "source": "https://github.com/symfony/process/tree/v6.1.3" }, "funding": [ { @@ -6934,24 +7327,112 @@ "type": "tidelift" } ], - "time": "2022-06-27T17:10:44+00:00" + "time": "2022-06-27T17:24:16+00:00" }, { - "name": "symfony/routing", - "version": "v6.0.11", + "name": "symfony/psr-http-message-bridge", + "version": "v2.1.3", "source": { "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "434b64f7d3a582ec33fcf69baaf085473e67d639" + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "d444f85dddf65c7e57c58d8e5b3a4dbb593b1840" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/434b64f7d3a582ec33fcf69baaf085473e67d639", - "reference": "434b64f7d3a582ec33fcf69baaf085473e67d639", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/d444f85dddf65c7e57c58d8e5b3a4dbb593b1840", + "reference": "d444f85dddf65c7e57c58d8e5b3a4dbb593b1840", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=7.1", + "psr/http-message": "^1.0", + "symfony/http-foundation": "^4.4 || ^5.0 || ^6.0" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "psr/log": "^1.1 || ^2 || ^3", + "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0", + "symfony/config": "^4.4 || ^5.0 || ^6.0", + "symfony/event-dispatcher": "^4.4 || ^5.0 || ^6.0", + "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.4@dev || ^6.0" + }, + "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.3" + }, + "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": "2022-09-05T10:34:54+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "95effeb9d6e2cec861cee06bf5bbf82d09aea7f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/95effeb9d6e2cec861cee06bf5bbf82d09aea7f5", + "reference": "95effeb9d6e2cec861cee06bf5bbf82d09aea7f5", + "shasum": "" + }, + "require": { + "php": ">=8.1" }, "conflict": { "doctrine/annotations": "<1.12", @@ -7006,7 +7487,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.0.11" + "source": "https://github.com/symfony/routing/tree/v6.1.7" }, "funding": [ { @@ -7022,24 +7503,24 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:45:53+00:00" + "time": "2022-10-18T13:12:43+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.0.2", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66" + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d78d39c1599bd1188b8e26bb341da52c3c6d8a66", - "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "psr/container": "^2.0" }, "conflict": { @@ -7051,7 +7532,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.1-dev" }, "thanks": { "name": "symfony/contracts", @@ -7061,7 +7542,10 @@ "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7088,7 +7572,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/service-contracts/tree/v3.1.1" }, "funding": [ { @@ -7104,24 +7588,24 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:17:58+00:00" + "time": "2022-05-30T19:18:58+00:00" }, { "name": "symfony/string", - "version": "v6.0.14", + "version": "v6.1.7", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "3db7da820a6e4a584b714b3933c34c6a7db4d86c" + "reference": "823f143370880efcbdfa2dbca946b3358c4707e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/3db7da820a6e4a584b714b3933c34c6a7db4d86c", - "reference": "3db7da820a6e4a584b714b3933c34c6a7db4d86c", + "url": "https://api.github.com/repos/symfony/string/zipball/823f143370880efcbdfa2dbca946b3358c4707e5", + "reference": "823f143370880efcbdfa2dbca946b3358c4707e5", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", @@ -7173,7 +7657,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.0.14" + "source": "https://github.com/symfony/string/tree/v6.1.7" }, "funding": [ { @@ -7189,24 +7673,24 @@ "type": "tidelift" } ], - "time": "2022-10-10T09:34:08+00:00" + "time": "2022-10-10T09:34:31+00:00" }, { "name": "symfony/translation", - "version": "v6.0.14", + "version": "v6.1.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "6f99eb179aee4652c0a7cd7c11f2a870d904330c" + "reference": "e6cd330e5a072518f88d65148f3f165541807494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/6f99eb179aee4652c0a7cd7c11f2a870d904330c", - "reference": "6f99eb179aee4652c0a7cd7c11f2a870d904330c", + "url": "https://api.github.com/repos/symfony/translation/zipball/e6cd330e5a072518f88d65148f3f165541807494", + "reference": "e6cd330e5a072518f88d65148f3f165541807494", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/polyfill-mbstring": "~1.0", "symfony/translation-contracts": "^2.3|^3.0" }, @@ -7231,6 +7715,7 @@ "symfony/http-kernel": "^5.4|^6.0", "symfony/intl": "^5.4|^6.0", "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0", "symfony/service-contracts": "^1.1.2|^2|^3", "symfony/yaml": "^5.4|^6.0" }, @@ -7268,7 +7753,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.0.14" + "source": "https://github.com/symfony/translation/tree/v6.1.6" }, "funding": [ { @@ -7284,24 +7769,24 @@ "type": "tidelift" } ], - "time": "2022-10-07T08:02:12+00:00" + "time": "2022-10-07T08:04:03+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.0.2", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "acbfbb274e730e5a0236f619b6168d9dedb3e282" + "reference": "606be0f48e05116baef052f7f3abdb345c8e02cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/acbfbb274e730e5a0236f619b6168d9dedb3e282", - "reference": "acbfbb274e730e5a0236f619b6168d9dedb3e282", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/606be0f48e05116baef052f7f3abdb345c8e02cc", + "reference": "606be0f48e05116baef052f7f3abdb345c8e02cc", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "suggest": { "symfony/translation-implementation": "" @@ -7309,7 +7794,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.1-dev" }, "thanks": { "name": "symfony/contracts", @@ -7319,7 +7804,10 @@ "autoload": { "psr-4": { "Symfony\\Contracts\\Translation\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7346,7 +7834,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/translation-contracts/tree/v3.1.1" }, "funding": [ { @@ -7362,24 +7850,24 @@ "type": "tidelift" } ], - "time": "2022-06-27T17:10:44+00:00" + "time": "2022-06-27T17:24:16+00:00" }, { "name": "symfony/uid", - "version": "v6.0.13", + "version": "v6.1.5", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "db426b27173f5e2d8b960dd10fa8ce19ea9ca5f3" + "reference": "e03519f7b1ce1d3c0b74f751892bb41d549a2d98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/db426b27173f5e2d8b960dd10fa8ce19ea9ca5f3", - "reference": "db426b27173f5e2d8b960dd10fa8ce19ea9ca5f3", + "url": "https://api.github.com/repos/symfony/uid/zipball/e03519f7b1ce1d3c0b74f751892bb41d549a2d98", + "reference": "e03519f7b1ce1d3c0b74f751892bb41d549a2d98", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/polyfill-uuid": "^1.15" }, "require-dev": { @@ -7420,7 +7908,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.0.13" + "source": "https://github.com/symfony/uid/tree/v6.1.5" }, "funding": [ { @@ -7436,24 +7924,24 @@ "type": "tidelift" } ], - "time": "2022-09-09T09:33:56+00:00" + "time": "2022-09-09T09:34:27+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.0.14", + "version": "v6.1.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "72af925ddd41ca0372d166d004bc38a00c4608cc" + "reference": "0f0adde127f24548e23cbde83bcaeadc491c551f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/72af925ddd41ca0372d166d004bc38a00c4608cc", - "reference": "72af925ddd41ca0372d166d004bc38a00c4608cc", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0f0adde127f24548e23cbde83bcaeadc491c551f", + "reference": "0f0adde127f24548e23cbde83bcaeadc491c551f", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -7508,7 +7996,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.0.14" + "source": "https://github.com/symfony/var-dumper/tree/v6.1.6" }, "funding": [ { @@ -7524,7 +8012,7 @@ "type": "tidelift" } ], - "time": "2022-10-07T08:02:12+00:00" + "time": "2022-10-07T08:04:03+00:00" }, { "name": "symfony/yaml", @@ -7812,6 +8300,241 @@ ], "time": "2022-03-08T17:03:00+00:00" }, + { + "name": "web-auth/cose-lib", + "version": "4.0.12", + "source": { + "type": "git", + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "fc733974fe12b550b54a94a08e4e184aca0015e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/fc733974fe12b550b54a94a08e4e184aca0015e5", + "reference": "fc733974fe12b550b54a94a08e4e184aca0015e5", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "fgrosse/phpasn1": "^2.1", + "php": ">=8.1" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.26.12", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.7", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^9.5", + "qossmic/deptrac-shim": "^0.24.0", + "rector/rector": "^0.14", + "symfony/phpunit-bridge": "^6.1", + "symplify/easy-coding-standard": "^11.0" + }, + "suggest": { + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cose\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/cose/contributors" + } + ], + "description": "CBOR Object Signing and Encryption (COSE) For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "COSE", + "RFC8152" + ], + "support": { + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/4.0.12" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-09-17T08:34:42+00:00" + }, + { + "name": "web-auth/metadata-service", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-metadata-service.git", + "reference": "af8c413d00f1e5f2074f0156c4d51c3217290efa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-metadata-service/zipball/af8c413d00f1e5f2074f0156c4d51c3217290efa", + "reference": "af8c413d00f1e5f2074f0156c4d51c3217290efa", + "shasum": "" + }, + "require": { + "ext-json": "*", + "lcobucci/clock": "^2.2", + "paragonie/constant_time_encoding": "^2.6", + "php": ">=8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/log": "^3.0", + "spomky-labs/pki-framework": "^1.0" + }, + "suggest": { + "psr/log-implementation": "Recommended to receive logs from the library", + "web-token/jwt-key-mgmt": "Mandatory for fetching Metadata Statement from distant sources", + "web-token/jwt-signature-algorithm-ecdsa": "Mandatory for fetching Metadata Statement from distant sources" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webauthn\\MetadataService\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/metadata-service/contributors" + } + ], + "description": "Metadata Service for FIDO2/Webauthn", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-metadata-service/tree/4.3.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-10-04T13:12:23+00:00" + }, + { + "name": "web-auth/webauthn-lib", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "3f5c41d629f20ce36cabb97be78b7cb4954d0dd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/3f5c41d629f20ce36cabb97be78b7cb4954d0dd8", + "reference": "3f5c41d629f20ce36cabb97be78b7cb4954d0dd8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "fgrosse/phpasn1": "^2.4", + "paragonie/constant_time_encoding": "^2.6", + "php": ">=8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/log": "^3.0", + "spomky-labs/cbor-php": "^3.0", + "symfony/uid": "^6.1", + "web-auth/cose-lib": "^4.0.12", + "web-auth/metadata-service": "self.version" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.1" + }, + "suggest": { + "psr/log-implementation": "Recommended to receive logs from the library", + "symfony/event-dispatcher": "Recommended to use dispatched events", + "web-token/jwt-key-mgmt": "Mandatory for the AndroidSafetyNet Attestation Statement support", + "web-token/jwt-signature-algorithm-ecdsa": "Recommended for the AndroidSafetyNet Attestation Statement support", + "web-token/jwt-signature-algorithm-eddsa": "Recommended for the AndroidSafetyNet Attestation Statement support", + "web-token/jwt-signature-algorithm-rsa": "Mandatory for the AndroidSafetyNet Attestation Statement support" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webauthn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/webauthn-library/contributors" + } + ], + "description": "FIDO2/Webauthn Support For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-lib/tree/4.3.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-10-06T11:15:37+00:00" + }, { "name": "webmozart/assert", "version": "1.11.0", @@ -7968,23 +8691,23 @@ }, { "name": "barryvdh/reflection-docblock", - "version": "v2.0.6", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/barryvdh/ReflectionDocBlock.git", - "reference": "6b69015d83d3daf9004a71a89f26e27d27ef6a16" + "reference": "bf44b757feb8ba1734659029357646466ded673e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/6b69015d83d3daf9004a71a89f26e27d27ef6a16", - "reference": "6b69015d83d3daf9004a71a89f26e27d27ef6a16", + "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/bf44b757feb8ba1734659029357646466ded673e", + "reference": "bf44b757feb8ba1734659029357646466ded673e", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "phpunit/phpunit": "~4.0,<4.5" + "phpunit/phpunit": "^8.5.14|^9" }, "suggest": { "dflydev/markdown": "~1.0", @@ -8014,9 +8737,9 @@ } ], "support": { - "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.0.6" + "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.1.0" }, - "time": "2018-12-13T10:34:14+00:00" + "time": "2022-10-31T15:35:43+00:00" }, { "name": "composer/pcre", @@ -9424,16 +10147,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.17", + "version": "9.2.18", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" + "reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", - "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/12fddc491826940cf9b7e88ad9664cf51f0f6d0a", + "reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a", "shasum": "" }, "require": { @@ -9489,7 +10212,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.18" }, "funding": [ { @@ -9497,7 +10220,7 @@ "type": "github" } ], - "time": "2022-08-30T12:24:04+00:00" + "time": "2022-10-27T13:35:33+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9742,16 +10465,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.25", + "version": "9.5.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d" + "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", - "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/851867efcbb6a1b992ec515c71cdcf20d895e9d2", + "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2", "shasum": "" }, "require": { @@ -9824,7 +10547,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.26" }, "funding": [ { @@ -9840,7 +10563,7 @@ "type": "tidelift" } ], - "time": "2022-09-25T03:44:45+00:00" + "time": "2022-10-28T06:00:21+00:00" }, { "name": "sebastian/cli-parser", @@ -11014,16 +11737,16 @@ }, { "name": "spatie/laravel-ignition", - "version": "1.6.0", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ignition.git", - "reference": "c21309ebf6657e0c38083afac8af9baa12885676" + "reference": "2b79cf6ed40946b64ac6713d7d2da8a9d87f612b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/c21309ebf6657e0c38083afac8af9baa12885676", - "reference": "c21309ebf6657e0c38083afac8af9baa12885676", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/2b79cf6ed40946b64ac6713d7d2da8a9d87f612b", + "reference": "2b79cf6ed40946b64ac6713d7d2da8a9d87f612b", "shasum": "" }, "require": { @@ -11100,24 +11823,24 @@ "type": "github" } ], - "time": "2022-10-25T08:38:04+00:00" + "time": "2022-10-26T17:39:54+00:00" }, { "name": "symfony/filesystem", - "version": "v6.0.13", + "version": "v6.1.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "3adca49133bd055ebe6011ed1e012be3c908af79" + "reference": "4d216a2beef096edf040a070117c39ca2abce307" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3adca49133bd055ebe6011ed1e012be3c908af79", - "reference": "3adca49133bd055ebe6011ed1e012be3c908af79", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/4d216a2beef096edf040a070117c39ca2abce307", + "reference": "4d216a2beef096edf040a070117c39ca2abce307", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, @@ -11147,7 +11870,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.0.13" + "source": "https://github.com/symfony/filesystem/tree/v6.1.5" }, "funding": [ { @@ -11163,24 +11886,24 @@ "type": "tidelift" } ], - "time": "2022-09-21T20:25:27+00:00" + "time": "2022-09-21T20:29:40+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.0.3", + "version": "v6.1.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "51f7006670febe4cbcbae177cbffe93ff833250d" + "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/51f7006670febe4cbcbae177cbffe93ff833250d", - "reference": "51f7006670febe4cbcbae177cbffe93ff833250d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a3016f5442e28386ded73c43a32a5b68586dd1c4", + "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/deprecation-contracts": "^2.1|^3" }, "type": "library", @@ -11214,7 +11937,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.0.3" + "source": "https://github.com/symfony/options-resolver/tree/v6.1.0" }, "funding": [ { @@ -11230,24 +11953,24 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2022-02-25T11:15:52+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.0.13", + "version": "v6.1.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "7554fde6848af5ef1178f8ccbdbdb8ae1092c70a" + "reference": "266636bb8f3fbdccc302491df7b3a1b9a8c238a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/7554fde6848af5ef1178f8ccbdbdb8ae1092c70a", - "reference": "7554fde6848af5ef1178f8ccbdbdb8ae1092c70a", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/266636bb8f3fbdccc302491df7b3a1b9a8c238a7", + "reference": "266636bb8f3fbdccc302491df7b3a1b9a8c238a7", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1", "symfony/service-contracts": "^1|^2|^3" }, "type": "library", @@ -11276,7 +11999,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.0.13" + "source": "https://github.com/symfony/stopwatch/tree/v6.1.5" }, "funding": [ { @@ -11292,7 +12015,7 @@ "type": "tidelift" } ], - "time": "2022-09-28T15:52:47+00:00" + "time": "2022-09-28T16:00:52+00:00" }, { "name": "theseer/tokenizer", @@ -11361,7 +12084,7 @@ }, "platform-dev": [], "platform-overrides": { - "php": "8.0.2" + "php": "8.1.0" }, "plugin-api-version": "2.3.0" } diff --git a/database/Factories/SecurityKeyFactory.php b/database/Factories/SecurityKeyFactory.php new file mode 100644 index 000000000..d98f54252 --- /dev/null +++ b/database/Factories/SecurityKeyFactory.php @@ -0,0 +1,43 @@ + Uuid::uuid4()->toString(), + 'name' => $this->faker->word, + 'type' => 'public-key', + 'transports' => [], + 'attestation_type' => 'none', + 'trust_path' => new EmptyTrustPath(), + 'counter' => 0, + ]; + } + + public function withUser(User $user): self + { + return $this->state([ + 'user_id' => $user->id, + 'user_handle' => $user->uuid, + ]); + } +} diff --git a/database/migrations/2021_08_07_170141_create_security_keys_table.php b/database/migrations/2021_08_07_170141_create_security_keys_table.php new file mode 100644 index 000000000..c3ecd4b1e --- /dev/null +++ b/database/migrations/2021_08_07_170141_create_security_keys_table.php @@ -0,0 +1,42 @@ +id(); + $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)->nullable(); + $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')->nullable(); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('security_keys'); + } +} diff --git a/resources/scripts/api/account/security-keys.ts b/resources/scripts/api/account/security-keys.ts new file mode 100644 index 000000000..cfc89576e --- /dev/null +++ b/resources/scripts/api/account/security-keys.ts @@ -0,0 +1,85 @@ +import useSWR, { ConfigInterface } from 'swr'; +import { useStoreState } from '@/state/hooks'; +import http, { FractalResponseList } from '@/api/http'; +import { SecurityKey, Transformers } from '@definitions/user'; +import { AxiosError } from 'axios'; +import { decodeBase64 } from '@/lib/base64'; +import { decodeBuffer, encodeBuffer } from '@/lib/buffer'; +import { LoginResponse } from '@/api/auth/login'; +import { useUserSWRKey } from '@/plugins/useSWRKey'; + +function decodeSecurityKeyCredentials(credentials: PublicKeyCredentialDescriptor[]) { + return credentials.map(c => ({ + id: decodeBuffer(decodeBase64(c.id.toString())), + type: c.type, + transports: c.transports, + })); +} + +function useSecurityKeys(config?: ConfigInterface) { + const uuid = useStoreState(state => state.user.data!.uuid); + const key = useUserSWRKey(['account', 'security-keys']); + + return useSWR( + key, + async (): Promise => { + const { data } = await http.get('/api/client/account/security-keys'); + + return (data as FractalResponseList).data.map((datum) => Transformers.toSecurityKey(datum.attributes)); + }, + { revalidateOnMount: false, ...(config || {}) }, + ); +} + +async function deleteSecurityKey(uuid: string): Promise { + await http.delete(`/api/client/account/security-keys/${uuid}`); +} + +async function registerCredentialForAccount(name: string, tokenId: string, credential: PublicKeyCredential): Promise { + const { data } = await http.post('/api/client/account/security-keys/register', { + name, + token_id: tokenId, + registration: { + id: credential.id, + type: credential.type, + rawId: encodeBuffer(credential.rawId), + response: { + attestationObject: encodeBuffer((credential.response as AuthenticatorAttestationResponse).attestationObject), + clientDataJSON: encodeBuffer(credential.response.clientDataJSON), + }, + }, + }); + + return Transformers.toSecurityKey(data.attributes); +} + +async function registerSecurityKey(name: string): Promise { + const { data } = await http.get('/api/client/account/security-keys/register'); + + const publicKey = data.data.credentials; + publicKey.challenge = decodeBuffer(decodeBase64(publicKey.challenge)); + publicKey.user.id = decodeBuffer(publicKey.user.id); + + if (publicKey.excludeCredentials) { + publicKey.excludeCredentials = decodeSecurityKeyCredentials(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}"`); + } + + return await registerCredentialForAccount(name, data.data.token_id, credentials as PublicKeyCredential); +} + +// eslint-disable-next-line camelcase +async function authenticateSecurityKey(data: { confirmation_token: string; data: string }): Promise { + const response = await http.post('/auth/login/checkpoint/key', data); + + return { + complete: response.data.complete, + intended: response.data.data?.intended || null, + }; +} + +export { useSecurityKeys, deleteSecurityKey, registerSecurityKey, authenticateSecurityKey }; diff --git a/resources/scripts/api/definitions/user/models.d.ts b/resources/scripts/api/definitions/user/models.d.ts index a5b40ca67..3d85b83d2 100644 --- a/resources/scripts/api/definitions/user/models.d.ts +++ b/resources/scripts/api/definitions/user/models.d.ts @@ -1,24 +1,6 @@ import { Model, UUID } from '@/api/definitions'; import { SubuserPermission } from '@/state/server/subusers'; -interface User extends Model { - uuid: string; - username: string; - email: string; - image: string; - twoFactorEnabled: boolean; - createdAt: Date; - permissions: SubuserPermission[]; - can(permission: SubuserPermission): boolean; -} - -interface SSHKey extends Model { - name: string; - publicKey: string; - fingerprint: string; - createdAt: Date; -} - interface ActivityLog extends Model<'actor'> { id: string; batch: UUID | null; @@ -33,3 +15,30 @@ interface ActivityLog extends Model<'actor'> { actor: User | null; }; } + +interface User extends Model { + uuid: string; + username: string; + email: string; + image: string; + twoFactorEnabled: boolean; + createdAt: Date; + permissions: SubuserPermission[]; + can(permission: SubuserPermission): boolean; +} + +interface SecurityKey extends Model { + uuid: UUID; + name: string; + type: 'public-key'; + publicKeyId: string; + createdAt: Date; + updatedAt: Date; +} + +interface SSHKey extends Model { + name: string; + publicKey: string; + fingerprint: string; + createdAt: Date; +} diff --git a/resources/scripts/api/definitions/user/transformers.ts b/resources/scripts/api/definitions/user/transformers.ts index 1fa62d3f9..4fa7fd3e3 100644 --- a/resources/scripts/api/definitions/user/transformers.ts +++ b/resources/scripts/api/definitions/user/transformers.ts @@ -3,6 +3,36 @@ import { FractalResponseData } from '@/api/http'; import { transform } from '@definitions/helpers'; export default class Transformers { + static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => { + const { actor } = attributes.relationships || {}; + + return { + id: attributes.id, + batch: attributes.batch, + event: attributes.event, + ip: attributes.ip, + isApi: attributes.is_api, + description: attributes.description, + properties: attributes.properties, + hasAdditionalMetadata: attributes.has_additional_metadata ?? false, + timestamp: new Date(attributes.timestamp), + relationships: { + actor: transform(actor as FractalResponseData, this.toUser, null), + }, + }; + }; + + static toSecurityKey (data: Record): Models.SecurityKey { + return { + uuid: data.uuid, + name: data.name, + type: data.type, + publicKeyId: data.public_key_id, + createdAt: new Date(data.created_at), + updatedAt: new Date(data.updated_at), + }; + } + static toSSHKey = (data: Record): Models.SSHKey => { return { name: data.name, @@ -26,25 +56,6 @@ export default class Transformers { }, }; }; - - static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => { - const { actor } = attributes.relationships || {}; - - return { - id: attributes.id, - batch: attributes.batch, - event: attributes.event, - ip: attributes.ip, - isApi: attributes.is_api, - description: attributes.description, - properties: attributes.properties, - hasAdditionalMetadata: attributes.has_additional_metadata ?? false, - timestamp: new Date(attributes.timestamp), - relationships: { - actor: transform(actor as FractalResponseData, this.toUser, null), - }, - }; - }; } export class MetaTransformers {} diff --git a/resources/scripts/lib/base64.spec.ts b/resources/scripts/lib/base64.spec.ts new file mode 100644 index 000000000..84e7124b7 --- /dev/null +++ b/resources/scripts/lib/base64.spec.ts @@ -0,0 +1,12 @@ +import { decodeBase64 } from '@/lib/base64'; + +describe('@/lib/base64.ts', function () { + describe('decodeBase64()', function () { + it.each([ + ['', ''], + ['', ''], + ])('should decode "%s" to "%s"', function (input, output) { + expect(decodeBase64(input)).toBe(output); + }); + }); +}); diff --git a/resources/scripts/lib/base64.ts b/resources/scripts/lib/base64.ts new file mode 100644 index 000000000..dec9bbe35 --- /dev/null +++ b/resources/scripts/lib/base64.ts @@ -0,0 +1,16 @@ +function decodeBase64 (input: string): string { + input = input.replace(/-/g, '+').replace(/_/g, '/'); + + const pad = input.length % 4; + if (pad) { + if (pad === 1) { + throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); + } + + input += new Array(5 - pad).join('='); + } + + return input; +} + +export { decodeBase64 } diff --git a/resources/scripts/lib/buffer.spec.ts b/resources/scripts/lib/buffer.spec.ts new file mode 100644 index 000000000..9e1297dee --- /dev/null +++ b/resources/scripts/lib/buffer.spec.ts @@ -0,0 +1,21 @@ +import { decodeBuffer, encodeBuffer } from '@/lib/buffer'; + +describe('@/lib/buffer.ts', function () { + describe('decodeBuffer()', function () { + it.each([ + ['', ''], + ['', ''], + ])('should decode "%s" to "%s"', function (input, output) { + expect(decodeBuffer(input)).toBe(output); + }); + }); + + describe('encodeBuffer()', function () { + it.each([ + [new Uint8Array(0), ''], + [new Uint8Array(0), ''], + ])('should encode "%s" to "%s"', function (input, output) { + expect(encodeBuffer(input)).toBe(output); + }); + }); +}); diff --git a/resources/scripts/lib/buffer.ts b/resources/scripts/lib/buffer.ts new file mode 100644 index 000000000..5c8593513 --- /dev/null +++ b/resources/scripts/lib/buffer.ts @@ -0,0 +1,9 @@ +function decodeBuffer(value: string): ArrayBuffer { + return Uint8Array.from(window.atob(value), c => c.charCodeAt(0)); +} + +function encodeBuffer(value: ArrayBuffer): string { + return btoa(String.fromCharCode(...new Uint8Array(value))); +} + +export { decodeBuffer, encodeBuffer }; diff --git a/routes/api-client.php b/routes/api-client.php index 64ceecf3e..14118dd9d 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -36,6 +36,11 @@ Route::prefix('/account')->middleware(AccountSubject::class)->group(function () Route::post('/api-keys', [Client\ApiKeyController::class, 'store']); Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']); + 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::prefix('/ssh-keys')->group(function () { Route::get('/', [Client\SSHKeyController::class, 'index']); Route::post('/', [Client\SSHKeyController::class, 'store']); diff --git a/routes/auth.php b/routes/auth.php index 7d0930f11..f54bcab4d 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -1,5 +1,6 @@ name('auth.login'); Route::get('/password', [Auth\LoginController::class, 'index'])->name('auth.forgot-password'); @@ -24,7 +25,8 @@ Route::get('/password/reset/{token}', [Auth\LoginController::class, 'index'])->n Route::middleware(['throttle:authentication'])->group(function () { // Login endpoints. Route::post('/login', [Auth\LoginController::class, 'login'])->middleware('recaptcha'); - Route::post('/login/checkpoint', Auth\LoginCheckpointController::class)->name('auth.login-checkpoint'); + Route::post('/login/checkpoint', [Auth\LoginCheckpointController::class, 'token'])->name('auth.checkpoint'); + Route::post('/login/checkpoint/key', [Auth\LoginCheckpointController::class, 'key'])->name('auth.checkpoint.key'); // Forgot password route. A post to this endpoint will trigger an // email to be sent containing a reset token. @@ -38,12 +40,12 @@ Route::middleware(['throttle:authentication'])->group(function () { // is created). Route::post('/password/reset', Auth\ResetPasswordController::class)->name('auth.reset-password'); -// Remove the guest middleware and apply the authenticated middleware to this endpoint +// Remove the guest middleware and apply the authenticated middleware to this endpoint, // so it cannot be used unless you're already logged in. Route::post('/logout', [Auth\LoginController::class, 'logout']) ->withoutMiddleware('guest') ->middleware('auth') ->name('auth.logout'); -// Catch any other combinations of routes and pass them off to the Vuejs component. +// Catch any other combinations of routes and pass them off to the React frontend. Route::fallback([Auth\LoginController::class, 'index']); diff --git a/tests/Unit/Http/Middleware/RequireTwoFactorAuthenticationTest.php b/tests/Unit/Http/Middleware/RequireTwoFactorAuthenticationTest.php new file mode 100644 index 000000000..dcd7db5e9 --- /dev/null +++ b/tests/Unit/Http/Middleware/RequireTwoFactorAuthenticationTest.php @@ -0,0 +1,328 @@ +set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_NONE); + + $user = $this->generateRequestUserModel(['use_totp' => false]); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testNoRequirementUserWithTotp2fa() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_NONE); + + $user = $this->generateRequestUserModel(['use_totp' => true]); + + $this->assertTrue($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testNoRequirementUserWithWebauthn2fa() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_NONE); + + /** @var \Pterodactyl\Models\User $user */ + $user = User::factory() + ->has(SecurityKey::factory()->count(1)) + ->create(['use_totp' => false]); + $this->setRequestUserModel($user); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + $this->assertNotEmpty($user->securityKeys); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testNoRequirementGuestUser() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_NONE); + + $this->setRequestUserModel(); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/auth/login'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn('auth.login'); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAllRequirementUserWithout2fa() + { + $this->expectException(TwoFactorAuthRequiredException::class); + + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ALL); + + $user = $this->generateRequestUserModel(['use_totp' => false]); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + $this->assertEmpty($user->securityKeys); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAllRequirementUserWithTotp2fa() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ALL); + + $user = $this->generateRequestUserModel(['use_totp' => true]); + + $this->assertTrue($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAllRequirementRuserWithWebauthn2fa() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ALL); + + /** @var \Pterodactyl\Models\User $user */ + $user = User::factory() + ->has(SecurityKey::factory()->count(1)) + ->create(['use_totp' => false]); + $this->setRequestUserModel($user); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + $this->assertNotEmpty($user->securityKeys); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAllRequirementGuestUser() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ALL); + + $this->setRequestUserModel(); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/auth/login'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn('auth.login'); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAdminRequirementUserWithout2fa() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN); + + $user = $this->generateRequestUserModel(['use_totp' => false]); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + $this->assertFalse($user->root_admin); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAdminRequirementAdminUserWithout2fa() + { + $this->expectException(TwoFactorAuthRequiredException::class); + + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN); + + $user = $this->generateRequestUserModel(['use_totp' => false, 'root_admin' => true]); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + $this->assertTrue($user->root_admin); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAdminRequirementUserWithTotp2fa() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN); + + $user = $this->generateRequestUserModel(['use_totp' => true]); + + $this->assertTrue($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + $this->assertFalse($user->root_admin); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAdminRequirementAdminUserWithTotp2fa() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN); + + $user = $this->generateRequestUserModel(['use_totp' => true, 'root_admin' => true]); + + $this->assertTrue($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + $this->assertTrue($user->root_admin); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAdminRequirementUserWithWebauthn2fa() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN); + + /** @var \Pterodactyl\Models\User $user */ + $user = User::factory()->has(SecurityKey::factory()->count(1))->create(['use_totp' => false]); + $this->setRequestUserModel($user); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + $this->assertFalse($user->root_admin); + $this->assertNotEmpty($user->securityKeys); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAdminRequirementAdminUserWithWebauthn2fa() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN); + + /** @var \Pterodactyl\Models\User $user */ + $user = User::factory() + ->has(SecurityKey::factory()->count(1)) + ->create(['use_totp' => false, 'root_admin' => true]); + $this->setRequestUserModel($user); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + $this->assertTrue($user->root_admin); + $this->assertNotEmpty($user->securityKeys); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } + + public function testAdminRequirementGuestUser() + { + // Disable the 2FA requirement + config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN); + + $this->setRequestUserModel(); + + $this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/auth/login'); + $this->request->shouldReceive('route->getName')->withNoArgs()->andReturn('auth.login'); + $this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true); + + /** @var \Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication $controller */ + $middleware = $this->app->make(RequireTwoFactorAuthentication::class); + $middleware->handle($this->request, $this->getClosureAssertions()); + } +} diff --git a/tsconfig.json b/tsconfig.json index 578a73e31..4a3ccb447 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,47 +1,47 @@ { - "compilerOptions": { - "target": "es2015", - "module": "es2020", - "jsx": "react", - "moduleResolution": "node", - "lib": [ - "es2015", - "dom" - ], - "strict": true, - "noEmit": true, - "sourceMap": true, - "noImplicitReturns": true, - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "baseUrl": ".", - "importsNotUsedAsValues": "preserve", - "paths": { - "@/*": [ - "./resources/scripts/*" - ], - "@definitions/*": [ - "./resources/scripts/api/definitions/*" - ], - "@feature/*": [ - "./resources/scripts/components/server/features/*" - ] + "compilerOptions": { + "target": "es2015", + "module": "es2020", + "jsx": "react", + "moduleResolution": "node", + "lib": [ + "es2015", + "dom" + ], + "strict": true, + "noEmit": true, + "sourceMap": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "importsNotUsedAsValues": "preserve", + "paths": { + "@/*": [ + "./resources/scripts/*" + ], + "@definitions/*": [ + "./resources/scripts/api/definitions/*" + ], + "@feature/*": [ + "./resources/scripts/components/server/features/*" + ] + }, + "plugins": [ + { + "name": "typescript-plugin-tw-template" + } + ], + "typeRoots": [ + "node_modules/@types" + ] }, - "plugins": [ - { - "name": "typescript-plugin-tw-template" - } + "include": [ + "./resources/scripts/**/*" ], - "typeRoots": [ - "node_modules/@types" + "exclude": [ + "/node_modules/" ] - }, - "include": [ - "./resources/scripts/**/*" - ], - "exclude": [ - "/node_modules/" - ] }