Add WebAuthn passkey challenge and verification endpoints
- Introduced endpoints for WebAuthn-based authentication flow (`/auth/passkey/challenge` and `/auth/passkey/verify`). - Added support for generating and verifying WebAuthn PublicKeyCredentialRequestOptions and challenge tokens. - Extended routing logic to expose matched route templates for improved parameter handling. - Updated OpenAPI specifications to document passkey challenge and verification workflows. - Included unit tests for validating both existing and non-existing user scenarios during passkey challenges.
This commit is contained in:
@@ -15,6 +15,11 @@ trait route_t
|
||||
{
|
||||
protected array $permissions = [];
|
||||
private string $route;
|
||||
/**
|
||||
* The route template (e.g., "/account/security/passkeys/{id}") of the currently matched route.
|
||||
* This is populated just before the route callback is executed.
|
||||
*/
|
||||
private ?string $__current_route_template = null;
|
||||
/**
|
||||
* Lightweight per-request caches to avoid repeated DB/auth checks during a single request lifecycle.
|
||||
*/
|
||||
@@ -566,7 +571,15 @@ trait route_t
|
||||
private function registerRoute($route, $method, $callback, $permissions = []): void
|
||||
{
|
||||
global $router;
|
||||
$router->add($route, $method, $callback, self::registerPermissions($permissions, $route, $method));
|
||||
// Wrap the original callback so we can expose the matched route template to fromRoute()
|
||||
$self = $this;
|
||||
$wrapped = function () use ($callback, $route, $self) {
|
||||
// Set the current route template for parameter extraction
|
||||
$self->__current_route_template = $route;
|
||||
// Execute the original callback
|
||||
$callback();
|
||||
};
|
||||
$router->add($route, $method, $wrapped, self::registerPermissions($permissions, $route, $method));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -643,15 +656,42 @@ trait route_t
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameter from the route URL by index
|
||||
* @param string $index
|
||||
* @return string|null
|
||||
* Get the value of a route parameter by name from the currently matched route.
|
||||
* Example: for template "/users/{id}", current URL "/users/123" → fromRoute('id') === "123".
|
||||
* @param string $index The parameter name (without braces), e.g., 'id'
|
||||
* @return string|null The extracted value or null if not present
|
||||
*/
|
||||
public function fromRoute(string $index): ?string
|
||||
{
|
||||
$params = explode('/', $this->route);
|
||||
$index = array_search($index, $params);
|
||||
return $params[$index] ?? null;
|
||||
// Resolve current path without query string
|
||||
$currentPath = explode('?', $this->route)[0] ?? '';
|
||||
$currentPath = trim($currentPath, '/');
|
||||
|
||||
// We need the route template used to register this callback
|
||||
$template = $this->__current_route_template;
|
||||
if ($template === null) {
|
||||
// Fallback: no template context — cannot reliably parse; return null
|
||||
return null;
|
||||
}
|
||||
$template = trim($template, '/');
|
||||
|
||||
$pathSegments = $currentPath === '' ? [] : explode('/', $currentPath);
|
||||
$tplSegments = $template === '' ? [] : explode('/', $template);
|
||||
|
||||
// Quick length guard: router allows alnum-only for params; still, differing counts means no match
|
||||
if (count($pathSegments) !== count($tplSegments)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($tplSegments as $i => $seg) {
|
||||
if (preg_match('/^{([a-zA-Z0-9_]+)}$/', $seg, $m)) {
|
||||
$name = $m[1];
|
||||
if ($name === $index) {
|
||||
return $pathSegments[$i] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user