Gqlgen: Question: Setting cookies

Created on 1 Mar 2019  路  25Comments  路  Source: 99designs/gqlgen

I was looking at #98 and was wondering if I should set cookies through a resolver or a handler.

Most helpful comment

Finally, below is my middleware. I hope this helps someone out there. Thank you, everyone. _/\_

middleware.go

package resolver

import (
    context "context"
    log "log"
    http "net/http"
)

type ContextKey string

type authResponseWriter struct {
    http.ResponseWriter
    userIDToResolver string
    userIDFromCookie string
}

func (w *authResponseWriter) Write(b []byte) (int, error) {
    if w.userIDToResolver != w.userIDFromCookie {
        http.SetCookie(w, &http.Cookie{
            Name:     "auth",
            Value:    w.userIDToResolver,
            HttpOnly: true,
            Path:     "/",
            Domain:   "127.0.0.1",
        })
    }
    return w.ResponseWriter.Write(b)
}

func AuthMiddleWare(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        arw := authResponseWriter{w, "", ""}
        userIDContextKey := ContextKey("userID")

        // before executing next
        c, _ := r.Cookie("auth")
        if c != nil {
            arw.userIDFromCookie = c.Value
            arw.userIDToResolver = c.Value
        }
        ctx := context.WithValue(r.Context(), userIDContextKey, &arw.userIDToResolver)
        r = r.WithContext(ctx)

        // executing next
        next(&arw, r)

        // after executing next
    }
}

inside resolver

// verify userID and password and set it into pointer
authPointer := ctx.Value(ContextKey("userID")).(*string)
*authPointer = "54321"

All 25 comments

If you want to access cookie information from a resolver, then follow the advice given in #98 and use a mmiddleware to inject a value into context.

I think this question was misunderstood. From my perspective it's not about accessing cookie information, it's about where to set a cookie. I'm currently struggeling with that myself and don't know how to set the cookie. Is it possible to extend the authentication example on the website with "how to set a cookie"? For example when you have a login mutation and after confirming that the passwords match you want to set a cookie that should be attached to the http response.

@saschajullmann Did you find the answer? I think maybe we should set the cookie from the client and let GraphQL return only the token string.

@LIYINGZHEN unfortunately no. That's an idea. Although tbh I gave up on this and didn't look into this any further.

@LIYINGZHEN I couldn't find a way to set cookies via a graphql resolver so I just made a separate login route that handled authentication.

@DouglasHoang That's probably a way to go. I also want to upload data to GraphQL but look's like gqlgen doesn't support multipart/form-data, so I have to create a rest API to do that.

This sucks. There should be a way to do this. I cannot create a separate app to handle login.

Ah right, my mistake I had misread the original request. That said, I don't think the advice it too different here. You would probably just be injecting some shared state into context, that a HTTP middleware could read after the query has been executed and set as a cookie.

@mathewbyrne can you elaborate with a sample code? Because inside a resolver, as far as my understanding goes, context is read-only. It cannot be set to a request, because there is no request object present to set.

I don't have time to put an example together right now. You wont be writing to context, but writing to a struct pointed to by something inside context. Your HTTP middleware would probably create and keep a pointer to this so that it can read it on the way out.

I understand now. I will try and share the code.

works as you explained. I added the userId pointer from the request into the context in the middleware.

userID := "12345"
ctx := context.WithValue(r.Context(), userIDContextKey, &userID)
r = r.WithContext(ctx)

I was able to set it inside the resolver using the pointer

userIDPointer := ctx.Value(userIDContextKey).(*string)
*userIDPointer = "54321"

later after the call of next(w,r) I am able to read the value set inside the resolver in the same middleware.

After doing all this, I am actually scared of the power of pointers. I guess Java made the best decision not to use pointers.

Actually, am stuck with a new issue. In the below code, if I set the cookie to w in the before section, it works. However, I am not able to do it in the after section. I want to set the cookie either in after section or inside resolver.

\\ code before calling next(w,r)
next(w,r)
\\ code after calling next(w,r)

It is weird. SetCookie in after section does not work. It works in the before section. However, w.write works in both.

Finally, below is my middleware. I hope this helps someone out there. Thank you, everyone. _/\_

middleware.go

package resolver

import (
    context "context"
    log "log"
    http "net/http"
)

type ContextKey string

type authResponseWriter struct {
    http.ResponseWriter
    userIDToResolver string
    userIDFromCookie string
}

func (w *authResponseWriter) Write(b []byte) (int, error) {
    if w.userIDToResolver != w.userIDFromCookie {
        http.SetCookie(w, &http.Cookie{
            Name:     "auth",
            Value:    w.userIDToResolver,
            HttpOnly: true,
            Path:     "/",
            Domain:   "127.0.0.1",
        })
    }
    return w.ResponseWriter.Write(b)
}

func AuthMiddleWare(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        arw := authResponseWriter{w, "", ""}
        userIDContextKey := ContextKey("userID")

        // before executing next
        c, _ := r.Cookie("auth")
        if c != nil {
            arw.userIDFromCookie = c.Value
            arw.userIDToResolver = c.Value
        }
        ctx := context.WithValue(r.Context(), userIDContextKey, &arw.userIDToResolver)
        r = r.WithContext(ctx)

        // executing next
        next(&arw, r)

        // after executing next
    }
}

inside resolver

// verify userID and password and set it into pointer
authPointer := ctx.Value(ContextKey("userID")).(*string)
*authPointer = "54321"

@ravilution

if I set the cookie to w in the before section, it works. However, I am not able to do it in the after section.

HTTP response headers (here, Set-Cookie) have to be set before the graphql handler starts writing the response to the client. https://golang.org/pkg/net/http/#ResponseWriter

You could work around this by buffering the writes from graphql, and sending them out only after you've set all your headers. However, this ruins two pretty important things: 1) subscriptions 2) websockets.

@mathewbyrne has provided a way to unconditionally set a cookie, and then use session-based authentication to later "change that the cookie means" by associating a logged-in user with the session. However, this has downsides:

  1. One cannot implement stateless authentication, putting all the user information in an encrypted cookie.
  2. If the graphql API is also hit by non-browser clients, you end up creating huge amounts of sessions that will never do anything useful.

@mathewbyrne I feel this bug is worth reconsidering. You've provided a way (via context) to access things that happened before the graphql handler was called, but not a way to set cookies based on what happens inside the graphql mutation.

I feel like I might need a separate endpoint that does authentication. REST or graphql with the above buffering trick, haven't decided yet. I feel like dragging two graphql "stacks" in my app is more confusing.

Actually, login (/forgot password) links in email are going to make me have a RESTy endpoint anyway. I guess that's a fine workaround.

To prevent others from chasing the same dead end: no, you cannot Set-Cookie with Trailers. It's explicitly forbidden.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer#Directives

@ravilution FYI, the code I have shared here is not the complete real code. I won't be storing userID in a cookie just like that. I am using a complicated encrypted refresh token logic with database checks etc. The code is just a sample. I use OTP to phone for forgot password etc. So graphql with encrypted refresh token based auth works well for me. Thanks.

@ravilution I implemented your middleware that called the Write([]byte b) method in the after section like this:

// AuthMiddleware handles reading and writing the access_token from the http cookie.
func AuthMiddleware() func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            arw := authResponseWriter{w, "", ""}
            accessTokenContextKey := AuthContextKey("access_token")

            // before executing next
            c, _ := r.Cookie("access_token")
            if c != nil {
                arw.accessTokenFromCookie = c.Value
                arw.accessTokenToResolver = c.Value
            }
            ctx := context.WithValue(r.Context(), accessTokenContextKey, &arw.accessTokenToResolver)
            r = r.WithContext(ctx)

            // executing next
            next.ServeHTTP(w, r)

            // after executing next
            arw.Write([]byte(""))
        })
    }
}

I added some Println statements within the Write([]byte b) function to ensure that the shared struct was correctly updated by the resolver.

When I send a query using playground it doesn't seem to return any cookies. Am I interpreting your sample code incorrectly?

@ravilution I implemented your middleware that called the Write([]byte b) method in the after section like this:

// AuthMiddleware handles reading and writing the access_token from the http cookie.
func AuthMiddleware() func(http.Handler) http.Handler {
  return func(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          arw := authResponseWriter{w, "", ""}
          accessTokenContextKey := AuthContextKey("access_token")

          // before executing next
          c, _ := r.Cookie("access_token")
          if c != nil {
              arw.accessTokenFromCookie = c.Value
              arw.accessTokenToResolver = c.Value
          }
          ctx := context.WithValue(r.Context(), accessTokenContextKey, &arw.accessTokenToResolver)
          r = r.WithContext(ctx)

          // executing next
          next.ServeHTTP(w, r)

          // after executing next
          arw.Write([]byte(""))
      })
  }
}

I added some Println statements within the Write([]byte b) function to ensure that the shared struct was correctly updated by the resolver.

When I send a query using playground it doesn't seem to return any cookies. Am I interpreting your sample code incorrectly?

I solved it by instead passing the address of the ResponseWriter into the context passed into the next call and let the resolver handle calling http.SetCookie:

ctx := context.WithValue(r.Context(), accessTokenContextKey, &w)
r = r.WithContext(ctx)

@jkwik glad you were able to solve. Sorry for the delay in response. Below is my code

package resolver

import (
    context "context"
    http "net/http"
    time "time"
)

type ContextKey string

type authResponseWriter struct {
    http.ResponseWriter
    config           Config
    userIDToResolver string
    userIDFromCookie string
}

func (w *authResponseWriter) Write(b []byte) (int, error) {
    if w.userIDToResolver != w.userIDFromCookie {

        // JWT token creation logic

        http.SetCookie(w, &http.Cookie{
            Name:     "auth",
            Value:    token,
            HttpOnly: true,
            Path:     "/",
            Domain:   w.config.Domain,
        })
    }
    return w.ResponseWriter.Write(b)
}

func AuthMiddleWare(config Config, next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // get userID from cookie. JWT logic.

        arw.userIDFromCookie = userID.(string)
        arw.userIDToResolver = userID.(string)

        ctx = context.WithValue(ctx, userIDContextKey, &arw.userIDToResolver)
        r = r.WithContext(ctx)

        // executing next
        next(&arw, r)
    }
}

should we pass the Header value from middleware? and set it with ctx.WithValue(............) ?

can't we get the ctx without set it from middleware ? like this below

func (r *queryResolver) Me(ctx context.Context) (*Person, error) {
    getHeader, err := decodedToken(ctx) 
   fmt.Println(getHeader) // header value wihtout set it from middleware?
}

if it was impossible, could you create that for the next feature? :)
i was using nexus in JS and that is the way they get the header :)

should we pass the Header value from middleware? and set it with ctx.WithValue(............) ?

can't we get the ctx without set it from middleware ? like this below

func (r *queryResolver) Me(ctx context.Context) (*Person, error) {
    getHeader, err := decodedToken(ctx) 
   fmt.Println(getHeader) // header value wihtout set it from middleware?
}

if it was impossible, could you create that for the next feature? :)
i was using nexus in JS and that is the way they get the header :)

@ibantoo

I think the main issue highlighted with this thread is that this gqlgen library doesn't inherently inject the http ResponseWriter (used by the http package to set cookies) and Request (used by the http package to read cookies) into the context of each resolver.

What I've done since encountering this problem is by inserting a pointer to both the ResponseWriter and Request from my middleware, into the context of each resolver and using these values in conjunction with gorilla/sessions (https://github.com/gorilla/sessions) to set cookie values. This allows you to set the cookie to any interface which allows complex cookie values.

Below is an example:

Keep in mind that the example below uses gorillas sessions feature but the package supports setting Cookies as well (their docs are here)

middleware/http.go

// InjectHTTPMiddleware handles injecting the ResponseWriter and Request structs
// into context so that resolver methods can use these to set and read cookies. It also passes a // CookieStore initialized in `server.go` into context for facilitated cookie handling.
func InjectHTTPMiddleware(session *sessions.CookieStore) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            httpContext := helpers.HTTP{
                W: &w,
                R: r,
            }
            httpKeyContext := helpers.HTTPKey("http")

            sessionKeyContext := helpers.HTTPKey("session")

            ctx := context.WithValue(r.Context(), httpKeyContext, httpContext)
            ctx = context.WithValue(ctx, sessionKeyContext, session)

            r = r.WithContext(ctx)
            next.ServeHTTP(w, r)
        })
    }
}

server.go

func main() {
        ...

    // Initialize a new cookie store and pass into the inject HTTP middleware
    store := sessions.NewCookieStore([]byte(os.GetEnv("SESSION_KEY")))

    // This middleware injects the ResponseWriter and Reader structs
    // into the context of each resolver so they have access http headers and cookies. The 
        // session is also passed in so that resolvers will ultimately have access to the store.
    router.Use(middleware.InjectHTTPMiddleware(store))
     ...

resolver.go

func (r *mutationResolver) Cookie(ctx context.Context) (bool, error) {
     // Grab a session (gorilla returns a session if the named session doesn't exist)
     session := helpers.GetSession(ctx, "sess")

     // Reading userID cookie value
     userID := session.Values["userID"]

     // Setting userID cookie value
     session.Values["userID"] = 1

     // Save session
     if err := helpers.SaveSession(ctx, session); err != nil {
    return nil, fmt.Errorf("Failed to save cart in session with error: %s", err)
     }
}

And finally, here are the helper methods

helper.go

// HTTPKey is the key used to extract the Http struct.
type HTTPKey string

// HTTP is the struct used to inject the response writer and request http structs.
type HTTP struct {
    W *http.ResponseWriter
    R *http.Request
}

// GetSession returns a cached session of the given name
func GetSession(ctx context.Context, name string) *sessions.Session {
    store := ctx.Value(HTTPKey("session")).(*sessions.CookieStore)
    httpContext := ctx.Value(HTTPKey("http")).(HTTP)

    // Ignore err because a session is always returned even if one doesn't exist
    session, _ := store.Get(httpContext.R, name)

    return session
}

// SaveSession saves the session by writing it to the response
func SaveSession(ctx context.Context, session *sessions.Session) error {
    httpContext := ctx.Value(HTTPKey("http")).(HTTP)

    err := session.Save(httpContext.R, *httpContext.W)

    return err
}

Gorilla also helps with ensuring cookie security by encrypting cookies.

This library works great for creating/accessing cookies from resolvers with minimal fuss: https://github.com/alexedwards/scs

Just wrap the gql handler in the middleware it provides and with that you can set/access values to/from the cookie-based session in the resolvers using context.

Being able to set a cookie shouldn't be confused with using a session. If the idea is to have token based auth using, for example, a JWT then the whole idea is not needing a session or a session store.

It would be nice if gqlgen provided a way to set cookies, and maybe other arbitrary headers as well from within resolvers. Maybe through the context, without having to program custom middlewares that have to buffer responses to workaround gqlgen writting to the response before the header is set.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

itsbalamurali picture itsbalamurali  路  4Comments

lynntobing picture lynntobing  路  3Comments

jacksontj picture jacksontj  路  4Comments

RobertoOrtis picture RobertoOrtis  路  3Comments

JulienBreux picture JulienBreux  路  3Comments