github.com/99designs/gqlgen@v0.17.45/docs/content/recipes/authentication.md (about)

     1  ---
     2  title: "Providing authentication details through context"
     3  description: How to using golang context.Context to authenticate users and pass user data to resolvers.
     4  linkTitle: Authentication
     5  menu: { main: { parent: 'recipes' } }
     6  ---
     7  
     8  We have an app where users are authenticated using a cookie in the HTTP request, and we want to check this authentication status somewhere in our graph. Because GraphQL is transport agnostic we can't assume there will even be an HTTP request, so we need to expose these authentication details to our graph using a middleware.
     9  
    10  
    11  ```go
    12  package auth
    13  
    14  import (
    15  	"database/sql"
    16  	"net/http"
    17  	"context"
    18  )
    19  
    20  // A private key for context that only this package can access. This is important
    21  // to prevent collisions between different context uses
    22  var userCtxKey = &contextKey{"user"}
    23  type contextKey struct {
    24  	name string
    25  }
    26  
    27  // A stand-in for our database backed user object
    28  type User struct {
    29  	Name string
    30  	IsAdmin bool
    31  }
    32  
    33  // Middleware decodes the share session cookie and packs the session into context
    34  func Middleware(db *sql.DB) func(http.Handler) http.Handler {
    35  	return func(next http.Handler) http.Handler {
    36  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    37  			c, err := r.Cookie("auth-cookie")
    38  
    39  			// Allow unauthenticated users in
    40  			if err != nil || c == nil {
    41  				next.ServeHTTP(w, r)
    42  				return
    43  			}
    44  
    45  			userId, err := validateAndGetUserID(c)
    46  			if err != nil {
    47  				http.Error(w, "Invalid cookie", http.StatusForbidden)
    48  				return
    49  			}
    50  
    51  			// get the user from the database
    52  			user := getUserByID(db, userId)
    53  
    54  			// put it in context
    55  			ctx := context.WithValue(r.Context(), userCtxKey, user)
    56  
    57  			// and call the next with our new context
    58  			r = r.WithContext(ctx)
    59  			next.ServeHTTP(w, r)
    60  		})
    61  	}
    62  }
    63  
    64  // ForContext finds the user from the context. REQUIRES Middleware to have run.
    65  func ForContext(ctx context.Context) *User {
    66  	raw, _ := ctx.Value(userCtxKey).(*User)
    67  	return raw
    68  }
    69  ```
    70  
    71  **Note:** `getUserByID` and `validateAndGetUserID` have been left to the user to implement.
    72  
    73  Now when we create the server we should wrap it in our authentication middleware:
    74  ```go
    75  package main
    76  
    77  import (
    78  	"net/http"
    79  
    80  	"github.com/99designs/gqlgen/_examples/starwars"
    81  	"github.com/99designs/gqlgen/graphql/handler"
    82  	"github.com/99designs/gqlgen/graphql/playground"
    83  	"github.com/go-chi/chi"
    84  )
    85  
    86  func main() {
    87  	router := chi.NewRouter()
    88  
    89  	router.Use(auth.Middleware(db))
    90  
    91  	srv := handler.NewDefaultServer(starwars.NewExecutableSchema(starwars.NewResolver()))
    92  	router.Handle("/", playground.Handler("Starwars", "/query"))
    93  	router.Handle("/query", srv)
    94  
    95  	err := http.ListenAndServe(":8080", router)
    96  	if err != nil {
    97  		panic(err)
    98  	}
    99  }
   100  ```
   101  
   102  And in our resolvers (or directives) we can call `ForContext` to retrieve the data back out:
   103  ```go
   104  
   105  func (r *queryResolver) Hero(ctx context.Context, episode Episode) (Character, error) {
   106  	if user := auth.ForContext(ctx) ; user == nil || !user.IsAdmin {
   107  		return Character{}, fmt.Errorf("Access denied")
   108  	}
   109  
   110  	if episode == EpisodeEmpire {
   111  		return r.humans["1000"], nil
   112  	}
   113  	return r.droid["2001"], nil
   114  }
   115  ```
   116  
   117  ### Websockets
   118  
   119  If you need access to the websocket init payload you can add your `InitFunc` in `AddTransport`.  
   120  Your InitFunc implementation could then attempt to extract items from the payload:  
   121  
   122  ```go
   123  package main
   124  
   125  import (
   126  	"context"
   127  	"errors"
   128  	"log"
   129  	"net/http"
   130  	"os"
   131  	"time"
   132  
   133  	"github.com/99designs/gqlgen/graphql/handler"
   134  	"github.com/99designs/gqlgen/graphql/handler/extension"
   135  	"github.com/99designs/gqlgen/graphql/handler/transport"
   136  	"github.com/99designs/gqlgen/graphql/playground"
   137  	"github.com/go-chi/chi"
   138  	"github.com/gorilla/websocket"
   139  	"github.com/gqlgen/_examples/websocket-initfunc/server/graph"
   140  	"github.com/gqlgen/_examples/websocket-initfunc/server/graph/generated"
   141  	"github.com/rs/cors"
   142  )
   143  
   144  func webSocketInit(ctx context.Context, initPayload transport.InitPayload) (context.Context, error) {
   145  	// Get the token from payload
   146  	any := initPayload["authToken"]
   147  	token, ok := any.(string)
   148  	if !ok || token == "" {
   149  		return nil, errors.New("authToken not found in transport payload")
   150  	}
   151  
   152  	// Perform token verification and authentication...
   153  	userId := "john.doe" // e.g. userId, err := GetUserFromAuthentication(token)
   154  
   155  	// put it in context
   156  	ctxNew := context.WithValue(ctx, "username", userId)
   157  
   158  	return ctxNew, nil
   159  }
   160  
   161  const defaultPort = "8080"
   162  
   163  func main() {
   164  	port := os.Getenv("PORT")
   165  	if port == "" {
   166  		port = defaultPort
   167  	}
   168  
   169  	router := chi.NewRouter()
   170  
   171  	// CORS setup, allow any for now
   172  	// https://gqlgen.com/recipes/cors/
   173  	c := cors.New(cors.Options{
   174  		AllowedOrigins:   []string{"*"},
   175  		AllowCredentials: true,
   176  		Debug:            false,
   177  	})
   178  
   179  	srv := handler.New(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
   180  	srv.AddTransport(transport.POST{})
   181  	srv.AddTransport(transport.Websocket{
   182  		KeepAlivePingInterval: 10 * time.Second,
   183  		Upgrader: websocket.Upgrader{
   184  			CheckOrigin: func(r *http.Request) bool {
   185  				return true
   186  			},
   187  		},
   188  		InitFunc: func(ctx context.Context, initPayload transport.InitPayload) (context.Context, error) {
   189  			return webSocketInit(ctx, initPayload)
   190  		},
   191  	})
   192  	srv.Use(extension.Introspection{})
   193  
   194  	router.Handle("/", playground.Handler("My GraphQL App", "/app"))
   195  	router.Handle("/app", c.Handler(srv))
   196  
   197  	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
   198  	log.Fatal(http.ListenAndServe(":"+port, router))
   199  }
   200  ```
   201  
   202  > Note
   203  >
   204  > Subscriptions are long lived, if your tokens can timeout or need to be refreshed you should keep the token in
   205  context too and verify it is still valid in `auth.ForContext`.