silverstripe /
silverstripe-webauthn-authenticator
| 1 | <?php |
||||
| 2 | |||||
| 3 | declare(strict_types=1); |
||||
| 4 | |||||
| 5 | namespace SilverStripe\WebAuthn; |
||||
| 6 | |||||
| 7 | use CBOR\Decoder; |
||||
| 8 | use Exception; |
||||
| 9 | use GuzzleHttp\Psr7\ServerRequest; |
||||
| 10 | use InvalidArgumentException; |
||||
| 11 | use Psr\Log\LoggerInterface; |
||||
| 12 | use SilverStripe\Control\HTTPRequest; |
||||
| 13 | use SilverStripe\MFA\Exception\AuthenticationFailedException; |
||||
| 14 | use SilverStripe\MFA\Method\Handler\VerifyHandlerInterface; |
||||
| 15 | use SilverStripe\MFA\Model\RegisteredMethod; |
||||
| 16 | use SilverStripe\MFA\State\Result; |
||||
| 17 | use SilverStripe\MFA\Store\StoreInterface; |
||||
| 18 | use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; |
||||
| 19 | use Webauthn\AuthenticatorAssertionResponse; |
||||
| 20 | use Webauthn\AuthenticatorAssertionResponseValidator; |
||||
| 21 | use Webauthn\PublicKeyCredentialDescriptor; |
||||
| 22 | use Webauthn\PublicKeyCredentialRequestOptions; |
||||
| 23 | use Webauthn\PublicKeyCredentialSource; |
||||
| 24 | use Webauthn\TokenBinding\TokenBindingNotSupportedHandler; |
||||
| 25 | |||||
| 26 | class VerifyHandler implements VerifyHandlerInterface |
||||
| 27 | { |
||||
| 28 | use BaseHandlerTrait; |
||||
| 29 | use CredentialRepositoryProviderTrait; |
||||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||||
| 30 | |||||
| 31 | /** |
||||
| 32 | * Dependency injection configuration |
||||
| 33 | * |
||||
| 34 | * @config |
||||
| 35 | * @var array |
||||
| 36 | */ |
||||
| 37 | private static $dependencies = [ |
||||
|
0 ignored issues
–
show
|
|||||
| 38 | 'Logger' => '%$' . LoggerInterface::class . '.mfa', |
||||
| 39 | ]; |
||||
| 40 | |||||
| 41 | /** |
||||
| 42 | * @var LoggerInterface |
||||
| 43 | */ |
||||
| 44 | protected $logger; |
||||
| 45 | |||||
| 46 | /** |
||||
| 47 | * Sets the {@see $logger} member variable |
||||
| 48 | * |
||||
| 49 | * @param LoggerInterface|null $logger |
||||
| 50 | * @return self |
||||
| 51 | */ |
||||
| 52 | public function setLogger(?LoggerInterface $logger): self |
||||
| 53 | { |
||||
| 54 | $this->logger = $logger; |
||||
| 55 | return $this; |
||||
| 56 | } |
||||
| 57 | |||||
| 58 | /** |
||||
| 59 | * Stores any data required to handle a log in process with a method, and returns relevant state to be applied to |
||||
| 60 | * the front-end application managing the process. |
||||
| 61 | * |
||||
| 62 | * @param StoreInterface $store An object that hold session data (and the Member) that can be mutated |
||||
| 63 | * @param RegisteredMethod $method The RegisteredMethod instance that is being verified |
||||
| 64 | * @return array Props to be passed to a front-end component |
||||
| 65 | */ |
||||
| 66 | public function start(StoreInterface $store, RegisteredMethod $method): array |
||||
| 67 | { |
||||
| 68 | return [ |
||||
| 69 | 'publicKey' => $this->getCredentialRequestOptions($store, $method, true), |
||||
| 70 | ]; |
||||
| 71 | } |
||||
| 72 | |||||
| 73 | /** |
||||
| 74 | * Verify the request has provided the right information to verify the member that aligns with any sessions state |
||||
| 75 | * that may have been set prior |
||||
| 76 | * |
||||
| 77 | * @param HTTPRequest $request |
||||
| 78 | * @param StoreInterface $store |
||||
| 79 | * @param RegisteredMethod $registeredMethod The RegisteredMethod instance that is being verified |
||||
| 80 | * @return Result |
||||
| 81 | */ |
||||
| 82 | public function verify(HTTPRequest $request, StoreInterface $store, RegisteredMethod $registeredMethod): Result |
||||
| 83 | { |
||||
| 84 | $data = json_decode((string) $request->getBody(), true); |
||||
| 85 | |||||
| 86 | try { |
||||
| 87 | if (empty($data['credentials'])) { |
||||
| 88 | throw new ResponseDataException('Incomplete data, required information missing'); |
||||
| 89 | } |
||||
| 90 | |||||
| 91 | $decoder = $this->getDecoder(); |
||||
| 92 | $attestationStatementSupportManager = $this->getAttestationStatementSupportManager($decoder); |
||||
| 93 | $attestationObjectLoader = $this->getAttestationObjectLoader($attestationStatementSupportManager, $decoder); |
||||
| 94 | $publicKeyCredential = $this |
||||
| 95 | ->getPublicKeyCredentialLoader($attestationObjectLoader, $decoder) |
||||
| 96 | ->load(base64_decode($data['credentials'])); |
||||
| 97 | |||||
| 98 | $response = $publicKeyCredential->getResponse(); |
||||
| 99 | if (!$response instanceof AuthenticatorAssertionResponse) { |
||||
| 100 | throw new ResponseTypeException('Unexpected response type found'); |
||||
| 101 | } |
||||
| 102 | |||||
| 103 | // Create a PSR-7 request |
||||
| 104 | $psrRequest = ServerRequest::fromGlobals(); |
||||
| 105 | |||||
| 106 | $this->getAuthenticatorAssertionResponseValidator($decoder, $store) |
||||
| 107 | ->check( |
||||
| 108 | $publicKeyCredential->getRawId(), |
||||
| 109 | $response, |
||||
| 110 | $this->getCredentialRequestOptions($store, $registeredMethod), |
||||
| 111 | $psrRequest, |
||||
| 112 | (string) $store->getMember()->ID |
||||
| 113 | ); |
||||
| 114 | } catch (Exception $e) { |
||||
| 115 | $this->logger->error($e->getMessage()); |
||||
| 116 | return Result::create(false, 'Verification failed: ' . $e->getMessage()); |
||||
| 117 | } |
||||
| 118 | |||||
| 119 | return Result::create(); |
||||
| 120 | } |
||||
| 121 | |||||
| 122 | /** |
||||
| 123 | * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end) |
||||
| 124 | * |
||||
| 125 | * @return string |
||||
| 126 | */ |
||||
| 127 | public function getComponent(): string |
||||
| 128 | { |
||||
| 129 | return 'WebAuthnVerify'; |
||||
| 130 | } |
||||
| 131 | |||||
| 132 | /** |
||||
| 133 | * @param StoreInterface $store |
||||
| 134 | * @param RegisteredMethod|null $registeredMethod |
||||
| 135 | * @param bool $reset |
||||
| 136 | * @return PublicKeyCredentialRequestOptions |
||||
| 137 | * @throws AuthenticationFailedException |
||||
| 138 | * @throws Exception |
||||
| 139 | */ |
||||
| 140 | protected function getCredentialRequestOptions( |
||||
| 141 | StoreInterface $store, |
||||
| 142 | RegisteredMethod $registeredMethod = null, |
||||
| 143 | $reset = false |
||||
| 144 | ): PublicKeyCredentialRequestOptions { |
||||
| 145 | $state = $store->getState(); |
||||
| 146 | |||||
| 147 | if (!$reset && !empty($state) && !empty($state['credentialOptions'])) { |
||||
| 148 | return PublicKeyCredentialRequestOptions::createFromArray($state['credentialOptions']); |
||||
| 149 | } |
||||
| 150 | |||||
| 151 | // Use the interface methods (despite the fact the "repository" is per-member in this module) |
||||
| 152 | $validCredentials = $this->getCredentialRepository($store, $registeredMethod) |
||||
| 153 | ->findAllForUserEntity($this->getUserEntity($store->getMember())); |
||||
|
0 ignored issues
–
show
It seems like
$store->getMember() can also be of type null; however, parameter $member of SilverStripe\WebAuthn\Ve...andler::getUserEntity() does only seem to accept SilverStripe\Security\Member, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 154 | |||||
| 155 | if (!count($validCredentials)) { |
||||
| 156 | throw new AuthenticationFailedException('User does not appear to have any credentials loaded for webauthn'); |
||||
| 157 | } |
||||
| 158 | |||||
| 159 | $descriptors = array_map(function (PublicKeyCredentialSource $source) { |
||||
| 160 | return $source->getPublicKeyCredentialDescriptor(); |
||||
| 161 | }, $validCredentials); |
||||
| 162 | |||||
| 163 | $options = new PublicKeyCredentialRequestOptions( |
||||
| 164 | random_bytes(32), |
||||
| 165 | 40000, |
||||
| 166 | null, |
||||
| 167 | $descriptors, |
||||
| 168 | PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED |
||||
| 169 | ); |
||||
| 170 | |||||
| 171 | // Persist the options for later |
||||
| 172 | $store->addState(['credentialOptions' => $options]); |
||||
| 173 | |||||
| 174 | return $options; |
||||
| 175 | } |
||||
| 176 | |||||
| 177 | /** |
||||
| 178 | * @param Decoder $decoder |
||||
| 179 | * @param StoreInterface $store |
||||
| 180 | * @return AuthenticatorAssertionResponseValidator |
||||
| 181 | */ |
||||
| 182 | protected function getAuthenticatorAssertionResponseValidator( |
||||
| 183 | Decoder $decoder, |
||||
| 184 | StoreInterface $store |
||||
| 185 | ): AuthenticatorAssertionResponseValidator { |
||||
| 186 | return new AuthenticatorAssertionResponseValidator( |
||||
| 187 | $this->getCredentialRepository($store), |
||||
| 188 | $decoder, |
||||
| 189 | new TokenBindingNotSupportedHandler(), |
||||
| 190 | new ExtensionOutputCheckerHandler() |
||||
| 191 | ); |
||||
| 192 | } |
||||
| 193 | } |
||||
| 194 |