Hoektronics / BotQueue

Control your 3D printer(s) through the Internet.
https://www.botqueue.com
GNU General Public License v3.0
167 stars 41 forks source link

Use Personal Access Token for host authentication #220

Closed Jnesselr closed 3 years ago

Jnesselr commented 3 years ago

We originally used our own grant type and a bunch of supporting code to make host authentication work. Specifically, we had to be careful to differentiate what the user could do vs what the host could do. It was very fragile and every single upgrade/migration would break something. What I originally wanted to do was use a personal access token client for the user and a different personal access client for the hosts. Unfortunately, the developer behind the laravel passport library we use for oauth didn't want to add that change.

The reason I originally wanted two separate clients was to help avoid a security risk. Say user 1 had hosts 1 and 2. User 2 doesn't have any hosts. The way the token lookup works is it tries each provider listed, in order, for the endpoint. If our endpoint listed auth:api,host as the middleware, it would try to auth using the api (against users) first, and then hosts. So the hosts 1 and 2 would end up being authorized as the user 1 and 2 instead, for that endpoint. And there would be no checks on that. It's just "Oh hey, I found a user with this id and that client does exist and isn't revoked, so here you go!" Theoretically, we'd also have checks on "token can" and the token can only "host", but it's just asking for a security vulnerability.

What this diff does is migrate all of the models to use UUIDs for keys instead of auto incrementing IDs. That way, when the auth lookup happens, we won't have a user with the same key as a host. They'll all be completely unique from each other. Using auth:api,host on an endpoint will now try to look up a user with some id, fail, then try the host and find it for a host key. It also helps us remove a ton of extra code like the HostManager. Auth::user() now returns a Host when we've authenticated using a host token. This should also make policies easier. Before, the user passed in was an instance of the User model, and we had to double check if this was actually a host. Now, the "authenticated user" passed in is either a User model or Host model. A policy might look like this:

public function view(User|Host $authed, Bot $bot)
{
    if($authed instanceof User && $authed->tokenCan('bot:read')) {
        return $bot->creator_id == $authed->id;
    }

    if($authed instanceof Host && $authed->tokenCan('host')) {
        return $bot->host_id == $authed->id;
    }

    return false;
}

We don't have any concept of a host with a non-host scope, so we could remove the tokenCan('host') call, theoretically. This is just an example, so it might change as time goes on.