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