You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

127 lines
4.7 KiB

<?php
namespace App\Security;
use Mdanter\Ecc\Crypto\Signature\SchnorrSignature;
use swentel\nostr\Event\Event;
use swentel\nostr\Key\Key;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
/**
* Authenticator for Nostr protocol-based authentication.
*
* This authenticator processes requests to the /login endpoint with a Nostr-based Authorization header.
* It decodes and verifies the Nostr event, checks for expiration, and validates the Schnorr signature.
* On successful authentication, it issues a SelfValidatingPassport with the user's public key in Bech32 format.
*
* Implements interactive authentication for Symfony security.
*/
class NostrAuthenticator extends AbstractAuthenticator implements InteractiveAuthenticatorInterface
{
/**
* Checks if the request should be handled by this authenticator.
*
* @param Request $request The HTTP request.
* @return bool|null True if the request is supported, false otherwise.
*/
public function supports(Request $request): ?bool
{
if ($request->getPathInfo() === '/login' && $request->headers->has('Authorization')) {
return true;
}
return false;
}
/**
* Performs authentication using the Nostr Authorization header.
*
* @param Request $request The HTTP request.
* @return SelfValidatingPassport The authenticated passport.
* @throws AuthenticationException If authentication fails (invalid header, expired, or invalid signature).
*/
public function authenticate(Request $request): SelfValidatingPassport
{
$authHeader = $request->headers->get('Authorization');
if (!str_starts_with($authHeader, 'Nostr ')) {
throw new AuthenticationException('Invalid Authorization header');
}
$eventStr = base64_decode(substr($authHeader, 6), true);
if (false === $eventStr) {
throw new AuthenticationException('Invalid Authorization header');
}
try {
$data = json_decode($eventStr, false, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException) {
throw new AuthenticationException('Invalid Authorization header');
}
if (!\is_object($data) || !isset(
$data->id, $data->pubkey, $data->created_at, $data->kind, $data->content, $data->sig
)) {
throw new AuthenticationException('Invalid Authorization header');
}
if (!isset($data->tags) || !\is_array($data->tags)) {
$data->tags = [];
}
$event = (new Event())->populate($data);
if (time() > $event->getCreatedAt() + 60) {
throw new AuthenticationException('Expired');
}
$validity = (new SchnorrSignature())->verify(
$event->getPublicKey(),
$event->getSignature(),
$event->getId()
);
if (!$validity) {
throw new AuthenticationException('Invalid Authorization header');
}
$key = new Key();
return new SelfValidatingPassport(
new UserBadge($key->convertPublicKeyToBech32($event->getPublicKey()))
);
}
/**
* Handles successful authentication.
*
* @param Request $request The HTTP request.
* @param TokenInterface $token The authenticated token.
* @param string $firewallName The firewall name.
* @return Response|null The response to return, or null to continue.
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new Response('Authentication Successful', 200);
}
/**
* Handles failed authentication.
*
* @param Request $request The HTTP request.
* @param AuthenticationException $exception The exception thrown during authentication.
* @return Response|null The response to return, or null to continue.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return null;
}
/**
* Indicates whether this authenticator is interactive.
*
* @return bool True if interactive.
*/
public function isInteractive(): bool
{
return true;
}
}