tempestphp / tempest-framework

The PHP framework that gets out of your way 🌊
https://tempestphp.com
MIT License
817 stars 56 forks source link

Authentication & Authorization #10

Open brendt opened 1 year ago

brendt commented 1 year ago

We need an abstraction to handle user logins. I don't want it to be tied directly to the database though, so we probably need to carefully think about how we design it.

WendellAdriel commented 4 months ago

Hey @brendt, since you don't want to tie the authentication to the DB, I was thinking about and I had an idea.

This is just a high-overview idea, but if you feel it's something we can explore I can think more about it and start drafting a PR.

What if we had something like this?

final class AuthConfig
{
    public function __construct(
        public readonly CredentialsResolver $credentials,
        public readonly SessionDriver $driver,
    ) {
    }
}
interface CredentialsResolver
{
    public function identifier(): string; // This would return for example an username or email

    public function secret(): string; // This would return for example a password or API key
}

Then we could have some implementations for the CredentialsResolver like DatabaseCredentials that would receive an Authenticable parameter that could be applied to any model class.

final readonly DatabaseCredentials implements CredentialsResolver
{
    public function __construct(
        Authenticable $authenticable,
    ) {
    }
}

We could have an OAuthCredentials that would wrap the common logic of an OAuth flow. Other examples would be:

brendt commented 4 months ago

I think I like where this is going. What's still missing is the public facing API. How would framework users interact with this auth layer? Did you have some thoughts about that?

WendellAdriel commented 4 months ago

Yes, but I think I sent some wrong names in my first comment, the idea would be to have something like:

interface Authenticable
{
    public function identifier(): string; // This would return for example an username or email

    public function secret(): string; // This would return for example a password or API key
}

Then in the configuration, it would have something like this:

final class AuthConfig
{
    public function __construct(
        public readonly Authenticator $authenticator,
        public readonly SessionDriver $driver,
    ) {
    }
}

Then we would have a common contract on how to handle authentication and that would be the public one. I thought about a really minimalist public API.

interface Authenticator
{
    public function authenticate(Authenticable $authenticable): void;  // It would throw an exception if the authentication fails

    public function invalidate(): void;

    public function user(): Authenticable;
}

Then we would have the different authenticator like DatabaseAuthenticator, OAuthAuthenticator, SocialAuthenticator, LdapAuthenticator.

For a Database login for example we would have something like:

final class User implements Authenticable
{
    // Model definition here.

    public function identifier(): string
    {
        return $this->email;
    }

    public function secret(): string
    {
        return $this->password;
    }
}
final readonly class AuthController
{
    public function __construct(
        private Authenticator $authenticator,
    ) {
    }

    public function login(Request $request): Response
    {
        $user = ... // get the user with the data from request
        $this->authenticator->authenticate($user);
        // return response
    }

    public function logout(): Response
    {
        $this->authenticator->invalidate();
        // return response
    }

    public function loggedUser(): Response
    {
        $user = $this->authenticator->user();
        // return response with user data
    }
}
brendt commented 4 months ago
final readonly class AuthController
{
    public function __construct(
        private Authenticator $authenticator,
    ) {
    }

    public function login(Request $request): Response
    {
        $user = ... // get the user with the data from request
        $this->authenticator->authenticate($user);
        // return response
    }

    public function logout(): Response
    {
        $this->authenticator->invalidate();
        // return response
    }

    public function loggedUser(): Response
    {
        $user = $this->authenticator->user();
        // return response with user data
    }
}

I like this! A couple of thoughts:

WendellAdriel commented 4 months ago

I agree on renaming the methods to login an logout and I feel that’s a great idea to register the Authenticable as a singleton!

I didn’t think on a better name for it as well, I feel the name is weird, but IDK how to call it 😅

Can I start drafting an initial implementation for this idea, @brendt?

brendt commented 4 months ago

@WendellAdriel Yes go for it! 👍

WendellAdriel commented 4 months ago

Yay!!! I’ll start working on this to prepare a draft PR then!