auth0 / symfony

Symfony SDK for Auth0 Authentication and Management APIs.
MIT License
124 stars 74 forks source link

Use interfaces to allow decoration #147

Closed WebDaMa closed 1 year ago

WebDaMa commented 1 year ago

Describe the problem you'd like to have solved

At this moment we can't decorate parts of the bundle. My use case is that I would like to add the user to my own database to control more there. For that I need to plug in to the Authenticator.

For ex. In the AuthenticationController we use a Authenticator but we should use the AuthenticatorInterface.

Describe the ideal solution

Use the contracts everywhere we can to enable proper decoration.

This could allow a decorated Controller for example:

<?php

namespace App\Controller;

use App\Security\DecoratingAuthenticator;
use Auth0\SDK\Auth0;
use Auth0\Symfony\Contracts\Controllers\AuthenticationControllerInterface;
use Auth0\Symfony\Controllers\AuthenticationController;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;

#[AsDecorator(decorates: AuthenticationController::class)]
class DecoratingAuthenticationController extends AbstractController implements AuthenticationControllerInterface
{
    private AuthenticationController $inner;

    public function __construct(
        #[MapDecorated] AuthenticationController $inner,
        private readonly RouterInterface $router,
        private readonly DecoratingAuthenticator $authenticator
    ) {
        $this->inner = $inner;
    }

    public function login(Request $request): Response
    {
        return $this->inner->login($request);
    }

    public function logout(Request $request): Response
    {
        return $this->inner->logout($request);
    }

    public function callback(Request $request): Response
    {
        return $this->inner->callback($request);
    }

    protected function getRedirectUrl(string $route): string
    {
       $routes = $this->authenticator->configuration['routes'] ?? [];
        $configuredRoute = $routes[$route] ?? null;

        if (null !== $configuredRoute && '' !== $configuredRoute) {
            try {
                return $this->router->generate($configuredRoute);
            } catch (\Throwable $th) {
            }
        }

        return '';
    }

    protected function getSdk(): Auth0
    {
        return $this->authenticator->getInner()->service->getSdk();
    }
}
<?php

namespace App\Security;

use Auth0\Symfony\Security\Authenticator;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;
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\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

#[AsDecorator(decorates: 'auth0.authenticator')]
readonly class DecoratingAuthenticator implements AuthenticatorInterface
{
    private Authenticator $inner;
    public function __construct(#[MapDecorated] Authenticator $inner)
    {
        $this->inner = $inner;
    }

    /**
     * @throws \JsonException
     */
    public function authenticate(Request $request): Passport
    {
        $session = ($sdk = $this->inner->service->getSdk()) ? $sdk->getCredentials() : null;

        if (null === $session) {
            throw new CustomUserMessageAuthenticationException('No Auth0 session was found.');
        }

        $user = json_encode(['type' => 'stateful', 'data' => $session], JSON_THROW_ON_ERROR);

        return new SelfValidatingPassport(
            new UserBadge($user, function () use ($user, $sdk) {
                $auth0User = $sdk->getUser();

                // Do custom action to save users locally, trigger events, ...

                return $user;
            })
        );
    }

    public function supports(Request $request): ?bool
    {
        return $this->inner->supports($request);
    }

    public function createToken(Passport $passport, string $firewallName): TokenInterface
    {
        return $this->inner->createToken($passport, $firewallName);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        return $this->inner->onAuthenticationSuccess($request, $token, $firewallName);
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        return $this->inner->onAuthenticationFailure($request, $exception);
    }

    public function getInner(): Authenticator
    {
        return $this->inner;
    }
}
evansims commented 1 year ago

Thanks for the feature suggestion, @WebDaMa! Let me investigate this and get back to you.

evansims commented 1 year ago

We've backlogged this feature request for future consideration. I'm unable to offer an ETA on it at the moment, but we generally sprint through these requests bi-quarterly. Thanks for your suggestion!

As we aren't able to offer an ETA we'll be closing this issue for now, but we are tracking it internally.