Merge pull request #1103 from jonnybarnes/develop

MTM Passkey Support
This commit is contained in:
Jonny Barnes 2023-10-27 20:17:14 +00:00 • committed by GitHub
commit 335acb130e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
114 changed files with 4856 additions and 2575 deletions

View file

@ -8,6 +8,9 @@ indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{js,css}]
indent_size = 2
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false

View file

@ -84,6 +84,7 @@ SESSION_SECURE_COOKIE=true
LOG_SLACK_WEBHOOK_URL= LOG_SLACK_WEBHOOK_URL=
FLARE_KEY= FLARE_KEY=
IGNITION_OPEN_AI_KEY=
FONT_LINK= FONT_LINK=

View file

@ -1,5 +1,6 @@
parserOptions: parserOptions:
sourceType: 'module' sourceType: 'module'
ecmaVersion: 'latest'
extends: 'eslint:recommended' extends: 'eslint:recommended'
env: env:
browser: true browser: true
@ -9,7 +10,7 @@ ignorePatterns:
rules: rules:
indent: indent:
- error - error
- 4 - 2
linebreak-style: linebreak-style:
- error - error
- unix - unix
@ -24,3 +25,14 @@ rules:
- allow: - allow:
- warn - warn
- error - error
no-await-in-loop:
- error
no-promise-executor-return:
- error
require-atomic-updates:
- error
max-nested-callbacks:
- error
- 3
prefer-promise-reject-errors:
- error

View file

@ -2,6 +2,8 @@ name: Deploy
on: on:
workflow_dispatch: workflow_dispatch:
release:
types: [published]
jobs: jobs:
deploy: deploy:

View file

@ -33,7 +33,7 @@ jobs:
- name: Setup PHP with pecl extensions - name: Setup PHP with pecl extensions
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: '8.1' php-version: '8.2'
extensions: phpredis,imagick extensions: phpredis,imagick
- name: Copy .env - name: Copy .env

View file

@ -16,7 +16,7 @@ jobs:
- name: Setup PHP with pecl extensions - name: Setup PHP with pecl extensions
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: '8.1' php-version: '8.2'
- name: Get Composer Cache Directory - name: Get Composer Cache Directory
id: composer-cache id: composer-cache

View file

@ -1,7 +1,3 @@
{ {
"extends": ["stylelint-config-standard"], "extends": ["stylelint-config-standard"]
"rules": {
"indentation": 4,
"import-notation": "string"
}
} }

View file

@ -8,6 +8,8 @@ use Illuminate\Support\Facades\DB;
/** /**
* @codeCoverageIgnore * @codeCoverageIgnore
*
* @psalm-suppress UnusedClass
*/ */
class MigratePlaceDataFromPostgis extends Command class MigratePlaceDataFromPostgis extends Command
{ {

View file

@ -9,6 +9,9 @@ use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\FileSystem\FileSystem; use Illuminate\FileSystem\FileSystem;
/**
* @psalm-suppress UnusedClass
*/
class ParseCachedWebMentions extends Command class ParseCachedWebMentions extends Command
{ {
/** /**

View file

@ -8,6 +8,9 @@ use App\Jobs\DownloadWebMention;
use App\Models\WebMention; use App\Models\WebMention;
use Illuminate\Console\Command; use Illuminate\Console\Command;
/**
* @psalm-suppress UnusedClass
*/
class ReDownloadWebMentions extends Command class ReDownloadWebMentions extends Command
{ {
/** /**

View file

@ -7,23 +7,12 @@ use Throwable;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/** /**
* Register the exception handling callbacks for the application. * Register the exception handling callbacks for the application.
*/ */
public function register(): void public function register(): void
{ {
$this->reportable(function (Throwable $e) { $this->reportable(function (Throwable $_e) {
// //
}); });
} }

View file

@ -1,7 +0,0 @@
<?php
namespace App\Exceptions;
class TwitterContentException extends \Exception
{
}

View file

@ -9,6 +9,9 @@ use App\Models\Article;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\View\View; use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class ArticlesController extends Controller class ArticlesController extends Controller
{ {
public function index(): View public function index(): View

View file

@ -10,6 +10,9 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class BioController extends Controller class BioController extends Controller
{ {
public function show(): View public function show(): View

View file

@ -7,9 +7,11 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\MicropubClient; use App\Models\MicropubClient;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class ClientsController extends Controller class ClientsController extends Controller
{ {
/** /**

View file

@ -9,10 +9,12 @@ use App\Models\Contact;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\View\View; use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class ContactsController extends Controller class ContactsController extends Controller
{ {
/** /**

View file

@ -7,6 +7,9 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\View\View; use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class HomeController extends Controller class HomeController extends Controller
{ {
/** /**

View file

@ -10,6 +10,9 @@ use App\Models\Like;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\View\View; use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class LikesController extends Controller class LikesController extends Controller
{ {
/** /**

View file

@ -11,6 +11,9 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class NotesController extends Controller class NotesController extends Controller
{ {
/** /**

View file

@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Passkey;
use App\Models\User;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Algorithm\Signature\EdDSA\Ed25519;
use Cose\Algorithm\Signature\RSA\RS256;
use Cose\Algorithms;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Throwable;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\Exception\WebauthnException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialUserEntity;
/**
* @psalm-suppress UnusedClass
*/
class PasskeysController extends Controller
{
public function index(): View
{
/** @var User $user */
$user = auth()->user();
$passkeys = $user->passkey;
return view('admin.passkeys.index', compact('passkeys'));
}
public function getCreateOptions(): JsonResponse
{
/** @var User $user */
$user = auth()->user();
// RP Entity i.e. the application
$rpEntity = PublicKeyCredentialRpEntity::create(
config('app.name'),
config('url.longurl'),
);
// User Entity
$userEntity = PublicKeyCredentialUserEntity::create(
$user->name,
(string) $user->id,
$user->name,
);
// Challenge
$challenge = random_bytes(16);
// List of supported public key parameters
$pubKeyCredParams = collect([
Algorithms::COSE_ALGORITHM_EDDSA,
Algorithms::COSE_ALGORITHM_ES256,
Algorithms::COSE_ALGORITHM_RS256,
])->map(
fn ($algorithm) => PublicKeyCredentialParameters::create('public-key', $algorithm)
)->toArray();
$authenticatorSelectionCriteria = AuthenticatorSelectionCriteria::create(
userVerification: AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED,
residentKey: AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED,
requireResidentKey: true,
);
$options = PublicKeyCredentialCreationOptions::create(
$rpEntity,
$userEntity,
$challenge,
$pubKeyCredParams,
authenticatorSelection: $authenticatorSelectionCriteria,
attestation: PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE
);
$options = json_encode($options, JSON_THROW_ON_ERROR);
session(['create_options' => $options]);
return JsonResponse::fromJsonString($options);
}
public function create(Request $request): JsonResponse
{
/** @var User $user */
$user = auth()->user();
$publicKeyCredentialCreationOptionsData = session('create_options');
if (empty($publicKeyCredentialCreationOptionsData)) {
throw new WebAuthnException('No public key credential request options found');
}
$publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::createFromString($publicKeyCredentialCreationOptionsData);
// Unset session data to mitigate replay attacks
session()->forget('create_options');
$attestationSupportManager = AttestationStatementSupportManager::create();
$attestationSupportManager->add(NoneAttestationStatementSupport::create());
$attestationObjectLoader = AttestationObjectLoader::create($attestationSupportManager);
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);
$publicKeyCredential = $publicKeyCredentialLoader->load(json_encode($request->all(), JSON_THROW_ON_ERROR));
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
throw new WebAuthnException('Invalid response type');
}
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
attestationStatementSupportManager: $attestationStatementSupportManager,
publicKeyCredentialSourceRepository: null,
tokenBindingHandler: null,
extensionOutputCheckerHandler: ExtensionOutputCheckerHandler::create(),
);
$securedRelyingPartyId = [];
if (App::environment('local', 'development')) {
$securedRelyingPartyId = [config('url.longurl')];
}
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
authenticatorAttestationResponse: $publicKeyCredential->response,
publicKeyCredentialCreationOptions: $publicKeyCredentialCreationOptions,
request: config('url.longurl'),
securedRelyingPartyId: $securedRelyingPartyId,
);
$user->passkey()->create([
'passkey_id' => Base64UrlSafe::encodeUnpadded($publicKeyCredentialSource->publicKeyCredentialId),
'passkey' => json_encode($publicKeyCredentialSource, JSON_THROW_ON_ERROR),
]);
return response()->json([
'success' => true,
'message' => 'Passkey created successfully',
]);
}
public function getRequestOptions(): JsonResponse
{
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::create(
challenge: random_bytes(16),
userVerification: PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_REQUIRED
);
$publicKeyCredentialRequestOptions = json_encode($publicKeyCredentialRequestOptions, JSON_THROW_ON_ERROR);
session(['request_options' => $publicKeyCredentialRequestOptions]);
return JsonResponse::fromJsonString($publicKeyCredentialRequestOptions);
}
public function login(Request $request): JsonResponse
{
$requestOptions = session('request_options');
session()->forget('request_options');
if (empty($requestOptions)) {
return response()->json([
'success' => false,
'message' => 'No request options found',
], 400);
}
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::createFromString($requestOptions);
$attestationSupportManager = AttestationStatementSupportManager::create();
$attestationSupportManager->add(NoneAttestationStatementSupport::create());
$attestationObjectLoader = AttestationObjectLoader::create($attestationSupportManager);
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);
$publicKeyCredential = $publicKeyCredentialLoader->load(json_encode($request->all(), JSON_THROW_ON_ERROR));
if (! $publicKeyCredential->response instanceof AuthenticatorAssertionResponse) {
return response()->json([
'success' => false,
'message' => 'Invalid response type',
], 400);
}
$passkey = Passkey::firstWhere('passkey_id', $publicKeyCredential->id);
if (! $passkey) {
return response()->json([
'success' => false,
'message' => 'Passkey not found',
], 404);
}
$credential = PublicKeyCredentialSource::createFromArray(json_decode($passkey->passkey, true, 512, JSON_THROW_ON_ERROR));
$algorithmManager = Manager::create();
$algorithmManager->add(new Ed25519());
$algorithmManager->add(new ES256());
$algorithmManager->add(new RS256());
$authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
publicKeyCredentialSourceRepository: null,
tokenBindingHandler: null,
extensionOutputCheckerHandler: ExtensionOutputCheckerHandler::create(),
algorithmManager: $algorithmManager,
);
$securedRelyingPartyId = [];
if (App::environment('local', 'development')) {
$securedRelyingPartyId = [config('url.longurl')];
}
try {
$authenticatorAssertionResponseValidator->check(
credentialId: $credential,
authenticatorAssertionResponse: $publicKeyCredential->response,
publicKeyCredentialRequestOptions: $publicKeyCredentialRequestOptions,
request: config('url.longurl'),
userHandle: null,
securedRelyingPartyId: $securedRelyingPartyId,
);
} catch (Throwable) {
return response()->json([
'success' => false,
'message' => 'Passkey could not be verified',
], 500);
}
$user = User::find($passkey->user_id);
Auth::login($user);
return response()->json([
'success' => true,
'message' => 'Passkey verified successfully',
]);
}
}

Some files were not shown because too many files have changed in this diff Show more