gocraft / web

Go Router + Middleware. Your Contexts.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Design pattern around routes and enforcing OAuth2 scopes

ae6rt opened this issue · comments

Hi. I was directed to file an issue here by one of your engineers in lieu of posting to an email list dedicated to this project.

I'm evaluating gocraft/web for use in a reference application my colleagues will use to pattern new microservices after in Go. The purpose of the reference app is to make choices around how to structure the code and model dependencies (database, service discovery, etc) configuration/injection so application developers don't have to individually make all these choices all over again themselves.

One of the specific questions I have around gocraft/web concerns how to idiomatically integrate OAuth2 token validation for a given endpoint. An endpoint is defined as the combination of an HTTP-verb:url-path.

Take this example from your readme:

router.Put("/users/:id", (*YourContext).UsersUpdate)

Imagine that you want to constrain consumption of this resource to users who present an OAuth2 bearer token with a specific scope. Say this scope has to be "admin/update-user". I'm wondering how you recommend enforcing such a constraint in a canonical gocraft/web app. One way is to insert a middleware that extracts the token from the request, places derived token detail information including scope into YourContext, then have the UsersUpdate method either allow or disallow the request based on some allowed, essentially hardcoded scope in the YourContext method itself.

For example, UsersUpdate pseudocode might look like this:

func (c *YourContext) UsersUpdate(rw web.ResponseWriter, req *web.Request) {
    if c.token.scope != "admin/update-user" {
       return unauthorized
    } 
    do work
}

The downside of this approach is that the endpoints are burdened with this sort of security check. A closure around the method might work, which closes over the required scope, but frankly I'm not sure how to articulate that in this situation.

Another way, which I cannot quite yet see my way clear to in idiomatic gocraft/web, is to imbue the middleware code with a table of tuples that look like HTTP-verb:URL-path:required-scope(s) and allow or deny the request in the middleware before the request makes it to UsersUpdate. This suggests some duplication in the articulation of the tuple with information already in the router.PUT() path-with-placeholders. iow, "/users/:id" would appear twice: once in the router.PUT() binding, and once in the middleware constraint rules. Even then, the middleware would have to match the request against the /path/:placeholder template, if such a match is easily performed with some gocraft/web stateless function.

Thank you for reading this far. Does the community have recommendations or strong patterns on how to handle such a requirement in gocraft/web?

Many thanks.
Mark

I use gocraft a lot, but don't typically check the issues page, so I hope this is still relevant.

The way we manage this situation in our organisation is to have multiple routers, each with different security/login middlewares, to minimise code repetition.

Such a setup might look like this:

rootRouter := web.New(Context{})
rootRouter.Middleware((*Context).AssignSessionsMiddleware)
...
rootRouter.Middleware((*Context).LoadUserMiddleware) //responsible for signing in the user if possible
...
//now we have url endpoints that *could* have users signed in but are not compulsory
rootRouter.Get(HomeUrl.String(), (*Context).HomeHandler)
rootRouter.Get(PricingUrl.String(), (*Context).PricingHandler)
rootRouter.Get(HelpUrl.String(), (*Context).HelpHandler)
...

//now, define a router that requires the user to be signed in
loggedInRequiredRouter := rootRouter.Subrouter(LoggedInRequiredContext{}, "/") 
loggedInRequiredRouter.Middleware((*LoggedInRequiredContext).RequireLoggedInMiddleware) //if user is not logged in, redirect them to login page
... //any other middlewares     

//now url endpoints that require the user to be logged in
loggedInRequiredRouter.Get(ViewAccountUrl.String(), (*LoggedInRequiredContext).ViewAccountHandler)

This sort of setup means that I don't ever have to have the

if c.token.scope != "admin/update-user" {
       return unauthorized
}

from your code, as I just keep defining contexts and middlewares as I need them (eg AdminRequiredContext, RequireAdminMiddleware, etc etc)

I hope this helps!