mnahkies / openapi-code-generator

A code generation tool for openapi 3 / 3.1 specifications written in typescript, primarily aimed at generating typescript clients and server stubs. Other target languages may be added in future.
https://openapi-code-generator.nahkies.co.nz/
MIT License
20 stars 2 forks source link

What's the best way to handle authentication currently? #135

Open ADRFranklin opened 7 months ago

ADRFranklin commented 7 months ago

So far I am passing in a generic auth middleware to validate cookies/tokens for my application and then setting the state to that of a user entity. Which so far is nothing crazy and pretty standard, but when it comes to dealing with it in the routes, I seem to be writing the same code over and over again and even if abstracted into a function I still have to remember to call that function at the top of each route call.

Should I wait for per route middleware or for security schemes to be supported or is there a better way of doing this? Ideally I would make use of the libraries security schemas to implement my logic and then have that implementation called on all the routes linked with the securitySchemas specified in the openapi file, but of course I am not yet able to do that.

Any ideas?

mnahkies commented 7 months ago

This is something I don't have a particularly great answer to yet, and tbh it's relatively low on my list of things to implement - improving the split by tag stuff, supporting file uploads, response headers, etc are likely to come ahead of security schemas.

One thing I've found to be relatively successful is a global middleware that asserts provided authentication is valid (eg: validate a jwt / fetch a session from db - authn), and then store the user information/claims in an https://nodejs.org/api/async_context.html and calling next wrapped in this.

You can then have a strongly typed function to get this information in your handlers and don't have to deal with untyped/any values accessing it from the ctx.state, and use it to make your authz decisions on a per route basis - these are often route specific anyway, though I do appreciate groups of routes may benefit from shared logic which is currently hard to do.

I actually do a similar thing with my logging to automatically capture context about a request on each log message without needing to explicitly pass it around as function arguments.

I don't really have a clear plan on how I will eventually implement first class authentication support, it's not something I've actually used much with openapi to date (I've always handled it outside of openapi specs) so I'll need to improve my understanding of the spec before I can implement anything.

If you want to brain dump any pseudo code examples of how you'd expect it to work please feel free and I'll take it into consideration (ideally example of openapi spec and how you'd expect this to translate to code)

ADRFranklin commented 7 months ago

At the moment I have resorted to wrapping the original method for the implementation which checks if the user object is valid and if not throws a 401, else it calls the original handler. I'm no expert at generics, so ignore how basic this is haha.

export function withUserCheck<
  TParams extends Params<any, any, any>,
  TRespond extends KoaRuntimeResponder,
  TReturn
>(
  handler: (params: TParams, respond: TRespond, ctx: RouterContext) => Promise<TReturn>
) {
  return async function cb(params: TParams, respond: TRespond, ctx: RouterContext) {
    if (!ctx.state.user) {
      throw new UnauthorisedException();
    }
    // Call the original handler with all its arguments
    return handler(params, respond, ctx);
  };
}

This doesn't really tell typescript that the user is ensured to exist yet, but that might be where your idea of using async_context might be useful, I've never really seen or used async_context but it looks interesting, do you have any examples of what you meant by wrapping the await for authentication with it?

I could probably modify that wrapper to instead just be a route middleware wrapper, and allow passing individual route middlewares which might be more useful.

I have always done authentication outside of openapi schemas too, until I worked with the deepmind openapi library for golang which did provide helpers for registering authentication on a per security schema basis, though that is my own experience using openapi security schemas.