IzzyBeraja / Flags

0 stars 0 forks source link

Decide on an authorization strategy #38

Open IzzyBeraja opened 1 year ago

IzzyBeraja commented 1 year ago

After investigation into various approaches toward type safety, it seems that most systems do not provide a totally type-safe approach to endpoints. Main problems are as follows:

There are a couple options:

I will continue to look into this, but this is a difficult thing to accomplish. I imagine once I have set it up, it will be simple to use, but the lack of understanding around this typing and the reliance on codegen support could be challenging and require a few days of work.

IzzyBeraja commented 1 year ago

I've written out some of my ideas in the Typescript Playground but will need to do more testing before I commit to updating everything. The main idea here is to combine types from the starting param ending with the last param. The current issue is that I don't understand how type intersections work with this. I would like to define only the required fields for each middleware rather than forcing each subsequent field to be aware of the previous type. This will allow for better middleware combinations that won't cause type issues. I'm having trouble explaining it, but basically if I run isAuthenticated and later run another like isOnChrome, I don't want to have issues adding middleware like isFromEU since it would require fields from isOnChrome yet not actually need them for the middleware to work. The last handler should then have the combined types of all the responses.

Within the actual route file, I plan to add an array that contains the middleware I plan to run.

const postMiddleware = [isAuthenticated, isAuthorized("user", "admin")];

I may just end up typing the array instead of the request builder since I have to be able to know the type of the handler BEFORE it is exported. There is some value in type checking at a later stage, but at that point, it will probably be codegen anyway, so typing is less important. By having the middleware exist within the same file as the handler itself, I can ensure that the handler has the proper types, and I can simply merge all the types within the array together.

I do still want elements in the array to type check each other. For example, if I run isAuthorized before isAuthenticated, I want to get an error because I can't verify if the user isAuthorized if there is no guarantee the user is logged in. This way, I can ensure that my middleware is organized in a way that satisfies my compile time rules. The middleware itself doesn't necessarily need to exist in this file, but it will at least need to be imported to ensure type safety.

I'm going to keep thinking on this as I think I can actually get this working.

IzzyBeraja commented 1 year ago

I've implemented a way to do a basic authentication strategy, but I realize that a strong authorization strategy is significantly more challenging. For the authentication strategy, I am doing what I proposed above; each route specifies the middleware that will run on it, and if the middleware fails, the request ends early. I've also managed to set up a simple way to handle types with this approach, although the type needs to be added separately from the implementation of the middleware which is not ideal, but not the worst either.

The tricky part of this is authorization. There are some simple ways to accomplish this and more involved ones. If I want to set up a simple role check, it's not hard. Query the database for the user's role and then add it to that role to the cache. Updating the role can be done and when the routes for role updates are hit, the cache for that user in particular is updated. The challenge is when there are multiple users in an organization and they need their roles updated. As of right now, the session middleware that I have implemented is quite restrictive in how I interact with the cache. I don't have a direct way to query for other sessions that are currently available and can only revoke the currently signed on user session. This means that I don't have an easy way to update other sessions.

When it comes to authorization that is more targeted than a role, such a flag or post having a specific privacy option, this isn't quite as straightforward. The database needs to be queried and checked for access privileges. I imagine this can be done in one of two ways:

The second option in my opinion is more robust, but probably overkill for this implementation. The idea would be that adding privacy rules adds options to the underlying query that filter data. There would also need to be some sort of error handling to alter the client that data cannot be accessed rather than returning null. I'd need to think about it more, but again, it doesn't seem to be necessary for what I am doing.

As such, for now, I am going to end up setting roles for the user and roles for the API keys. The idea being that the API key can have differing levels of access so that if it is available on the client for example, there isn't a threat of that API key being used to alter the state of flags, just access. This is pretty common practice and would involve adding access levels on the row of the API key and then updating the cache when values are updated.