Two approaches of authentication have been used a lot in many books and blogs for Go:
Use http.Request
:
func getCurrentUser(r *http.Request) (*User, error) {
// get JWT token or cookie and find corresponding sessions
// and account, then return the user and nil error;
// if user is not found, return a nil user and a non-nil error
}
Then handler functions call the getCurrentUser to get the user for each request. It's possible to use a function decorator that wraps around other handlers and check the authentication before other handler functions get executed.
func secretInfoHandler (w http.ResponseWriter, r * http.Request) {
user, err := getCurrentUser(r)
if err != nil {
// write http.Unauthorized, and return
}
// otherwise, process request return data
}
func MustAuthenticate (h http.HandlerFunc) http.HandlerFunc {
return func (w http.ResponseWriter, r *http.Request) {
// chekc authentication, if pass:
h.ServeHTTP(w, r)
// if fail:
w.WriteHeader(http.StatusUnauthorized)
return
}
}
Using context.Context
:
// use the same getCurrentUser() as the one above
func MustAuthenticate (h http.HandlerFunc) http.HandlerFunc {
return func (w http.ResponseWriter, r *http.Request) {
user, err := getCurrentUser(r)
if err != nil {
// write error code then return
}
ctx := context.WithValue(r.Context(), someKey, someValue)
h(w, r.WithContext(ctx))
}
}
Then for each handler (like secretInfoHandler
), instead of calling getCurrentUser(r *http.Request)
, I just need to check if the Context
that comes with the http.Request
contains certain authenticaiton info.
They appear to be equivalent. So, what are the technical advantages/disadvantages of each approach? If they are indeed equivalent, which one is better for real-world production code to use?
I think your examples are a little confused. The seem to contain a combination of authorising in handlers, and authorising in middleware wrapping handlers.
This is the first example, calling some getCurrentUser(request)
function from the request handler.
func secretInfoHandler (w http.ResponseWriter, r * http.Request) {
user, err := getCurrentUser(r)
if err != nil {
// write http.Unauthorized, and return
}
// otherwise, process request return data
}
^^ this is taken from your example, but you've also included the MustAuthenticate
middleware which I think is irrelevant here.
This is your second example, using the context.Context
's key-value bucket as a means of sending values down into the handler.
func MustAuthenticate (h http.HandlerFunc) http.HandlerFunc {
return func (w http.ResponseWriter, r *http.Request) {
user, err := getCurrentUser(r)
if err != nil {
// write error code then return
}
ctx := context.WithValue(r.Context(), someKey, someValue)
h.ServeHTTP(w, r.WithContext(ctx))
}
}
It's important to note from the start that; while I've split these two in to auth in handler and auth in middleware, each with their own using. There's no reason why you couldn't swap way that the authorisation happens. e.g. Authorisation in middleware using getCurrentUser(request)
, whether you should is discussed below.
Breaking this down, the question you really need to ask is:
"Where do you need access to your user struct?"
This will help you to decide which to use.
In general it's perfectly valid to put request scoped variables in context.Context
. Variables such as tracing information regularly go in to the context. The main problem with context is that it's not compile time checked, and not type safe. You are making the assumption in later code that your user object has been set in the context, and this makes your code coupled in a non-obvious way.
The benefits of context.Context
method is that if your user object needs to descend multiple levels of function calls, you don't need to wire it through your entire code base. You can just grab it out of the context later. It's a values bucket, allowing for greater flexibility in the code later.
With the authorisation in handlers method; you could potentially have the same authorisation and error handling code in many handlers. If you try and extract this to a middlware; you will quickly find that there's no nice way of maintaining the http.Handler
interface and passing the user object extracted in the middlware. (which is exactly why, in the example above, the user object is put inside the request object).
The benefits of authorisation in the controller using getCurrentUser(request)
is that it's obvious where the user struct is created, it's clear what authorisation has happened looking at that handler, and there's no way that user object could not be present (assuming no error is returned).
Depends on where you need you user object; and how many handlers there are.
getCurrentUser(req)
inside handlergetCurrentUser(req)
in middlware, and don't put it in the context (as you don't need it later)You already have all information needed for authentication in request. Context is also request property. Supplying request with auth context you do not provide any new information, you just make this information easily acceptable. From documentation
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.