Skip to content
This repository has been archived by the owner on Mar 25, 2024. It is now read-only.

Commit

Permalink
New server authorization flow (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
Firehed committed Oct 26, 2021
1 parent 52234e4 commit 09c82a5
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 90 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Challenge class
- ChallengeProviderInterface (will replace ChallengeProvider)
- Server::generateChallenge(): ChallengeProviderInterface (now public; signature changed from previous private implementation)
- Server::validateRegistration(RegisterRequest, RegistrationResponseInterface) (will replace Server::setRegisterRequest + Server::register)
- Server::validateLogin(ChallengeProviderInterface, LoginResponseInterface, RegistrationInterface[]): RegistrationInterface (will replace Server::setRegistrations + Server::setSignRequests + Server::authenticate)
- Server::validateRegistration(RegisterRequest, RegistrationResponseInterface): RegistrationInterface (will replace Server::setRegisterRequest + Server::register)

### Deprecated
- ChallengeProvider
- Server::authenticate(LoginResponseInterface)
- Server::register(RegistrationResponseInterface)
- Server::setRegisterRequest(RegisterRequest)
- Server::setRegistrations(RegistrationInterface[])
- Server::setSignRequests(SignRequest[])


## [1.2.0] - 2021-10-26
Expand Down
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,16 +217,16 @@ After doing so, send them to the user:
```php
$registrations = $user->getU2FRegistrations(); // this must be an array of Registration objects

$signRequests = $server->generateSignRequests($registrations);
$_SESSION['sign_requests'] = $signRequests;
$challenge = $server->generateChallenge();
$_SESSION['challenge'] = $challenge;

// WebAuthn expects a single challenge for all key handles, and the Server generates the requests accordingly.
header('Content-type: application/json');
echo json_encode([
'challenge' => $signRequests[0]->getChallenge(),
'key_handles' => array_map(function (SignRequest $sr) {
return $sr->getKeyHandleWeb();
}, $signRequests),
'challenge' => $challenge,
'key_handles' => array_map(function (\Firehed\U2F\RegistrationInterface $reg) {
return $reg->getKeyHandleWeb();
}, $registrations),
]);
```

Expand Down Expand Up @@ -280,9 +280,11 @@ $data = json_decode($rawPostBody, true);
$response = \Firehed\U2F\WebAuthn\LoginResponse::fromDecodedJson($data);

$registrations = $user->getU2FRegistrations(); // Registration[]
$server->setRegistrations($registrations)
->setSignRequests($_SESSION['sign_requests']);
$registration = $server->authenticate($response);
$registration = $server->validateLogin(
$_SESSION['challenge'],
$response,
$registrations
);
```

#### Persist the updated `$registration`
Expand Down
104 changes: 67 additions & 37 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class Server
* Holds Registrations that were previously established by the
* authenticating party during `authenticate()`
*
* @deprecated
*
* @var RegistrationInterface[]
*/
private $registrations = [];
Expand All @@ -56,6 +58,8 @@ class Server
* Holds SignRequests used by `authenticate` which contain the challenge
* that's part of the signed response.
*
* @deprecated
*
* @var SignRequest[]
*/
private $signRequests = [];
Expand All @@ -74,63 +78,39 @@ public function __construct()

/**
* This method authenticates a `LoginResponseInterface` against outstanding
* registrations and their corresponding `SignRequest`s. If the response's
* signature validates and the counter hasn't done anything strange, the
* registration will be returned with an updated counter value, which *must*
* be persisted for the next authentication. If any verification component
* registrations and a known challenge. If the response's signature
* validates and the counter hasn't done anything strange, the registration
* will be returned with an updated counter value, which *must* be
* persisted for the next authentication. If any verification component
* fails, a `SE` will be thrown.
*
* @param LoginResponseInterface $response the parsed response from the user
* @param RegistrationInterface[] $registrations
* @return RegistrationInterface if authentication succeeds
* @throws SE if authentication fails
* @throws BadMethodCallException if a precondition is not met
*/
public function authenticate(LoginResponseInterface $response): RegistrationInterface
{
if (!$this->registrations) {
throw new BadMethodCallException(
'Before calling authenticate(), provide objects implementing'.
'RegistrationInterface with setRegistrations()'
);
}
if (!$this->signRequests) {
throw new BadMethodCallException(
'Before calling authenticate(), provide `SignRequest`s with '.
'setSignRequests()'
);
}

public function validateLogin(
ChallengeProviderInterface $challenge,
LoginResponseInterface $response,
array $registrations
): RegistrationInterface {
// Search for the registration to use based on the Key Handle
/** @var ?Registration */
$registration = $this->findObjectWithKeyHandle(
$this->registrations,
$registrations,
$response->getKeyHandleBinary()
);
if (!$registration) {
if ($registration === null) {
// This would suggest either some sort of forgery attempt or
// a hilariously-broken token responding to handles it doesn't
// support and not returning a DEVICE_INELIGIBLE client error.
throw new SE(SE::KEY_HANDLE_UNRECOGNIZED);
}

// Search for the Signing Request to use based on the Key Handle
$request = $this->findObjectWithKeyHandle(
$this->signRequests,
$registration->getKeyHandleBinary()
);
if (!$request) {
// Similar to above, there is a bizarre mismatch between the known
// possible sign requests and the key handle determined above. This
// would probably be caused by a logic error causing bogus sign
// requests to be passed to this method.
throw new SE(SE::KEY_HANDLE_UNRECOGNIZED);
}

// If the challenge in the (signed) response ClientData doesn't
// match the one in the signing request, the client signed the
// wrong thing. This could possibly be an attempt at a replay
// attack.
$this->validateChallenge($request, $response);
$this->validateChallenge($challenge, $response);

$pem = $registration->getPublicKey()->getPemFormatted();

Expand Down Expand Up @@ -188,6 +168,52 @@ public function authenticate(LoginResponseInterface $response): RegistrationInte
->setCounter($response->getCounter());
}

/**
* @deprecated This is being replaced by validateLogin
*
* This method authenticates a `LoginResponseInterface` against outstanding
* registrations and their corresponding `SignRequest`s. If the response's
* signature validates and the counter hasn't done anything strange, the
* registration will be returned with an updated counter value, which *must*
* be persisted for the next authentication. If any verification component
* fails, a `SE` will be thrown.
*
* @param LoginResponseInterface $response the parsed response from the user
* @return RegistrationInterface if authentication succeeds
* @throws SE if authentication fails
* @throws BadMethodCallException if a precondition is not met
*/
public function authenticate(LoginResponseInterface $response): RegistrationInterface
{
if (!$this->registrations) {
throw new BadMethodCallException(
'Before calling authenticate(), provide objects implementing'.
'RegistrationInterface with setRegistrations()'
);
}
if (!$this->signRequests) {
throw new BadMethodCallException(
'Before calling authenticate(), provide `SignRequest`s with '.
'setSignRequests()'
);
}

// Search for the Signing Request to use based on the Key Handle
$request = $this->findObjectWithKeyHandle(
$this->signRequests,
$response->getKeyHandleBinary()
);
if (!$request) {
// Similar to above, there is a bizarre mismatch between the known
// possible sign requests and the key handle determined above. This
// would probably be caused by a logic error causing bogus sign
// requests to be passed to this method.
throw new SE(SE::KEY_HANDLE_UNRECOGNIZED);
}

return $this->validateLogin($request, $response, $this->registrations);
}

/**
* This method authenticates a RegistrationResponseInterface against its
* corresponding RegisterRequest by verifying the certificate and signature.
Expand Down Expand Up @@ -303,6 +329,8 @@ public function setRegisterRequest(RegisterRequest $request): self
}

/**
* @deprecated
*
* Provide a user's existing registration to be used during
* authentication
*
Expand All @@ -318,6 +346,8 @@ public function setRegistrations(array $registrations): self
}

/**
* @deprecated
*
* Provide the previously-generated SignRequests, corresponing to the
* existing Registrations, of of which should be signed and will be
* verified during authenticate()
Expand Down
Loading

0 comments on commit 09c82a5

Please sign in to comment.